From 20b8dc2442268a7a352c42b7acdeb0a12627b0ac Mon Sep 17 00:00:00 2001 From: fms-zth <2579013254@qq.com> Date: Sun, 20 Sep 2026 21:22:47 +0800 Subject: [PATCH] feat(dcu-kernel-auto-opt): refresh DKAO harness from the DCU dev container MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updates the task plugin to the latest container content (/root/zth_agent/MetaInfer/metainfer/tasks/dcu_kernel_auto_opt), applied as an incremental 3-way merge so the PR-side alignment fixes stay intact (init_or_resume(task_id), CPU-only-CI torch guards, hermetic dsh tests). Added: - orchestrator/contention.py, orchestrator/round_strategy.py and harness_default/systemprompt/round_strategy.yaml - tools/retest_variant.py, tools/baseline/tp8_m8_* (Triton m8 baselines) - reports/: down-proj diagnostic scripts and tooling checks - tests: test_contention, test_execution_profile, test_gpu_mode, test_retest_tool, test_round_strategy, test_shape_catalog_panel, test_ahe_child_embed Changed: planner/gate_policy/gpu_preflight/harness_io/config/prompts, gen_and_opt_pipeline, w8a8_pipeline, w8a8_baselines, server/routes, static detail/shape panels, form.yaml, WORKFLOW.md, bridge/dsh. Dependency guards (tests only): Three features measured by this plugin live outside this PR's scope for now — metainfer.orchestrator.gpu_broker (GPU broker leases), the DCU toolchain (/opt/dtk/bin/hipprof PMC profiling) and the harness_evolve sibling task. Eleven tests that require them are marked with pytest.mark.skipif via importlib.util.find_spec / path checks (same convention as the torch guards), so the suite stays green in CI and they re-enable automatically once those modules land. --- .../tasks/dcu_kernel_auto_opt/WORKFLOW.md | 23 +- .../api/int8w8a8gemm/int8_w8a8_gemm_api.py | 19 +- .../dcu_kernel_auto_opt/assets/w8a8_bench.py | 161 ++ .../dcu_kernel_auto_opt/bridge/dsh/README.md | 10 +- .../bridge/dsh/dsh_agent.py | 36 +- .../bridge/dsh/run_dsh_task.sh | 3 +- .../bridge/dsh/tests/smoke_sdk.py | 2 +- .../bridge/dsh/tests/test_dsh_agent.py | 37 +- metainfer/tasks/dcu_kernel_auto_opt/form.yaml | 21 +- .../harness_default/README.md | 73 +- .../harness_default/manifest.yaml | 20 +- .../harness_default/planner_catalog.yaml | 6 +- .../systemprompt/round_strategy.yaml | 204 ++ .../orchestrator/config.py | 180 +- .../orchestrator/contention.py | 187 ++ .../orchestrator/gen_and_opt_pipeline.py | 109 +- .../orchestrator/gpu_preflight.py | 341 ++- .../orchestrator/harness_io.py | 64 +- .../orchestrator/planner.py | 24 +- .../orchestrator/prompts.py | 264 +- .../orchestrator/round_strategy.py | 454 ++++ .../orchestrator/w8a8_baselines.py | 29 + .../orchestrator/w8a8_pipeline.py | 293 ++- .../reports/_diag_down_proj.sh | 40 + .../reports/_diag_down_proj_profiler.py | 99 + .../reports/_diag_down_proj_sweep.sh | 51 + .../dcu_kernel_auto_opt/reports/_make_xlsx.py | 14 +- .../reports/_tool_check_decompose.py | 109 + .../reports/_tool_check_floor.py | 103 + .../reports/_tool_check_timing.py | 124 + .../dkao_optimized_operators_bestvariant.csv | 72 +- .../dkao_optimized_operators_bestvariant.json | 191 +- .../dkao_optimized_operators_bestvariant.xlsx | Bin 16612 -> 18133 bytes .../dcu_kernel_auto_opt/server/routes.py | 183 +- .../static/dkao-agent-fields.js | 10 +- .../dcu_kernel_auto_opt/static/dkao-detail.js | 135 +- .../static/dkao-shape-input.js | 32 +- .../dcu_kernel_auto_opt/tests/conftest.py | 14 + .../tests/test_ahe_child_embed.py | 175 ++ .../dcu_kernel_auto_opt/tests/test_config.py | 39 +- .../tests/test_contention.py | 91 + .../tests/test_contract_snapshot.py | 74 +- .../tests/test_execution_profile.py | 266 ++ .../tests/test_gpu_mode.py | 108 + .../tests/test_gpu_preflight.py | 407 ++- .../dcu_kernel_auto_opt/tests/test_plugin.py | 2 +- .../tests/test_retest_tool.py | 52 + .../tests/test_round_strategy.py | 490 ++++ .../tests/test_shape_catalog_panel.py | 126 + .../tests/test_w8a8_baselines.py | 31 + .../dcu_kernel_auto_opt/tools/__init__.py | 0 .../tools/baseline/bench_triton_tp8_m8.py | 238 ++ .../tools/baseline/tp8_m8_graph_merged.json | 197 ++ .../tools/baseline/tp8_m8_graph_run1.json | 662 +++++ .../tools/baseline/tp8_m8_graph_run2.json | 662 +++++ .../tools/baseline/tp8_m8_graph_run3.json | 662 +++++ .../tools/retest_variant.py | 331 +++ .../glm52/TP8/M4096/fused_qkv_a_proj.hip | 1455 ++++------- .../int8w8a8-gemm/glm52/TP8/M4096/o_proj.hip | 1370 +++------- .../glm52/TP8/M4096/shared_down_proj.hip | 1900 ++++++-------- .../glm52/TP8/M4096/shared_gate_up_proj.hip | 1018 +++----- .../int8w8a8-gemm/hy3/TP4/M4096/o_proj.hip | 842 ++++-- .../int8w8a8-gemm/hy3/TP4/M4096/qkv_proj.hip | 1171 ++++----- .../int8w8a8-gemm/hy3/TP8/M16/qkv_proj.hip | 2263 +++++++++-------- .../hy3/TP8/M16/shared_gate_up_proj.hip | 1419 +++++------ 65 files changed, 12664 insertions(+), 7124 deletions(-) create mode 100644 metainfer/tasks/dcu_kernel_auto_opt/harness_default/systemprompt/round_strategy.yaml create mode 100644 metainfer/tasks/dcu_kernel_auto_opt/orchestrator/contention.py create mode 100644 metainfer/tasks/dcu_kernel_auto_opt/orchestrator/round_strategy.py create mode 100644 metainfer/tasks/dcu_kernel_auto_opt/reports/_diag_down_proj.sh create mode 100644 metainfer/tasks/dcu_kernel_auto_opt/reports/_diag_down_proj_profiler.py create mode 100644 metainfer/tasks/dcu_kernel_auto_opt/reports/_diag_down_proj_sweep.sh create mode 100644 metainfer/tasks/dcu_kernel_auto_opt/reports/_tool_check_decompose.py create mode 100644 metainfer/tasks/dcu_kernel_auto_opt/reports/_tool_check_floor.py create mode 100644 metainfer/tasks/dcu_kernel_auto_opt/reports/_tool_check_timing.py create mode 100644 metainfer/tasks/dcu_kernel_auto_opt/tests/test_ahe_child_embed.py create mode 100644 metainfer/tasks/dcu_kernel_auto_opt/tests/test_contention.py create mode 100644 metainfer/tasks/dcu_kernel_auto_opt/tests/test_execution_profile.py create mode 100644 metainfer/tasks/dcu_kernel_auto_opt/tests/test_gpu_mode.py create mode 100644 metainfer/tasks/dcu_kernel_auto_opt/tests/test_retest_tool.py create mode 100644 metainfer/tasks/dcu_kernel_auto_opt/tests/test_round_strategy.py create mode 100644 metainfer/tasks/dcu_kernel_auto_opt/tests/test_shape_catalog_panel.py create mode 100644 metainfer/tasks/dcu_kernel_auto_opt/tools/__init__.py create mode 100644 metainfer/tasks/dcu_kernel_auto_opt/tools/baseline/bench_triton_tp8_m8.py create mode 100644 metainfer/tasks/dcu_kernel_auto_opt/tools/baseline/tp8_m8_graph_merged.json create mode 100644 metainfer/tasks/dcu_kernel_auto_opt/tools/baseline/tp8_m8_graph_run1.json create mode 100644 metainfer/tasks/dcu_kernel_auto_opt/tools/baseline/tp8_m8_graph_run2.json create mode 100644 metainfer/tasks/dcu_kernel_auto_opt/tools/baseline/tp8_m8_graph_run3.json create mode 100644 metainfer/tasks/dcu_kernel_auto_opt/tools/retest_variant.py diff --git a/metainfer/tasks/dcu_kernel_auto_opt/WORKFLOW.md b/metainfer/tasks/dcu_kernel_auto_opt/WORKFLOW.md index 53f8912c..ea1cfe68 100644 --- a/metainfer/tasks/dcu_kernel_auto_opt/WORKFLOW.md +++ b/metainfer/tasks/dcu_kernel_auto_opt/WORKFLOW.md @@ -50,7 +50,7 @@ metainfer/tasks/dcu_kernel_auto_opt/ ### 2.1 新建任务(表单字段,见 form.yaml) 关键字段:`operator`(Quantized GEMM)、`kernel_language`(HIP C++)、`target_hardware`(K500SM_AI/gfx928)、 -`dtype`(INT8 W8A8)、`agent_framework`(ccb / dsh)、`agent_model`(ccb: Opus/Sonnet;dsh: deepseek-v4-flash)、`execution_mode`(Mock / Real INT8 W8A8 GEMM / +`dtype`(INT8 W8A8)、`agent_framework`(ccb / dsh)、`agent_model`(ccb: Opus/Sonnet;dsh: deepseek-flash-4.1 / deepseek-v4-flash)、`execution_mode`(Mock / Real INT8 W8A8 GEMM / Generate & optimize / Infra smoke)、`target_repo_path`、`shape_assignment_mode`(AI automatic / Manual by GPU)、 `shape_scope`(All API shapes / Selected shapes only)、`shape_config`、`max_iterations`、`minimum_improvement_percent`、`extra_notes`。 @@ -177,11 +177,25 @@ TP8 另有 wq_b/wo_b (1024,4096)、gate_up (4096,512)、down (256,4096)、indexe 注意 TP1 目录前端可选、契约仍只收 tp_size 4/8,选 TP1 会同样在 prepare 被拒。 - **模型目录 TP8 大 prefill 边界(2026-08-27)**:Hy3/MiniMax M3/GLM5.2 的 TP8 形状支持 M=4096 优化("Selected shapes only" 手动选择,如 minimax 任务)。契约加 - `MODEL_TP8_EXTRA_OPTIMIZATION_M_VALUES=(4096,)`(**不进** `DEFAULT_OPTIMIZATION_SHAPES`, + `MODEL_TP8_EXTRA_OPTIMIZATION_M_VALUES`(**不进** `DEFAULT_OPTIMIZATION_SHAPES`, 默认 42 不变,串行验证 fallback 不受影响);前端三个模型的 TP8 topology 加 - `mValues: [2,16,3072,4096]`;基线表 15 条 `(8,4096,…)` 已实测 + `mValues`(用 `W8A8_MODEL_TP8_M_VALUES` 常量);基线表 15 条 `(8,4096,…)` 已实测 (`tools/baseline/bench_triton_tp8_m4096.py`,int8_utils.matmul_kernel + CUDA-graph replay, 结果存 `tools/baseline/tp8_m4096_graph.json`)。DeepSeek TP8 不加 M=4096。 +- **模型目录 TP8 短 decode 边界(2026-09-20)**:Hy3/MiniMax M3/GLM5.2 的 TP8 形状再加 + M=8,即 `MODEL_TP8_EXTRA_OPTIMIZATION_M_VALUES=(8, 4096)`;前端 + `W8A8_MODEL_TP8_M_VALUES = [2, 8, 16, 3072, 4096]`(DeepSeek TP1/TP8 仍是 + `W8A8_M_VALUES=[2,16,3072]`,TP4 仍是 `W8A8_TP4_M_VALUES=[2,16,3072,4096]`)。 + 基线表 15 条 `(8,8,…)` 在 worker29/gfx928 实测三次 + (`tools/baseline/bench_triton_tp8_m8.py --m 8 --mode graph`,协议同 2026-08-12 目录: + CUDA-graph replay、hot cache、warmups=10/samples=20/launches=5、BM16/BN32/BK256), + 公布值取「每次 pass median 再取 median」;原始数据 + `tools/baseline/tp8_m8_graph_run{1,2,3}.json`,汇总 `tp8_m8_graph_merged.json`。 + 12/15 形状三次 pass 极差 ≤1.6%(共享卡上 pass 2 的 minimax shared_down_proj 被同租户 + 干扰到 33.76 µs,取 median 后为 19.98 µs)。DeepSeek TP8 不加 M=8。 +- **面板/契约/基线三层一致性测试**:`tests/test_shape_catalog_panel.py` 用 node 真实求值 + `static/dkao-shape-input.js` 的 `MODEL_WORKLOADS`,断言面板给出的每个 TP4/TP8 shape + 都能过契约校验且能查到基线(否则任务要到 baseline 阶段才报 ValueError)。 - `MIN_M=1, MAX_M=4096`;`WORKSPACE_BUDGET_BYTES=16MB`。 - **M=4096 时大部分 (N,K) 的 split-K workspace 容量为 0** → 大 M kernel 必须走 2D M-tile 路径, 不能依赖 split-K workspace。 @@ -257,7 +271,8 @@ TP8 另有 wq_b/wo_b (1024,4096)、gate_up (4096,512)、down (256,4096)、indexe **规范 skill 以 `metainfer/tasks/dcu_kernel_auto_opt/skills/` 内的副本为种子**(int8-w8a8 家族, 2026-08-27 起随插件一起维护):新机器上 `sync_skill_libraries()`(或 WebUI 同步按钮)会自动 把缺失的 skill 补种进 `~/.dsh/skills/`,再镜像到 `~/.claude/skills/`;已有的库 skill 不被覆盖。 - baseline 测量工具在 `tools/baseline/`(`bench_triton_tp4/tp8_m4096.py` + `int8_utils.py`)。 + baseline 测量工具在 `tools/baseline/`(`bench_triton_tp4_m4096.py`、 + `bench_triton_tp8_m4096.py`、`bench_triton_tp8_m8.py` + `int8_utils.py`)。 kernel-repos 默认在 MetaInfer 同级(`METAINFER_KERNEL_REPOS` 可改),不随插件目录走。 5. 改动任何行为后:更新本文件相关段落 + 跑 tests + 用真实任务验证(优先在 zth_meta 里)。 diff --git a/metainfer/tasks/dcu_kernel_auto_opt/api/int8w8a8gemm/int8_w8a8_gemm_api.py b/metainfer/tasks/dcu_kernel_auto_opt/api/int8w8a8gemm/int8_w8a8_gemm_api.py index 2951f48c..5f52266f 100644 --- a/metainfer/tasks/dcu_kernel_auto_opt/api/int8w8a8gemm/int8_w8a8_gemm_api.py +++ b/metainfer/tasks/dcu_kernel_auto_opt/api/int8w8a8gemm/int8_w8a8_gemm_api.py @@ -193,14 +193,19 @@ def _merge_kn_tables( # the original three M values, so each TP4 operator gets exactly one M=4096 # shape while TP8 defaults are unchanged. TP4_EXTRA_OPTIMIZATION_M_VALUES: Final[tuple[int, ...]] = (4096,) -# Model-catalog TP8 large-prefill boundary added on 2026-08-27. The DeepSeek -# TP8 default workload keeps the original three M values; Hy3 / MiniMax M3 / -# GLM5.2 TP8 operators are additionally optimizable at M=4096 via -# "Selected shapes only" (their (K,N) pairs are already in the allowed sets -# and their Triton CUDA-graph baselines are measured). This constant is not -# applied by _default_optimization_shapes(), so DEFAULT_OPTIMIZATION_SHAPES +# Model-catalog TP8 extra M values. The DeepSeek TP8 default workload keeps +# the original three M values; Hy3 / MiniMax M3 / GLM5.2 TP8 operators are +# additionally optimizable at these M values via "Selected shapes only" +# (their (K,N) pairs are already in the allowed sets and their Triton +# CUDA-graph baselines are measured). This constant is not applied by +# _default_optimization_shapes(), so DEFAULT_OPTIMIZATION_SHAPES # (DeepSeek-only) and its serial-validation fallback scope are unchanged. -MODEL_TP8_EXTRA_OPTIMIZATION_M_VALUES: Final[tuple[int, ...]] = (4096,) +# M=4096: model-catalog large-prefill boundary added on 2026-08-27. +# M=8: model-catalog short decode boundary added on 2026-09-20; the +# Triton Graph baselines for the 15 catalog (K, N) pairs were +# measured on worker29/gfx928 with tools/baseline/ +# bench_triton_tp8_m8.py (raw passes in tp8_m8_graph_run{1,2,3}.json). +MODEL_TP8_EXTRA_OPTIMIZATION_M_VALUES: Final[tuple[int, ...]] = (8, 4096) def _default_optimization_shapes() -> tuple[dict[str, int | str], ...]: diff --git a/metainfer/tasks/dcu_kernel_auto_opt/assets/w8a8_bench.py b/metainfer/tasks/dcu_kernel_auto_opt/assets/w8a8_bench.py index 6e4b54a5..af7af3ab 100644 --- a/metainfer/tasks/dcu_kernel_auto_opt/assets/w8a8_bench.py +++ b/metainfer/tasks/dcu_kernel_auto_opt/assets/w8a8_bench.py @@ -8,8 +8,11 @@ import json import math import os +import re import statistics +import subprocess import sys +import time from pathlib import Path try: @@ -18,6 +21,159 @@ torch = None # type: ignore[assignment] +#: Device admission rule for anything that measures: VRAM <= 90% and HCU == 0. +#: This harness is the only thing that runs the candidate kernel, and an agent +#: can invoke it directly from its own shell, so the rule is enforced here too — +#: not only in the orchestrator that happens to launch us. A number taken while +#: another workload shares the card is noise (the same unchanged shape has been +#: seen at 884us -> 3047us), so we refuse to produce one. +GATE_EXIT_CODE = 75 +_GATE_DRM_ROOT = Path("/sys/class/drm") +_GATE_SMI_ROW = re.compile( + r"^(?P\d+)\s+[\d.]+C\s+[\d.]+W\s+\S+\s+[\d.]+W\s+" + r"(?P[\d.]+|N/A|n/a)%\s+(?P[\d.]+|N/A|n/a)%" +) + + +def _gate_number(value): + try: + return float(value) + except (TypeError, ValueError): + return None + + +def _gate_hy_smi() -> dict: + """``{gpu: {vram_percent, util_percent}}`` from hy-smi; empty if unreadable.""" + for binary in ("hy-smi", "rocm-smi"): + try: + proc = subprocess.run([binary], capture_output=True, text=True, + timeout=10) + except (OSError, subprocess.SubprocessError): + continue + if proc.returncode != 0 or not proc.stdout.strip(): + continue + out = {} + for line in proc.stdout.splitlines(): + match = _GATE_SMI_ROW.match(line.strip()) + if not match: + continue + out[int(match.group("index"))] = { + "vram_percent": _gate_number(match.group("vram").strip("%")), + "util_percent": _gate_number(match.group("util").strip("%")), + } + if out: + return out + return {} + + +def _gate_sysfs() -> dict: + """Driver sysfs fallback: works when the management interface says N/A.""" + out = {} + cards = [] + try: + entries = sorted(_GATE_DRM_ROOT.iterdir(), key=lambda p: p.name) + except OSError: + return out + for entry in entries: + if not re.fullmatch(r"card\d+", entry.name): + continue + device = entry / "device" + try: + uevent = (device / "uevent").read_text(encoding="utf-8") + except OSError: + continue + if not any(driver in uevent for driver in ("hycu", "amdgpu", "hydcu")): + continue + if not (device / "mem_info_vram_total").is_file(): + continue + cards.append(device) + for index, device in enumerate(cards): + row = {} + try: + row["util_percent"] = float( + (device / "gpu_busy_percent").read_text().strip()) + except (OSError, ValueError): + pass + try: + used = float((device / "mem_info_vram_used").read_text().strip()) + total = float((device / "mem_info_vram_total").read_text().strip()) + if total > 0: + row["vram_percent"] = round(100.0 * used / total, 2) + except (OSError, ValueError, ZeroDivisionError): + pass + if row: + out[index] = row + return out + + +def _gate_violation(device: int) -> str: + """``""`` when the device passes, else why it does not.""" + from_smi = _gate_hy_smi().get(device) or {} + complete = (from_smi.get("vram_percent") is not None + and from_smi.get("util_percent") is not None) + readings = from_smi if complete else dict(_gate_sysfs().get(device) or {}) + if not complete: + for key in ("vram_percent", "util_percent"): + if readings.get(key) is None and from_smi.get(key) is not None: + readings[key] = from_smi[key] + vram = readings.get("vram_percent") + util = readings.get("util_percent") + if vram is None or util is None: + return "device state unavailable (no HCU/VRAM reading)" + if vram > 90.0: + return f"VRAM {vram:.0f}% > 90%" + if util > 0.0: + return f"HCU {util:.0f}% > 0%" + return "" + + +def enforce_measurement_gate(device: int | None = None) -> None: + """Wait for ``VRAM <= 90% and HCU == 0``; exit if it never happens. + + ``METAINFER_GPU_PREFLIGHT=0`` is the deliberate opt-out. Waiting is + deliberately bounded (``METAINFER_GATE_WAIT_SECONDS`` x + ``METAINFER_GATE_MAX_WAITS``, default 30 min x 48 = 24 h): a shared card + must never silently turn into a measurement, and waiting forever is not an + answer either. + """ + if str(os.environ.get("METAINFER_GPU_PREFLIGHT", "")).strip().lower() in { + "0", "false", "no", "off"}: + return + if device is None: + raw = str(os.environ.get("HIP_VISIBLE_DEVICES") + or os.environ.get("ROCR_VISIBLE_DEVICES") or "0") + device = int(raw.split(",")[0].strip() or 0) + try: + wait_s = int(os.environ.get("METAINFER_GATE_WAIT_SECONDS", "") or 1800) + except ValueError: + wait_s = 1800 + try: + max_waits = int(os.environ.get("METAINFER_GATE_MAX_WAITS", "") or 48) + except ValueError: + max_waits = 48 + for attempt in range(1, max_waits + 1): + reason = _gate_violation(int(device)) + if not reason: + return + if attempt > max_waits: + break + print(json.dumps({ + "measurement_gate": "blocked", "device": int(device), + "attempt": attempt, "max_waits": max_waits, + "wait_seconds": wait_s, "reason": reason, + }), file=sys.stderr) + if attempt == max_waits: + break + time.sleep(max(0.0, float(wait_s))) + reason = _gate_violation(int(device)) or "device never became idle" + print(json.dumps({ + "measurement_gate": "give_up", "device": int(device), + "checks": max_waits, "reason": reason, + }), file=sys.stderr) + raise SystemExit(GATE_EXIT_CODE) + + + def load_module(module_path: Path, name: str): spec = importlib.util.spec_from_file_location( name, module_path @@ -331,6 +487,11 @@ def _env_default(name: str, fallback: int) -> int: }, sort_keys=True)) return 0 + # Everything below initialises the device and measures on it. CPU-only runs + # (--self-test, --prepare-reference) returned above, so they are never + # blocked; every device-touching run has to pass the gate first. + enforce_measurement_gate() + source = args.source.resolve() fixed_contract = source / "int8_w8a8_gemm_api.py" if fixed_contract.is_file(): diff --git a/metainfer/tasks/dcu_kernel_auto_opt/bridge/dsh/README.md b/metainfer/tasks/dcu_kernel_auto_opt/bridge/dsh/README.md index 22aace2d..2444e929 100644 --- a/metainfer/tasks/dcu_kernel_auto_opt/bridge/dsh/README.md +++ b/metainfer/tasks/dcu_kernel_auto_opt/bridge/dsh/README.md @@ -32,8 +32,9 @@ prompt 从 stdin 传入,stdout 输出逐行 stream-json 事件 WebUI new-task 表单中 dcu-kernel-auto-opt 增加 **Agent framework** 选择: - `ccb` → Claude Code(模型 `Sonnet` / `Opus`) -- `dsh` → DeepSeek Harness(模型 `deepseek-v4-flash`,即 - `deepseek/deepseek-v4-flash-0731`) +- `dsh` → DeepSeek Harness: + - `deepseek-flash-4.1` → `deepseek/deepseek-flash`(**默认**,4.1 Flash) + - `deepseek-v4-flash` → `deepseek/deepseek-v4-flash-0731`(旧 pinned 构建,可复现历史运行) orchestrator CLI 在 `agent_framework=dsh` 时自动把 `claude_bin` 指向本目录的 `dsh_agent.py`(`--claude-bin` 显式传入时优先),无需改动 MetaInfer 共享代码。 @@ -55,7 +56,8 @@ runtime 只内置 `deepseek-official` adapter(`llm-deepseek`),它通过 - provider:`deepseek-official`(runtime adapter;TokenHub 经 base_url 覆盖) - baseURL:`https://tokenhub.tencentmaas.com/plan/v3` -- 模型:`deepseek/deepseek-v4-flash-0731`(默认) +- 模型:`deepseek/deepseek-flash`(默认,= 表单里的 `deepseek-flash-4.1`); + 旧 pinned 构建 `deepseek/deepseek-v4-flash-0731`(= `deepseek-v4-flash`) - API key:`~/.dsh/.credentials.yaml` 的 `TENCENT_API_KEY` `dsh_agent.py` 的环境变量: @@ -63,7 +65,7 @@ runtime 只内置 `deepseek-official` adapter(`llm-deepseek`),它通过 | 变量 | 默认 | 说明 | |---|---|---| | `DSH_AGENT_PROVIDER` | `deepseek-official` | DSH runtime provider(dev runtime 仅内置此 adapter) | -| `DSH_AGENT_MODEL` | `deepseek/deepseek-v4-flash-0731` | 模型覆盖 | +| `DSH_AGENT_MODEL` | `deepseek/deepseek-flash` | 默认模型覆盖(只改 `deepseek-flash-4.1` 这个默认标签的 id) | | `DSH_AGENT_BASE_URL` | `https://tokenhub.tencentmaas.com/plan/v3` | 模型端点(`DEEPSEEK_BASE_URL` 优先) | | `TENCENT_API_KEY` / `DEEPSEEK_API_KEY` | 凭据文件兜底 | API key | | `DSH_AGENT_CORDIS` | 本目录 `cordis.yml` | 自定义组合路径 | diff --git a/metainfer/tasks/dcu_kernel_auto_opt/bridge/dsh/dsh_agent.py b/metainfer/tasks/dcu_kernel_auto_opt/bridge/dsh/dsh_agent.py index 0bfbd0fc..e4aaa77b 100755 --- a/metainfer/tasks/dcu_kernel_auto_opt/bridge/dsh/dsh_agent.py +++ b/metainfer/tasks/dcu_kernel_auto_opt/bridge/dsh/dsh_agent.py @@ -27,7 +27,8 @@ DSH_AGENT_PROVIDER provider name for the DSH runtime (default: deepseek-official — the only adapter shipped by the dev-checkout runtime) - DSH_AGENT_MODEL model override (default deepseek/deepseek-v4-flash-0731) + DSH_AGENT_MODEL model override (default deepseek/deepseek-flash, + the 4.1 Flash label deepseek-flash-4.1) DSH_AGENT_BASE_URL model endpoint; falls back to DEEPSEEK_BASE_URL (default: https://tokenhub.tencentmaas.com/plan/v3) TENCENT_API_KEY preferred API key (matches ~/.dsh/settings.yaml) @@ -71,9 +72,11 @@ def default_provider() -> str: def default_model() -> str: + """Default DSH model id (the 4.1 Flash build) unless DSH_AGENT_MODEL pins + another one. Matches orchestrator/config.py DSH_DEFAULT_MODEL_ID.""" return ( - os.environ.get("DSH_AGENT_MODEL", "deepseek/deepseek-v4-flash-0731") - .strip() or "deepseek/deepseek-v4-flash-0731" + os.environ.get("DSH_AGENT_MODEL", "deepseek/deepseek-flash") + .strip() or "deepseek/deepseek-flash" ) @@ -144,13 +147,20 @@ def default_api_key() -> str: # --------------------------------------------------------------------------- # -# Model mapping (legacy Claude labels -> this host's DSH model ids) +# Model mapping (legacy Claude labels / form labels -> this host's DSH ids) # --------------------------------------------------------------------------- # +#: Form labels produced by both MetaInfer task frontends (dcu-kernel-auto-opt's +#: agent_model field and harness-evolve's evolve_model field). Kept in sync with +#: orchestrator/config.py (DSH_MODEL_IDS) — the orchestrator resolves the label +#: itself, so this map only serves callers that hand the wrapper a bare label. _MODEL_MAP = { - "opus": "deepseek-v4-pro", - "sonnet": "deepseek-v4-flash", - "haiku": "deepseek-v4-flash", + "deepseek-flash-4.1": "deepseek/deepseek-flash", + "deepseek-flash": "deepseek/deepseek-flash", + "deepseek-v4-flash": "deepseek/deepseek-v4-flash-0731", + "opus": "deepseek/deepseek-v4-flash-0731", + "sonnet": "deepseek/deepseek-v4-flash-0731", + "haiku": "deepseek/deepseek-v4-flash-0731", } @@ -160,11 +170,8 @@ def map_model(requested: Optional[str]) -> str: key = requested.strip().lower() if key in _MODEL_MAP: return _MODEL_MAP[key] - if key == "deepseek-v4-flash": - # Bare user-facing label -> the pinned host model id. - return default_model() if key.startswith("deepseek"): - # Full model id (e.g. deepseek/deepseek-v4-flash-0731): pass through. + # Full model id (e.g. deepseek/deepseek-flash): pass through. return key # Unknown label: keep the caller's intent but stay on a known model id. return default_model() @@ -294,6 +301,13 @@ def _headless_backend_main(args: Any, prompt: str) -> int: sub-agent is still working so SubAgentManager's no-output watchdog never mistakes a long task for a stuck process; the final assistant text is emitted as the last message, matching the ccb stream-json contract. + + Model selection caveat: the ``dsh --profile headless`` app takes no + ``--model`` flag, so it runs whatever the user's DSH settings document + (``~/.dsh/settings.yaml`` -> ``agent-default-model``) selects — the host + default, not ``--model``. Emit the requested model in the system event so + the mismatch is visible instead of silent. The SDK backend below does honour + ``--model``. """ import subprocess import tempfile diff --git a/metainfer/tasks/dcu_kernel_auto_opt/bridge/dsh/run_dsh_task.sh b/metainfer/tasks/dcu_kernel_auto_opt/bridge/dsh/run_dsh_task.sh index 49196165..ff344660 100755 --- a/metainfer/tasks/dcu_kernel_auto_opt/bridge/dsh/run_dsh_task.sh +++ b/metainfer/tasks/dcu_kernel_auto_opt/bridge/dsh/run_dsh_task.sh @@ -12,7 +12,8 @@ # # Optional env: # DSH_AGENT_MAX_TOKENS per-request output cap (default 65536) -# DSH_AGENT_MODEL model override (default deepseek/deepseek-v4-flash-0731) +# DSH_AGENT_MODEL model override (default deepseek/deepseek-flash, +# the 4.1 Flash build) # METAINFER_GPU_IDS restrict GPUs, e.g. 4,5,6,7 set -euo pipefail diff --git a/metainfer/tasks/dcu_kernel_auto_opt/bridge/dsh/tests/smoke_sdk.py b/metainfer/tasks/dcu_kernel_auto_opt/bridge/dsh/tests/smoke_sdk.py index 94f29330..39868d26 100644 --- a/metainfer/tasks/dcu_kernel_auto_opt/bridge/dsh/tests/smoke_sdk.py +++ b/metainfer/tasks/dcu_kernel_auto_opt/bridge/dsh/tests/smoke_sdk.py @@ -30,7 +30,7 @@ def main() -> int: WORK.mkdir(parents=True, exist_ok=True) cfg = DeepSeekHarnessConfig( provider="deepseek-official", - model="deepseek-v4-flash", + model="deepseek/deepseek-flash", max_tokens=2048, cwd=str(WORK), session_root=str(SESSION_ROOT), diff --git a/metainfer/tasks/dcu_kernel_auto_opt/bridge/dsh/tests/test_dsh_agent.py b/metainfer/tasks/dcu_kernel_auto_opt/bridge/dsh/tests/test_dsh_agent.py index fb0e2302..bd2d9b53 100644 --- a/metainfer/tasks/dcu_kernel_auto_opt/bridge/dsh/tests/test_dsh_agent.py +++ b/metainfer/tasks/dcu_kernel_auto_opt/bridge/dsh/tests/test_dsh_agent.py @@ -5,7 +5,8 @@ replaced by a fake whose behavior is scripted per test. Verify: 1. arg parsing tolerates every SubAgentManager flag (incl. unknown ones) - 2. model mapping: sonnet/haiku -> deepseek-v4-flash, opus -> deepseek-v4-pro + 2. model mapping: form labels deepseek-flash-4.1 / deepseek-v4-flash and the + legacy Claude labels resolve to real DSH model ids 3. the emitted stream contains system -> assistant* -> result, exit 0 4. a resume failure (finish_reason == "error") falls back to a fresh session and the stream's first session_id is the fallback one @@ -166,10 +167,40 @@ def test_model_mapping(self): mod = install_fake_sdk([ ("s1", "ok", "completed"), ]) - # opus -> deepseek-v4-pro + # Legacy Claude labels stay routable (mapped onto the pinned V4 build). run_wrapper(mod, [a if a != "sonnet" else "opus" for a in BASE_ARGS], "hi") cfg = FakeDeepSeekHarness.instances[0].config - self.assertEqual(cfg.model, "deepseek-v4-pro") + self.assertEqual(cfg.model, "deepseek/deepseek-v4-flash-0731") + + def test_form_labels_map_to_dsh_model_ids(self): + """Form labels from both frontends resolve to real platform ids.""" + cases = { + "deepseek-flash-4.1": "deepseek/deepseek-flash", # new default + "deepseek-v4-flash": "deepseek/deepseek-v4-flash-0731", + # Full ids pass through untouched. + "deepseek/deepseek-flash": "deepseek/deepseek-flash", + } + for label, expected in cases.items(): + mod = install_fake_sdk([("s1", "ok", "completed")]) + args = [a if a != "sonnet" else label for a in BASE_ARGS] + code, _out, err = run_wrapper(mod, args, "hi") + self.assertEqual(code, 0, err) + cfg = FakeDeepSeekHarness.instances[0].config + self.assertEqual(cfg.model, expected, label) + + def test_default_model_is_flash_41(self): + """With no --model the wrapper defaults to the 4.1 Flash build.""" + mod = install_fake_sdk([("s1", "ok", "completed")]) + args = [a for a in BASE_ARGS if a not in ("--model", "sonnet")] + old = os.environ.pop("DSH_AGENT_MODEL", None) + try: + code, _out, err = run_wrapper(mod, args, "hi") + finally: + if old is not None: + os.environ["DSH_AGENT_MODEL"] = old + self.assertEqual(code, 0, err) + cfg = FakeDeepSeekHarness.instances[0].config + self.assertEqual(cfg.model, "deepseek/deepseek-flash") def test_resume_fallback(self): mod = install_fake_sdk([ diff --git a/metainfer/tasks/dcu_kernel_auto_opt/form.yaml b/metainfer/tasks/dcu_kernel_auto_opt/form.yaml index 3f1066a4..ae870e84 100644 --- a/metainfer/tasks/dcu_kernel_auto_opt/form.yaml +++ b/metainfer/tasks/dcu_kernel_auto_opt/form.yaml @@ -66,10 +66,10 @@ - label: "ccb" description: "Claude Code (claude-sonnet-5 / claude-opus-5)" - label: "dsh" - description: "DeepSeek Harness (deepseek-v4-flash)" + description: "DeepSeek Harness (deepseek-flash-4.1 / deepseek-v4-flash)" - key: agent_model - question: "Model used by the selected agent framework (ccb: Sonnet/Opus; dsh: deepseek-v4-flash)." + question: "Model used by the selected agent framework (ccb: Sonnet/Opus; dsh: deepseek-flash-4.1 / deepseek-v4-flash)." header: "Agent model" required: true multi: false @@ -81,8 +81,10 @@ description: "Pinned to Claude Opus 5 (claude-opus-5) for difficult kernel architecture and correctness work" - label: "Sonnet" description: "Pinned to Claude Sonnet 5 (claude-sonnet-5) for faster, lower-cost optimization runs" + - label: "deepseek-flash-4.1" + description: "DeepSeek Harness 4.1 Flash (deepseek/deepseek-flash) — current default DSH model" - label: "deepseek-v4-flash" - description: "DeepSeek Harness model (deepseek-v4-flash)" + description: "DeepSeek Harness V4 Flash, pinned build (deepseek/deepseek-v4-flash-0731)" - key: execution_mode question: "Task execution mode for this run." @@ -224,3 +226,16 @@ header: "Notes" required: false form: textarea + +- key: execution_profile + question: "执行画像:production = 生产任务(走 broker 的最高优先级、排在 HE 之前;仍然必须过 VRAM<=90%/HCU==0 的准入闸门,因为共享卡上的测量是脏数据);default = 现状" + header: "Profile" + required: false + form: select + default: "default" + options: + - label: "default" + description: "与现状一致(调控/占据模式按 gpu_mode 决定)" + - label: "production" + description: "生产模式:优先级 100 插队、优先派发,仍过闸门、不用等 HE 的轮次" + diff --git a/metainfer/tasks/dcu_kernel_auto_opt/harness_default/README.md b/metainfer/tasks/dcu_kernel_auto_opt/harness_default/README.md index a5a0a41b..67372060 100644 --- a/metainfer/tasks/dcu_kernel_auto_opt/harness_default/README.md +++ b/metainfer/tasks/dcu_kernel_auto_opt/harness_default/README.md @@ -1,38 +1,61 @@ # harness_default —— DKAO 可演化 harness 的种子目录 -这是 AHE「组件可观测性」的第一步落地(M1 切片 1,纯新增、不改运行时行为)。 +这是 AHE「组件可观测性」的落地目录:DKAO 的 harness(验收门阈值、轮次指令文案、 +planner 策略、以及后续的 skills/工具/中间件/记忆)**文件化**在这里,成为可版本化、 +可校验、可演化的组件。AHE 外循环(`harness_evolve`)以本目录为种子,拷贝出可写 +workspace(git 仓库),每轮评测 pin 一个快照;Evolve Agent 只改 workspace。 -## 现状与用途 - -DKAO 的 harness(gates 阈值、prompts/菜单、planner、skills、工具集、中间件策略、记忆) -目前散在 Python 代码与静态文件里。AHE 只能演化"文件化、可版本化、可校验"的组件, -因此我们先把它们**播种成文件**,放在本目录: +## 目录内容与接线状态 ``` harness_default/ -├── manifest.yaml # 组件清单(AHE Evolve 唯一可写空间的入口描述) -├── gates.yaml # 验收/plateau/ISA 门的规范值(seed 阶段) -├── planner_catalog.yaml # 优化方案选择机制:方案目录(★ 见 orchestrator/planner.py v0) -├── README.md # 本文件 +├── manifest.yaml # 组件清单 + 每个组件的 wired 开关(AHE 可写空间的入口描述) +├── gates.yaml # 验收/plateau/ISA 门阈值 ← 已接线 gate_policy.gates() +├── planner_policy.yaml # 状态→方案选择规则(P0-P4 分层) ← 已接线 planner.policy() +├── planner_catalog.yaml # 方案目录:稳定 plan id + 面向 worker 的描述 ← 已接线 planner.catalog() +├── systemprompt/ +│ └── round_strategy.yaml # 每轮给 worker 的指令文案(P0 修复/P1 阶段/各 M regime 组合) +│ # ← 已接线 round_strategy.load_round_strategy() +├── README.md # 本文件 └── (后续切片加入) - ├── systemprompt/ # prompts 模板(coordinator/bootstrap/worker/synthesis) - ├── tools/ # cordis 组合与 per-role 工具白名单 - ├── middleware/ # resume/compaction/重试策略参数 - ├── skills/ # SKILL.md 种子 - └── memory/ # LongTermMEMORY(measured 事实) + ├── systemprompt/ # 其余 prompt 模板(coordinator/bootstrap/worker/synthesis) + ├── tools/ # cordis 组合与 per-role 工具白名单 + ├── middleware/ # resume/compaction/重试策略参数 + ├── skills/ # SKILL.md 种子 + └── memory/ # LongTermMEMORY(measured 事实) ``` -## seed 阶段语义(重要) +## 语义(重要) + +**`wired` 是总开关。** `manifest.yaml` 里每个组件标 `wired: true` 时,运行时读该文件; +标 `wired: false`(或删掉该组件项)时,运行时回落到 Python 内置默认值 —— 也就是说 +**编辑一个未接线的组件不会改变任何行为**,而 `harness_evolve` 的 +`_wired_scope_violations()` 会把这种"改了但不生效"的改动报出来。 + +**回落是逐键的。** 文件缺失、YAML 损坏、某个键写错类型或写成空串,都只让**那一个键** +回落到内置文本,其余照常生效;`RoundStrategy.notes` / 加载器会记录原因,但**绝不抛异常** +(一个手改坏的 harness 是性能问题,不该让一轮评测崩掉)。 + +**决策逻辑不在文件里。** 以 `round_strategy.yaml` 为例:走哪个分支由实测状态(history / +ISA 阶段 / PMC 证据 / M regime / 轮次预算)在 `orchestrator/round_strategy.py` + +`prompts.py` 里判定,文件只承载每个分支的**文案**。这样"这轮让 worker 试什么"可演化, +而选择状态机本身不可被改写。 -- **wired: false**:运行时管线仍读 Python 常量(config.py:105、w8a8_pipeline.py:78-80 等), - 本目录文件**尚未被加载器接线**——因此默认行为零变化。 -- 一致性由测试守卫:`tests/test_harness_io.py` 断言 gates.yaml 的值与 Python 常量相等, - 防止两份来源漂移;接线(renderer/loader)在后续切片落地,落地时删掉该守卫并让 - 运行时以本目录为唯一来源。 -- AHE 外循环(harness_evolve)将以本目录为种子,拷贝出可写 workspace(git 仓库), - 每轮评测 pin 一个 workspace 快照;Evolve Agent 只改 workspace。 +**一致性由测试守卫**(drift guard,见 manifest 每个组件的 `drift_guard`): +每个 YAML 种子的取值都被断言等于 Python 内置默认值,因此两条来源不会无声漂移。 +`tests/test_round_strategy.py` 另外用 5400 个渲染用例的 sha256 指纹把文案钉死 —— 任何 +意外改词都会让该测试失败。**有意改文案时**:同步改 `BUILTIN_STRATEGY` 与种子文件, +并有意重算指纹。 ## 读取方式 -见 `orchestrator/harness_io.py`(harness_root 解析、load_manifest/load_gates、seed_workspace)。 -可用环境变量 `METAINFER_HARNESS_ROOT` 覆盖根目录(默认本目录)。 +- 根目录解析:`orchestrator/harness_io.harness_root()` —— 优先环境变量 + `METAINFER_HARNESS_ROOT`(AHE 用它指向本轮候选快照),否则回落本目录。 +- 组件读取:`gate_policy.gates()` / `planner.policy()` / `planner.catalog()` / + `round_strategy.load_round_strategy()`;通用 YAML 读取用 + `harness_io.load_component_yaml()`(返回 `(data, error)`,不抛异常)。 +- 播种:`harness_io.seed_workspace(dst)` 或 CLI 路径上的任务建仓逻辑会把整棵树拷进 + kernel repo 的 `harness_snapshot/`,并把 `source` / `revision` / 逐文件 digest 写进 + `scaffold_manifest.json`,用于"哪一轮用了哪版 harness"的追溯。 +- 缓存:各加载器按 `(路径, 文件 mtime, manifest mtime)` 缓存,所以运行中途换 harness + 会被下一轮读到(测试亦用 `reset_cache()` 隔离)。 diff --git a/metainfer/tasks/dcu_kernel_auto_opt/harness_default/manifest.yaml b/metainfer/tasks/dcu_kernel_auto_opt/harness_default/manifest.yaml index 3512f59a..955405d9 100644 --- a/metainfer/tasks/dcu_kernel_auto_opt/harness_default/manifest.yaml +++ b/metainfer/tasks/dcu_kernel_auto_opt/harness_default/manifest.yaml @@ -28,9 +28,19 @@ components: role: evolvable state-to-plan policy (repair / phase / bottleneck / coverage / fallback) wired: true drift_guard: tests/test_planner_policy.py + systemprompt: + path: systemprompt/round_strategy.yaml + kind: data + role: >- + per-round mandate wording handed to the worker (round_strategy.yaml is + the only file today); the branch decision stays in + orchestrator/round_strategy.py, only the text is data + wired: true + drift_guard: tests/test_round_strategy.py layout_note: >- - Future components land here as files: systemprompt/, planner/ (optimization - plan selection mechanism), tools/, middleware/, skills/, memory/. Together - they form the evolvable harness workspace that the harness_evolve outer loop - versions (git) and evolves per the evaluation protocol v2 - (docs/dkao_harness_eval_protocol.md). + Future components land here as files: systemprompt/ (worker-facing + instruction wording; systemprompt/round_strategy.yaml is seeded), + planner/ (optimization plan selection mechanism), tools/, middleware/, + skills/, memory/. Together they form the evolvable harness workspace that + the harness_evolve outer loop versions (git) and evolves per the evaluation + protocol v2 (docs/dkao_harness_eval_protocol.md). diff --git a/metainfer/tasks/dcu_kernel_auto_opt/harness_default/planner_catalog.yaml b/metainfer/tasks/dcu_kernel_auto_opt/harness_default/planner_catalog.yaml index 7eb8302c..2a8868ee 100644 --- a/metainfer/tasks/dcu_kernel_auto_opt/harness_default/planner_catalog.yaml +++ b/metainfer/tasks/dcu_kernel_auto_opt/harness_default/planner_catalog.yaml @@ -3,8 +3,10 @@ # DKAO 优化方案选择机制的「方案目录」:把"每一轮可以试什么方案"枚举成稳定 id, # 供 orchestrator/planner.py(状态条件化选择器 v0)与后续 harness 演化使用。 # -# wired: false —— 运行时仍由 prompts.py::w8a8_round_strategy 按轮数菜单出文字, -# 本目录与 planner.py 当前是纯新增的"选择机制 v0",用于沉淀/测试/演进,尚未接线。 +# wired: true(以 manifest.yaml 为准)—— orchestrator/planner.py::catalog() 会读本文件, +# 但其选择结果只在 METAINFER_PLANNER=1 时决定轮次指令;planner 关闭时轮次菜单仍由 +# prompts.py::w8a8_round_strategy 出文字(该文案已外部化为 +# systemprompt/round_strategy.yaml,见 manifest 的 systemprompt 组件)。 # 规则分层见 orchestrator/planner.py:P0 修复 -> P1 预算/阶段 -> P2 瓶颈签名 # -> P3 覆盖/防循环 -> P4 兜底(legacy 菜单近似)。 diff --git a/metainfer/tasks/dcu_kernel_auto_opt/harness_default/systemprompt/round_strategy.yaml b/metainfer/tasks/dcu_kernel_auto_opt/harness_default/systemprompt/round_strategy.yaml new file mode 100644 index 00000000..4fa491e9 --- /dev/null +++ b/metainfer/tasks/dcu_kernel_auto_opt/harness_default/systemprompt/round_strategy.yaml @@ -0,0 +1,204 @@ +schema_version: 1 +component: systemprompt +status: seed +wired: true +role: >- + Per-round mandate wording handed to the optimization worker ("what to try + this round"). The decision logic stays in + orchestrator/round_strategy.py + prompts.py: measured history, ISA phase, PMC + evidence and the M regime select the branch. This file only carries the text + of each branch, so the Evolve loop can version and evolve the instructions + without making the round-selection state machine writable. +loader: orchestrator/round_strategy.load_round_strategy +read_at_runtime: >- + orchestrator/prompts.py::w8a8_round_strategy, reached from + orchestrator/w8a8_pipeline.py (planner disabled, and the planner's + legacy_menu deferral). DKAO reads it through METAINFER_HARNESS_ROOT. +format_fields: >- + Templates may use these str.format fields only: {iteration}, {speedup}, + {artifact_dir}, {completed}, {split_candidates}, {grid_blocks}, {cu_count}. + An unknown field makes that one suffix fall back to the built-in text + instead of raising, so a malformed harness degrades to a weaker instruction + rather than breaking a round. +drift_guard: tests/test_round_strategy.py::test_seed_matches_builtin_defaults + +# P0 repairs: a broken previous attempt outranks any exploration plan. +repair: + faster_wrong: >- + Highest priority: repair the faster but incorrect candidate from iteration + {iteration} (measured speedup {speedup}x). Read its archived source at + `{artifact_dir}` and preserve the fast mapping. Fix only the smallest + correctness defect: signed int8 unpacking, tail bounds, scale indexing, + bf16 conversion, or a race. Do not replace it with an unrelated + architecture. + infrastructure_failure: >- + The preceding attempt failed in the agent infrastructure. Return to the + accepted best source and start a new bounded experiment; do not repair or + replay a partially written candidate. This failure does not count as a + completed optimization round. + build_failure: >- + Repair the immediately preceding candidate from iteration {iteration} at + `{artifact_dir}`. Keep its strategy and make only the minimum + compile/API/syntax correction; do not start another redesign this round. + +# P1 phase gates: the ISA-gated rounds, chosen by the control plane. +phases: + isa_guided_hip: >- + ISA-guided HIP round. This is successful ISA experiment {completed} of at + least 2. Select one measured memory or compute bottleneck, compare the + exact primary-kernel ISA with the preceding code object, and make one + HIP/DUMMA/intrinsic code-shaping change. Raw inline asm remains forbidden. + Record a compiler limitation only when the before/after binary proves it + and name the exact target instructions. + conditional_inline_asm: >- + Conditional inline-asm experiment. Target only the compiler limitation and + exact instructions verified by the immediately preceding ISA-guided HIP + round. Keep the asm block minimal, preserve complete constraints/clobbers, + and reject it unless the candidate ISA, exact correctness, median, P90, and + resources all validate. Do not write raw global/buffer/flat loads or MMAC. + +# Evidence-qualified suffixes appended to a portfolio mandate. +append: + split_candidates: >- + Trusted occupancy-probe split candidates for the measured CU count are + {split_candidates}. They include non-power-of-two values where useful, are + not a whitelist, and must fit the workspace and stage-alignment + constraints. Explore outside this set when evidence supports it. + grid_warning: >- + Trusted control-plane warning: current grid has {grid_blocks} blocks for + {cu_count} CUs, below the two-blocks-per-CU latency-hiding target. Before + micro-optimization, benchmark a finer one-wave zero-barrier grid or + multiple legal split-K candidates including combine cost. + +# Portfolios: the mandate for each round index, per M regime. +# small = M < 16, m16 = 16 <= M < 128, large = M >= 128. +# late_start = max(9, max_iterations - 1): rounds before it use the numbered +# slot below; from it on, slot 9 is used while iteration < max_iterations and +# slot 10 on the final round. The small regime stops at slot 8 and keeps that +# consolidation mandate to the end. +portfolios: + small: + "1": Vectorize contiguous K loads with exact signed-int8 semantics. + "2": >- + Increase instruction-level parallelism with independent int32 + accumulators or adjacent N outputs; avoid per-K-tile LDS barriers. + "3": >- + Stage all of tiny A once with at most one barrier, or tune unroll one + step if whole-A staging is not cheaper. + "4": >- + Change one launch variable only: waves per block, N columns per wave, or + unroll factor. + "5": >- + HIP-only memory round: change one vector-load width, contiguous N + mapping, or whole-A reuse decision. Raw inline asm is forbidden. + "6": >- + HIP-only pipeline round: reduce one dependency chain or barrier using + ordinary HIP/intrinsics. Raw inline asm is forbidden. + "7": >- + HIP-only resource round: tune one block size, unroll factor, or live + range while preserving coalescing. Raw inline asm is forbidden. + "8": >- + HIP-only consolidation round: revisit the fastest correct archived + mapping and make one final architecture/codegen improvement. Raw inline + asm is forbidden. + m16: + "1": >- + Establish a minimal 16x16x32 DUMMA tile with the exact API below, one + wave per output tile and explicit int32 accumulation. If the seed already + has a correct DUMMA kernel, preserve it and instead test the smallest + one-wave-per-block, one-N-tile geometry with no cross-wave barrier; do + not spend the round reimplementing it. + "2": >- + Architecture round: measure grid parallelism before polishing. Explore + one complete launch geometry among 1/2/4 waves per block and 1/2/4 + adjacent N tiles. Prefer enough independent blocks to cover at least all + device CUs; report grid_blocks, waves_per_block and estimated_active_cus + in proposal.json. + "3": >- + Architecture round: if the unsplit grid has fewer than two blocks per + device CU and K >= 1024, implement and measure split-K=2 plus at least + one CU-aligned candidate (which may be non-power-of-two), or test a + one-wave zero-barrier geometry that reaches the same parallelism. Write + int32 partials into the caller workspace and include the combine+scale + kernel in the timed Graph. + "4": >- + Architecture/pipeline round: explore one of multi-N-tile reuse, A-only + staging, or bounded register/LDS prefetch. Retain enough blocks to cover + all CUs, state which A/B bytes are reused, and measure whether the change + improves normal median/P90. + "5": >- + HIP-only packed-weight/staging round: compare one packed layout, A-only + staging, or B-only staging design. Raw inline asm is forbidden. + "6": >- + Pipeline round: choose exactly one staging family from direct, A-only + LDS, B-only LDS, or A+B LDS using L2/VMEM evidence. Use coalesced 8- or + 16-byte cooperative loads and report HBM/LDS byte changes; do not claim + asynchronous overlap without evidence. + "7": >- + Pipeline round: compare single buffering with double buffering only when + K>=1024, L2 hit rate is below 70%, and the doubled LDS budget stays below + 48 KiB. Count barriers per K step. + "8": >- + HIP-only resource round: tune one occupancy limiter using actual PMC + evidence: waves per block, VGPR live range, LDS footprint, or spill + removal. Do not trade repeated HBM reads for occupancy. + "9": >- + Late ISA-diagnosis round. Only if the control-plane plateau gate is open, + use one selected ISA Skill and trusted disassembly to shape compiler + output through HIP/DUMMA/intrinsics. Raw inline asm remains forbidden. + Otherwise continue HIP-only exploration. + "10": >- + Final conditional inline-asm round. Raw asm is allowed only when the + control plane confirms a HIP plateau and the prior ISA-guided round + recorded one concrete compiler limitation plus target instructions. + Otherwise make one HIP-only consolidation change. + large: + "1": >- + Establish a correct DUMMA throughput baseline using a 2-D macro-tile. + Benchmark 64x64, 64x128, and 128x64 block tiles; record waves per block, + VGPRs, LDS bytes, occupancy and TOPS. + "2": >- + Operand-reuse round: compare direct loads with cooperative A+B LDS + staging. Quantify A/B reuse per macro-tile and use vectorized coalesced + global loads with a bank-safe LDS layout. + "3": >- + Pipeline round: compare single and double buffering across K tiles. + Retain double buffering only when ISA/PMC evidence shows reduced VMEM + stalls without harmful LDS or occupancy growth. + "4": >- + Tile-shape round: tune M-tile versus N-tile aspect ratio for this exact + M/N/K, balancing B reuse, A reuse and enough blocks to occupy every CU. + Do not inherit decode launch geometry. + "5": >- + Packing round: test one weight packing/swizzle that makes each DUMMA B + tile vector-loadable and LDS-bank-safe. Include packing outside timing + and validate the graph-stable packed layout. + "6": >- + Epilogue round: fuse per-row and per-column scales, bf16 conversion and + the final coalesced store into the compute kernel; remove any + unnecessary workspace/combine pass. + "7": >- + Compute-pipeline round: tune DUMMA issue grouping, prefetch distance and + accumulator independence using ISA stall evidence. Raw inline asm + remains forbidden. + "8": >- + Resource round: tune waves per block, VGPR live ranges and LDS footprint + from measured occupancy. Recheck the best tile family with normal + median/P90 measurements. + "9": >- + Late ISA-diagnosis round. Only if the control-plane plateau gate is open, + use one selected ISA Skill and trusted disassembly to shape compiler + output through HIP/DUMMA/intrinsics. Raw inline asm remains forbidden. + Otherwise continue HIP-only exploration. + "10": >- + Final conditional inline-asm round. Raw asm is allowed only when the + control plane confirms a HIP plateau and the prior ISA-guided round + recorded one concrete compiler limitation plus target instructions. + Otherwise make one HIP-only consolidation change. +notes: >- + These texts are byte-identical to the Python literals they replaced + (orchestrator/round_strategy.py BUILTIN_STRATEGY), so an un-evolved harness + behaves exactly as before. Editing this file now changes the round + instructions the worker receives, and the Evolve loop can version it. + A key omitted here keeps its built-in text; an unknown regime or slot is + reported in RoundStrategy.notes and ignored. diff --git a/metainfer/tasks/dcu_kernel_auto_opt/orchestrator/config.py b/metainfer/tasks/dcu_kernel_auto_opt/orchestrator/config.py index d908ba13..b32662d6 100644 --- a/metainfer/tasks/dcu_kernel_auto_opt/orchestrator/config.py +++ b/metainfer/tasks/dcu_kernel_auto_opt/orchestrator/config.py @@ -3,7 +3,7 @@ from __future__ import annotations import os -from dataclasses import dataclass +from dataclasses import dataclass, field from pathlib import Path from typing import Any, Dict, List, Mapping @@ -24,25 +24,57 @@ # dsh routes claude_bin to bridge/dsh/dsh_agent.py (see resolve_claude_bin). CCB_FRAMEWORK = "ccb" DSH_FRAMEWORK = "dsh" -DSH_DEFAULT_MODEL_ID = "deepseek/deepseek-v4-flash-0731" + +#: DSH model label -> platform model id, in form order. The first entry is the +#: framework default. The label is what the New Task form shows (and what the +#: frontend model widget in ``static/dkao-agent-fields.js`` mirrors); the id is +#: what the agent binary receives through ``--model``. +#: +#: ``deepseek-flash-4.1`` is the 4.1 revision of the DeepSeek Flash line, served +#: by the TokenHub gateway as ``deepseek/deepseek-flash`` (the id this host's +#: DSH profile lists as ``dsv4-flash4.1``). ``deepseek-v4-flash`` stays +#: selectable for runs that must reproduce the older pinned build. +DSH_MODEL_IDS: Dict[str, str] = { + "deepseek-flash-4.1": "deepseek/deepseek-flash", + "deepseek-v4-flash": "deepseek/deepseek-v4-flash-0731", +} +DSH_DEFAULT_MODEL_LABEL = "deepseek-flash-4.1" +DSH_DEFAULT_MODEL_ID = DSH_MODEL_IDS[DSH_DEFAULT_MODEL_LABEL] + + +def _dsh_model_override() -> str: + """The DSH_AGENT_MODEL pin, stripped, or "" when unset.""" + return (os.environ.get("DSH_AGENT_MODEL") or "").strip() def dsh_model_id() -> str: - """Model id used for the dsh framework (env DSH_AGENT_MODEL overrides).""" - override = os.environ.get("DSH_AGENT_MODEL") - return (override.strip() if override and override.strip() - else DSH_DEFAULT_MODEL_ID) + """Default model id for the dsh framework, honoring the DSH_AGENT_MODEL pin. + + ``DSH_AGENT_MODEL`` is the host-level override the launcher exports: when it + is set, every dsh model label resolves to it. That keeps a host pinned to one + verified build whatever the form submits, which is what a mixed-version + deployment wants — the catalog still tells the operator which labels exist, + and an unset variable leaves each label on its own id. + """ + return _dsh_model_override() or DSH_DEFAULT_MODEL_ID def agent_framework_models(framework: str) -> Dict[str, str]: """Label -> model id for one agent framework.""" if framework == DSH_FRAMEWORK: - return {"deepseek-v4-flash": dsh_model_id()} + if _dsh_model_override(): + # A host pin routes every dsh label to that one build; the catalog + # keeps its labels so an old requirements file still resolves. + return {label: _dsh_model_override() + for label in DSH_MODEL_IDS} + return dict(DSH_MODEL_IDS) return dict(CLAUDE_MODELS) def agent_framework_default_model(framework: str) -> str: - return "deepseek-v4-flash" if framework == DSH_FRAMEWORK else "Opus" + if framework == DSH_FRAMEWORK: + return DSH_DEFAULT_MODEL_LABEL + return "Opus" def resolve_agent_framework(req: Mapping[str, Any]) -> str: @@ -180,6 +212,18 @@ class OptimizerConfig: # Agent framework (ccb | dsh) that produced claude_model; the pipeline # uses it to pick the agent binary via resolve_claude_bin. agent_framework: str = CCB_FRAMEWORK + #: 占据模式 (occupy, default): workers keep the devices the assignment mode + #: picked (manual pins worker_N -> GPU N). 调控模式 (scheduled): devices are + #: leased from the shared GPU broker so several tasks can share the machine + #: without measuring on a contended card. + gpu_mode: str = "occupy" + gpu_leases: Dict[str, Any] = field(default_factory=dict) + #: "default" | "production". Production means: an operator-launched real + #: task that outranks the evolving harness in the device queue + #: (``PRIORITY_PRODUCTION``) and keeps waiting for a device that passes the + #: admission gate. It never skips the gate itself — a measurement taken on a + #: shared card is stale data regardless of who asked for it. + execution_profile: str = "default" def _answers(req: Mapping[str, Any]) -> Mapping[str, Any]: @@ -397,6 +441,37 @@ def load_config(req: Mapping[str, Any]) -> OptimizerConfig: if threshold < 0: raise ValueError("minimum_improvement_percent must be non-negative") + #: 生产模式 (``execution_profile: production``): an operator-launched real + #: task. It is *not* a way around the admission gate — a measurement taken on + #: a shared card is stale data whatever the task is — it is a **priority** + #: statement: production work goes through the broker at + #: ``PRIORITY_PRODUCTION`` so the evolving harness (priority 50) yields to + #: it, and it keeps waiting for a device that passes the gate instead of + #: giving up. + profile = str(answers.get("execution_profile") or "").strip().lower() + if profile in {"", "default", "harness", "evolve", "ahe"}: + profile = "default" + elif profile in {"production", "prod"}: + profile = "production" + else: + raise ValueError( + "execution_profile must be 'default' or 'production'") + production = profile == "production" + + gpu_mode = str(answers.get("gpu_mode") or "occupy").strip().lower() + if production: + # production always goes through the broker (that is what carries the + # priority); pinning devices would step around the queue entirely. + gpu_mode = "scheduled" + if gpu_mode not in {"occupy", "scheduled"}: + raise ValueError("gpu_mode must be occupy or scheduled") + lease_info: Dict[str, Any] = {} + if gpu_mode == "scheduled": + # 调控模式 / 生产模式: devices come from the shared broker, one per worker + assignments, lease_info = lease_gpus_for_task( + assignments, answers, production=production) + assignment_mode = "scheduled" + return OptimizerConfig( operator=str(answers.get("operator") or "Custom operator"), dtype=str(answers.get("dtype") or "Other"), @@ -412,9 +487,95 @@ def load_config(req: Mapping[str, Any]) -> OptimizerConfig: mock_iterations=iterations, minimum_improvement_percent=threshold, agent_framework=agent_framework, + execution_profile=profile, + gpu_mode=gpu_mode, + gpu_leases=lease_info, ) +def rebalance_for_gpus(assignments: List[WorkerAssignment], + gpus: List[int]) -> List[WorkerAssignment]: + """One worker per leased device, shapes spread round-robin. + + With fewer leases than planned workers the shapes are merged into fewer + lanes instead of putting two workers on one device — the whole point of + scheduled mode is that a device is never shared while measuring. + """ + ordered = sorted({int(g) for g in gpus}) + if not ordered: + return [] + shape_ids = [sid for assignment in assignments for sid in assignment.shape_ids] + buckets: Dict[int, List[str]] = {g: [] for g in ordered} + for index, shape_id in enumerate(shape_ids): + buckets[ordered[index % len(ordered)]].append(shape_id) + rebuilt = [ + WorkerAssignment(worker_id=f"worker_{gpu}", gpu=gpu, shape_ids=sids) + for gpu, sids in sorted(buckets.items()) if sids + ] + return rebuilt or [WorkerAssignment(worker_id=f"worker_{ordered[0]}", + gpu=ordered[0], shape_ids=shape_ids)] + + +def lease_gpus_for_task(assignments: List[WorkerAssignment], + answers: Mapping[str, Any], + *, production: bool = False, + ) -> tuple[List[WorkerAssignment], Dict[str, Any]]: + """调控模式 / 生产模式: take GPU leases and lay the workers out on them. + + Both ask the broker at ``PRIORITY_PRODUCTION`` so a real task outranks the + evolving harness; ``production`` additionally keeps waiting for a device that + passes the admission gate instead of failing after a fixed number of checks, + and records that intent in the lease metadata. + """ + from metainfer.orchestrator.gpu_broker import ( + PRIORITY_PRODUCTION, GpuBroker, + ) + + broker = GpuBroker() + task_id = str(answers.get("task_id") or "dkao-task") + holder = f"dkao:{task_id}" + want = max(1, len(assignments)) + idle_wait = float(answers.get("gpu_lease_wait_seconds") or 1800) + max_waits = int(answers.get("gpu_lease_max_waits") or 48) + if production: + # waiting longer is the point: a production task yields the queue to + # nobody, but it still refuses to measure on a device that fails the gate + max_waits = max(max_waits, int(answers.get("production_max_waits") or 48)) + leases: List[Dict[str, Any]] = [] + for round_no in range(1, max_waits + 1): + leases = broker.acquire( + holder, want=want, priority=PRIORITY_PRODUCTION, + timeout_s=(0.0 if round_no == 1 else idle_wait), poll_s=30, + pid=os.getpid(), + meta={"mode": "scheduled", "task": task_id, "round": round_no, + "profile": "production" if production else "default"}, + ) + if leases: + break + if not leases: + raise RuntimeError( + ("production: no device passed the admission gate (VRAM<=90% and " + f"HCU==0) after {max_waits} checks; the gate is not skipped for " + "production tasks because a shared card yields stale numbers") + if production else + ("gpu_mode=scheduled: no GPU lease became available after " + f"{max_waits} checks; switch to gpu_mode=occupy to pin devices") + ) + gpus = [int(lease["gpu"]) for lease in leases] + rebuilt = rebalance_for_gpus(assignments, gpus) + info = { + "mode": "scheduled", + "profile": "production" if production else "default", + "priority": PRIORITY_PRODUCTION, + "holder": holder, + "gpus": gpus, + "lease_seconds": int(answers.get("gpu_lease_ttl_s") or 0), + "mapping": {a.worker_id: a.gpu for a in rebuilt}, + "requested_workers": len(assignments), + } + return rebuilt, info + + def replace_assignments( config: OptimizerConfig, assignments: List[WorkerAssignment], @@ -435,6 +596,9 @@ def replace_assignments( mock_iterations=config.mock_iterations, minimum_improvement_percent=config.minimum_improvement_percent, agent_framework=config.agent_framework, + execution_profile=config.execution_profile, + gpu_mode=config.gpu_mode, + gpu_leases=dict(getattr(config, "gpu_leases", {}) or {}), ) diff --git a/metainfer/tasks/dcu_kernel_auto_opt/orchestrator/contention.py b/metainfer/tasks/dcu_kernel_auto_opt/orchestrator/contention.py new file mode 100644 index 00000000..023d2414 --- /dev/null +++ b/metainfer/tasks/dcu_kernel_auto_opt/orchestrator/contention.py @@ -0,0 +1,187 @@ +"""Discard a worker round that was measured while its device was shared. + +The parent harness freezes a question's child when the device it holds is taken +over, and records that window in ``/contention.json``. The +child, on the way back up, must then do two things for that round: + +1. **not keep the number** — a measurement taken while another workload shared + the card is noise (the same unchanged shape has read 884us and 3047us), and + if it is kept it also pollutes the iteration history the planner reasons + about; +2. **go back to the previous good state** — the accepted kernel commit, the + official best metrics and any shadow candidate from that round are undone, so + the round can simply be re-measured instead of starting the whole question + over. + +The record is *quarantined*, never deleted: the failed number stays available as +evidence of the contamination. + +Re-planning comes for free: ``plan.json`` is written once from static config, +while the per-round planner inputs (``isa_policy``, ``pmc_profile_decision``, +round mandates) are derived from ``experiments.jsonl`` at the top of every +iteration. Removing the contaminated row therefore means the next round is +planned against clean history. +""" + +from __future__ import annotations + +import json +import os +import time +from pathlib import Path +from typing import Any, Dict, List, Optional + +CONTENTION_FILE = "contention.json" +QUARANTINE_DIR = "contaminated" + + +def contention_file(child_state_dir: Optional[Path]) -> Optional[Path]: + """Locate the parent's contention record for this child.""" + candidates: List[Path] = [] + if child_state_dir: + candidates.append(Path(child_state_dir) / CONTENTION_FILE) + env = str(os.environ.get("METAINFER_CHILD_STATE_DIR") or "").strip() + if env: + candidates.append(Path(env) / CONTENTION_FILE) + for path in candidates: + try: + if path.is_file(): + return path + except OSError: + continue + return None + + +def read_contention(child_state_dir: Optional[Path] = None) -> Dict[str, Any]: + """The contention window recorded for this child (empty when none).""" + path = contention_file(child_state_dir) + if path is None: + return {} + try: + data = json.loads(path.read_text(encoding="utf-8")) + except (OSError, ValueError): + return {} + return data if isinstance(data, dict) else {} + + +def iteration_is_contaminated(iteration: int, window: Dict[str, Any], *, + iteration_dir: Optional[Path] = None, + now: Optional[float] = None) -> tuple[bool, str]: + """Whether ``iteration`` was measured inside the contention window. + + The window is ``[frozen_from, recovered_at]``: the child was frozen the + moment contention was seen, so any round whose work lands in that interval + (or which was still unfinished when the window closed) is suspect. A window + that is still open contaminates the round that was running when it opened. + """ + if not window: + return False, "no contention recorded" + frozen_from = window.get("frozen_from") + if frozen_from is None: + return False, "window has no start" + recovered_at = window.get("recovered_at") + stamp = float(now if now is not None else time.time()) + # where the round's work sits on disk (mtime is what we can trust: the + # records themselves carry no timestamp) + mtime = None + if iteration_dir is not None: + try: + mtime = Path(iteration_dir).stat().st_mtime + except OSError: + mtime = None + reference = mtime if mtime is not None else stamp + if reference < float(frozen_from): + return False, "round finished before the device was taken over" + if recovered_at is None: + # the window is still open: this round ran while the device was shared + return True, "device was taken over while this round was running" + if reference <= float(recovered_at): + return True, "round lands inside the window while the device was shared" + return False, "round was measured after the device recovered" + + +def _quarantine(experiments_path: Path, iteration: int, record: Dict[str, Any], + meta: Dict[str, Any], *, state_dir: Optional[Path] = None, + ) -> Optional[Path]: + """Move one round's record out of the live history into quarantine.""" + target_dir = Path(state_dir) if state_dir else experiments_path.parent + target_dir = target_dir / QUARANTINE_DIR + target_dir.mkdir(parents=True, exist_ok=True) + target = target_dir / f"iteration-{int(iteration):03d}.json" + payload = { + "reason": "device_shared_during_measurement", + "discarded_at": time.time(), + "iteration": int(iteration), + "metrics": { + k: (record.get("metrics") or {}).get(k) + for k in ("median_us", "p90_us", "passed", "mismatch_count") + }, + **meta, + "record": record, + } + try: + target.write_text(json.dumps(payload, indent=2, ensure_ascii=False), + encoding="utf-8") + except OSError: + return None + return target + + +def discard_contaminated_round(*, experiments_path: Path, iteration: int, + state_dir: Optional[Path] = None, + iteration_dir: Optional[Path] = None, + window: Optional[Dict[str, Any]] = None, + ) -> Dict[str, Any]: + """Remove a contaminated round from the live history (quarantined). + + Returns ``{"discarded": bool, "reason": str, ...}``. The caller is expected + to restore the kernel source and metrics to the previous good state (the + discard itself only touches the history and the quarantined copy). + """ + experiments_path = Path(experiments_path) + window = window if window is not None else read_contention(state_dir) + contaminated, why = iteration_is_contaminated(iteration, window, + iteration_dir=iteration_dir) + if not contaminated: + return {"discarded": False, "reason": why} + if not experiments_path.is_file(): + return {"discarded": False, "reason": "no experiment history to prune"} + + rows: List[Dict[str, Any]] = [] + dropped: List[Dict[str, Any]] = [] + for line in experiments_path.read_text(encoding="utf-8").splitlines(): + if not line.strip(): + continue + try: + row = json.loads(line) + except ValueError: + rows.append({"_raw": line}) + continue + if int(row.get("iteration") or 0) == int(iteration): + dropped.append(row) + else: + rows.append(row) + + quarantined = [] + for record in dropped: + path = _quarantine(experiments_path, iteration, record, { + "window": window, "reason_detail": why, + }, state_dir=state_dir) + if path is not None: + quarantined.append(str(path)) + + try: + experiments_path.write_text( + "".join(json.dumps(r) + "\n" for r in rows if "_raw" not in r + ) + "".join(r["_raw"] + "\n" for r in rows if "_raw" in r), + encoding="utf-8") + except OSError as exc: # noqa: BLE001 + return {"discarded": False, "reason": f"cannot rewrite history: {exc}"} + return { + "discarded": True, + "reason": why, + "rounds_removed": len(dropped), + "quarantined": quarantined, + "history_remaining": len([r for r in rows if "_raw" not in r]), + "window": window, + } diff --git a/metainfer/tasks/dcu_kernel_auto_opt/orchestrator/gen_and_opt_pipeline.py b/metainfer/tasks/dcu_kernel_auto_opt/orchestrator/gen_and_opt_pipeline.py index de293088..3e0bc0cf 100644 --- a/metainfer/tasks/dcu_kernel_auto_opt/orchestrator/gen_and_opt_pipeline.py +++ b/metainfer/tasks/dcu_kernel_auto_opt/orchestrator/gen_and_opt_pipeline.py @@ -14,7 +14,7 @@ import time from concurrent.futures import ThreadPoolExecutor, as_completed from pathlib import Path -from typing import Any, Dict, List +from typing import Any, Callable, Dict, List, Optional, Sequence from metainfer.orchestrator.state import StateStore from metainfer.orchestrator.subagent_manager import AgentSpec, SubAgentManager @@ -47,6 +47,7 @@ generate_kernel_prompt, shape_balanced_assignment, ) +from .gpu_binding import bind_worker_gpu from .harness_io import harness_root, load_manifest, seed_workspace from .real_pipeline import _run, _safe, _status from .result_store import SCHEMA_VERSION, write_json @@ -55,7 +56,9 @@ RealW8A8OptimizationPipeline, W8A8Runner, _SOURCE_ONLY_AGENT_ARGS, + _gate_preflight_enabled, _sha256_file, + ensure_any_measurement_gate, evaluate_final_target, snapshot_accepted_kernel_artifact, ) @@ -209,11 +212,48 @@ def _last_json_object(text: str) -> Dict[str, Any]: raise ValueError(f"no JSON object in output: {text[-1000:]}") +def _gate_state_dir_for(pipeline: Any) -> "Path | None": + """Where the Generate probe's gate rows go: the task's own state dir. + + Without it a 30-minute wait on a busy device leaves no trace in the task's + audit trail, which is exactly the "why did nothing happen" question this + gate has to answer. + """ + raw = getattr(getattr(pipeline, "store", None), "task_dir", None) + return Path(raw) if raw else None + + +def _probe_gate_devices(config: Any) -> List[int]: + """Devices the Generate probe may use: this task's own GPUs first. + + The probe only needs *a* device that passes the gate (a device is visible and + HIP Graph is available). Its own assignment is preferred so a task never + competes for a card it does not own, but an assigned card that is currently + shared must not stall the whole task while a sibling device sits idle. + """ + assigned: List[int] = [] + for item in list(getattr(config, "assignments", None) or []): + try: + gpu = int(getattr(item, "gpu", -1)) + except (TypeError, ValueError): + continue + if gpu >= 0 and gpu not in assigned: + assigned.append(gpu) + try: + from metainfer.orchestrator.gpu_broker import GpuBroker + known = [int(d) for d in GpuBroker().devices] + except Exception: # noqa: BLE001 - fall back to the task's own devices + known = list(assigned) or [0] + return assigned + [d for d in known if d not in assigned] + + def _validate_generate_scaffold( source: Path, *, contract_sha256: str, shapes: Dict[str, Dict[str, Any]], + probe_devices: Optional[Sequence[int]] = None, + state_dir: "Path | None" = None, ) -> Dict[str, Any]: """Run trusted, implementation-free Generate preflight checks.""" kernel_path = source / _GENERATED_KERNEL_FILE @@ -253,11 +293,22 @@ def _validate_generate_scaffold( ) probe_env = dict(os.environ) - probe_env.update({ - "HIP_VISIBLE_DEVICES": "0", - "ROCR_VISIBLE_DEVICES": "0", - "PYTHONDONTWRITEBYTECODE": "1", - }) + probe_gpu = 0 + # The probe initialises a device, so it follows the same admission rule as a + # timed measurement (VRAM <= 90% and HCU == 0). It is not pinned to GPU 0: + # any device that passes the gate can answer "is a device visible and is HIP + # Graph available", and sleeping 24 h on a busy GPU 0 while GPU 1 is idle is + # how a task stalls without doing anything wrong. + if _gate_preflight_enabled(): + probe_gpu = ensure_any_measurement_gate( + list(probe_devices or [0]), state_dir=state_dir, env=probe_env, + site="generate_probe") + # Binding goes through the task's one policy: HIP_VISIBLE_DEVICES only. + # Setting ROCR_VISIBLE_DEVICES to the same *non-zero* index filters twice + # and hides the device ("No HIP GPUs are available" at device 1, while + # device 0 happened to survive). + bind_worker_gpu(probe_env, probe_gpu) + probe_env["PYTHONDONTWRITEBYTECODE"] = "1" probe_result = _run( [ "python3", str(harness), @@ -927,7 +978,8 @@ def _generate_kernel_repo( sid: shape.params for sid, shape in config.shapes.items() } fixed_assignment: Dict[str, Dict[str, Any]] - if config.assignment_mode == "manual": + if (config.assignment_mode == "manual" + and getattr(config, "gpu_mode", "occupy") == "occupy"): actual = { item.worker_id: item.gpu for item in config.assignments } @@ -981,6 +1033,8 @@ def _generate_kernel_repo( agent_source_dir, contract_sha256=trusted_contract_digest, shapes=shapes_for_prompt, + probe_devices=_probe_gate_devices(config), + state_dir=_gate_state_dir_for(self), ) preflight_path = seed / "generation_preflight.json" write_json(preflight_path, preflight) @@ -2287,6 +2341,46 @@ def _synthesize_final_candidate( # Main pipeline # ------------------------------------------------------------------ # + def _register_gpu_leases(self, config: Any) -> Optional[Callable[[], Any]]: + """调控模式: announce leased devices and return them when we exit. + + The lease also has a TTL, so a hard crash still frees the devices; this + hook keeps the common paths prompt (a device should not sit idle for two + hours after a task finishes). + """ + leases = dict(getattr(config, "gpu_leases", {}) or {}) + if not leases: + return None + holder = str(leases.get("holder") or "") + self.store.append_timeline("gpu_leases_acquired", { + "holder": holder, + "gpus": leases.get("gpus"), + "mapping": leases.get("mapping"), + "requested_workers": leases.get("requested_workers"), + "mode": leases.get("mode"), + # production leases are visible as such, so the GPU view can tell a + # real operator task from an evolving-harness round + "profile": leases.get("profile"), + "priority": leases.get("priority"), + }) + + def _release() -> list: + try: + from metainfer.orchestrator.gpu_broker import GpuBroker + freed = GpuBroker().release_prefix(holder) + if freed: + self.store.append_timeline("gpu_leases_released", { + "holder": holder, "gpus": freed, + }) + return freed + except Exception: # noqa: BLE001 - release must never raise + return [] + + import atexit + atexit.register(_release) + self.release_gpu_leases = _release + return _release + def run(self, *, dry_run: bool = False) -> Dict[str, Any]: # Record the gate values this run actually used, so the harness_evolve # mechanism gate can verify a gates.yaml change (not just observe it). @@ -2317,6 +2411,7 @@ def run(self, *, dry_run: bool = False) -> Dict[str, Any]: # ---- PREPARE -------------------------------------------------- # self._phase(phases.PREPARE) config = load_config(self.req) + self._register_gpu_leases(config) self._validate_contract(config) self._prepare_worktrees(config, task_id) plan = self._plan(config) diff --git a/metainfer/tasks/dcu_kernel_auto_opt/orchestrator/gpu_preflight.py b/metainfer/tasks/dcu_kernel_auto_opt/orchestrator/gpu_preflight.py index 723a8a5c..d5a92e4b 100644 --- a/metainfer/tasks/dcu_kernel_auto_opt/orchestrator/gpu_preflight.py +++ b/metainfer/tasks/dcu_kernel_auto_opt/orchestrator/gpu_preflight.py @@ -1,21 +1,24 @@ """GPU admission pre-check shared by production DKAO tasks and AHE children. -Two different questions are answered separately, because they need different -rules: - -* **Can we run here at all?** Only VRAM pressure decides that. Our W8A8 - kernels need a few GB at most, so the operating rule is simple: a device is - usable while its used-VRAM percentage is at or below - ``vram_limit_percent`` (default 85). Above that the caller prefers another - device, and only then waits. -* **Will the measurement be trustworthy?** Busy compute (high HCU%, high - board power) or foreign processes hold the device (KFD lists pids invisible - inside this container) do NOT block the run — they only mark the question as - ``measurement_suspect`` so a polluted reading cannot silently decide an - A/B comparison. +The operating rule is one gate: **a device may only be used for a measurement +when VRAM <= 90% and HCU == 0** (``usable``). Anything else — a full card, a +card shared with another workload, a card whose state we cannot read — is not +usable, and the caller waits and re-checks instead of measuring on it. + +A second, weaker signal is reporting only: board power and foreign KFD pids are +recorded for diagnosis, and a device that passes the gate but still looks +suspicious is marked ``measurement_suspect`` so a polluted reading cannot +silently decide an A/B comparison. + +Telemetry has two sources, because ``hy-smi`` alone is not enough: some driver +configurations print ``N/A`` for HCU% (the container cannot open the management +interface), and a parser that silently yields nothing would make *every* card +look idle. ``sample_gpu_state`` therefore falls back to the driver sysfs +(``gpu_busy_percent`` / ``mem_info_vram_used``) and reports ``unavailable`` when +neither source answers. The module is deliberately dependency-free: it parses ``hy-smi`` text and the -KFD sysfs listing, so it works inside the task containers without extra tools. +KFD/sysfs listings, so it works inside the task containers without extra tools. """ from __future__ import annotations @@ -26,9 +29,13 @@ from pathlib import Path from typing import Any, Dict, List, Optional -#: A device is usable while at least this much VRAM stays free. Absolute free -#: memory is the right unit for us (one W8A8 worker needs ~1-3GB), whereas a -#: percentage punishes big cards that hold idle reservations. +#: Operating rule requested by the operator: a device may only be used when +#: VRAM <= ``DEFAULT_VRAM_LIMIT_PERCENT`` **and** the device is truly idle +#: (HCU == 0). The percentage form is kept for reference. +DEFAULT_VRAM_LIMIT_PERCENT = 90.0 +#: HCU tolerance: 0.0 means "strictly idle". +DEFAULT_UTIL_TOLERANCE = 0.0 +#: Kept for reference/reporting (VRAM <= 90% implies >= ~6.4GB free on 64GB). DEFAULT_MIN_FREE_GB = 4.0 #: Assumed total VRAM when the driver sysfs does not expose it (K500SM_AI). DEFAULT_TOTAL_GB = 65.5 @@ -38,34 +45,131 @@ DEFAULT_POWER_SUSPECT_W = 230.0 KFD_PROC_ROOT = Path("/sys/class/kfd/kfd/proc") +#: Driver sysfs used when ``hy-smi`` cannot report HCU%/VRAM. +DRM_ROOT = Path("/sys/class/drm") +#: DRM drivers that mean "this card is a Hygon HCU". +_HCU_DRM_DRIVERS = ("hycu", "amdgpu", "hydcu") +#: ``N/A`` (or ``-``) where a number is expected: the device is present but the +#: management interface cannot be read. Such a field is recorded as unknown +#: instead of dropping the whole row, which used to make every card look idle. +_UNKNOWN = r"(?:N/A|n/a|-{1,2})" _SMI_ROW = re.compile( - r"^(?P\d+)\s+" # device index + r"^(?P\d+)\s+" # device index r"(?P[\d.]+)C\s+" - r"(?P[\d.]+)W\s+" - r"\S+\s+" # perf mode - r"[\d.]+W\s+" # power cap - r"(?P[\d.]+)%\s+" - r"(?P[\d.]+)%" + r"(?:{u}|(?P[\d.]+)W)\s+" # average power + r"\S+\s+" # perf mode + r"(?:{u}|[\d.]+W)\s+" # power cap + r"(?:(?P[\d.]+)%|{u})\s+" # VRAM% + r"(?:(?P[\d.]+)%|{u})" # HCU% + .format(u=_UNKNOWN) ) -def parse_hy_smi(text: str) -> Dict[int, Dict[str, float]]: - """Parse ``hy-smi`` output into ``{index: {vram_percent, util_percent, power_w, temp_c}}``.""" +def parse_hy_smi( + text: str, *, device_ids: Optional[List[int]] = None, +) -> Dict[int, Dict[str, float]]: + """Parse ``hy-smi`` into ``{index: {vram_percent, util_percent, power_w, temp_c}}``. + + A field printed as ``N/A`` is *omitted* from that device's row: the caller + then sees "unknown" rather than a fabricated ``0.0``, which is what the + gate needs to refuse the device instead of silently admitting it. + """ out: Dict[int, Dict[str, float]] = {} for line in (text or "").splitlines(): match = _SMI_ROW.match(line.strip()) if not match: continue index = int(match.group("index")) - out[index] = { - "vram_percent": float(match.group("vram")), - "util_percent": float(match.group("util")), - "power_w": float(match.group("power")), - "temp_c": float(match.group("temp")), - } + if device_ids is not None and index not in device_ids: + continue + row: Dict[str, float] = {} + for key in ("vram", "util", "power", "temp"): + raw = match.group(key) + if raw is None: + continue + row[{"vram": "vram_percent", "util": "util_percent", + "power": "power_w", "temp": "temp_c"}[key]] = float(raw) + out[index] = row + return out + + +def smi_is_incomplete( + text: str, *, device_ids: Optional[List[int]] = None, +) -> bool: + """True when ``hy-smi`` names a device but prints ``N/A`` for HCU%. + + That is the signature of "the management interface cannot be read from + here" (``Open mkfd failed`` inside a container without the HCU device). On + those hosts the *whole* utilisation block is a placeholder — including a + ``0%`` VRAM column — so the driver sysfs is the more trustworthy source + and takes precedence over the rest of the same row. + """ + rows = 0 + for line in (text or "").splitlines(): + match = _SMI_ROW.match(line.strip()) + if not match: + continue + index = int(match.group("index")) + if device_ids is not None and index not in device_ids: + continue + rows += 1 + if match.group("util") is None: + return True + return rows == 0 + + +def read_sysfs_gpu_state( + drm_root: Path = DRM_ROOT, +) -> Dict[int, Dict[str, float]]: + """Driver sysfs fallback: ``{index: {vram_percent, util_percent}}``. + + ``/sys/class/drm`` is host-wide and does not depend on the management + interface, so it still answers when ``hy-smi`` prints ``N/A``. Cards are + filtered to the HCU driver and ordered by card index (``card1`` is the + first HCU on the K500SM_AI machines, since ``card0`` is the BMC VGA), which + matches the ``hy-smi``/KFD device numbering the tasks use. + """ + cards: List[Path] = [] + try: + entries = list(drm_root.iterdir()) + except OSError: + return {} + for entry in sorted(entries, key=lambda p: p.name): + match = re.fullmatch(r"card(\d+)", entry.name) + if not match: + continue + device = entry / "device" + try: + uevent = (device / "uevent").read_text(encoding="utf-8") + except OSError: + continue + if not any(driver in uevent for driver in _HCU_DRM_DRIVERS): + continue + if not (device / "mem_info_vram_total").is_file(): + continue + cards.append(device) + out: Dict[int, Dict[str, float]] = {} + for index, device in enumerate(cards): + row: Dict[str, float] = {} + busy = _read_int(device / "gpu_busy_percent") + if busy is not None: + row["util_percent"] = float(busy) + used = _read_int(device / "mem_info_vram_used") + total = _read_int(device / "mem_info_vram_total") + if used is not None and total: + row["vram_percent"] = round(100.0 * float(used) / float(total), 2) + if row: + out[index] = row return out +def _read_int(path: Path) -> Optional[int]: + try: + return int((path.read_text(encoding="utf-8") or "").strip()) + except (OSError, ValueError): + return None + + def read_hy_smi(timeout_s: float = 10.0) -> str: import subprocess @@ -104,13 +208,27 @@ def foreign_kfd_pids(kfd_root: Path = KFD_PROC_ROOT) -> List[int]: def sample_gpu_state(gpu_ids: Optional[List[int]] = None, *, samples: int = 3, interval_s: float = 3.0, - smi_reader=read_hy_smi) -> Dict[int, Dict[str, float]]: - """Average a few hy-smi samples per device (drops idle flicker).""" + smi_reader=read_hy_smi, + sysfs_reader=read_sysfs_gpu_state, + ) -> Dict[int, Dict[str, float]]: + """Average a few hy-smi samples per device (drops idle flicker). + + ``hy-smi`` is the primary source. When it cannot report HCU% (it prints + ``N/A`` on driver/container combinations where the management interface is + unreadable), the missing fields are taken from the driver sysfs instead of + being treated as zero: a fabricated ``util=0`` would admit a card that is + actively busy, which is exactly the failure this gate exists to prevent. + A device neither source can describe is reported with ``unavailable=True`` + so the gate refuses it. + """ + wanted = None if gpu_ids is None else {int(g) for g in gpu_ids} acc: Dict[int, Dict[str, List[float]]] = {} + smi_texts: List[str] = [] for attempt in range(max(1, samples)): - parsed = parse_hy_smi(smi_reader()) - for index, row in parsed.items(): - if gpu_ids is not None and index not in gpu_ids: + text = smi_reader() + smi_texts.append(text or "") + for index, row in parse_hy_smi(text).items(): + if wanted is not None and index not in wanted: continue bucket = acc.setdefault(index, {"vram_percent": [], "util_percent": [], "power_w": [], "temp_c": []}) @@ -118,12 +236,71 @@ def sample_gpu_state(gpu_ids: Optional[List[int]] = None, *, bucket.setdefault(key, []).append(value) if attempt + 1 < samples: time.sleep(max(0.0, interval_s)) + # ``hy-smi`` printouts that say N/A for HCU% come from a host where the + # management interface is unreadable: the rest of that row (including a + # "0%" VRAM column) is a placeholder, so sysfs wins. + smi_unreliable = any( + smi_is_incomplete(text, device_ids=gpu_ids) for text in smi_texts) out: Dict[int, Dict[str, float]] = {} for index, bucket in acc.items(): - out[index] = { - key: (sum(values) / len(values) if values else 0.0) - for key, values in bucket.items() - } + row: Dict[str, float] = {} + for key, values in bucket.items(): + if values and not (smi_unreliable and key == "vram_percent"): + row[key] = sum(values) / len(values) + if bucket.get("vram_percent") and not smi_unreliable: + row["vram_max_percent"] = max(bucket["vram_percent"]) + if bucket.get("util_percent"): + row["util_max_percent"] = max(bucket["util_percent"]) + if row: + row["state_source"] = "hy-smi" + out[index] = row + need_fallback = ( + not out + or smi_unreliable + or any( + row.get("util_percent") is None or row.get("vram_percent") is None + for row in out.values() + ) + or any( + index not in out + or out[index].get("util_percent") is None + or out[index].get("vram_percent") is None + for index in (wanted or ()) + ) + ) + fallback: Dict[int, Dict[str, float]] = {} + if sysfs_reader is not None and need_fallback: + try: + fallback = dict(sysfs_reader() or {}) + except Exception: # noqa: BLE001 - the fallback is best effort + fallback = {} + for index, source in fallback.items(): + if wanted is not None and index not in wanted: + continue + row = out.setdefault(index, {}) + filled: List[str] = [] + for key in ("util_percent", "vram_percent"): + if source.get(key) is None: + continue + if row.get(key) is not None and not (smi_unreliable + and key == "vram_percent"): + continue + row[key] = float(source[key]) + peak = ("util_max_percent" if key == "util_percent" + else "vram_max_percent") + row.setdefault(peak, float(source[key])) + filled.append(key) + if filled: + had_smi = row.get("power_w") is not None or row.get("temp_c") is not None + row["state_source"] = "hy-smi+sysfs" if had_smi else "sysfs" + if wanted is not None: + for index in sorted(wanted): + out.setdefault(index, {}) + for index, row in out.items(): + missing = [k for k in ("vram_percent", "util_percent") if k not in row] + if missing: + row["unavailable"] = 1.0 + row["missing_fields"] = missing return out @@ -142,44 +319,66 @@ def total_vram_gb(card_index: int = 0) -> float: def check_gpu(gpu_id: int, state: Dict[str, float], *, min_free_gb: float = DEFAULT_MIN_FREE_GB, total_gb: Optional[float] = None, + vram_limit_percent: float = DEFAULT_VRAM_LIMIT_PERCENT, + util_tolerance: float = DEFAULT_UTIL_TOLERANCE, util_suspect_percent: float = DEFAULT_UTIL_SUSPECT_PERCENT, power_suspect_w: float = DEFAULT_POWER_SUSPECT_W, foreign_pids: Optional[List[int]] = None, ) -> Dict[str, Any]: - """Decide usability (free VRAM >= ``min_free_gb``) and suspicion (busy).""" - vram = float(state.get("vram_percent") or 0.0) + """Usable only when VRAM <= limit *and* the device is idle (HCU == 0). + + A device whose state could not be read is **not** usable: "we cannot tell" + must never be treated as "idle", otherwise a busy card silently admits + measurements. Callers then wait and re-check (30 min, up to 48 times). + """ + unavailable = bool(state.get("unavailable")) + vram = float(state.get("vram_max_percent") + if state.get("vram_max_percent") is not None + else state.get("vram_percent") or 0.0) + util_peak = float(state.get("util_max_percent") + if state.get("util_max_percent") is not None + else state.get("util_percent") or 0.0) util = float(state.get("util_percent") or 0.0) power = float(state.get("power_w") or 0.0) foreign = list(foreign_pids or []) total = float(total_gb if total_gb is not None else total_vram_gb()) free_gb = total * max(0.0, 100.0 - vram) / 100.0 - usable = free_gb >= float(min_free_gb) + vram_ok = vram <= float(vram_limit_percent) + idle_ok = util_peak <= float(util_tolerance) + usable = bool(vram_ok and idle_ok and not unavailable) reasons: List[str] = [] - if util > float(util_suspect_percent): - reasons.append(f"busy: HCU {util:.0f}% > {util_suspect_percent:.0f}%") - if power > float(power_suspect_w): - reasons.append(f"high power draw {power:.0f}W > {power_suspect_w:.0f}W") - # Foreign KFD entries alone are NOT evidence of contention: idle holders - # (long-lived runtimes in other containers) keep their handle while the - # device sits at 0% util / idle power, which is harmless for us. They are - # reported as notes, and the busy/power signals above decide suspicion. + if unavailable: + missing = ",".join(state.get("missing_fields") or []) or "all" + reasons.append(f"state unavailable: no HCU/VRAM reading ({missing})") + # The gate is exactly "VRAM <= 90% and HCU == 0" (operator rule). Power draw + # is recorded for diagnosis only: a card idling at HCU 0 while another + # process holds a handle is still safe to measure on, and treating power as + # a blocker would refuse cards the rule admits. notes: List[str] = [] if foreign: notes.append(f"{len(foreign)} foreign KFD pid(s) hold this GPU") - if not usable: + if not vram_ok: + reasons.append( + f"VRAM {vram:.0f}% > {float(vram_limit_percent):.0f}% limit") + if not idle_ok: reasons.append( - f"only {free_gb:.1f}GB VRAM free (< {float(min_free_gb):.1f}GB)") + f"device busy: HCU {util_peak:.1f}% > {float(util_tolerance):.1f}%") return { "gpu": gpu_id, "usable": usable, "free_gb": round(free_gb, 2), "total_gb": round(total, 1), - "min_free_gb": float(min_free_gb), + "vram_limit_percent": float(vram_limit_percent), + "util_tolerance": float(util_tolerance), + "vram_peak_percent": round(vram, 1), + "util_peak_percent": round(util_peak, 1), "vram_percent": round(vram, 1), "util_percent": round(util, 1), "power_w": round(power, 1), "temp_c": round(float(state.get("temp_c") or 0.0), 1), "foreign_pids": foreign[:8], + "unavailable": unavailable, + "state_source": str(state.get("state_source") or "unknown"), "measurement_suspect": bool(any( r for r in reasons if "VRAM free" not in r)), "reasons": reasons, @@ -189,36 +388,48 @@ def check_gpu(gpu_id: int, state: Dict[str, float], *, def preflight_gpus(gpu_ids: List[int], *, samples: int = 3, interval_s: float = 3.0, enabled: bool = True, - min_free_gb: float = DEFAULT_MIN_FREE_GB, + vram_limit_percent: float = DEFAULT_VRAM_LIMIT_PERCENT, + util_tolerance: float = DEFAULT_UTIL_TOLERANCE, **kwargs: Any) -> Dict[str, Any]: - """Return ``{gpu_id: check}`` plus a preferred order (clean devices first).""" + """Return ``{gpu_id: check}`` plus a preferred order (clean devices first). + + ``clean_ids`` is the gate result: the only devices a measurement may run + on. ``blocked_ids`` are the devices that failed it (busy, full, or + unreadable) — callers wait and re-check rather than falling back to them. + """ if not enabled: return {"enabled": False, "gpus": {}, "preferred": list(gpu_ids), - "suspect_ids": []} + "suspect_ids": [], "clean_ids": [], "blocked_ids": [], + "over_limit_ids": []} states = sample_gpu_state(gpu_ids, samples=samples, interval_s=interval_s) foreign = foreign_kfd_pids() checks: Dict[int, Dict[str, Any]] = {} for gpu_id in gpu_ids: checks[gpu_id] = check_gpu( - gpu_id, states.get(gpu_id) or {}, min_free_gb=min_free_gb, + gpu_id, states.get(gpu_id) or {}, + vram_limit_percent=vram_limit_percent, + util_tolerance=util_tolerance, foreign_pids=foreign, **kwargs, ) usable = [g for g in gpu_ids if checks[g]["usable"]] clean = [g for g in usable if not checks[g]["measurement_suspect"]] suspect = [g for g in usable if checks[g]["measurement_suspect"]] - busy_but_usable = [g for g in gpu_ids if not checks[g]["usable"]] - # Devices over the VRAM limit stay usable in principle (configuration - # choice), but when any device is within the limit they are pushed to the - # back so concurrent children do not collide with a full device. - preferred = (clean + suspect + busy_but_usable) if not usable \ - else (clean + suspect) + blocked = [g for g in gpu_ids if not checks[g]["usable"]] + # Only gate-passing devices are ever offered for a measurement: a busy or + # unreadable card is left out so the caller waits (30 min, x48) instead of + # producing a measurement that another workload has polluted. + preferred = clean + suspect return { "enabled": True, "gpus": checks, "preferred": preferred, "clean_ids": clean, "suspect_ids": suspect, - "over_limit_ids": busy_but_usable, + "blocked_ids": blocked, + "over_limit_ids": [g for g in blocked + if checks[g]["vram_peak_percent"] + > float(vram_limit_percent)], + "gate_reasons": {str(g): checks[g]["reasons"] for g in blocked}, "foreign_kfd_pids": foreign[:8], "sampled_at": time.time(), "samples": samples, diff --git a/metainfer/tasks/dcu_kernel_auto_opt/orchestrator/harness_io.py b/metainfer/tasks/dcu_kernel_auto_opt/orchestrator/harness_io.py index 82b09fc6..04833baa 100644 --- a/metainfer/tasks/dcu_kernel_auto_opt/orchestrator/harness_io.py +++ b/metainfer/tasks/dcu_kernel_auto_opt/orchestrator/harness_io.py @@ -1,17 +1,19 @@ """Harness workspace IO: locate the evolvable-harness seed, read components, and copy a seed workspace to a destination. -This is the first piece of AHE "component observability" for dcu_kernel_auto_opt: -harness components (gates, and later prompts/planner/skills/tools/middleware/ -memory) are seeded as files under ``harness_default/`` so they can become a -versioned, evolvable workspace driven by the ``harness_evolve`` outer loop. - -Slice-1 semantics (important): this module is **read-only/additive**. -Runtime pipelines still read the current Python constants (``config.py``, -``w8a8_pipeline.py``); ``harness_io`` only exposes the seed for inspection and -copying. Consistency tests guard the YAML seed against the Python constants so -the two sources cannot drift before the loader/renderer wiring lands in a later -slice (wiring will make the harness workspace the single source of truth). +This is the AHE "component observability" layer for dcu_kernel_auto_opt: +harness components (gates, planner policy/catalog, systemprompt, and later +skills/tools/middleware/memory) are seeded as files under ``harness_default/`` +so they form a versioned, evolvable workspace driven by the ``harness_evolve`` +outer loop. + +Wiring status: ``gates.yaml`` is read through :mod:`gate_policy`, +``planner_policy.yaml``/``planner_catalog.yaml`` through :mod:`planner`, and +``systemprompt/round_strategy.yaml`` through :mod:`round_strategy`. Each of +those falls back to built-in defaults when its file is absent, so an untouched +harness behaves exactly as it did before the component was externalised. +Consistency tests guard every YAML seed against its Python defaults so the two +cannot drift silently. """ from __future__ import annotations @@ -63,6 +65,46 @@ def _load_yaml(path: Path) -> Dict[str, Any]: return data if isinstance(data, dict) else {} +def env_root() -> Path: + """The harness root as the *environment* defines it (no explicit argument). + + Same resolution as :func:`harness_root`, named separately so components + that already accept an explicit ``root`` argument (``gate_policy``, + ``planner``, ``round_strategy``) can share one implementation and one + cache key. + """ + return harness_root() + + +def load_component_file(relative: str, root: Path | None = None) -> Path: + """Path of one component file inside a harness workspace.""" + return (root or harness_root()) / str(relative) + + +def load_component_yaml(relative: str, root: Path | None = None, + ) -> tuple[Dict[str, Any], str]: + """Read one YAML component. Returns ``(data, error)``; never raises. + + A component file that is absent yields ``({}, "")`` — "not present" is a + normal state that makes the caller fall back to its built-in defaults. A + file that exists but cannot be parsed yields ``({}, "")`` so the + caller can report it instead of silently ignoring a hand-edited mistake. + """ + path = load_component_file(relative, root) + if not path.is_file(): + return {}, "" + try: + with path.open("r", encoding="utf-8") as handle: + data = yaml.safe_load(handle) + except (OSError, ValueError, yaml.YAMLError) as exc: + return {}, f"{relative}: {exc}" + if data is None: + return {}, "" + if not isinstance(data, dict): + return {}, f"{relative}: expected a mapping at the top level" + return data, "" + + def load_manifest(root: Path | None = None) -> Dict[str, Any]: """Load the component inventory (manifest.yaml) of a harness workspace.""" root = (root or harness_root()) diff --git a/metainfer/tasks/dcu_kernel_auto_opt/orchestrator/planner.py b/metainfer/tasks/dcu_kernel_auto_opt/orchestrator/planner.py index e4ef12c1..372853d8 100644 --- a/metainfer/tasks/dcu_kernel_auto_opt/orchestrator/planner.py +++ b/metainfer/tasks/dcu_kernel_auto_opt/orchestrator/planner.py @@ -1,17 +1,21 @@ -"""State-conditioned optimization plan selector (v0, not yet wired). +"""State-conditioned optimization plan selector. -DKAO currently chooses the next optimization direction with the round menus in -``prompts.py::w8a8_round_strategy`` (keyed by iteration number). This module is -the first step toward a *state-conditioned* plan selector that decides "what to -try this round" from the current measured state (history + PMC + budget), per -the design in ``docs/dkao_harness_eval_protocol.md`` / M1 slice-2. +DKAO chooses the next optimization direction in one of two ways: -Slice-2 semantics: -- **Additive and inert**: nothing in the runtime pipelines calls this module yet. - ``prompts.py``/``w8a8_pipeline.py`` keep their current behaviour unchanged. +* ``METAINFER_PLANNER`` unset — the round menus in + ``prompts.py::w8a8_round_strategy`` (keyed by iteration number; the wording of + those menus is now data in ``systemprompt/round_strategy.yaml``); +* ``METAINFER_PLANNER=1`` — this module, choosing from the current measured + state (history + PMC + budget), per ``docs/dkao_harness_eval_protocol.md`` / + M1 slice-2. ``w8a8_pipeline`` reaches it through ``choose_plan_from_history`` + and records every decision in the worker's ``planner_plans.jsonl``. + +Semantics: +- **Opt-in**: the pipeline consults this module only when ``METAINFER_PLANNER`` + is set; with it unset, behaviour is byte-identical to the legacy menus. - The plan catalog lives as data in ``harness_default/planner_catalog.yaml`` (read through ``harness_io`` with ``METAINFER_HARNESS_ROOT`` override) so it is - already an evolvable harness component; this module only consumes it. + an evolvable harness component; this module only consumes it. - Selection layering: P0 repair -> P1 budget/phase -> P2 bottleneck signature -> P3 coverage / anti-loop -> P4 fallback (legacy round-menu approximation). Rules are deterministic pure functions, tested without GPU. diff --git a/metainfer/tasks/dcu_kernel_auto_opt/orchestrator/prompts.py b/metainfer/tasks/dcu_kernel_auto_opt/orchestrator/prompts.py index fe80a0c7..cc56ee10 100644 --- a/metainfer/tasks/dcu_kernel_auto_opt/orchestrator/prompts.py +++ b/metainfer/tasks/dcu_kernel_auto_opt/orchestrator/prompts.py @@ -7,6 +7,12 @@ from pathlib import Path from typing import Any, Dict, Optional +from .round_strategy import ( + APPEND_GRID_WARNING as APPEND_GRID_WARNING_TEXT, + APPEND_SPLIT_CANDIDATES as APPEND_SPLIT_CANDIDATES_TEXT, + LATE_START_FLOOR, + load_round_strategy, +) HARNESS_PATH = ( Path(__file__).resolve().parent.parent / "assets" / "w8a8_bench.py" @@ -156,6 +162,23 @@ def w8a8_strategy_guidance( one launch geometry onto all regimes.""" +def _strategy_append(suffix: str, text: str, **fields: Any) -> str: + """Render one evidence suffix, formatted and separated by a leading space. + + A harness that renames a format field (``{cu_count}`` -> ``{cus}``) must + yield a weaker suffix, never a crash: a malformed harness is a performance + problem, not a reason to abort a round. The separator lives here so the + built-in text and the harness template render byte-identically. + """ + if not suffix: + return "" + try: + rendered = str(suffix).format(**fields) + except (KeyError, IndexError, ValueError): + rendered = text + return " " + rendered if rendered else "" + + def w8a8_round_strategy( shape: Dict[str, Any], iteration: int, @@ -164,11 +187,20 @@ def w8a8_round_strategy( max_iterations: int = 10, isa_policy: Dict[str, Any] | None = None, ) -> str: - """Choose an architecture-first round plan from measured evidence.""" + """Choose an architecture-first round plan from measured evidence. + + The *decision* is code (measured history, phase, PMC evidence and the M + region pick the branch); the *wording* of each branch is data, loaded from + the harness workspace's ``systemprompt/round_strategy.yaml`` and falling + back to the historical built-in texts. See + :mod:`orchestrator.round_strategy`. + """ history = history or [] pmc_evidence = pmc_evidence or {} isa_policy = isa_policy or {} + strategy = load_round_strategy() + faster_wrong = [ record for record in history if record.get("correctness_passed") is False @@ -179,14 +211,11 @@ def w8a8_round_strategy( candidate = max( faster_wrong, key=lambda record: float(record["speedup"]) ) - return ( - "Highest priority: repair the faster but incorrect candidate from " - f"iteration {candidate.get('iteration')} (measured speedup " - f"{candidate.get('speedup')}x). Read its archived source at " - f"`{candidate.get('artifact_dir')}` and preserve the fast mapping. " - "Fix only the smallest correctness defect: signed int8 unpacking, " - "tail bounds, scale indexing, bf16 conversion, or a race. Do not " - "replace it with an unrelated architecture." + return strategy.text( + "repair.faster_wrong", + iteration=candidate.get("iteration"), + speedup=candidate.get("speedup"), + artifact_dir=candidate.get("artifact_dir"), ) if history: @@ -195,198 +224,41 @@ def w8a8_round_strategy( if any(token in failure for token in ( "timeout", "timed out", "killed", "no result", "exit 143", )): - return ( - "The preceding attempt failed in the agent infrastructure. " - "Return to the accepted best source and start a new bounded " - "experiment; do not repair or replay a partially written " - "candidate. This failure does not count as a completed " - "optimization round." - ) + return strategy.text("repair.infrastructure_failure") if latest.get("build_success") is False: - return ( - "Repair the immediately preceding candidate from iteration " - f"{latest.get('iteration')} at `{latest.get('artifact_dir')}`. " - "Keep its strategy and make only the minimum compile/API/syntax " - "correction; do not start another redesign this round." + return strategy.text( + "repair.build_failure", + iteration=latest.get("iteration"), + artifact_dir=latest.get("artifact_dir"), ) phase = str(isa_policy.get("phase") or "hip_only") if phase == "isa_guided_hip": completed = int(isa_policy.get("valid_isa_guided_rounds") or 0) - return ( - "ISA-guided HIP round. This is successful ISA experiment " - f"{completed + 1} of at least 2. Select one measured memory or " - "compute bottleneck, compare the exact primary-kernel ISA with " - "the preceding code object, and make one HIP/DUMMA/intrinsic " - "code-shaping change. Raw inline asm remains forbidden. Record " - "a compiler limitation only when the before/after binary proves " - "it and name the exact target instructions." - ) + return strategy.text("phases.isa_guided_hip", completed=completed + 1) if phase == "conditional_inline_asm": - return ( - "Conditional inline-asm experiment. Target only the compiler " - "limitation and exact instructions verified by the immediately " - "preceding ISA-guided HIP round. Keep the asm block minimal, " - "preserve complete constraints/clobbers, and reject it unless " - "the candidate ISA, exact correctness, median, P90, and resources " - "all validate. Do not write raw global/buffer/flat loads or MMAC." - ) + return strategy.text("phases.conditional_inline_asm") - small_m = { - 1: "Vectorize contiguous K loads with exact signed-int8 semantics.", - 2: ( - "Increase instruction-level parallelism with independent int32 " - "accumulators or adjacent N outputs; avoid per-K-tile LDS barriers." - ), - 3: ( - "Stage all of tiny A once with at most one barrier, or tune unroll " - "one step if whole-A staging is not cheaper." - ), - 4: ( - "Change one launch variable only: waves per block, N columns per " - "wave, or unroll factor." - ), - 5: ( - "HIP-only memory round: change one vector-load width, contiguous " - "N mapping, or whole-A reuse decision. Raw inline asm is forbidden." - ), - 6: ( - "HIP-only pipeline round: reduce one dependency chain or barrier " - "using ordinary HIP/intrinsics. Raw inline asm is forbidden." - ), - 7: ( - "HIP-only resource round: tune one block size, unroll factor, or " - "live range while preserving coalescing. Raw inline asm is forbidden." - ), - 8: ( - "HIP-only consolidation round: revisit the fastest correct archived " - "mapping and make one final architecture/codegen improvement. Raw " - "inline asm is forbidden." - ), - } - m16 = { - 1: ( - "Establish a minimal 16x16x32 DUMMA tile with the exact API below, " - "one wave per output tile and explicit int32 accumulation. If the " - "seed already has a correct DUMMA kernel, preserve it and instead " - "test the smallest one-wave-per-block, one-N-tile geometry with no " - "cross-wave barrier; do not spend the round reimplementing it." - ), - 2: ( - "Architecture round: measure grid parallelism before polishing. " - "Explore one complete launch geometry among 1/2/4 waves per block " - "and 1/2/4 adjacent N tiles. Prefer enough independent blocks to " - "cover at least all device CUs; report grid_blocks, waves_per_block " - "and estimated_active_cus in proposal.json." - ), - 3: ( - "Architecture round: if the unsplit grid has fewer than two " - "blocks per device CU and K >= 1024, implement and measure " - "split-K=2 plus at least one CU-aligned candidate (which may be " - "non-power-of-two), or test a one-wave zero-barrier geometry that " - "reaches the same parallelism. Write int32 partials into the " - "caller workspace and include the combine+scale kernel in the " - "timed Graph." - ), - 4: ( - "Architecture/pipeline round: explore one of multi-N-tile reuse, " - "A-only staging, or bounded register/LDS prefetch. Retain enough " - "blocks to cover all CUs, state which A/B bytes are reused, and " - "measure whether the change improves normal median/P90." - ), - 5: ( - "HIP-only packed-weight/staging round: compare one packed layout, " - "A-only staging, or B-only staging design. Raw inline asm is forbidden." - ), - 6: ( - "Pipeline round: choose exactly one staging family from direct, " - "A-only LDS, B-only LDS, or A+B LDS using L2/VMEM evidence. Use " - "coalesced 8- or 16-byte cooperative loads and report HBM/LDS byte " - "changes; do not claim asynchronous overlap without evidence." - ), - 7: ( - "Pipeline round: compare single buffering with double buffering " - "only when K>=1024, L2 hit rate is below 70%, and the doubled LDS " - "budget stays below 48 KiB. Count barriers per K step." - ), - 8: ( - "HIP-only resource round: tune one occupancy limiter using actual " - "PMC evidence: waves per block, VGPR live range, LDS footprint, " - "or spill removal. Do not trade repeated HBM reads for occupancy." - ), - 9: ( - "Late ISA-diagnosis round. Only if the control-plane plateau gate " - "is open, use one selected ISA Skill and trusted disassembly to " - "shape compiler output through HIP/DUMMA/intrinsics. Raw inline " - "asm remains forbidden. Otherwise continue HIP-only exploration." - ), - 10: ( - "Final conditional inline-asm round. Raw asm is allowed only when " - "the control plane confirms a HIP plateau and the prior ISA-guided " - "round recorded one concrete compiler limitation plus target " - "instructions. Otherwise make one HIP-only consolidation change." - ), - } - large_m = { - 1: ( - "Establish a correct DUMMA throughput baseline using a 2-D " - "macro-tile. Benchmark 64x64, 64x128, and 128x64 block tiles; " - "record waves per block, VGPRs, LDS bytes, occupancy and TOPS." - ), - 2: ( - "Operand-reuse round: compare direct loads with cooperative " - "A+B LDS staging. Quantify A/B reuse per macro-tile and use " - "vectorized coalesced global loads with a bank-safe LDS layout." - ), - 3: ( - "Pipeline round: compare single and double buffering across K " - "tiles. Retain double buffering only when ISA/PMC evidence shows " - "reduced VMEM stalls without harmful LDS or occupancy growth." - ), - 4: ( - "Tile-shape round: tune M-tile versus N-tile aspect ratio for " - "this exact M/N/K, balancing B reuse, A reuse and enough blocks " - "to occupy every CU. Do not inherit decode launch geometry." - ), - 5: ( - "Packing round: test one weight packing/swizzle that makes each " - "DUMMA B tile vector-loadable and LDS-bank-safe. Include packing " - "outside timing and validate the graph-stable packed layout." - ), - 6: ( - "Epilogue round: fuse per-row and per-column scales, bf16 " - "conversion and the final coalesced store into the compute " - "kernel; remove any unnecessary workspace/combine pass." - ), - 7: ( - "Compute-pipeline round: tune DUMMA issue grouping, prefetch " - "distance and accumulator independence using ISA stall evidence. " - "Raw inline asm remains forbidden." - ), - 8: ( - "Resource round: tune waves per block, VGPR live ranges and LDS " - "footprint from measured occupancy. Recheck the best tile family " - "with normal median/P90 measurements." - ), - 9: m16[9], - 10: m16[10], - } m = int(shape.get("M", 0)) if m >= 128: - portfolio = large_m + regime = "large" elif m < 16: - portfolio = small_m + regime = "small" else: - portfolio = m16 - late_start = max(9, max_iterations - 1) + regime = "m16" + late_start = max(LATE_START_FLOOR, max_iterations - 1) if m < 16: if iteration < late_start: - return portfolio.get(iteration, portfolio[8]) - return portfolio[9] if iteration < max_iterations else portfolio[10] + return strategy.mandate(regime, iteration) or strategy.slot(regime, 8) + # The <16 portfolio has no dedicated late slot: it keeps its final + # consolidation round to the end (it used to raise KeyError here for + # every max_iterations in 9..10 with iteration >= late_start). + return strategy.slot(regime, 8) if iteration < late_start: - decision = portfolio.get(iteration, portfolio[8]) + decision = strategy.mandate(regime, iteration) or strategy.slot(regime, 8) else: - decision = portfolio[9] if iteration < max_iterations else portfolio[10] + decision = strategy.slot( + regime, LATE_START_FLOOR if iteration < max_iterations else 10) grid_blocks = pmc_evidence.get("grid_blocks") cu_count = pmc_evidence.get("device_cu_count") split_candidates = split_k_candidate_set( @@ -394,26 +266,20 @@ def w8a8_round_strategy( cu_count=int(cu_count) if isinstance(cu_count, (int, float)) else 0, ) if iteration >= 2 and split_candidates: - decision += ( - " Trusted occupancy-probe split candidates for the measured CU " - f"count are {split_candidates}. They include non-power-of-two " - "values where useful, are not a whitelist, and must fit the " - "workspace and stage-alignment constraints. Explore outside this " - "set when evidence supports it." - ) + decision += _strategy_append( + strategy.append.get("split_candidates"), + APPEND_SPLIT_CANDIDATES_TEXT, + split_candidates=split_candidates) if ( iteration >= 2 and isinstance(grid_blocks, (int, float)) and isinstance(cu_count, (int, float)) and grid_blocks < 2 * cu_count ): - decision += ( - f" Trusted control-plane warning: current grid has {grid_blocks} " - f"blocks for {cu_count} CUs, below the two-blocks-per-CU latency-" - "hiding target. Before micro-optimization, benchmark a finer " - "one-wave zero-barrier grid or multiple legal split-K candidates " - "including combine cost." - ) + decision += _strategy_append( + strategy.append.get("grid_warning"), + APPEND_GRID_WARNING_TEXT, + grid_blocks=grid_blocks, cu_count=cu_count) return decision diff --git a/metainfer/tasks/dcu_kernel_auto_opt/orchestrator/round_strategy.py b/metainfer/tasks/dcu_kernel_auto_opt/orchestrator/round_strategy.py new file mode 100644 index 00000000..25e9feb2 --- /dev/null +++ b/metainfer/tasks/dcu_kernel_auto_opt/orchestrator/round_strategy.py @@ -0,0 +1,454 @@ +"""Round-strategy texts: the per-round mandate menu, as evolvable data. + +``w8a8_round_strategy`` in :mod:`prompts` used to embed every round mandate as +a Python string literal, so nothing about it could be versioned, reviewed or +evolved. This module is the *data* half of that function: + +* :data:`BUILTIN_STRATEGY` holds the historical texts, byte-identical to the + literals they replace, so an un-evolved harness behaves exactly as before; +* :func:`load_round_strategy` reads ``/systemprompt/ + round_strategy.yaml`` when the ``systemprompt`` component is marked wired in + ``manifest.yaml``, and falls back to the built-in defaults otherwise + (missing file, unwired component, malformed YAML — the same layering + ``gates.yaml`` and ``planner_policy.yaml`` already use). + +The decision *logic* stays in code: which branch applies is determined by +measured state (history, phase, PMC evidence, M, iteration budget), and only +the wording of each branch is data. That split is deliberate — it keeps +"what we tell the worker to do this round" evolvable without making the +round-selection state machine itself writable. + +Templates use ``str.format`` fields. The only fields any template may use are +``iteration``, ``speedup``, ``artifact_dir``, ``completed``, ``split_candidates``, +``grid_blocks`` and ``cu_count``. +""" + +from __future__ import annotations + +import os +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Dict, List, Mapping, Optional, Tuple + +from .harness_io import env_root, load_component_yaml + +#: Component name in ``manifest.yaml``; ``wired: false`` makes this file inert. +COMPONENT = "systemprompt" +#: Path of the strategy menu inside a harness workspace. +RELATIVE_PATH = "systemprompt/round_strategy.yaml" + +#: ``max_iterations`` below this makes the late-round branch unreachable. +LATE_START_FLOOR = 9 + +# --------------------------------------------------------------------------- # +# Built-in defaults (the historical literals, verbatim) +# --------------------------------------------------------------------------- # + +REPAIR_UNKNOWN = ( + "Highest priority: repair the faster but incorrect candidate from " + "iteration {iteration} (measured speedup " + "{speedup}x). Read its archived source at " + "`{artifact_dir}` and preserve the fast mapping. " + "Fix only the smallest correctness defect: signed int8 unpacking, " + "tail bounds, scale indexing, bf16 conversion, or a race. Do not " + "replace it with an unrelated architecture." +) + +REPAIR_INFRASTRUCTURE = ( + "The preceding attempt failed in the agent infrastructure. " + "Return to the accepted best source and start a new bounded " + "experiment; do not repair or replay a partially written " + "candidate. This failure does not count as a completed " + "optimization round." +) + +REPAIR_BUILD = ( + "Repair the immediately preceding candidate from iteration " + "{iteration} at `{artifact_dir}`. " + "Keep its strategy and make only the minimum compile/API/syntax " + "correction; do not start another redesign this round." +) + +ISA_GUIDED = ( + "ISA-guided HIP round. This is successful ISA experiment " + "{completed} of at least 2. Select one measured memory or " + "compute bottleneck, compare the exact primary-kernel ISA with " + "the preceding code object, and make one HIP/DUMMA/intrinsic " + "code-shaping change. Raw inline asm remains forbidden. Record " + "a compiler limitation only when the before/after binary proves " + "it and name the exact target instructions." +) + +CONDITIONAL_INLINE_ASM = ( + "Conditional inline-asm experiment. Target only the compiler " + "limitation and exact instructions verified by the immediately " + "preceding ISA-guided HIP round. Keep the asm block minimal, " + "preserve complete constraints/clobbers, and reject it unless " + "the candidate ISA, exact correctness, median, P90, and resources " + "all validate. Do not write raw global/buffer/flat loads or MMAC." +) + +#: Evidence-qualified suffixes appended after a portfolio mandate. The leading +#: space is added by the caller, never stored here: the same text is used both +#: as the built-in fallback and as the template loaded from the harness file, +#: and the two paths have to render identically. +APPEND_SPLIT_CANDIDATES = ( + "Trusted occupancy-probe split candidates for the measured CU " + "count are {split_candidates}. They include non-power-of-two " + "values where useful, are not a whitelist, and must fit the " + "workspace and stage-alignment constraints. Explore outside this " + "set when evidence supports it." +) + +APPEND_GRID_WARNING = ( + "Trusted control-plane warning: current grid has {grid_blocks} " + "blocks for {cu_count} CUs, below the two-blocks-per-CU latency-" + "hiding target. Before micro-optimization, benchmark a finer " + "one-wave zero-barrier grid or multiple legal split-K candidates " + "including combine cost." +) + +#: Shared late-round mandates (the 9th/10th slot of the >=128 and 16..127 +#: portfolios; the <16 portfolio never reaches them). +LATE_ISA_DIAGNOSIS = ( + "Late ISA-diagnosis round. Only if the control-plane plateau gate " + "is open, use one selected ISA Skill and trusted disassembly to " + "shape compiler output through HIP/DUMMA/intrinsics. Raw inline " + "asm remains forbidden. Otherwise continue HIP-only exploration." +) + +LATE_CONDITIONAL_INLINE_ASM = ( + "Final conditional inline-asm round. Raw asm is allowed only when " + "the control plane confirms a HIP plateau and the prior ISA-guided " + "round recorded one concrete compiler limitation plus target " + "instructions. Otherwise make one HIP-only consolidation change." +) + +SMALL: Dict[int, str] = { + 1: "Vectorize contiguous K loads with exact signed-int8 semantics.", + 2: ( + "Increase instruction-level parallelism with independent int32 " + "accumulators or adjacent N outputs; avoid per-K-tile LDS barriers." + ), + 3: ( + "Stage all of tiny A once with at most one barrier, or tune unroll " + "one step if whole-A staging is not cheaper." + ), + 4: ( + "Change one launch variable only: waves per block, N columns per " + "wave, or unroll factor." + ), + 5: ( + "HIP-only memory round: change one vector-load width, contiguous " + "N mapping, or whole-A reuse decision. Raw inline asm is forbidden." + ), + 6: ( + "HIP-only pipeline round: reduce one dependency chain or barrier " + "using ordinary HIP/intrinsics. Raw inline asm is forbidden." + ), + 7: ( + "HIP-only resource round: tune one block size, unroll factor, or " + "live range while preserving coalescing. Raw inline asm is forbidden." + ), + 8: ( + "HIP-only consolidation round: revisit the fastest correct archived " + "mapping and make one final architecture/codegen improvement. Raw " + "inline asm is forbidden." + ), +} + +M16: Dict[int, str] = { + 1: ( + "Establish a minimal 16x16x32 DUMMA tile with the exact API below, " + "one wave per output tile and explicit int32 accumulation. If the " + "seed already has a correct DUMMA kernel, preserve it and instead " + "test the smallest one-wave-per-block, one-N-tile geometry with no " + "cross-wave barrier; do not spend the round reimplementing it." + ), + 2: ( + "Architecture round: measure grid parallelism before polishing. " + "Explore one complete launch geometry among 1/2/4 waves per block " + "and 1/2/4 adjacent N tiles. Prefer enough independent blocks to " + "cover at least all device CUs; report grid_blocks, waves_per_block " + "and estimated_active_cus in proposal.json." + ), + 3: ( + "Architecture round: if the unsplit grid has fewer than two " + "blocks per device CU and K >= 1024, implement and measure " + "split-K=2 plus at least one CU-aligned candidate (which may be " + "non-power-of-two), or test a one-wave zero-barrier geometry that " + "reaches the same parallelism. Write int32 partials into the " + "caller workspace and include the combine+scale kernel in the " + "timed Graph." + ), + 4: ( + "Architecture/pipeline round: explore one of multi-N-tile reuse, " + "A-only staging, or bounded register/LDS prefetch. Retain enough " + "blocks to cover all CUs, state which A/B bytes are reused, and " + "measure whether the change improves normal median/P90." + ), + 5: ( + "HIP-only packed-weight/staging round: compare one packed layout, " + "A-only staging, or B-only staging design. Raw inline asm is forbidden." + ), + 6: ( + "Pipeline round: choose exactly one staging family from direct, " + "A-only LDS, B-only LDS, or A+B LDS using L2/VMEM evidence. Use " + "coalesced 8- or 16-byte cooperative loads and report HBM/LDS byte " + "changes; do not claim asynchronous overlap without evidence." + ), + 7: ( + "Pipeline round: compare single buffering with double buffering " + "only when K>=1024, L2 hit rate is below 70%, and the doubled LDS " + "budget stays below 48 KiB. Count barriers per K step." + ), + 8: ( + "HIP-only resource round: tune one occupancy limiter using actual " + "PMC evidence: waves per block, VGPR live range, LDS footprint, " + "or spill removal. Do not trade repeated HBM reads for occupancy." + ), + 9: LATE_ISA_DIAGNOSIS, + 10: LATE_CONDITIONAL_INLINE_ASM, +} + +LARGE: Dict[int, str] = { + 1: ( + "Establish a correct DUMMA throughput baseline using a 2-D " + "macro-tile. Benchmark 64x64, 64x128, and 128x64 block tiles; " + "record waves per block, VGPRs, LDS bytes, occupancy and TOPS." + ), + 2: ( + "Operand-reuse round: compare direct loads with cooperative " + "A+B LDS staging. Quantify A/B reuse per macro-tile and use " + "vectorized coalesced global loads with a bank-safe LDS layout." + ), + 3: ( + "Pipeline round: compare single and double buffering across K " + "tiles. Retain double buffering only when ISA/PMC evidence shows " + "reduced VMEM stalls without harmful LDS or occupancy growth." + ), + 4: ( + "Tile-shape round: tune M-tile versus N-tile aspect ratio for " + "this exact M/N/K, balancing B reuse, A reuse and enough blocks " + "to occupy every CU. Do not inherit decode launch geometry." + ), + 5: ( + "Packing round: test one weight packing/swizzle that makes each " + "DUMMA B tile vector-loadable and LDS-bank-safe. Include packing " + "outside timing and validate the graph-stable packed layout." + ), + 6: ( + "Epilogue round: fuse per-row and per-column scales, bf16 " + "conversion and the final coalesced store into the compute " + "kernel; remove any unnecessary workspace/combine pass." + ), + 7: ( + "Compute-pipeline round: tune DUMMA issue grouping, prefetch " + "distance and accumulator independence using ISA stall evidence. " + "Raw inline asm remains forbidden." + ), + 8: ( + "Resource round: tune waves per block, VGPR live ranges and LDS " + "footprint from measured occupancy. Recheck the best tile family " + "with normal median/P90 measurements." + ), + 9: LATE_ISA_DIAGNOSIS, + 10: LATE_CONDITIONAL_INLINE_ASM, +} + +BUILTIN_STRATEGY: Dict[str, Any] = { + "repair": { + "faster_wrong": REPAIR_UNKNOWN, + "infrastructure_failure": REPAIR_INFRASTRUCTURE, + "build_failure": REPAIR_BUILD, + }, + "phases": { + "isa_guided_hip": ISA_GUIDED, + "conditional_inline_asm": CONDITIONAL_INLINE_ASM, + }, + "append": { + "split_candidates": APPEND_SPLIT_CANDIDATES, + "grid_warning": APPEND_GRID_WARNING, + }, + "portfolios": { + "small": {str(k): v for k, v in SMALL.items()}, + "m16": {str(k): v for k, v in M16.items()}, + "large": {str(k): v for k, v in LARGE.items()}, + }, +} + +#: Regime keys a harness file may define; a subset falls back per-key. +_PORTFOLIOS = ("small", "m16", "large") + + +# --------------------------------------------------------------------------- # +# Loading +# --------------------------------------------------------------------------- # + +_CACHE_KEY: Optional[Tuple[str, float, float]] = None +_CACHE_VALUE: Optional["RoundStrategy"] = None + + +@dataclass(frozen=True) +class RoundStrategy: + """Round mandates for one harness workspace (built-in or harness file).""" + + repair: Mapping[str, str] + phases: Mapping[str, str] + append: Mapping[str, str] + portfolios: Mapping[str, Mapping[str, str]] + source: str = "builtin" + #: Non-fatal problems found while loading (reported, never raised). + notes: Tuple[str, ...] = field(default=()) + + def text(self, key: str, **fields: Any) -> str: + """Render one template by key path (``repair.build_failure``).""" + node: Any = { + "repair": self.repair, + "phases": self.phases, + "append": self.append, + } + for part in key.split("."): + node = (node or {}).get(part) if isinstance(node, Mapping) else None + if not isinstance(node, str) or not node: + return "" + try: + return node.format(**fields) + except (KeyError, IndexError, ValueError): + return node + + def mandate(self, regime: str, iteration: int) -> Optional[str]: + """The portfolio entry for one round, or None when it has no slot.""" + portfolio = self.portfolios.get(regime) or self.portfolios.get("m16") or {} + entry = portfolio.get(str(int(iteration))) + return entry if isinstance(entry, str) and entry else None + + def slot(self, regime: str, slot: int) -> str: + """A numbered slot, falling back within the portfolio (never raises).""" + portfolio = self.portfolios.get(regime) or {} + for candidate in (str(int(slot)), str(LATE_START_FLOOR)): + entry = portfolio.get(candidate) + if isinstance(entry, str) and entry: + return entry + # Last resort: the highest-numbered slot the portfolio does define. + keys = sorted((k for k in portfolio if str(k).isdigit()), key=int) + return str(portfolio[keys[-1]]) if keys else "" + + +def builtin_strategy() -> Dict[str, Any]: + """A deep-ish copy of the historical defaults (safe to mutate).""" + return { + "repair": dict(BUILTIN_STRATEGY["repair"]), + "phases": dict(BUILTIN_STRATEGY["phases"]), + "append": dict(BUILTIN_STRATEGY["append"]), + "portfolios": { + name: dict(BUILTIN_STRATEGY["portfolios"][name]) + for name in _PORTFOLIOS + }, + } + + +def _component_wired(root: Optional[Path]) -> bool: + """``manifest.yaml`` decides whether the systemprompt component applies.""" + try: + from . import gate_policy as _gates + return _gates.component_wired(COMPONENT, root) + except Exception: # noqa: BLE001 - a missing manifest means "apply it" + return True + + +def _merge(defaults: Dict[str, Any], overlay: Mapping[str, Any], + notes: List[str]) -> Dict[str, Any]: + """Overlay a harness file onto the defaults, key by key. + + A key the file omits keeps its built-in value, so a partial harness file + never blanks out a branch. Wrong types are ignored and reported in + ``notes`` instead of raising: a malformed harness must not stop a run. + """ + out = builtin_strategy() + if not isinstance(overlay, Mapping): + notes.append("strategy file is not a mapping; built-in defaults used") + return out + for section in ("repair", "phases", "append"): + data = overlay.get(section) + if data is None: + continue + if not isinstance(data, Mapping): + notes.append(f"{section}: expected a mapping, ignored") + continue + for key, value in data.items(): + if isinstance(value, str) and value.strip(): + out[section][str(key)] = value + else: + notes.append(f"{section}.{key}: expected a non-empty string, ignored") + portfolios = overlay.get("portfolios") + if portfolios is not None: + if not isinstance(portfolios, Mapping): + notes.append("portfolios: expected a mapping, ignored") + else: + for name, entries in portfolios.items(): + if name not in _PORTFOLIOS: + notes.append(f"portfolios.{name}: unknown regime, ignored") + continue + if not isinstance(entries, Mapping): + notes.append(f"portfolios.{name}: expected a mapping, ignored") + continue + for slot, value in entries.items(): + if isinstance(value, str) and value.strip(): + out["portfolios"][str(name)][str(slot)] = value + else: + notes.append( + f"portfolios.{name}.{slot}: expected a non-empty " + "string, ignored") + return out + + +def load_round_strategy(root: Optional[Path] = None, + *, use_cache: bool = True) -> RoundStrategy: + """Effective round-strategy texts for this harness workspace. + + Order: ``/systemprompt/round_strategy.yaml`` when the component is + wired -> built-in defaults. Cached on (path, file mtime, manifest mtime) so + a mid-run harness swap is picked up, exactly like :mod:`gate_policy`. + """ + global _CACHE_KEY, _CACHE_VALUE + + workspace = Path(root) if root is not None else env_root() + path = workspace / RELATIVE_PATH + manifest = workspace / "manifest.yaml" + try: + key = (str(path), + path.stat().st_mtime if path.is_file() else 0.0, + manifest.stat().st_mtime if manifest.is_file() else 0.0) + except OSError: + key = (str(path), 0.0, 0.0) + + if use_cache and _CACHE_KEY == key and _CACHE_VALUE is not None: + return _CACHE_VALUE + + notes: List[str] = [] + source = "builtin" + data: Dict[str, Any] = builtin_strategy() + if not _component_wired(workspace): + notes.append(f"{COMPONENT} component is wired:false; built-in defaults used") + else: + overlay, error = load_component_yaml(RELATIVE_PATH, root=workspace) + if error: + notes.append(error) + elif overlay: + data = _merge(data, overlay, notes) + source = str(path) + + value = RoundStrategy( + repair=data["repair"], phases=data["phases"], append=data["append"], + portfolios=data["portfolios"], source=source, notes=tuple(notes), + ) + if use_cache: + _CACHE_KEY, _CACHE_VALUE = key, value + return value + + +def reset_cache() -> None: + """Drop the memoised strategy (tests and mid-run swaps).""" + global _CACHE_KEY, _CACHE_VALUE + _CACHE_KEY, _CACHE_VALUE = None, None diff --git a/metainfer/tasks/dcu_kernel_auto_opt/orchestrator/w8a8_baselines.py b/metainfer/tasks/dcu_kernel_auto_opt/orchestrator/w8a8_baselines.py index c5f8df79..0aacc03d 100644 --- a/metainfer/tasks/dcu_kernel_auto_opt/orchestrator/w8a8_baselines.py +++ b/metainfer/tasks/dcu_kernel_auto_opt/orchestrator/w8a8_baselines.py @@ -225,6 +225,35 @@ (8, 4096, 6144, 2048): ("o_proj", 54775.797), (8, 4096, 512, 6144): ("shared_gate_up_proj", 21254.589), (8, 4096, 6144, 256): ("shared_down_proj", 17700.262), + # Model-catalog TP8 M=8 short-decode baselines measured on 2026-09-20 + # (worker29, gfx928) with the same protocol as the 2026-08-12 catalog: + # lmslim int8_utils.matmul_kernel (the W8A8 Triton baseline), GPU events, + # CUDA Graph replay, hot cache, warmups=10, samples=20, launches/sample=5, + # BM16/BN32/BK256 (matmul_int8 default for M<=32). Covers the Hy3 / + # MiniMax M3 / GLM5.2 TP8 operators at M=8 (see + # MODEL_TP8_EXTRA_OPTIMIZATION_M_VALUES in the operator API contract). + # Each value is the per-shape median of three independent passes + # (tools/baseline/bench_triton_tp8_m8.py -> tp8_m8_graph_run{1,2,3}.json, + # merged view in tp8_m8_graph_merged.json). The median keeps the estimate + # robust where a single pass was disturbed by a co-tenant on the shared + # card (e.g. minimax shared_down_proj pass 2: 33.760 vs 19.968/19.984). + # Cross-pass spread is <= 1.6% for 12 of 15 shapes, 5.8%/6.8% for + # glm52 q_b_proj / shared_down_proj. + (8, 8, 1280, 4096): ("qkv_proj", 66.432), + (8, 8, 4096, 1024): ("o_proj", 31.472), + (8, 8, 384, 4096): ("shared_gate_up_proj", 57.008), + (8, 8, 4096, 192): ("shared_down_proj", 14.592), + (8, 8, 1280, 6144): ("qkv_proj", 98.865), + (8, 8, 1536, 6144): ("qkv_proj_and_indexer_qk", 99.473), + (8, 8, 6144, 1024): ("o_proj", 32.960), + (8, 8, 768, 6144): ("shared_gate_up_proj", 92.928), + (8, 8, 6144, 384): ("shared_down_proj", 19.984), + (8, 8, 2624, 6144): ("fused_qkv_a_proj", 87.200), + (8, 8, 2048, 2048): ("q_b_proj", 36.704), + (8, 8, 3584, 512): ("kv_b_proj", 15.808), + (8, 8, 6144, 2048): ("o_proj", 58.864), + (8, 8, 512, 6144): ("shared_gate_up_proj", 88.448), + (8, 8, 6144, 256): ("shared_down_proj", 15.136), } diff --git a/metainfer/tasks/dcu_kernel_auto_opt/orchestrator/w8a8_pipeline.py b/metainfer/tasks/dcu_kernel_auto_opt/orchestrator/w8a8_pipeline.py index 4ec2e402..ac9068a0 100644 --- a/metainfer/tasks/dcu_kernel_auto_opt/orchestrator/w8a8_pipeline.py +++ b/metainfer/tasks/dcu_kernel_auto_opt/orchestrator/w8a8_pipeline.py @@ -10,7 +10,7 @@ import time from concurrent.futures import ThreadPoolExecutor, as_completed from pathlib import Path -from typing import Any, Dict +from typing import Any, Dict, Sequence from metainfer.orchestrator.state import StateStore from metainfer.orchestrator.subagent_manager import AgentSpec, SubAgentManager @@ -33,6 +33,232 @@ _ENV_PLANNER = "METAINFER_PLANNER" +#: Sub-task measurement gate. A DKAO child iterates for hours, so the device it +#: was given can be taken over long after the parent admitted it. Every timed +#: benchmark and every PMC profile re-checks the gate first: a number produced +#: while another workload shares the card is not evidence, it is noise +#: (the same unchanged shape has been observed at 884us -> 3047us). +MEASUREMENT_GATE_VRAM_PERCENT = 90.0 +MEASUREMENT_GATE_UTIL_PERCENT = 0.0 +MEASUREMENT_GATE_WAIT_S = 1800 # re-check every 30 minutes +MEASUREMENT_GATE_MAX_WAITS = 48 # ... for at most 24 hours +#: where the per-check audit trail lands when the task did not pass a state dir +_GATE_LOG_NAME = "measurement_gate.jsonl" + + +class MeasurementGateBlocked(RuntimeError): + """The device did not pass ``VRAM <= 90% and HCU == 0`` in time. + + Raising this instead of measuring is the point: a blocked device yields no + number at all, which the parent treats as an environment failure and + re-measures later (and eventually stops the round) rather than scoring a + polluted reading as a kernel result. + """ + + def __init__(self, gpu: int, checks: int, reason: str = "") -> None: + self.gpu = gpu + self.checks = checks + self.gate_reason = reason + super().__init__( + f"GPU {gpu} did not pass the measurement gate (VRAM <=" + f"{MEASUREMENT_GATE_VRAM_PERCENT:.0f}% and HCU ==" + f"{MEASUREMENT_GATE_UTIL_PERCENT:.0f}%) after {checks} checks" + + (f": {reason}" if reason else "") + ) + + +def _gate_preflight_enabled() -> bool: + """``METAINFER_GPU_PREFLIGHT=off`` is the operator's opt-out of gating. + + Fails *closed*: if the gate module cannot be imported we still gate (all + ``preflight_enabled`` does is read the env/form switch, so a failure here + means something is wrong, not that gating was turned off). + """ + try: + from .gpu_preflight import preflight_enabled + except Exception: # noqa: BLE001 + return True + try: + return bool(preflight_enabled(None)) + except Exception: # noqa: BLE001 + return True + + +def _gate_wait_config(env: Dict[str, str]) -> tuple[int, int]: + def _int(name: str, default: int) -> int: + try: + return int(str(env.get(name) or "").strip() or default) + except (TypeError, ValueError): + return default + + return (_int("METAINFER_GATE_WAIT_SECONDS", MEASUREMENT_GATE_WAIT_S), + _int("METAINFER_GATE_MAX_WAITS", MEASUREMENT_GATE_MAX_WAITS)) + + +def _env_flag_off(env: Dict[str, str]) -> bool: + """True when the operator turned the GPU probe (and so the gate) off.""" + raw = str(env.get("METAINFER_GPU_PREFLIGHT") or "").strip().lower() + return raw in {"0", "false", "no", "off"} + + +def _gate_log(payload: Dict[str, Any], *, state_dir: "Path | None", + env: Dict[str, str]) -> None: + """Best-effort audit row for one gate check (never raises).""" + row = {"ts": time.time(), **payload} + targets = [] + if state_dir is not None: + targets.append(Path(state_dir) / _GATE_LOG_NAME) + raw = str(env.get("METAINFER_CHILD_STATE_DIR") or "").strip() + if raw: + targets.append(Path(raw) / _GATE_LOG_NAME) + for path in targets: + try: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as fh: + fh.write(json.dumps(row, ensure_ascii=False) + "\n") + except OSError: + continue + + +def discard_if_contaminated(*, experiments_path: Path, iteration: int, + iteration_dir: Path, source: Path, + state_dir: Path, + best_commit: str = "") -> Dict[str, Any]: + """Drop a round measured while this worker's device was shared. + + Returns the discard record. When a round is discarded the kernel source is + rolled back to its last good commit, so the round can be re-measured from + the previous state instead of on top of a polluted one. + """ + result = discard_contaminated_round( + experiments_path=experiments_path, iteration=iteration, + state_dir=state_dir, iteration_dir=iteration_dir) + if not result.get("discarded"): + return result + try: + if best_commit: + _run(["git", "reset", "--hard", best_commit], cwd=source) + else: + _run(["git", "checkout", "--", "."], cwd=source) + result["source_restored"] = True + except Exception as exc: # noqa: BLE001 - the discard still stands + result["source_restored"] = False + result["restore_error"] = repr(exc) + return result + + +def ensure_measurement_gate(gpu: int, *, state_dir: "Path | None" = None, + env: Dict[str, str] | None = None, + wait_seconds: int | None = None, + max_waits: int | None = None, + site: str = "benchmark") -> Dict[str, Any]: + """Wait until ``gpu`` passes VRAM <= 90% and HCU == 0 before measuring. + + Returns the passing device check. Raises :class:`MeasurementGateBlocked` + after ``max_waits`` re-checks (default 48 x 30 min = 24 h) so a shared card + can never silently produce a measurement. + """ + env = dict(env or os.environ) + if _env_flag_off(env): + return {"skipped": True, "reason": "METAINFER_GPU_PREFLIGHT disabled"} + from .gpu_preflight import preflight_gpus + + default_wait, default_max = _gate_wait_config(env) + wait_s = int(wait_seconds if wait_seconds is not None else default_wait) + max_waits = int(max_waits if max_waits is not None else default_max) + attempts = 0 + last: Dict[str, Any] = {} + while True: + attempts += 1 + try: + plan = preflight_gpus( + [int(gpu)], samples=1, interval_s=0.0, + vram_limit_percent=MEASUREMENT_GATE_VRAM_PERCENT, + util_tolerance=MEASUREMENT_GATE_UTIL_PERCENT, + ) + check = dict((plan.get("gpus") or {}).get(int(gpu)) or {}) + except Exception as exc: # noqa: BLE001 - fail closed, never measure + check = {"usable": False, "reasons": [f"gate probe failed: {exc!r}"]} + if not check: + check = {"usable": False, "reasons": ["no device state was read"]} + last = check + _gate_log({ + "event": "gate_ok" if check.get("usable") else "gate_blocked", + "site": site, "gpu": int(gpu), "attempt": attempts, + "max_waits": max_waits, "wait_seconds": wait_s, + "usable": bool(check.get("usable")), + "util_percent": check.get("util_percent"), + "vram_percent": check.get("vram_percent"), + "state_source": check.get("state_source"), + "reasons": list(check.get("reasons") or []), + }, state_dir=state_dir, env=env) + if check.get("usable"): + return check + if attempts > max_waits: + reason = "; ".join(str(r) for r in (check.get("reasons") or [])) + raise MeasurementGateBlocked(int(gpu), attempts - 1, reason) + time.sleep(max(0.0, float(wait_s))) + + +def ensure_any_measurement_gate(devices: Sequence[int], *, + state_dir: "Path | None" = None, + env: Dict[str, str] | None = None, + wait_seconds: int | None = None, + max_waits: int | None = None, + site: str = "probe") -> int: + """Wait until *one* of ``devices`` passes the gate; return that device. + + Pinning a helper to a single card is how a task ends up sleeping 24 h while + its siblings sit idle: "GPU 0 is occupied" says nothing about GPU 1. The + candidates keep their given order, so a preferred device still wins when it + is free, and the wait only happens when *no* candidate passes. + """ + candidates = [int(d) for d in devices] + if not candidates: + raise ValueError("ensure_any_measurement_gate needs at least one device") + env = dict(env or os.environ) + if _env_flag_off(env): + return candidates[0] + from .gpu_preflight import preflight_gpus + + default_wait, default_max = _gate_wait_config(env) + wait_s = int(wait_seconds if wait_seconds is not None else default_wait) + max_waits = int(max_waits if max_waits is not None else default_max) + attempts = 0 + while True: + attempts += 1 + try: + plan = preflight_gpus( + candidates, samples=1, interval_s=0.0, + vram_limit_percent=MEASUREMENT_GATE_VRAM_PERCENT, + util_tolerance=MEASUREMENT_GATE_UTIL_PERCENT, + ) + states = dict(plan.get("gpus") or {}) + except Exception as exc: # noqa: BLE001 - fail closed, never measure + states = {d: {"usable": False, + "reasons": [f"gate probe failed: {exc!r}"]} + for d in candidates} + passing = [d for d in candidates if (states.get(d) or {}).get("usable")] + blocked = [d for d in candidates if d not in passing] + _gate_log({ + "event": "gate_ok" if passing else "gate_blocked", + "site": site, "gpu": passing[0] if passing else candidates[0], + "candidates": candidates, "passing": passing, + "attempt": attempts, "max_waits": max_waits, + "wait_seconds": wait_s, "usable": bool(passing), + "reasons": [r for d in blocked + for r in ((states.get(d) or {}).get("reasons") or [])], + }, state_dir=state_dir, env=env) + if passing: + return int(passing[0]) + if attempts > max_waits: + reason = "; ".join( + f"gpu{d}: " + ", ".join( + str(r) for r in ((states.get(d) or {}).get("reasons") or []) + ) for d in blocked) + raise MeasurementGateBlocked(int(candidates[0]), attempts - 1, reason) + time.sleep(max(0.0, float(wait_s))) + def _planner_enabled() -> bool: return os.environ.get(_ENV_PLANNER, "").strip().lower() in { @@ -132,6 +358,7 @@ def _round_strategy_text( parse_pmc_csv, ) from .real_pipeline import _last_json, _run, _safe, _status +from .contention import discard_contaminated_round from .result_store import SCHEMA_VERSION, append_jsonl, write_json from .skill_store import generate_merged_skill, generate_worker_skill from .w8a8_baselines import fixed_triton_graph_baseline @@ -858,6 +1085,7 @@ def _check_required_files(repo: Path) -> bool: class W8A8Runner: def __init__(self, worker_root: Path, gpu: int) -> None: self.worker_root = worker_root + self.gpu = int(gpu) self.source = worker_root / "source" staged_harness = self.source / "w8a8_bench.py" self.harness = ( @@ -880,6 +1108,27 @@ def __init__(self, worker_root: Path, gpu: int) -> None: ): Path(self.env[key]).mkdir(parents=True, exist_ok=True) + def _ensure_gate(self, site: str) -> Dict[str, Any]: + """Block until this worker's device passes the measurement gate. + + Called immediately before every timed benchmark and PMC profile, so a + device taken over mid-task stops producing numbers instead of producing + polluted ones. ``METAINFER_GPU_PREFLIGHT=off`` keeps the old behaviour. + """ + if not _gate_preflight_enabled(): + return {} + return ensure_measurement_gate( + int(self.gpu), + state_dir=self._gate_state_dir(), + env=self.env, + site=site, + ) + + def _gate_state_dir(self) -> "Path | None": + """Child state dir for gate audit rows, when the task provides one.""" + raw = str(self.env.get("METAINFER_CHILD_STATE_DIR") or "").strip() + return Path(raw) if raw else None + def _prepare_compile_cache(self) -> Dict[str, Any]: """Stage immutable content-addressed compiler inputs for Ninja reuse.""" inputs = [self.source / "csrc" / "bindings.cpp"] @@ -980,6 +1229,7 @@ def benchmark( if not check_correctness: command.append("--skip-correctness") build = self._prepare_compile_cache() + self._ensure_gate("benchmark") result = _run( command, cwd=self.source, env=self.env, timeout=_BENCHMARK_TIMEOUT_S, @@ -1049,6 +1299,7 @@ def profile_pmc( ) output_dir.mkdir(parents=True, exist_ok=True) build = self._prepare_compile_cache() + self._ensure_gate("profile_pmc") _run( [ "bash", @@ -2601,7 +2852,45 @@ def preflight() -> list[str]: write_json( iteration_dir / "iteration.json", experiment ) - append_jsonl(experiments_path, experiment) + # A round measured while the parent had this device frozen + # (another workload took it over) is not evidence: drop the + # number, quarantine the record and roll the worker back to the + # previous good state so the round is simply re-measured. + contaminated = discard_if_contaminated( + experiments_path=experiments_path, + iteration=iteration, + iteration_dir=iteration_dir, + source=source, + state_dir=self.state_dir, + ) + if contaminated.get("discarded"): + accepted = False + best_commit = _run( + ["git", "rev-parse", "HEAD"], cwd=source + ).stdout.strip() + experiment["contaminated_discarded"] = { + "reason": contaminated.get("reason"), + "quarantined": contaminated.get("quarantined"), + } + self.store.append_timeline( + "worker_round_discarded_contended", + { + "worker_id": assignment.worker_id, + "physical_gpu": assignment.gpu, + "shape_id": shape_id, + "iteration": iteration, + "window": contaminated.get("window"), + "reason": contaminated.get("reason"), + "quarantined": contaminated.get("quarantined"), + "policy": ( + "the device was shared while this round was " + "measured; the number is discarded and the " + "round is re-measured" + ), + }, + ) + else: + append_jsonl(experiments_path, experiment) if candidate_destination is not None: publish_iteration_candidate( iteration_dir, candidate_destination diff --git a/metainfer/tasks/dcu_kernel_auto_opt/reports/_diag_down_proj.sh b/metainfer/tasks/dcu_kernel_auto_opt/reports/_diag_down_proj.sh new file mode 100644 index 00000000..50003279 --- /dev/null +++ b/metainfer/tasks/dcu_kernel_auto_opt/reports/_diag_down_proj.sh @@ -0,0 +1,40 @@ +#!/bin/bash +# Verify the recorded 8.67us for hy3_tp8_shared_down_proj_m16 (shape M16 N4096 K192). +# Same protocol as the worker acceptance bench: warmups=100 samples=30 replays=100, +# correctness checked against the cached exact int64 reference. +set -u +WS=/root/zth_agent/MetaInfer/nodes/worker29/workspaces/hy3-dsh-tp8-m16-9-8-0161e718 +GPU=${GPU:-1} +REPEAT=${REPEAT:-2} + +bench () { + local root=$1 label=$2 + cd "$root/source" || return 1 + env HIP_VISIBLE_DEVICES="$GPU" MAX_JOBS=2 PYTHONDONTWRITEBYTECODE=1 \ + PYTORCH_ROCM_ARCH=gfx928 \ + TORCH_EXTENSIONS_DIR="$root/cache/torch" \ + TRITON_CACHE_DIR="$root/cache/triton" \ + XDG_CACHE_HOME="$root/cache/xdg" \ + TMPDIR="$root/cache/tmp" \ + python3 "$root/source/w8a8_bench.py" --source "$root/source" \ + --m 16 --n 4096 --k 192 \ + --reference-cache-dir "$root/cache/references" 2>&1 | tail -3 +} + +for i in $(seq 1 "$REPEAT"); do + echo "########## run $i on GPU$GPU ##########" + for spec in "workers/worker_3:WORKER_3-accepted-build" "final:FINAL-synthesized-build"; do + root="$WS/${spec%%:*}"; label="${spec##*:}" + echo "=== $label (run $i) ===" + bench "$root" "$label" | python3 -c ' +import sys,json +for line in sys.stdin: + line=line.strip() + if not line.startswith("{"): continue + d=json.loads(line) + print("median_us=%.3f min_us=%.3f p90_us=%.3f samples=%s passed=%s ref_cache_hit=%s tops=%.2f bw=%.1f GB/s" % ( + d["median_us"], d["min_us"], d["p90_us"], d.get("samples"), d.get("passed"), + d.get("reference_cache_hit"), d["logical_tops"], d["algorithmic_bandwidth_gb_s"])) +' + done +done diff --git a/metainfer/tasks/dcu_kernel_auto_opt/reports/_diag_down_proj_profiler.py b/metainfer/tasks/dcu_kernel_auto_opt/reports/_diag_down_proj_profiler.py new file mode 100644 index 00000000..3fe7ef2e --- /dev/null +++ b/metainfer/tasks/dcu_kernel_auto_opt/reports/_diag_down_proj_profiler.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +"""Independent cross-check for hy3_tp8_shared_down_proj_m16 (M16 N4096 K192). + +Method A: CUDA-event timing over CUDA-graph replay (same as the official harness). +Method B: torch.profiler -> pure GPU kernel time (excludes host launch overhead). + +Run with the worker_3 accepted build (extension cache reused, no rebuild). +""" +from __future__ import annotations + +import importlib.util +import json +import os +import statistics +import sys +from pathlib import Path + +import torch + +WS = Path("/root/zth_agent/MetaInfer/nodes/worker29/workspaces/hy3-dsh-tp8-m16-9-8-0161e718") +ROOT = Path(sys.argv[1]) if len(sys.argv) > 1 else WS / "workers" / "worker_3" +SRC = ROOT / "source" +BUILD = json.loads((ROOT / "cache" / "current_build.json").read_text()) +os.environ["METAINFER_W8A8_COMPILE_SOURCE_DIR"] = BUILD["compile_source_dir"] +os.environ["METAINFER_W8A8_BUILD_KEY"] = BUILD["build_key"] +os.environ["TORCH_EXTENSIONS_DIR"] = str(ROOT / "cache" / "torch") + + +def load_module(path: Path, name: str): + spec = importlib.util.spec_from_file_location(name, path) + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + spec.loader.exec_module(module) + return module + + +bench = load_module(SRC / "w8a8_bench.py", "bench_mod") +backend = load_module(SRC / "w8a8_backend.py", "metainfer_w8a8_backend") +backend.load_extension() +api = load_module(SRC / "int8_w8a8_gemm_api.py", "metainfer_w8a8_contract") + +M = int(os.environ.get("SHAPE_M", "16")) +N = int(os.environ.get("SHAPE_N", "4096")) +K = int(os.environ.get("SHAPE_K", "192")) +print(f"== shape M={M} N={N} K={K} root={ROOT.name} ==", flush=True) +a, b, a_scale, b_scale = bench.generate_w8a8_inputs(M, N, K) +out = torch.empty((M, N), dtype=torch.bfloat16, device="cuda") +packed_weight, packed_weight_scale = api.prepare_weight(b, b_scale) +workspace = api.allocate_workspace(M, N, K, a.device) + + +def candidate() -> None: + api.w8a8_gemm_out(a, packed_weight, a_scale, packed_weight_scale, out, workspace) + + +for _ in range(100): + candidate() +torch.cuda.synchronize() + +graph = torch.cuda.CUDAGraph() +with torch.cuda.graph(graph): + candidate() + +# --- Method A: event timing, 30 samples x 100 replays (official protocol) --- +events = [(torch.cuda.Event(True), torch.cuda.Event(True)) for _ in range(30)] +for start, end in events: + start.record() + for _ in range(100): + graph.replay() + end.record() +torch.cuda.synchronize() +per_launch = [s.elapsed_time(e) * 1000.0 / 100.0 for s, e in events] +print("== Method A: CUDA-event graph replay (official protocol) ==") +print(" median_us=%.3f min_us=%.3f p90_us=%.3f n=30" % ( + statistics.median(per_launch), min(per_launch), + sorted(per_launch)[int(0.9 * len(per_launch)) - 1])) + +# --- Method B: profiler kernel-only device time --- +replays = 50 +from torch.profiler import ProfilerActivity, profile # noqa: E402 + +with profile(activities=[ProfilerActivity.CUDA]) as prof: + for _ in range(replays): + graph.replay() + torch.cuda.synchronize() + +kernels = [] +for entry in prof.key_averages(): + dev_us = getattr(entry, "self_device_time_total", 0.0) or 0.0 + if dev_us > 0: + kernels.append((entry.key, int(entry.count), dev_us / replays)) +kernels.sort(key=lambda item: -item[2]) +total_per_launch = sum(item[2] for item in kernels) +print("== Method B: torch.profiler GPU kernel time (per graph replay) ==") +for key, count, us in kernels[:6]: + print(" %-58s count=%-4d %8.3f us/replay" % (key[:58], count, us)) +print(" TOTAL kernel time per replay = %.3f us" % total_per_launch) +print() +print("recorded worker accepted median = 8.671 us (min 7.484, p90 9.144); baseline 17.312 us") diff --git a/metainfer/tasks/dcu_kernel_auto_opt/reports/_diag_down_proj_sweep.sh b/metainfer/tasks/dcu_kernel_auto_opt/reports/_diag_down_proj_sweep.sh new file mode 100644 index 00000000..256271a2 --- /dev/null +++ b/metainfer/tasks/dcu_kernel_auto_opt/reports/_diag_down_proj_sweep.sh @@ -0,0 +1,51 @@ +#!/bin/bash +# Sweep shared_down_proj (hy3_tp8_shared_down_proj_m16, M16 N4096 K192) on idle GPUs. +# Goal: check whether the recorded 8.671us (worker accepted median) is credible. +set -u +WS=/root/zth_agent/MetaInfer/nodes/worker29/workspaces/hy3-dsh-tp8-m16-9-8-0161e718 +OUT=/root/zth_agent/MetaInfer/nodes/worker29/workspaces/hy3-dsh-tp8-m16-9-8-0161e718/diag_down_proj_sweep.log +: > "$OUT" + +bench_once () { # $1=root $2=gpu + local root=$1 gpu=$2 + cd "$root/source" || return 1 + env HIP_VISIBLE_DEVICES="$gpu" MAX_JOBS=2 PYTHONDONTWRITEBYTECODE=1 \ + PYTORCH_ROCM_ARCH=gfx928 \ + TORCH_EXTENSIONS_DIR="$root/cache/torch" TRITON_CACHE_DIR="$root/cache/triton" \ + XDG_CACHE_HOME="$root/cache/xdg" TMPDIR="$root/cache/tmp" \ + python3 "$root/source/w8a8_bench.py" --source "$root/source" \ + --m 16 --n 4096 --k 192 --reference-cache-dir "$root/cache/references" 2>&1 \ + | python3 -c ' +import sys,json +for line in sys.stdin: + line=line.strip() + if line.startswith("{"): + d=json.loads(line) + print("%.3f %.3f %.3f %s" % (d["median_us"], d["min_us"], d["p90_us"], d.get("passed"))) +' +} + +for gpu in 0 1; do + for spec in "workers/worker_3:worker" "final:final"; do + root="$WS/${spec%%:*}"; label="${spec##*:}" + for i in 1 2 3 4; do + res=$(bench_once "$root" "$gpu") + echo "gpu$gpu $label run$i : $res" | tee -a "$OUT" + done + done +done + +echo | tee -a "$OUT" +python3 - "$OUT" <<'EOF' | tee -a "$OUT" +import sys,collections,statistics +groups=collections.defaultdict(list); groupsP=collections.defaultdict(list) +for line in open(sys.argv[1]): + parts=line.split() + if len(parts)<6 or not parts[0].startswith('gpu'): continue + key=(parts[0],parts[1]); groups[key].append(float(parts[3])); groupsP[key].append(float(parts[4])) +print(f"{'group':22s} {'n':>2s} {'median_of_medians':>17s} {'best_median':>12s} {'worst':>8s} {'best_min':>9s}") +for k in sorted(groups): + v=groups[k] + print(f"{k[0]+' '+k[1]:22s} {len(v):>2d} {statistics.median(v):>17.3f} {min(v):>12.3f} {max(v):>8.3f} {min(groupsP[k]):>9.3f}") +EOF +echo "RECORDED worker accepted: median 8.671us / min 7.484us / p90 9.144us ; Triton baseline 17.312us" diff --git a/metainfer/tasks/dcu_kernel_auto_opt/reports/_make_xlsx.py b/metainfer/tasks/dcu_kernel_auto_opt/reports/_make_xlsx.py index 72e4d538..e9fb3e98 100644 --- a/metainfer/tasks/dcu_kernel_auto_opt/reports/_make_xlsx.py +++ b/metainfer/tasks/dcu_kernel_auto_opt/reports/_make_xlsx.py @@ -126,7 +126,8 @@ def main(): key = (r["model"], r["kernel_repo"], r["tp"]) groups.setdefault(key, []).append(r) head2 = ["模型(任务)", "kernel_repo", "TP", "算子数", "平均加速比×", - "最小加速比×", "最大加速比×", "平均最终µs", "平均TOPS", "平均提升%"] + "最小加速比×", "最大加速比×", "平均最终µs", "平均TOPS", "平均提升%", + "验收状态"] ws2.append(head2) for c in range(1, len(head2) + 1): cell = ws2.cell(row=1, column=c) @@ -142,19 +143,21 @@ def main(): sum(sp) / len(sp), min(sp), max(sp), sum(fin) / len(fin), sum(tops) / len(tops), - sum(imp) / len(imp)] + sum(imp) / len(imp), + "/".join(sorted({r["validation_state"] for r in rr}))] ws2.append(vals) for j, v in enumerate(vals, start=1): cell = ws2.cell(row=i, column=j) cell.border = border - if j >= 5 and j != 4: + if 5 <= j <= 10: cell.number_format = "0.00" + cell.alignment = Alignment(horizontal="right") if j == 4: cell.alignment = Alignment(horizontal="right") if i % 2 == 0: for j in range(1, len(head2) + 1): ws2.cell(row=i, column=j).fill = alt_fill - for j, w in enumerate([26, 24, 6, 9, 13, 13, 13, 14, 12, 12], start=1): + for j, w in enumerate([26, 24, 6, 9, 13, 13, 13, 14, 12, 12, 34], start=1): ws2.column_dimensions[get_column_letter(j)].width = w ws2.freeze_panes = "A2" ws2.auto_filter.ref = f"A1:{get_column_letter(len(head2))}{ws2.max_row}" @@ -232,7 +235,8 @@ def main(): ["计算性能(TOPS)", "= 2×M×N×K / final_us,纯算法逻辑 INT8 运算速率(非实测带宽)"], ["达标/正确性", "target_met: 最终验收 ≥3% 提升门槛;passed: 最终串行验证正确性通过(CPU int64 exact reference)"], ["验收状态", "'最终验收通过' = 该任务串行最终验收全部 shape 通过并写出 final_report.json (status=success);'仅worker验收(未过最终验收)' = 该任务未产出成功 final_report,本行取的是 worker 并行探索阶段已验收最优 variant 的 median(未经最终门限确认)"], - ["覆盖", "Hy3: TP4 M16/M4096, TP8 M4096; MiniMax-M3: TP4 M4096, TP8 M16/M4096; GLM5.2: TP8 M4096 —— 共 33 个优化 shape,全部为各任务自身 plan 的优化算子(不含默认42-shape回归项)"], + ["覆盖", "Hy3: TP4 M16/M4096, TP8 M4096 (+TP8 M16 见下条); MiniMax-M3: TP4 M4096, TP8 M16/M4096; GLM5.2: TP8 M4096 —— 共 37 个优化 shape 行,其中 33 行为各任务自身 plan 的最终验收通过算子,4 行为 Hy3 TP8 M16(仅 worker 验收,见下条);不含默认 42-shape 回归项"], + ["Hy3 TP8 M16 (9.8 任务)", "任务 hy3-dsh-tp8-m16-9-8-0161e718 的 4 个 M=16 shape 目前无成功 final_report:2026-09-09 07:04 最终验收因性能门限失败(o_proj final 11.745 vs worker best 11.044 > 1.05×)。2026-09-10 重跑验收:qkv_proj ✅、o_proj ✅(首测 11.706 超限,第 1 次重测通过)、shared_gate_up_proj 首测 13.1295 vs 门限 13.112(超 0.13%)时按用户要求暂停,shared_down_proj 未测。本表该 4 行数值取 worker 探索期已验收最优 median"], ["生成时间", "2026-09(数据截至各任务 final_report.json 快照)"], ] for i, row in enumerate(notes, start=1): diff --git a/metainfer/tasks/dcu_kernel_auto_opt/reports/_tool_check_decompose.py b/metainfer/tasks/dcu_kernel_auto_opt/reports/_tool_check_decompose.py new file mode 100644 index 00000000..27754a47 --- /dev/null +++ b/metainfer/tasks/dcu_kernel_auto_opt/reports/_tool_check_decompose.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +"""Decompose the harness metric for hy3_tp8_shared_down_proj_m16. + +per_replay_latency(G) = fixed_overhead + G * kernel_time +Graphs contain G sequential w8a8_gemm_out calls; measure with the official +protocol (samples=30, replays=100) and compare the slope with the profiler +kernel time. +""" +from __future__ import annotations + +import importlib.util +import json +import os +import statistics +from pathlib import Path + +import torch +from torch.profiler import ProfilerActivity, profile + +WS = Path("/root/zth_agent/MetaInfer/nodes/worker29/workspaces/hy3-dsh-tp8-m16-9-8-0161e718") +ROOT = WS / "workers" / "worker_3" +SRC = ROOT / "source" +BUILD = json.loads((ROOT / "cache" / "current_build.json").read_text()) +os.environ["METAINFER_W8A8_COMPILE_SOURCE_DIR"] = BUILD["compile_source_dir"] +os.environ["METAINFER_W8A8_BUILD_KEY"] = BUILD["build_key"] +os.environ["TORCH_EXTENSIONS_DIR"] = str(ROOT / "cache" / "torch") + + +def load_module(path: Path, name: str): + spec = importlib.util.spec_from_file_location(name, path) + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + spec.loader.exec_module(module) + return module + + +def event_timing(graph, samples=30, replays=100): + evs = [(torch.cuda.Event(True), torch.cuda.Event(True)) for _ in range(samples)] + for s, e in evs: + s.record() + for _ in range(replays): + graph.replay() + e.record() + torch.cuda.synchronize() + per = sorted(s.elapsed_time(e) * 1000.0 / replays for s, e in evs) + return statistics.median(per), per[0] + + +def profiler_kernel_us(graph, replays=50): + with profile(activities=[ProfilerActivity.CUDA]) as prof: + for _ in range(replays): + graph.replay() + torch.cuda.synchronize() + return sum(float(getattr(e, "self_device_time_total", 0.0) or 0.0) + for e in prof.key_averages()) / replays + + +bench = load_module(SRC / "w8a8_bench.py", "bench_mod") +backend = load_module(SRC / "w8a8_backend.py", "metainfer_w8a8_backend") +backend.load_extension() +api = load_module(SRC / "int8_w8a8_gemm_api.py", "metainfer_w8a8_contract") +M, N, K = 16, 4096, 192 +a8, b8, a_scale, b_scale = bench.generate_w8a8_inputs(M, N, K) +out8 = torch.empty((M, N), dtype=torch.bfloat16, device="cuda") +pw, pws = api.prepare_weight(b8, b_scale) +ws = api.allocate_workspace(M, N, K, a8.device) + + +def gemm(): + api.w8a8_gemm_out(a8, pw, a_scale, pws, out8, ws) + + +# sustained warmup so clocks/state are stable before any measurement +for _ in range(3000): + gemm() +torch.cuda.synchronize() + +results = [] +for g_count in (1, 2, 4, 8): + def body(): + for _ in range(g_count): + gemm() + for _ in range(200): + body() + torch.cuda.synchronize() + graph = torch.cuda.CUDAGraph() + with torch.cuda.graph(graph): + body() + med, lo = event_timing(graph) + ker = profiler_kernel_us(graph) + results.append((g_count, med, lo, ker)) + print("G=%-2d event median %8.3f us (min %8.3f) | profiler kernel total %8.3f us -> per kernel %.3f us" + % (g_count, med, lo, ker, ker / g_count), flush=True) + +# linear regression over G +xs = [r[0] for r in results] +ys = [r[1] for r in results] +n = len(xs) +mx, my = sum(xs) / n, sum(ys) / n +slope = sum((x - mx) * (y - my) for x, y in zip(xs, ys)) / sum((x - mx) ** 2 for x in xs) +intercept = my - slope * mx +print() +print("fit: per_replay(G) = %.3f us + G * %.3f us" % (intercept, slope)) +print(" -> fixed per-replay overhead (graph launch/event boundary) = %.3f us" % intercept) +print(" -> marginal cost per gemm kernel = %.3f us" % slope) +print(" -> profiler per-kernel (avg) = %.3f us" % ( + sum(r[3] / r[0] for r in results) / n)) +print() +print("recorded worker accepted median = 8.671 us ; baseline (Triton) = 17.312 us") diff --git a/metainfer/tasks/dcu_kernel_auto_opt/reports/_tool_check_floor.py b/metainfer/tasks/dcu_kernel_auto_opt/reports/_tool_check_floor.py new file mode 100644 index 00000000..ab6bc0c5 --- /dev/null +++ b/metainfer/tasks/dcu_kernel_auto_opt/reports/_tool_check_floor.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +"""Measure the fixed per-replay overhead the harness metric includes. + +If (empty-graph floor) + (profiler kernel time) ~= harness per-replay number, +the harness is measuring "per graph-replay call latency" = launch overhead + kernel, +which is the explanation for 8.67us vs ~4.5us kernel-only. +""" +from __future__ import annotations + +import importlib.util +import json +import os +import statistics +from pathlib import Path + +import torch +from torch.profiler import ProfilerActivity, profile + +WS = Path("/root/zth_agent/MetaInfer/nodes/worker29/workspaces/hy3-dsh-tp8-m16-9-8-0161e718") +ROOT = WS / "workers" / "worker_3" +SRC = ROOT / "source" +BUILD = json.loads((ROOT / "cache" / "current_build.json").read_text()) +os.environ["METAINFER_W8A8_COMPILE_SOURCE_DIR"] = BUILD["compile_source_dir"] +os.environ["METAINFER_W8A8_BUILD_KEY"] = BUILD["build_key"] +os.environ["TORCH_EXTENSIONS_DIR"] = str(ROOT / "cache" / "torch") + + +def load_module(path: Path, name: str): + spec = importlib.util.spec_from_file_location(name, path) + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + spec.loader.exec_module(module) + return module + + +def event_timing(graph, samples=30, replays=100): + evs = [(torch.cuda.Event(True), torch.cuda.Event(True)) for _ in range(samples)] + for s, e in evs: + s.record() + for _ in range(replays): + graph.replay() + e.record() + torch.cuda.synchronize() + per = sorted(s.elapsed_time(e) * 1000.0 / replays for s, e in evs) + return statistics.median(per), per[0], per[int(0.9 * len(per)) - 1] + + +def profiler_kernel_us(graph, replays=50): + with profile(activities=[ProfilerActivity.CUDA]) as prof: + for _ in range(replays): + graph.replay() + torch.cuda.synchronize() + return sum(float(getattr(e, "self_device_time_total", 0.0) or 0.0) + for e in prof.key_averages()) / replays + + +# ---- 1. empty-ish graph: trivial kernel only (floor of the metric) ---- +tiny_in = torch.zeros(8, dtype=torch.float32, device="cuda") +tiny_out = torch.empty_like(tiny_in) +for _ in range(50): + tiny_out.copy_(tiny_in) +torch.cuda.synchronize() +g_tiny = torch.cuda.CUDAGraph() +with torch.cuda.graph(g_tiny): + tiny_out.copy_(tiny_in) +med, lo, p90 = event_timing(g_tiny) +tiny_kernel = profiler_kernel_us(g_tiny) +print("== trivial-kernel graph (metric floor) ==") +print(" event per replay : median %.3f us min %.3f p90 %.3f" % (med, lo, p90)) +print(" profiler kernel : %.3f us" % tiny_kernel) + +# ---- 2. the real shape, same protocol ---- +bench = load_module(SRC / "w8a8_bench.py", "bench_mod") +backend = load_module(SRC / "w8a8_backend.py", "metainfer_w8a8_backend") +backend.load_extension() +api = load_module(SRC / "int8_w8a8_gemm_api.py", "metainfer_w8a8_contract") +M, N, K = 16, 4096, 192 +a8, b8, a_scale, b_scale = bench.generate_w8a8_inputs(M, N, K) +out8 = torch.empty((M, N), dtype=torch.bfloat16, device="cuda") +pw, pws = api.prepare_weight(b8, b_scale) +ws = api.allocate_workspace(M, N, K, a8.device) + + +def gemm(): + api.w8a8_gemm_out(a8, pw, a_scale, pws, out8, ws) + + +for _ in range(200): + gemm() +torch.cuda.synchronize() +g8 = torch.cuda.CUDAGraph() +with torch.cuda.graph(g8): + gemm() +med8, lo8, p908 = event_timing(g8) +ker8 = profiler_kernel_us(g8) +print() +print("== hy3_tp8_shared_down_proj_m16 (official protocol: samples=30, replays=100) ==") +print(" event per replay : median %.3f us min %.3f p90 %.3f" % (med8, lo8, p908)) +print(" profiler kernel : %.3f us" % ker8) +print(" implied fixed overhead (median - kernel) : %.3f us" % (med8 - ker8)) +print(" floor - tiny kernel : %.3f us" % (med - tiny_kernel)) +print() +print(" recorded worker accepted: median 8.671 us | baseline 17.312 us") diff --git a/metainfer/tasks/dcu_kernel_auto_opt/reports/_tool_check_timing.py b/metainfer/tasks/dcu_kernel_auto_opt/reports/_tool_check_timing.py new file mode 100644 index 00000000..b26ae80f --- /dev/null +++ b/metainfer/tasks/dcu_kernel_auto_opt/reports/_tool_check_timing.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +"""Validate the measurement methodology of the DKAO W8A8 harness. + +Part 1: Large bf16 GEMM -> event timing (same pattern as w8a8_bench.py) vs + torch.profiler GPU kernel time. For a ms-scale kernel the fixed + per-replay overhead is negligible, so both must agree: this validates + the event primitive + report formula on this platform. +Part 2: shared_down_proj (M16 N4096 K192) -> sweep replays_per_sample to expose + the fixed per-replay overhead, and compare with the profiler kernel time. +""" +from __future__ import annotations + +import importlib.util +import json +import os +import statistics +from pathlib import Path + +import torch +from torch.profiler import ProfilerActivity, profile + +WS = Path("/root/zth_agent/MetaInfer/nodes/worker29/workspaces/hy3-dsh-tp8-m16-9-8-0161e718") +ROOT = WS / "workers" / "worker_3" +SRC = ROOT / "source" +BUILD = json.loads((ROOT / "cache" / "current_build.json").read_text()) +os.environ["METAINFER_W8A8_COMPILE_SOURCE_DIR"] = BUILD["compile_source_dir"] +os.environ["METAINFER_W8A8_BUILD_KEY"] = BUILD["build_key"] +os.environ["TORCH_EXTENSIONS_DIR"] = str(ROOT / "cache" / "torch") + + +def load_module(path: Path, name: str): + spec = importlib.util.spec_from_file_location(name, path) + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + spec.loader.exec_module(module) + return module + + +def event_timing(graph, samples: int, replays: int) -> tuple[float, float, float]: + evs = [(torch.cuda.Event(True), torch.cuda.Event(True)) for _ in range(samples)] + for s, e in evs: + s.record() + for _ in range(replays): + graph.replay() + e.record() + torch.cuda.synchronize() + per = [s.elapsed_time(e) * 1000.0 / replays for s, e in evs] + per.sort() + return statistics.median(per), per[0], per[int(0.9 * len(per)) - 1] + + +def profiler_kernel_us(graph, replays: int) -> float: + with profile(activities=[ProfilerActivity.CUDA]) as prof: + for _ in range(replays): + graph.replay() + torch.cuda.synchronize() + total = 0.0 + for entry in prof.key_averages(): + total += float(getattr(entry, "self_device_time_total", 0.0) or 0.0) + return total / replays + + +print("=" * 78) +print("PART 1: large bf16 GEMM 4096x4096x4096 (kernel >> launch overhead)") +print("=" * 78) +a = torch.randn(4096, 4096, dtype=torch.bfloat16, device="cuda") +b = torch.randn(4096, 4096, dtype=torch.bfloat16, device="cuda") +out = torch.empty(4096, 4096, dtype=torch.bfloat16, device="cuda") + + +def big() -> None: + torch.matmul(a, b, out=out) + + +for _ in range(10): + big() +torch.cuda.synchronize() +gbig = torch.cuda.CUDAGraph() +with torch.cuda.graph(gbig): + big() +med, lo, p90 = event_timing(gbig, samples=5, replays=10) +ker = profiler_kernel_us(gbig, replays=10) +flops = 2.0 * 4096 ** 3 +print(" event timing : median %9.2f us (min %.2f, p90 %.2f)" % (med, lo, p90)) +print(" profiler : kernel %9.2f us" % ker) +print(" difference : %+0.2f%% -> effective %.1f TFLOPS (bf16)" % ( + (med - ker) / ker * 100.0, flops / (med * 1e-6) / 1e12)) + +print() +print("=" * 78) +print("PART 2: hy3_tp8_shared_down_proj_m16 (M16 N4096 K192), worker_3 accepted build") +print("=" * 78) +bench = load_module(SRC / "w8a8_bench.py", "bench_mod") +backend = load_module(SRC / "w8a8_backend.py", "metainfer_w8a8_backend") +backend.load_extension() +api = load_module(SRC / "int8_w8a8_gemm_api.py", "metainfer_w8a8_contract") + +M, N, K = 16, 4096, 192 +a8, b8, a_scale, b_scale = bench.generate_w8a8_inputs(M, N, K) +out8 = torch.empty((M, N), dtype=torch.bfloat16, device="cuda") +pw, pws = api.prepare_weight(b8, b_scale) +ws = api.allocate_workspace(M, N, K, a8.device) + + +def gemm() -> None: + api.w8a8_gemm_out(a8, pw, a_scale, pws, out8, ws) + + +for _ in range(200): + gemm() +torch.cuda.synchronize() +g8 = torch.cuda.CUDAGraph() +with torch.cuda.graph(g8): + gemm() + +print(" replays/sample sweep (samples=30, official formula = elapsed/replays):") +for replays in (1, 5, 20, 100, 500): + med, lo, p90 = event_timing(g8, samples=30, replays=replays) + print(" replays=%-4d median %7.3f us min %7.3f us p90 %7.3f us" % ( + replays, med, lo, p90)) +ker8 = profiler_kernel_us(g8, replays=50) +print(" profiler pure kernel time: %.3f us (bw %.1f GB/s of int8 weights)" % ( + ker8, (K * N) / (ker8 * 1e-6) / 1e9)) +print(" recorded worker accepted median = 8.671 us / min 7.484 us / baseline 17.312 us") diff --git a/metainfer/tasks/dcu_kernel_auto_opt/reports/dkao_optimized_operators_bestvariant.csv b/metainfer/tasks/dcu_kernel_auto_opt/reports/dkao_optimized_operators_bestvariant.csv index 10929acb..14e8cab0 100644 --- a/metainfer/tasks/dcu_kernel_auto_opt/reports/dkao_optimized_operators_bestvariant.csv +++ b/metainfer/tasks/dcu_kernel_auto_opt/reports/dkao_optimized_operators_bestvariant.csv @@ -1,34 +1,38 @@ -task_id,model,kernel_repo,tp,operator,M,N,K,dtype,baseline_us,worker_best_us,best_variant_source,final_us,final_p90_us,speedup_x,improvement_pct,target_met,passed,logical_tops,finished_at -hy3-dsh-tp4-m16-2-368c654c,Hy3 (gfx928 INT8 W8A8),hy3-dsh-TP4-m16-2,4,qkv_proj,16,2560,4096,INT8 W8A8,80.19,27.359989881515503,worker_0 accepted/hy3_tp4_qkv_proj_m16/kernel.hip,27.722550630569458,27.860960960388184,2.89259098373059,189.259098373059,True,True,12.103659741538994,2026-08-26 14:59 -hy3-dsh-tp4-m16-2-368c654c,Hy3 (gfx928 INT8 W8A8),hy3-dsh-TP4-m16-2,4,o_proj,16,4096,2048,INT8 W8A8,54.617,15.513530373573303,worker_1 accepted/hy3_tp4_o_proj_m16/kernel.hip,15.558480024337769,15.603289604187012,3.510432890267166,251.0432890267166,True,True,17.25332137715848,2026-08-26 14:59 -hy3-dsh-tp4-m16-2-368c654c,Hy3 (gfx928 INT8 W8A8),hy3-dsh-TP4-m16-2,4,shared_gate_up_proj,16,768,4096,INT8 W8A8,73.262,11.581599712371826,worker_2 accepted/hy3_tp4_shared_gate_up_proj_m16/kernel.hip,11.451255083084106,11.476860046386719,6.397726665631898,539.7726665631898,True,True,8.790590661865588,2026-08-26 14:59 -hy3-dsh-tp4-m16-2-368c654c,Hy3 (gfx928 INT8 W8A8),hy3-dsh-TP4-m16-2,4,shared_down_proj,16,4096,384,INT8 W8A8,23.216,9.17439490556717,worker_3 accepted/hy3_tp4_shared_down_proj_m16/kernel.hip,9.128850102424622,9.246439933776855,2.5431461508863897,154.31461508863896,True,True,5.513470747715742,2026-08-26 14:59 -hy3-tp4-m4096-dsh-test1-bfdf3002,Hy3 (gfx928 INT8 W8A8),hy3_TP4_M4096_dsh-test1,4,qkv_proj,4096,2560,4096,INT8 W8A8,24045.12,756.7258453369141,worker_0 accepted/hy3_tp4_qkv_proj_m4096/kernel.hip,756.7258453369141,760.15869140625,31.77520650070368,3077.520650070368,True,True,113.51448671844342,2026-08-22 05:44 -hy3-tp4-m4096-dsh-test1-bfdf3002,Hy3 (gfx928 INT8 W8A8),hy3_TP4_M4096_dsh-test1,4,o_proj,4096,4096,2048,INT8 W8A8,19881.949,803.8980102539062,worker_1 accepted/hy3_tp4_o_proj_m4096/kernel.hip,803.8980102539062,805.1364135742188,24.731929605996175,2373.1929605996174,True,True,85.48282973644304,2026-08-22 05:44 -hy3-tp4-m4096-dsh-test1-bfdf3002,Hy3 (gfx928 INT8 W8A8),hy3_TP4_M4096_dsh-test1,4,shared_gate_up_proj,4096,768,4096,INT8 W8A8,6678.282,220.71203231811523,worker_2 accepted/hy3_tp4_shared_gate_up_proj_m4096/kernel.hip,220.71203231811523,221.53764724731445,30.25789726939083,2925.7897269390833,True,True,116.75758455641257,2026-08-22 05:44 -hy3-tp4-m4096-dsh-test1-bfdf3002,Hy3 (gfx928 INT8 W8A8),hy3_TP4_M4096_dsh-test1,4,shared_down_proj,4096,4096,384,INT8 W8A8,4381.289,214.83811378479004,worker_3 accepted/hy3_tp4_shared_down_proj_m4096/kernel.hip,214.83811378479004,215.11651992797852,20.393443802010253,1939.3443802010254,True,True,59.97493489868936,2026-08-22 05:44 -hy3-dsh-tp8-m4096-1-0ebb994d,Hy3 (gfx928 INT8 W8A8),hy3-dsh-TP8-M4096-1,8,qkv_proj,4096,1280,4096,INT8 W8A8,19381.383,418.2191276550293,worker_0 accepted/hy3_tp8_qkv_proj_m4096/kernel.hip,318.4573459625244,326.0519790649414,60.86021643313191,5986.021643313191,True,True,134.86789833717404,2026-08-27 07:23 -hy3-dsh-tp8-m4096-1-0ebb994d,Hy3 (gfx928 INT8 W8A8),hy3-dsh-TP8-M4096-1,8,o_proj,4096,4096,1024,INT8 W8A8,14151.792,322.98728942871094,worker_1 accepted/hy3_tp8_o_proj_m4096/kernel.hip,282.8514766693115,283.3587074279785,50.0325901304917,4903.25901304917,True,True,121.47625592271805,2026-08-27 07:23 -hy3-dsh-tp8-m4096-1-0ebb994d,Hy3 (gfx928 INT8 W8A8),hy3-dsh-TP8-M4096-1,8,shared_gate_up_proj,4096,384,4096,INT8 W8A8,16712.231,115.95122337341309,worker_2 accepted/hy3_tp8_shared_gate_up_proj_m4096/kernel.hip,115.34959316253662,115.53359985351562,144.8833111743286,14388.331117432861,True,True,111.70305446889755,2026-08-27 07:23 -hy3-dsh-tp8-m4096-1-0ebb994d,Hy3 (gfx928 INT8 W8A8),hy3-dsh-TP8-M4096-1,8,shared_down_proj,4096,4096,192,INT8 W8A8,10762.616,171.18735313415527,worker_3 accepted/hy3_tp8_shared_down_proj_m4096/kernel.hip,171.81203842163086,172.1600341796875,62.64180379251588,6164.180379251588,True,True,37.497086951439755,2026-08-27 07:23 -minimax-dsh-tp8-m16-1-78402260,MiniMax-M3 (gfx928 INT8 W8A8),minimax-dsh-TP8-m16-1,8,qkv_proj,16,1280,6144,INT8 W8A8,110.478,33.20169925689697,worker_0 accepted/minimax_tp8_qkv_proj_m16/kernel.hip,33.19578528404236,33.328590393066406,3.328073098879459,232.8073098879459,True,True,7.581029876132359,2026-08-28 17:17 -minimax-dsh-tp8-m16-1-78402260,MiniMax-M3 (gfx928 INT8 W8A8),minimax-dsh-TP8-m16-1,8,qkv_proj_and_indexer_qk,16,1536,6144,INT8 W8A8,111.486,39.72720980644226,worker_1 accepted/minimax_tp8_qkv_proj_and_indexer_qk_m16/kernel.hip,39.73748564720154,39.8534893989563,2.8055625106680915,180.55625106680915,True,True,7.599622449219237,2026-08-28 17:17 -minimax-dsh-tp8-m16-1-78402260,MiniMax-M3 (gfx928 INT8 W8A8),minimax-dsh-TP8-m16-1,8,o_proj,16,6144,1024,INT8 W8A8,38.655,11.368839740753174,worker_2 accepted/minimax_tp8_o_proj_m16/kernel.hip,11.77299976348877,12.421009540557861,3.2833602969975013,228.33602969975013,True,True,17.10070466699301,2026-08-28 17:17 -minimax-dsh-tp8-m16-1-78402260,MiniMax-M3 (gfx928 INT8 W8A8),minimax-dsh-TP8-m16-1,8,shared_gate_up_proj,16,768,6144,INT8 W8A8,104.527,17.114474773406982,worker_3 accepted/minimax_tp8_shared_gate_up_proj_m16/kernel.hip,17.31948971748352,17.501089572906494,6.0352239993816355,503.5223999381635,True,True,8.718209743071991,2026-08-28 17:17 -minimax-dsh-tp8-m16-1-78402260,MiniMax-M3 (gfx928 INT8 W8A8),minimax-dsh-TP8-m16-1,8,shared_down_proj,16,6144,384,INT8 W8A8,23.152,8.95284503698349,worker_3 accepted/minimax_tp8_shared_down_proj_m16/kernel.hip,9.341430068016052,10.175809860229492,2.478421379962978,147.8421379962978,True,True,8.082003660070677,2026-08-28 17:17 -minimaxm3-dsh-tp4-m4096-1-0c2f84a9,MiniMax-M3 (gfx928 INT8 W8A8),minimaxM3-dsh_TP4_M4096-1,4,qkv_proj,4096,2304,6144,INT8 W8A8,31556.192,1559.944839477539,worker_0 accepted/minimax_tp4_qkv_proj_m4096/kernel.hip,1561.9083404541016,1577.305908203125,20.20361322279994,1920.361322279994,True,True,74.24514869949742,2026-08-23 18:45 -minimaxm3-dsh-tp4-m4096-1-0c2f84a9,MiniMax-M3 (gfx928 INT8 W8A8),minimaxM3-dsh_TP4_M4096-1,4,qkv_proj_and_indexer_qk,4096,2560,6144,INT8 W8A8,35918.768,1081.603660583496,worker_1 accepted/minimax_tp4_qkv_proj_and_indexer_qk_m4096/kernel.hip,1089.0154266357422,1102.6435089111328,32.9827908048673,3198.27908048673,True,True,118.31698222866211,2026-08-23 18:45 -minimaxm3-dsh-tp4-m4096-1-0c2f84a9,MiniMax-M3 (gfx928 INT8 W8A8),minimaxM3-dsh_TP4_M4096-1,4,o_proj,4096,6144,2048,INT8 W8A8,29702.473,765.9097671508789,worker_2 accepted/minimax_tp4_o_proj_m4096/kernel.hip,752.4849700927734,753.6545562744141,39.47251331324,3847.251331324,True,True,136.98508169710212,2026-08-23 18:45 -minimaxm3-dsh-tp4-m4096-1-0c2f84a9,MiniMax-M3 (gfx928 INT8 W8A8),minimaxM3-dsh_TP4_M4096-1,4,shared_gate_up_proj,4096,1536,6144,INT8 W8A8,19776.488,986.5248489379883,worker_3 accepted/minimax_tp4_shared_gate_up_proj_m4096/kernel.hip,962.3474502563477,965.7874298095703,20.55025759639306,1955.025759639306,True,True,80.33419874225937,2026-08-23 18:45 -minimaxm3-dsh-tp4-m4096-1-0c2f84a9,MiniMax-M3 (gfx928 INT8 W8A8),minimaxM3-dsh_TP4_M4096-1,4,shared_down_proj,4096,6144,768,INT8 W8A8,11759.473,636.2568092346191,worker_3 accepted/minimax_tp4_shared_down_proj_m4096/kernel.hip,634.1561698913574,634.6337509155273,18.543496946524407,1754.3496946524406,True,True,60.95455267843923,2026-08-23 18:45 -minimaxm3-dsh-tp8-m4096-1-b0482833,MiniMax-M3 (gfx928 INT8 W8A8),minimaxM3-dsh-TP8-M4096-1,8,qkv_proj,4096,1280,6144,INT8 W8A8,41000.618,475.5491065979004,worker_0 accepted/minimax_tp8_qkv_proj_m4096/kernel.hip,474.8045539855957,475.41736602783203,86.35262163311907,8535.262163311907,True,True,135.68637642417067,2026-08-29 03:28 -minimaxm3-dsh-tp8-m4096-1-b0482833,MiniMax-M3 (gfx928 INT8 W8A8),minimaxM3-dsh-TP8-M4096-1,8,qkv_proj_and_indexer_qk,4096,1536,6144,INT8 W8A8,35604.698,531.2225532531738,worker_1 accepted/minimax_tp8_qkv_proj_and_indexer_qk_m4096/kernel.hip,519.466609954834,520.4890060424805,68.54087888939718,6754.087888939718,True,True,148.82460170966874,2026-08-29 03:28 -minimaxm3-dsh-tp8-m4096-1-b0482833,MiniMax-M3 (gfx928 INT8 W8A8),minimaxM3-dsh-TP8-M4096-1,8,o_proj,4096,6144,1024,INT8 W8A8,19897.517,397.44619369506836,worker_2 accepted/minimax_tp8_o_proj_m4096/kernel.hip,394.50483322143555,394.9032211303711,50.43668752426038,4943.668752426038,True,True,130.64379245024566,2026-08-29 03:28 -minimaxm3-dsh-tp8-m4096-1-b0482833,MiniMax-M3 (gfx928 INT8 W8A8),minimaxM3-dsh-TP8-M4096-1,8,shared_gate_up_proj,4096,768,6144,INT8 W8A8,31882.257,319.16600227355957,worker_3 accepted/minimax_tp8_shared_gate_up_proj_m4096/kernel.hip,311.9345474243164,312.3681640625,102.20816278048036,10120.816278048036,True,True,123.91928365478229,2026-08-29 03:28 -minimaxm3-dsh-tp8-m4096-1-b0482833,MiniMax-M3 (gfx928 INT8 W8A8),minimaxM3-dsh-TP8-M4096-1,8,shared_down_proj,4096,6144,384,INT8 W8A8,22389.162,312.4720001220703,worker_3 accepted/minimax_tp8_shared_down_proj_m4096/kernel.hip,311.00934982299805,311.1349678039551,71.98871034823276,7098.871034823276,True,True,62.14396076195009,2026-08-29 03:28 -glm5-2-dsh-tp8-m4096-1-e6a280a2,GLM5.2 (gfx928 INT8 W8A8),glm5.2-dsh-TP8-M4096-1,8,fused_qkv_a_proj,4096,2624,6144,INT8 W8A8,68661.417,937.5793075561523,worker_0 accepted/glm_tp8_fused_qkv_a_proj_m4096/kernel.hip,938.0712509155273,939.3240356445312,73.19424503521313,7219.4245035213125,True,True,140.78913965554716,2026-08-31 11:23 -glm5-2-dsh-tp8-m4096-1-e6a280a2,GLM5.2 (gfx928 INT8 W8A8),glm5.2-dsh-TP8-M4096-1,8,q_b_proj,4096,2048,2048,INT8 W8A8,14385.571,220.13651847839355,worker_1 accepted/glm_tp8_q_b_proj_m4096/kernel.hip,215.435791015625,215.74857711791992,66.77428542482365,6577.428542482365,True,True,159.48946182998893,2026-08-31 11:23 -glm5-2-dsh-tp8-m4096-1-e6a280a2,GLM5.2 (gfx928 INT8 W8A8),glm5.2-dsh-TP8-M4096-1,8,kv_b_proj,4096,3584,512,INT8 W8A8,6939.939,137.40396976470947,worker_1 accepted/glm_tp8_kv_b_proj_m4096/kernel.hip,134.75635528564453,134.93956565856934,51.499901324055074,5049.990132405507,True,True,111.55233090221004,2026-08-31 11:23 -glm5-2-dsh-tp8-m4096-1-e6a280a2,GLM5.2 (gfx928 INT8 W8A8),glm5.2-dsh-TP8-M4096-1,8,o_proj,4096,6144,2048,INT8 W8A8,54775.797,759.9214935302734,worker_2 accepted/glm_tp8_o_proj_m4096/kernel.hip,755.8016204833984,756.3575744628906,72.47377554571302,7147.377554571302,True,True,136.3839562001365,2026-08-31 11:23 -glm5-2-dsh-tp8-m4096-1-e6a280a2,GLM5.2 (gfx928 INT8 W8A8),glm5.2-dsh-TP8-M4096-1,8,shared_gate_up_proj,4096,512,6144,INT8 W8A8,21254.589,273.8345146179199,worker_3 accepted/glm_tp8_shared_gate_up_proj_m4096/kernel.hip,272.7110004425049,272.99739837646484,77.93814318275388,7693.814318275387,True,True,94.49491855548743,2026-08-31 11:23 -glm5-2-dsh-tp8-m4096-1-e6a280a2,GLM5.2 (gfx928 INT8 W8A8),glm5.2-dsh-TP8-M4096-1,8,shared_down_proj,4096,6144,256,INT8 W8A8,17700.262,278.44139099121094,worker_3 accepted/glm_tp8_shared_down_proj_m4096/kernel.hip,273.6263179779053,274.4647216796875,64.6877176537867,6368.77176537867,True,True,47.089410050974806,2026-08-31 11:23 +task_id,model,kernel_repo,tp,operator,M,N,K,dtype,baseline_us,worker_best_us,best_variant_source,final_us,final_p90_us,speedup_x,improvement_pct,target_met,passed,logical_tops,finished_at,validation_state +hy3-dsh-tp4-m16-2-368c654c,Hy3 (gfx928 INT8 W8A8),hy3-dsh-TP4-m16-2,4,qkv_proj,16,2560,4096,INT8 W8A8,80.19,27.359989881515503,worker_0 accepted/hy3_tp4_qkv_proj_m16/kernel.hip,27.722550630569458,27.860960960388184,2.89259098373059,189.259098373059,True,True,12.103659741538994,2026-08-26 14:59,最终验收通过 +hy3-dsh-tp4-m16-2-368c654c,Hy3 (gfx928 INT8 W8A8),hy3-dsh-TP4-m16-2,4,o_proj,16,4096,2048,INT8 W8A8,54.617,15.513530373573303,worker_1 accepted/hy3_tp4_o_proj_m16/kernel.hip,15.558480024337769,15.603289604187012,3.510432890267166,251.0432890267166,True,True,17.25332137715848,2026-08-26 14:59,最终验收通过 +hy3-dsh-tp4-m16-2-368c654c,Hy3 (gfx928 INT8 W8A8),hy3-dsh-TP4-m16-2,4,shared_gate_up_proj,16,768,4096,INT8 W8A8,73.262,11.581599712371826,worker_2 accepted/hy3_tp4_shared_gate_up_proj_m16/kernel.hip,11.451255083084106,11.476860046386719,6.397726665631898,539.7726665631898,True,True,8.790590661865588,2026-08-26 14:59,最终验收通过 +hy3-dsh-tp4-m16-2-368c654c,Hy3 (gfx928 INT8 W8A8),hy3-dsh-TP4-m16-2,4,shared_down_proj,16,4096,384,INT8 W8A8,23.216,9.17439490556717,worker_3 accepted/hy3_tp4_shared_down_proj_m16/kernel.hip,9.128850102424622,9.246439933776855,2.5431461508863897,154.31461508863896,True,True,5.513470747715742,2026-08-26 14:59,最终验收通过 +hy3-tp4-m4096-dsh-test1-bfdf3002,Hy3 (gfx928 INT8 W8A8),hy3_TP4_M4096_dsh-test1,4,qkv_proj,4096,2560,4096,INT8 W8A8,24045.12,756.7258453369141,worker_0 accepted/hy3_tp4_qkv_proj_m4096/kernel.hip,756.7258453369141,760.15869140625,31.77520650070368,3077.520650070368,True,True,113.51448671844342,2026-08-22 05:44,最终验收通过 +hy3-tp4-m4096-dsh-test1-bfdf3002,Hy3 (gfx928 INT8 W8A8),hy3_TP4_M4096_dsh-test1,4,o_proj,4096,4096,2048,INT8 W8A8,19881.949,803.8980102539062,worker_1 accepted/hy3_tp4_o_proj_m4096/kernel.hip,803.8980102539062,805.1364135742188,24.731929605996175,2373.1929605996174,True,True,85.48282973644304,2026-08-22 05:44,最终验收通过 +hy3-tp4-m4096-dsh-test1-bfdf3002,Hy3 (gfx928 INT8 W8A8),hy3_TP4_M4096_dsh-test1,4,shared_gate_up_proj,4096,768,4096,INT8 W8A8,6678.282,220.71203231811523,worker_2 accepted/hy3_tp4_shared_gate_up_proj_m4096/kernel.hip,220.71203231811523,221.53764724731445,30.25789726939083,2925.7897269390833,True,True,116.75758455641257,2026-08-22 05:44,最终验收通过 +hy3-tp4-m4096-dsh-test1-bfdf3002,Hy3 (gfx928 INT8 W8A8),hy3_TP4_M4096_dsh-test1,4,shared_down_proj,4096,4096,384,INT8 W8A8,4381.289,214.83811378479004,worker_3 accepted/hy3_tp4_shared_down_proj_m4096/kernel.hip,214.83811378479004,215.11651992797852,20.393443802010253,1939.3443802010254,True,True,59.97493489868936,2026-08-22 05:44,最终验收通过 +hy3-dsh-tp8-m4096-1-0ebb994d,Hy3 (gfx928 INT8 W8A8),hy3-dsh-TP8-M4096-1,8,qkv_proj,4096,1280,4096,INT8 W8A8,19381.383,418.2191276550293,worker_0 accepted/hy3_tp8_qkv_proj_m4096/kernel.hip,318.4573459625244,326.0519790649414,60.86021643313191,5986.021643313191,True,True,134.86789833717404,2026-08-27 07:23,最终验收通过 +hy3-dsh-tp8-m4096-1-0ebb994d,Hy3 (gfx928 INT8 W8A8),hy3-dsh-TP8-M4096-1,8,o_proj,4096,4096,1024,INT8 W8A8,14151.792,322.98728942871094,worker_1 accepted/hy3_tp8_o_proj_m4096/kernel.hip,282.8514766693115,283.3587074279785,50.0325901304917,4903.25901304917,True,True,121.47625592271805,2026-08-27 07:23,最终验收通过 +hy3-dsh-tp8-m4096-1-0ebb994d,Hy3 (gfx928 INT8 W8A8),hy3-dsh-TP8-M4096-1,8,shared_gate_up_proj,4096,384,4096,INT8 W8A8,16712.231,115.95122337341309,worker_2 accepted/hy3_tp8_shared_gate_up_proj_m4096/kernel.hip,115.34959316253662,115.53359985351562,144.8833111743286,14388.331117432861,True,True,111.70305446889755,2026-08-27 07:23,最终验收通过 +hy3-dsh-tp8-m4096-1-0ebb994d,Hy3 (gfx928 INT8 W8A8),hy3-dsh-TP8-M4096-1,8,shared_down_proj,4096,4096,192,INT8 W8A8,10762.616,171.18735313415527,worker_3 accepted/hy3_tp8_shared_down_proj_m4096/kernel.hip,171.81203842163086,172.1600341796875,62.64180379251588,6164.180379251588,True,True,37.497086951439755,2026-08-27 07:23,最终验收通过 +hy3-dsh-tp8-m16-9-8-0161e718,Hy3 (gfx928 INT8 W8A8),hy3-dsh-TP8-M16-9.8,8,qkv_proj,16,1280,4096,INT8 W8A8,74.782,19.29679036140442,worker_0 accepted/ (n=30 samples),19.29679036140442,19.323190450668335,3.875359507950698,287.53595079506977,,,8.694303915720704,,仅worker验收(未过最终验收) +hy3-dsh-tp8-m16-9-8-0161e718,Hy3 (gfx928 INT8 W8A8),hy3-dsh-TP8-M16-9.8,8,o_proj,16,4096,1024,INT8 W8A8,32.137,11.043989658355713,worker_1 accepted/ (n=30 samples),11.043989658355713,11.16318941116333,2.9099085560701914,190.99085560701914,,,12.153011017938878,,仅worker验收(未过最终验收) +hy3-dsh-tp8-m16-9-8-0161e718,Hy3 (gfx928 INT8 W8A8),hy3-dsh-TP8-M16-9.8,8,shared_gate_up_proj,16,384,4096,INT8 W8A8,66.527,12.48797059059143,worker_2 accepted/ (n=30 samples),12.48797059059143,12.534370422363281,5.327286729048045,432.7286729048045,,,4.030410516655156,,仅worker验收(未过最终验收) +hy3-dsh-tp8-m16-9-8-0161e718,Hy3 (gfx928 INT8 W8A8),hy3-dsh-TP8-M16-9.8,8,shared_down_proj,16,4096,192,INT8 W8A8,17.312,8.670825064182281,worker_3 accepted/ (n=30 samples),8.670825064182281,9.144449830055237,1.9965804720836726,99.65804720836726,,,2.9023563286907703,,仅worker验收(未过最终验收) +minimax-dsh-tp8-m16-1-78402260,MiniMax-M3 (gfx928 INT8 W8A8),minimax-dsh-TP8-m16-1,8,qkv_proj,16,1280,6144,INT8 W8A8,110.478,33.20169925689697,worker_0 accepted/minimax_tp8_qkv_proj_m16/kernel.hip,33.19578528404236,33.328590393066406,3.328073098879459,232.8073098879459,True,True,7.581029876132359,2026-08-28 17:17,最终验收通过 +minimax-dsh-tp8-m16-1-78402260,MiniMax-M3 (gfx928 INT8 W8A8),minimax-dsh-TP8-m16-1,8,qkv_proj_and_indexer_qk,16,1536,6144,INT8 W8A8,111.486,39.72720980644226,worker_1 accepted/minimax_tp8_qkv_proj_and_indexer_qk_m16/kernel.hip,39.73748564720154,39.8534893989563,2.8055625106680915,180.55625106680915,True,True,7.599622449219237,2026-08-28 17:17,最终验收通过 +minimax-dsh-tp8-m16-1-78402260,MiniMax-M3 (gfx928 INT8 W8A8),minimax-dsh-TP8-m16-1,8,o_proj,16,6144,1024,INT8 W8A8,38.655,11.368839740753174,worker_2 accepted/minimax_tp8_o_proj_m16/kernel.hip,11.77299976348877,12.421009540557861,3.2833602969975013,228.33602969975013,True,True,17.10070466699301,2026-08-28 17:17,最终验收通过 +minimax-dsh-tp8-m16-1-78402260,MiniMax-M3 (gfx928 INT8 W8A8),minimax-dsh-TP8-m16-1,8,shared_gate_up_proj,16,768,6144,INT8 W8A8,104.527,17.114474773406982,worker_3 accepted/minimax_tp8_shared_gate_up_proj_m16/kernel.hip,17.31948971748352,17.501089572906494,6.0352239993816355,503.5223999381635,True,True,8.718209743071991,2026-08-28 17:17,最终验收通过 +minimax-dsh-tp8-m16-1-78402260,MiniMax-M3 (gfx928 INT8 W8A8),minimax-dsh-TP8-m16-1,8,shared_down_proj,16,6144,384,INT8 W8A8,23.152,8.95284503698349,worker_3 accepted/minimax_tp8_shared_down_proj_m16/kernel.hip,9.341430068016052,10.175809860229492,2.478421379962978,147.8421379962978,True,True,8.082003660070677,2026-08-28 17:17,最终验收通过 +minimaxm3-dsh-tp4-m4096-1-0c2f84a9,MiniMax-M3 (gfx928 INT8 W8A8),minimaxM3-dsh_TP4_M4096-1,4,qkv_proj,4096,2304,6144,INT8 W8A8,31556.192,1559.944839477539,worker_0 accepted/minimax_tp4_qkv_proj_m4096/kernel.hip,1561.9083404541016,1577.305908203125,20.20361322279994,1920.361322279994,True,True,74.24514869949742,2026-08-23 18:45,最终验收通过 +minimaxm3-dsh-tp4-m4096-1-0c2f84a9,MiniMax-M3 (gfx928 INT8 W8A8),minimaxM3-dsh_TP4_M4096-1,4,qkv_proj_and_indexer_qk,4096,2560,6144,INT8 W8A8,35918.768,1081.603660583496,worker_1 accepted/minimax_tp4_qkv_proj_and_indexer_qk_m4096/kernel.hip,1089.0154266357422,1102.6435089111328,32.9827908048673,3198.27908048673,True,True,118.31698222866211,2026-08-23 18:45,最终验收通过 +minimaxm3-dsh-tp4-m4096-1-0c2f84a9,MiniMax-M3 (gfx928 INT8 W8A8),minimaxM3-dsh_TP4_M4096-1,4,o_proj,4096,6144,2048,INT8 W8A8,29702.473,765.9097671508789,worker_2 accepted/minimax_tp4_o_proj_m4096/kernel.hip,752.4849700927734,753.6545562744141,39.47251331324,3847.251331324,True,True,136.98508169710212,2026-08-23 18:45,最终验收通过 +minimaxm3-dsh-tp4-m4096-1-0c2f84a9,MiniMax-M3 (gfx928 INT8 W8A8),minimaxM3-dsh_TP4_M4096-1,4,shared_gate_up_proj,4096,1536,6144,INT8 W8A8,19776.488,986.5248489379883,worker_3 accepted/minimax_tp4_shared_gate_up_proj_m4096/kernel.hip,962.3474502563477,965.7874298095703,20.55025759639306,1955.025759639306,True,True,80.33419874225937,2026-08-23 18:45,最终验收通过 +minimaxm3-dsh-tp4-m4096-1-0c2f84a9,MiniMax-M3 (gfx928 INT8 W8A8),minimaxM3-dsh_TP4_M4096-1,4,shared_down_proj,4096,6144,768,INT8 W8A8,11759.473,636.2568092346191,worker_3 accepted/minimax_tp4_shared_down_proj_m4096/kernel.hip,634.1561698913574,634.6337509155273,18.543496946524407,1754.3496946524406,True,True,60.95455267843923,2026-08-23 18:45,最终验收通过 +minimaxm3-dsh-tp8-m4096-1-b0482833,MiniMax-M3 (gfx928 INT8 W8A8),minimaxM3-dsh-TP8-M4096-1,8,qkv_proj,4096,1280,6144,INT8 W8A8,41000.618,475.5491065979004,worker_0 accepted/minimax_tp8_qkv_proj_m4096/kernel.hip,474.8045539855957,475.41736602783203,86.35262163311907,8535.262163311907,True,True,135.68637642417067,2026-08-29 03:28,最终验收通过 +minimaxm3-dsh-tp8-m4096-1-b0482833,MiniMax-M3 (gfx928 INT8 W8A8),minimaxM3-dsh-TP8-M4096-1,8,qkv_proj_and_indexer_qk,4096,1536,6144,INT8 W8A8,35604.698,531.2225532531738,worker_1 accepted/minimax_tp8_qkv_proj_and_indexer_qk_m4096/kernel.hip,519.466609954834,520.4890060424805,68.54087888939718,6754.087888939718,True,True,148.82460170966874,2026-08-29 03:28,最终验收通过 +minimaxm3-dsh-tp8-m4096-1-b0482833,MiniMax-M3 (gfx928 INT8 W8A8),minimaxM3-dsh-TP8-M4096-1,8,o_proj,4096,6144,1024,INT8 W8A8,19897.517,397.44619369506836,worker_2 accepted/minimax_tp8_o_proj_m4096/kernel.hip,394.50483322143555,394.9032211303711,50.43668752426038,4943.668752426038,True,True,130.64379245024566,2026-08-29 03:28,最终验收通过 +minimaxm3-dsh-tp8-m4096-1-b0482833,MiniMax-M3 (gfx928 INT8 W8A8),minimaxM3-dsh-TP8-M4096-1,8,shared_gate_up_proj,4096,768,6144,INT8 W8A8,31882.257,319.16600227355957,worker_3 accepted/minimax_tp8_shared_gate_up_proj_m4096/kernel.hip,311.9345474243164,312.3681640625,102.20816278048036,10120.816278048036,True,True,123.91928365478229,2026-08-29 03:28,最终验收通过 +minimaxm3-dsh-tp8-m4096-1-b0482833,MiniMax-M3 (gfx928 INT8 W8A8),minimaxM3-dsh-TP8-M4096-1,8,shared_down_proj,4096,6144,384,INT8 W8A8,22389.162,312.4720001220703,worker_3 accepted/minimax_tp8_shared_down_proj_m4096/kernel.hip,311.00934982299805,311.1349678039551,71.98871034823276,7098.871034823276,True,True,62.14396076195009,2026-08-29 03:28,最终验收通过 +glm5-2-dsh-tp8-m4096-1-e6a280a2,GLM5.2 (gfx928 INT8 W8A8),glm5.2-dsh-TP8-M4096-1,8,fused_qkv_a_proj,4096,2624,6144,INT8 W8A8,68661.417,937.5793075561523,worker_0 accepted/glm_tp8_fused_qkv_a_proj_m4096/kernel.hip,938.0712509155273,939.3240356445312,73.19424503521313,7219.4245035213125,True,True,140.78913965554716,2026-08-31 11:23,最终验收通过 +glm5-2-dsh-tp8-m4096-1-e6a280a2,GLM5.2 (gfx928 INT8 W8A8),glm5.2-dsh-TP8-M4096-1,8,q_b_proj,4096,2048,2048,INT8 W8A8,14385.571,220.13651847839355,worker_1 accepted/glm_tp8_q_b_proj_m4096/kernel.hip,215.435791015625,215.74857711791992,66.77428542482365,6577.428542482365,True,True,159.48946182998893,2026-08-31 11:23,最终验收通过 +glm5-2-dsh-tp8-m4096-1-e6a280a2,GLM5.2 (gfx928 INT8 W8A8),glm5.2-dsh-TP8-M4096-1,8,kv_b_proj,4096,3584,512,INT8 W8A8,6939.939,137.40396976470947,worker_1 accepted/glm_tp8_kv_b_proj_m4096/kernel.hip,134.75635528564453,134.93956565856934,51.499901324055074,5049.990132405507,True,True,111.55233090221004,2026-08-31 11:23,最终验收通过 +glm5-2-dsh-tp8-m4096-1-e6a280a2,GLM5.2 (gfx928 INT8 W8A8),glm5.2-dsh-TP8-M4096-1,8,o_proj,4096,6144,2048,INT8 W8A8,54775.797,759.9214935302734,worker_2 accepted/glm_tp8_o_proj_m4096/kernel.hip,755.8016204833984,756.3575744628906,72.47377554571302,7147.377554571302,True,True,136.3839562001365,2026-08-31 11:23,最终验收通过 +glm5-2-dsh-tp8-m4096-1-e6a280a2,GLM5.2 (gfx928 INT8 W8A8),glm5.2-dsh-TP8-M4096-1,8,shared_gate_up_proj,4096,512,6144,INT8 W8A8,21254.589,273.8345146179199,worker_3 accepted/glm_tp8_shared_gate_up_proj_m4096/kernel.hip,272.7110004425049,272.99739837646484,77.93814318275388,7693.814318275387,True,True,94.49491855548743,2026-08-31 11:23,最终验收通过 +glm5-2-dsh-tp8-m4096-1-e6a280a2,GLM5.2 (gfx928 INT8 W8A8),glm5.2-dsh-TP8-M4096-1,8,shared_down_proj,4096,6144,256,INT8 W8A8,17700.262,278.44139099121094,worker_3 accepted/glm_tp8_shared_down_proj_m4096/kernel.hip,273.6263179779053,274.4647216796875,64.6877176537867,6368.77176537867,True,True,47.089410050974806,2026-08-31 11:23,最终验收通过 diff --git a/metainfer/tasks/dcu_kernel_auto_opt/reports/dkao_optimized_operators_bestvariant.json b/metainfer/tasks/dcu_kernel_auto_opt/reports/dkao_optimized_operators_bestvariant.json index 7d4d11bb..989c0d54 100644 --- a/metainfer/tasks/dcu_kernel_auto_opt/reports/dkao_optimized_operators_bestvariant.json +++ b/metainfer/tasks/dcu_kernel_auto_opt/reports/dkao_optimized_operators_bestvariant.json @@ -19,7 +19,8 @@ "target_met": true, "passed": true, "logical_tops": 12.103659741538994, - "finished_at": "2026-08-26 14:59" + "finished_at": "2026-08-26 14:59", + "validation_state": "最终验收通过" }, { "task_id": "hy3-dsh-tp4-m16-2-368c654c", @@ -41,7 +42,8 @@ "target_met": true, "passed": true, "logical_tops": 17.25332137715848, - "finished_at": "2026-08-26 14:59" + "finished_at": "2026-08-26 14:59", + "validation_state": "最终验收通过" }, { "task_id": "hy3-dsh-tp4-m16-2-368c654c", @@ -63,7 +65,8 @@ "target_met": true, "passed": true, "logical_tops": 8.790590661865588, - "finished_at": "2026-08-26 14:59" + "finished_at": "2026-08-26 14:59", + "validation_state": "最终验收通过" }, { "task_id": "hy3-dsh-tp4-m16-2-368c654c", @@ -85,7 +88,8 @@ "target_met": true, "passed": true, "logical_tops": 5.513470747715742, - "finished_at": "2026-08-26 14:59" + "finished_at": "2026-08-26 14:59", + "validation_state": "最终验收通过" }, { "task_id": "hy3-tp4-m4096-dsh-test1-bfdf3002", @@ -107,7 +111,8 @@ "target_met": true, "passed": true, "logical_tops": 113.51448671844342, - "finished_at": "2026-08-22 05:44" + "finished_at": "2026-08-22 05:44", + "validation_state": "最终验收通过" }, { "task_id": "hy3-tp4-m4096-dsh-test1-bfdf3002", @@ -129,7 +134,8 @@ "target_met": true, "passed": true, "logical_tops": 85.48282973644304, - "finished_at": "2026-08-22 05:44" + "finished_at": "2026-08-22 05:44", + "validation_state": "最终验收通过" }, { "task_id": "hy3-tp4-m4096-dsh-test1-bfdf3002", @@ -151,7 +157,8 @@ "target_met": true, "passed": true, "logical_tops": 116.75758455641257, - "finished_at": "2026-08-22 05:44" + "finished_at": "2026-08-22 05:44", + "validation_state": "最终验收通过" }, { "task_id": "hy3-tp4-m4096-dsh-test1-bfdf3002", @@ -173,7 +180,8 @@ "target_met": true, "passed": true, "logical_tops": 59.97493489868936, - "finished_at": "2026-08-22 05:44" + "finished_at": "2026-08-22 05:44", + "validation_state": "最终验收通过" }, { "task_id": "hy3-dsh-tp8-m4096-1-0ebb994d", @@ -195,7 +203,8 @@ "target_met": true, "passed": true, "logical_tops": 134.86789833717404, - "finished_at": "2026-08-27 07:23" + "finished_at": "2026-08-27 07:23", + "validation_state": "最终验收通过" }, { "task_id": "hy3-dsh-tp8-m4096-1-0ebb994d", @@ -217,7 +226,8 @@ "target_met": true, "passed": true, "logical_tops": 121.47625592271805, - "finished_at": "2026-08-27 07:23" + "finished_at": "2026-08-27 07:23", + "validation_state": "最终验收通过" }, { "task_id": "hy3-dsh-tp8-m4096-1-0ebb994d", @@ -239,7 +249,8 @@ "target_met": true, "passed": true, "logical_tops": 111.70305446889755, - "finished_at": "2026-08-27 07:23" + "finished_at": "2026-08-27 07:23", + "validation_state": "最终验收通过" }, { "task_id": "hy3-dsh-tp8-m4096-1-0ebb994d", @@ -261,7 +272,100 @@ "target_met": true, "passed": true, "logical_tops": 37.497086951439755, - "finished_at": "2026-08-27 07:23" + "finished_at": "2026-08-27 07:23", + "validation_state": "最终验收通过" + }, + { + "task_id": "hy3-dsh-tp8-m16-9-8-0161e718", + "model": "Hy3 (gfx928 INT8 W8A8)", + "kernel_repo": "hy3-dsh-TP8-M16-9.8", + "tp": 8, + "operator": "qkv_proj", + "M": 16, + "N": 1280, + "K": 4096, + "dtype": "INT8 W8A8", + "baseline_us": 74.782, + "worker_best_us": 19.29679036140442, + "best_variant_source": "worker_0 accepted/ (n=30 samples)", + "final_us": 19.29679036140442, + "final_p90_us": 19.323190450668335, + "speedup_x": 3.875359507950698, + "improvement_pct": 287.53595079506977, + "target_met": null, + "passed": null, + "logical_tops": 8.694303915720704, + "finished_at": "", + "validation_state": "仅worker验收(未过最终验收)" + }, + { + "task_id": "hy3-dsh-tp8-m16-9-8-0161e718", + "model": "Hy3 (gfx928 INT8 W8A8)", + "kernel_repo": "hy3-dsh-TP8-M16-9.8", + "tp": 8, + "operator": "o_proj", + "M": 16, + "N": 4096, + "K": 1024, + "dtype": "INT8 W8A8", + "baseline_us": 32.137, + "worker_best_us": 11.043989658355713, + "best_variant_source": "worker_1 accepted/ (n=30 samples)", + "final_us": 11.043989658355713, + "final_p90_us": 11.16318941116333, + "speedup_x": 2.9099085560701914, + "improvement_pct": 190.99085560701914, + "target_met": null, + "passed": null, + "logical_tops": 12.153011017938878, + "finished_at": "", + "validation_state": "仅worker验收(未过最终验收)" + }, + { + "task_id": "hy3-dsh-tp8-m16-9-8-0161e718", + "model": "Hy3 (gfx928 INT8 W8A8)", + "kernel_repo": "hy3-dsh-TP8-M16-9.8", + "tp": 8, + "operator": "shared_gate_up_proj", + "M": 16, + "N": 384, + "K": 4096, + "dtype": "INT8 W8A8", + "baseline_us": 66.527, + "worker_best_us": 12.48797059059143, + "best_variant_source": "worker_2 accepted/ (n=30 samples)", + "final_us": 12.48797059059143, + "final_p90_us": 12.534370422363281, + "speedup_x": 5.327286729048045, + "improvement_pct": 432.7286729048045, + "target_met": null, + "passed": null, + "logical_tops": 4.030410516655156, + "finished_at": "", + "validation_state": "仅worker验收(未过最终验收)" + }, + { + "task_id": "hy3-dsh-tp8-m16-9-8-0161e718", + "model": "Hy3 (gfx928 INT8 W8A8)", + "kernel_repo": "hy3-dsh-TP8-M16-9.8", + "tp": 8, + "operator": "shared_down_proj", + "M": 16, + "N": 4096, + "K": 192, + "dtype": "INT8 W8A8", + "baseline_us": 17.312, + "worker_best_us": 8.670825064182281, + "best_variant_source": "worker_3 accepted/ (n=30 samples)", + "final_us": 8.670825064182281, + "final_p90_us": 9.144449830055237, + "speedup_x": 1.9965804720836726, + "improvement_pct": 99.65804720836726, + "target_met": null, + "passed": null, + "logical_tops": 2.9023563286907703, + "finished_at": "", + "validation_state": "仅worker验收(未过最终验收)" }, { "task_id": "minimax-dsh-tp8-m16-1-78402260", @@ -283,7 +387,8 @@ "target_met": true, "passed": true, "logical_tops": 7.581029876132359, - "finished_at": "2026-08-28 17:17" + "finished_at": "2026-08-28 17:17", + "validation_state": "最终验收通过" }, { "task_id": "minimax-dsh-tp8-m16-1-78402260", @@ -305,7 +410,8 @@ "target_met": true, "passed": true, "logical_tops": 7.599622449219237, - "finished_at": "2026-08-28 17:17" + "finished_at": "2026-08-28 17:17", + "validation_state": "最终验收通过" }, { "task_id": "minimax-dsh-tp8-m16-1-78402260", @@ -327,7 +433,8 @@ "target_met": true, "passed": true, "logical_tops": 17.10070466699301, - "finished_at": "2026-08-28 17:17" + "finished_at": "2026-08-28 17:17", + "validation_state": "最终验收通过" }, { "task_id": "minimax-dsh-tp8-m16-1-78402260", @@ -349,7 +456,8 @@ "target_met": true, "passed": true, "logical_tops": 8.718209743071991, - "finished_at": "2026-08-28 17:17" + "finished_at": "2026-08-28 17:17", + "validation_state": "最终验收通过" }, { "task_id": "minimax-dsh-tp8-m16-1-78402260", @@ -371,7 +479,8 @@ "target_met": true, "passed": true, "logical_tops": 8.082003660070677, - "finished_at": "2026-08-28 17:17" + "finished_at": "2026-08-28 17:17", + "validation_state": "最终验收通过" }, { "task_id": "minimaxm3-dsh-tp4-m4096-1-0c2f84a9", @@ -393,7 +502,8 @@ "target_met": true, "passed": true, "logical_tops": 74.24514869949742, - "finished_at": "2026-08-23 18:45" + "finished_at": "2026-08-23 18:45", + "validation_state": "最终验收通过" }, { "task_id": "minimaxm3-dsh-tp4-m4096-1-0c2f84a9", @@ -415,7 +525,8 @@ "target_met": true, "passed": true, "logical_tops": 118.31698222866211, - "finished_at": "2026-08-23 18:45" + "finished_at": "2026-08-23 18:45", + "validation_state": "最终验收通过" }, { "task_id": "minimaxm3-dsh-tp4-m4096-1-0c2f84a9", @@ -437,7 +548,8 @@ "target_met": true, "passed": true, "logical_tops": 136.98508169710212, - "finished_at": "2026-08-23 18:45" + "finished_at": "2026-08-23 18:45", + "validation_state": "最终验收通过" }, { "task_id": "minimaxm3-dsh-tp4-m4096-1-0c2f84a9", @@ -459,7 +571,8 @@ "target_met": true, "passed": true, "logical_tops": 80.33419874225937, - "finished_at": "2026-08-23 18:45" + "finished_at": "2026-08-23 18:45", + "validation_state": "最终验收通过" }, { "task_id": "minimaxm3-dsh-tp4-m4096-1-0c2f84a9", @@ -481,7 +594,8 @@ "target_met": true, "passed": true, "logical_tops": 60.95455267843923, - "finished_at": "2026-08-23 18:45" + "finished_at": "2026-08-23 18:45", + "validation_state": "最终验收通过" }, { "task_id": "minimaxm3-dsh-tp8-m4096-1-b0482833", @@ -503,7 +617,8 @@ "target_met": true, "passed": true, "logical_tops": 135.68637642417067, - "finished_at": "2026-08-29 03:28" + "finished_at": "2026-08-29 03:28", + "validation_state": "最终验收通过" }, { "task_id": "minimaxm3-dsh-tp8-m4096-1-b0482833", @@ -525,7 +640,8 @@ "target_met": true, "passed": true, "logical_tops": 148.82460170966874, - "finished_at": "2026-08-29 03:28" + "finished_at": "2026-08-29 03:28", + "validation_state": "最终验收通过" }, { "task_id": "minimaxm3-dsh-tp8-m4096-1-b0482833", @@ -547,7 +663,8 @@ "target_met": true, "passed": true, "logical_tops": 130.64379245024566, - "finished_at": "2026-08-29 03:28" + "finished_at": "2026-08-29 03:28", + "validation_state": "最终验收通过" }, { "task_id": "minimaxm3-dsh-tp8-m4096-1-b0482833", @@ -569,7 +686,8 @@ "target_met": true, "passed": true, "logical_tops": 123.91928365478229, - "finished_at": "2026-08-29 03:28" + "finished_at": "2026-08-29 03:28", + "validation_state": "最终验收通过" }, { "task_id": "minimaxm3-dsh-tp8-m4096-1-b0482833", @@ -591,7 +709,8 @@ "target_met": true, "passed": true, "logical_tops": 62.14396076195009, - "finished_at": "2026-08-29 03:28" + "finished_at": "2026-08-29 03:28", + "validation_state": "最终验收通过" }, { "task_id": "glm5-2-dsh-tp8-m4096-1-e6a280a2", @@ -613,7 +732,8 @@ "target_met": true, "passed": true, "logical_tops": 140.78913965554716, - "finished_at": "2026-08-31 11:23" + "finished_at": "2026-08-31 11:23", + "validation_state": "最终验收通过" }, { "task_id": "glm5-2-dsh-tp8-m4096-1-e6a280a2", @@ -635,7 +755,8 @@ "target_met": true, "passed": true, "logical_tops": 159.48946182998893, - "finished_at": "2026-08-31 11:23" + "finished_at": "2026-08-31 11:23", + "validation_state": "最终验收通过" }, { "task_id": "glm5-2-dsh-tp8-m4096-1-e6a280a2", @@ -657,7 +778,8 @@ "target_met": true, "passed": true, "logical_tops": 111.55233090221004, - "finished_at": "2026-08-31 11:23" + "finished_at": "2026-08-31 11:23", + "validation_state": "最终验收通过" }, { "task_id": "glm5-2-dsh-tp8-m4096-1-e6a280a2", @@ -679,7 +801,8 @@ "target_met": true, "passed": true, "logical_tops": 136.3839562001365, - "finished_at": "2026-08-31 11:23" + "finished_at": "2026-08-31 11:23", + "validation_state": "最终验收通过" }, { "task_id": "glm5-2-dsh-tp8-m4096-1-e6a280a2", @@ -701,7 +824,8 @@ "target_met": true, "passed": true, "logical_tops": 94.49491855548743, - "finished_at": "2026-08-31 11:23" + "finished_at": "2026-08-31 11:23", + "validation_state": "最终验收通过" }, { "task_id": "glm5-2-dsh-tp8-m4096-1-e6a280a2", @@ -723,6 +847,7 @@ "target_met": true, "passed": true, "logical_tops": 47.089410050974806, - "finished_at": "2026-08-31 11:23" + "finished_at": "2026-08-31 11:23", + "validation_state": "最终验收通过" } ] \ No newline at end of file diff --git a/metainfer/tasks/dcu_kernel_auto_opt/reports/dkao_optimized_operators_bestvariant.xlsx b/metainfer/tasks/dcu_kernel_auto_opt/reports/dkao_optimized_operators_bestvariant.xlsx index f97cf0db2c976a0ac6e277f77a96a236065ee91a..dde8c8a40ba55a5e864b4ee6a4ec2e6ead9fcdfd 100644 GIT binary patch delta 15039 zcmZ8|V|ZoFvu|wMwrx#p+qUf;+nm@=Cbm6sCf3BZjhpwq_ndS8XMfmh@AdpvS9NuD zRae!j{XpQwSYUWX8Bj1ZARr(pAd@_XMEE}B?~C50Cab~K_f1+5ARy%A9ey;xRFaa+ zAQRGPZ`6Kwck%py@G|BwC3t_}u8yR=j%IS$e(~Q{&KjNYW5{b}kFWPj564&pbm!Fm_S z6>e^d&f+j5s_@0^30^2qfWLRnGJUl|ao5N@t(H_-0Iqw+drJpkMWCT&S55EQjo7|b z_;+XX{=?}0>);j5xwnbOBO$H<*gAy&U$Hmv{}78_M&mUw9|XcbqM^#qQ)b2j0$OQL zz9Pj2Ff(}C*>>yMxZt)XebCa`*^NI16Lk@$T~uc6Z%Wcek%*YzDT(Q%)Wnj}pxIHr zonhubCv^A#IsR6wylghZXPbuO3g_pCMR&g{iE&Pc=~HjX_5teKB5A0?gb<1a@zpCAc0=|5`(->&Un z-TZxA1W#so;tj4YM;dzp*&Fh`y@E)PRefdPuHe6 zXEztlc0gCgriXd**%Ra!ngc*Z0A7>m^UeJB<7Rb>_e4=Zz6?-IC||otcpZ%P>OTi~ z@p*RiY5ZBunkV2&xqO#L(b~$K&vzUnIOWhI_lo@ZxxMEqkhfW~C6Re|!}mG=VEfve z{5G7Oye>5)&VmT_j%JF@hQF65GXQzECsmyLeVt=kWv`wlpILsdTso-V76Pn>dgGDz3Lhrlc+vIGmSq0L#-q)Fc#U_+moCR~l7)3<2P=UK1q0E`Lf^7?H( z46f_^$CIu~4AXHjE4+G1uP@Yrph4R^aH;3!RfoPusA}u}C8=sL-SGjJ5xHI4?}dz0 z;k6DaJ#X~xCfuX(ytV}X|znRmp5}1dfovLFF`}7~J%0ap~GOSK66jU%l z`=4AMRNWWm=a9M4?n*HjwTr{X+Jv&ZC$@a34n6z>Zt{vE`U@XXYWkDJp;52bhk_ME z!MmZ>dYajiHjBb`T}Oui&BT6xdYN;PD`@5#oW-dxBqsIH0h{?zUe3Q#}X3nOFM-Gv^`iL%Q|y z_iH|Ri^(lp{DYkG)aQ^oL_0*1Q)Elv*h69~7f`iN`EnQUkdD}R@};;%!PQd782lAP zJg51I;wtR84=lv{EHb<32e zi$5&Z5kr_cvFHWpilKG8PrC(LMoi8KXxF4>|Xg&?rRLYtP44S5eZksz{BmpY#ht4#BC9CG{) zOs2JMriTn{4+jE|E^*3BsPVChkcaFU8Z#An4)*}FT-@&#`LSHUyGFn}-s>MmUcVWs zjAuCH2fRoT^u|~uS53_9z3ktfi4>vjSS6qtMKlVT%0snqfsUh2(2dKzuL#IFu zh8T_viku#1(4|Tmlat6>htefhyP`=F$*@gpB^>J+W}DG)5l#5TIwKT;xX6QS3$@Be zXvgCjJLk6d^$W+|!m1TXzCih?k~ON6HIDjH0SK$~6HmF!(8tj33f{>kqK~X!KYEnn zuJI(}pm4ZY2Z#=U*XQ}xtMRZ?uzpiXB%ET0fm38dh<139$gnHNS~m$4peu{ABoR=_ z#s^8+0pGyxvN08*x9yzbl6*x(7Ng)~*9BVT5jOuL5T%zhs$O|^KBQPClB z>pNHoWU<0b$uh4(ExCp^*}`^?n_Gy=kicop!(w15Jx+zvQq`2Wx_rrITW_#gXNnle zlSyz08svEb&}osaLNrmQqo0X*6p}3fx1a^@eWXTIlD~e|<4~A;v z5-UYC?aegp#ykx*GzX+hRmzE4HQ#?bQt~R>eMLn_GF3r&`iTkyQpz$G$--gj`+fR@ z!iH^P?sb_7^C=F-QfwsAqg16zBz6inXi_MQ@)stlHT$l33*(A+{eD5!#q9&uj-)Qa z5up7ynJ&n=Sc~3C7A2Jh1_;QfJW~-KR|Y0lIal(?kBVC= z-ozg`hrR*r$qqTZ%>>*-=OKUy?vlW8-tk(ESj7X#BcIM9;WNeQNe3cd_G7w>h~rw|Z~aT%ja2qP^ZUR5=Fo5z1`YJUx z_8vD3HT_;)o$C)EdJDj&vu2<+hyhb7Vzwa##Wy;Z~vY3Y>%13UOrH-46iuow$$&YNV>{4h(l7 zTeeduU#iS@G@KaZucR6jm=w!A;R_~rNhLJroveVz(g9~RD^-CZ0e0)XbByMtOX=9P z^v=#C4XL6!9~>)H|HAPGG-hOSO{~z%L-|VK91P2&2bj*>P&Z)~Xu{e&rCHBCG&mXs zbSU=4GT*aTRZswX$xizzJ5g*9#3L+Bg^B!bK~6m*)UsQb+oLQsu-r~zH*MFqIEEF6 zuE-EDy;LQ3a=iI|;;G8}nJ^K+?@)A9 z%5vJ>QQ<2vBkTJhRvY3^@L~{ieC=-*QD7min zQ!v#1&>p@Ax_sp$btJhU6tPlFRna}qXLJ+wW|P;XV@wg_y9W9I3%ZSfoE9Du4zzOh z7F7AkVlF9{zspVXGwWViu99+f@jL!4DP!do%07~aQmcvgZh-HO=nFj}EW4;JX zee3Caw8k%4mC~dojG%yhVr`}t4qNbzq4iYZwC3BJX)3i`_GhnIeQp<9o$XFtI-i*O zhc5dHWo*+oU9}9znV`_p@g+I20kK>FzVF{4CU@S_S#?wp%z4kz#97tVjzt7v6^UYr zZ4gxLs=60MX#^(*<4xl~jcfw~_#lmrMH7bLlN4NLlL?tjYrz#3e)U!cE6@n`OsMvc- zLA4gs;3AX1#7v@N5JlAiTYt*HE4_C5A{nZ<_e&IOc zpnGi2g@_8hZCK_f5vZj?8xHhev<5NRjOtSAM>y(4S~@J*3%W{PyHr?#s#Zuhwp>9y zanQ3W$@#Olwydl)i*0(Z3YXjhfLk>xyo0<_2LvpOm0v2B(&wK)Pj3%Dt#38D?mypy z(dhZGG3Y^pVbUnUqEKq^;Wj{U)8pS;V)$F1H5c{z6wr;Gn zn{DQ`cPF5O*wdTpAWgz#(D8P{s<5ZmMy|9Sk&PIN@f48k+`0u?_FRw|dB%CjLFf)y zfH92gyj%9KcY$E*DtI&KEb47r@*uplQ)ioM7k(xWSsBh1T~MH8m6XVulRihtwmdnv zejfvlhkYI+B>1UlYc{X}el#H~Wde598!T5-wgn0=0qbWf{QX@5sd|4yD`;f4hD0^1 znIIhY$%RDY)QEDHX$H2Y*p=Q&18!J^{Mb9dIoc>5bgS3cql(%qJd>eHqQP(w>|!y& zW?6nIGNMVfmBVY?y!Sh#1a9;ncMq-*i~V7x%-1tf)oi2r@Mg~fe9yh>d+w{ky|=)v z%eCK2@j=7e1N-gr<|DQ0JdNN|t<*FJ6kucScEy_7)Mfjs?qmAAFyPL6|Ij;DB>l4M z7)S1!W6%M%0|k?Jq`<2V)KjD+Lw`{m7vt{2zoUx8{f*z@e>MAvdv_P(GsgMV&!S;; zQMquo7_;eYat}!JDm%PIb$_aUIvBtFkka?zFMU1u(1hyf4pK|xGf3FDr?FJ5cLrP! zxUOoTs(ST6r-&k^DnOy7`dt{lriqr>8pW6$*Y9Bi$NugM3X3i402gncdry-)mQ+@+ z>)ZJ}Ab@sbmf3TZ*#HMl&6YxNfO<fw^9RBHk~iRZ9AI)rF%_S%Fs_h{?(tTlvKCYYG3y>>Y$f?S}+Z1GD3Se zM2{&fwu4M=BZHf}DMG*E+t$0p21ehAzBdPcUa0S*(&$^B0)NLTA?1b`0dMksqAK6E z+Jd3kQvpy?U;GTqure?<@aXD(kmY&LOnbB&TSvr40Ta`;1<%8choRLhv8!Fm(zVY% zVsTKjtFU{?U$@agACtDDeWGWSO|Ljy#+m{UD=z3!h$ByN!K&UVi~p<--aDCwaknDQ zpU&Lh5SP+UrOc^5aac@EHa1PtKwBK4$~Ru>vj^;9f+>f3vJ?kaifIm~E}!7ja0#v# zguft|gri9m*rFK)+cVSRW*iltE%Ibc#1`ls=G4-c&=R`(oK%u%QR^K{ABLV-!+xmQ zO+%yYlo2VaO1A`0>7m$PZ)XpW+^~XV;?QA7I&f5VU7Ai+`+dnr<)+G7?UxfJR_su0 z&;tB^5Sg(^J01t7i$>oV!tIqmy$ZEl1#J2sClDR)@z>9l?;5Ov@>5#YE`jOU>)x?k zRoYQoeoFvW`&$A(XgS3gWi`}ZYONFG^UJH@ie_WCtO8y1C!yZj94Qo~u){_W&18jV z#rHu9XqeZ3S%8emu^Mgjc6h@}R-t*r0EDt}^7NU4Z&?RU2xbc8W6ihybHClLe~ZYS zYgEwzU4E16KX!uMr}^{5*<>_b&HjmZyR(bzIr8?zk9B(n$&P`_<7_z5} zuYjQo%0X5v0!tpt5D`4O!T|hiZn|EU#P3%da-AQ*kLHKLb|7cB|Na11-|4?vD2=c%hGzGTDu4-mfm+Pu98q%{TW|G!yS>-WpIH&6|sf z)$_b+kB{Hd=vwNowBQXSx#a!Uj!QpJ8;V&H`)uZcL zB<7?6Lum{iEeAC;z;WfaIhZ*p5BhITMKHM zwK#wOF`)K#U7wZX(!DkI7pqtQNTGE+_iMm4H6A1)k_x%X=>wIBM2vQJ+6L=SrJg6_ zJZ)8en9^(?*d)Na16+XK_lwNKKyr2p?A3pvI{@s z?5A{d@E0JwUL>hll`emoG!8~av&6c#1ysn+9jKfp_@kw=7 z$QVOID&#?rCNBj>+X~t;2_&{98WZqr+ew%&K0{?4^CVoOys;#uZtgGr0l)XZI0y+X4c*4OCQS#A6cii}#0Q)86c1r(9u z+`!ui02M^c%$)ZeN>Oye#i2$r)S1cz3rDpJ-c6CV16=soOGtMxG_3zlr_2bAai%Re zx0WDM_9AVE6d0tK0CA~tH(ny?3(@2Nh^n0j$E|{r#8N-6T>N-bSV3k5r zQ4z{#;RrvN6?4*nabDMTC{2M~cMx@R4QfNcLE3d_`)Akt3Z1T+KL8~3qLxyvd>&-A z%MtmPM@?6xH<&?}h|v65dT5XX-312Jj`5dtoBC0+d>xd)08@=(d_6?52z?|2RB`hp z_ex}UZJpC*sh^UzaQLNAv&Z8<{uV0AwDHE#1c5rP z!Y-Q`WwG-V?nA5Jn_Z-L4eM2H(-p8TUU!S!idxOmDcw)gTFE$>>UF4jutBM)Y6n|X zzHbeA)I+k?XmezI2<(?>0mjyD92|1R|BoOn;%NKV4!uZ+N0?@eWWIAUvMr3^3sFK0 zkH-CS$uMu*de$xlcro0H7qPGZC{B>o88S`Gx7MnWtJaz?Xy6w0jK2Y_)}hqCS3_9s zdo@%K1fD|MtqEDwx$mLT#&hMNkkrQ4`U`67;Pqgv*jQ(w+{Az67!&Z1oPEsRjQ;@YaghnG|}SQBzdDeDSfPZocP<5Smz=?$*6d7^3~Nk zGX>6PXFhd2I~bP$qOx17_n)wq=`dykG9_iv>!Fe6@#_k(DCqspk}(M&m>DJ9jP0E+ z`|4@WQ83S0-RF5&0c26(`FTDuee!V=&IoL62)f%-E+@iUFyscUP8~6x5gyKmQ(Zyk z#LvI@dbd8GrH?&Zv&aNIznz`9_Pi30@JSPH51$i$BK~JJ0;Kh&rUwEDXcHGG`I;09 z!17NGq9(&ifm8x1>?`Vk>dztQg+OKPLBbrS2$~6Jc$>^&)pQG{3d9ti&#J49iZXaH(Nr%h7WEsi6ho#SBT#OV_ zk@3TS4Fza*opmB9Rs&QjA(F{*9^(-Jl^&SGe|~%_nx8D_p>@9unkJ#mTuKmf`voqAli z?nqzKjo zx%DynaDW}a_>ih?gxDTQ>RN38=LPlQeYuhBAgfUOEg*|f3oRgjpsC64t2=a4=XSKUW`!<@RJ?iKhFP6=o8v10r)H}Y`O#uN|paG@1}!& z=MppWx?gA1Wi@-c84s-g;Pe#XuF_|s82-G13`W7dT?)<`Ie9}U+tOnOxUIVFn0rOP ziiE$O7K+{1yWj;;E4k@F)x4`|n?362`km&Y7zh;BJR~#Up3;_(x?zfJ2^G~6SE#V4h~*|lQ;uitqtVff;uem(?8N`@9v16v9GR^p{5r`s1@luaKOPXC6D z`!IlR#-_&IbkpSkf`P{>nBRrb=VoUIlh2V*&6D28a>;eC}<$FADY&ymAhQ(lHz^ZGT6qVCB@!qPAIfZ6zW(2h#>4=B}_kh6->Z0 zQDT{cT+C8Ndew%R)R`cLXvjm2S2!yUDtD{R_K(x(Vtv(^PBq&+IYYN!>^5{y#IJ`y z_>@|qZPxhwFm#lccaA@`*nanKhE(uxLTp6En`?n=Tcb8^?yA@srPF~i3v0*n=F!HX zh4u}@$dlayMM+r$;11&hS~c?{>Tv#&BEV!c5C8!a4oUA}@{QI!16p3T;c^is0ax z=3VCc2&1-#W6&1Ttu`DYK?n;vT!1PO3(&1j5PjxI87Tn=ph4^$HI=~2$3U2bnn5)O zFbxgEh|x)oQv+F!g&<@^qw6|180{CuncbODs5o!s9q1yX991cTpHn*PMSS)IO0^p* zU(!Ihb7>nn`>mxqp}}RO8%d^{aR(>z^t52kN&#g_U zg2yNy{%{vij6z4rZL&Bs-?{WuQwvE5qq-x=8c;3-`=N=w+iY~QW&x9=G@tw~@UR6x z^5ISzfP-sC+q0+s5ft70(x|%c6r7(mA4dTm#}w;cdo3Z%7VD+lWXByUPpi*@1N6)K zndhDTI>#Z^RSxe%7)TE7pD$m8VVXQ?!{6aUsg3;u_@gkg9YiTCSTjI2e#jtLv`nHX zbU5|fbETDFpg<-M5+R%W%1v(E zq4M>BEIsf(c;0J(o>}2rK`r;7uu2Wqv4QRIAg@hnDuxyxx2%M~466-C2>ttMU_AxE zq2yyJ>xDJpGVx|02LL}%XEC!(^2wGQXFdBy25r$kwb6ur&i#~RHr@r`)VVIP*iu`N zX$hVyjgDUg_4LmzUyX@m5SD43L~W4km)X9sxJWn|}cR>Fr!C3Nn_#fPmhUFG&dj7n(8-dmKog zv_TjvD)f;K&dVILu_jGI!8ln>{5aCG>(-`Yw11rD47Li2E$#laWL_48Uy3nKa}Hwr zvx+#i%|wqn*EHYca$2d!y*D$jBN}N6R6kHK1p8L8Vrd-?Q*c~sR}VPr9D6DSa#%$a zMTBQ@jlJL0y>ifw4EkCC=E&w72WUOA|$uO@;mX1tyAj^ZB=j)s57`&Mz!Z%$lr z$kFKJjD11p`nDgJ3T+!dNY?d6l^n6Ki)*GvD ziR7j&F0L;({CwRU{1vjwX85z8V(irCePieuD4>uPXamR*Q)n7U5|ro*O$Zn00^}MS z$Ofny6zRx!8$Lh-P+uFYCgzQj<;h2P+pVE#HET{-c~E&F5|TXG@VJD=lnpc+v(-x) zf%c6Yf%% zLAE~h;HyL7ScjPwiGt!%JL(5G#o$38AJ09aN%NEm9O2 zjl`871O;4d0+LuB{LK;yq@?m#V08$!>7XOp0zo+7E$B74)t`8Z3Evip&=9M52c8E9 zgbc1iXi3uIx_FJ7DpRA@h1Sy5O0lL3BTyh!DBikR^X#}P(ERzZFoU$976KA7jNv-0 zxyvZM{pCQIrD4<5RKC!W0Ls!o>BnkN&wy4q_rJUksS1@8;z?Dfq*M-O6QuVN<9p$% z1N<{`T70r$X+2OHQ~b4UeW`#PSSNCqexc`by!2U_Lhi5!=YWYdIDlB*g(t=ZFNs@hj*7QZYV+v7_Ad_7v ziBy@7<#E;$z0G3CmLx2UgFI1#Ag3D=1dyj9#W4y&PpY%HoGqpV`T8&(bFbew6Hh;D z3&`0f*|M-m)_Qc;@T%-|KK9HO(=}dMcSAzOBvu4+lh2P>L4oGIo2JRB>j!~o1{s{` zL`m@|XT7+2_i^bKC0az0)I0PR$yc+z_ig2$AU-hlP8S&;6RzA|7J?2#s%Chv0JyzX zM+>DuD6g3VA+0RAaqpAZ+9`(g10Y9d^VM2s9zDNuL@A}v2=LTbT3-Y3 zQdA(I?{|)XJd+I1`f0YwXVv!FHk_IM^4&p^Y#85wWWK`0Lq4@?Yr>`4=(}@Mod21 zCsfRiT15YnvsgvM_Y+Ut+82CGfWuL0*Bx)FTIOA${KPaAtS zN=08;33 zIiYSa*K^o5xxQ{NZ?fE`n_^7FKZzmva!{|(=aL|H094Cf=+Pde7U%-=Mb}>xF6hyo zJio@fRVpkiN*UNTN9xFuFe=BdM9B@xSd?VH-a&!Eid?+HnEH-dHB1a7jUJW^T1YV6 z6OokZgP=l=AR=X=he&H-a`{z#b#NML=~kayMCP7|Ije8;+j<9VLT{|rg6Eo%{Y{!UgY zqHLL*fQ~4K&wUKWrZNSSFtT;B3lT&_7%Mz##(CqbZ84mGP{(A9ja*Ji)>VK?tdk7ypeN z-7Ud2C;%NGbtw|tRrPP$leloJJ`(#Ykx5nyevdIm(+`BD!5zKt80PMP=6mckhtKqM0zXL{;UEI(=qvlJ%gg5nvR>7?CQ8;`nm_OU0Wb z!sT&rA~YioBYQ zZQ5mG7p8oP1KV^dw62Ti8*H3~8>fEZ=4X-y_MTn4sW1sw$qi_=oYt&`>zP^qJM9q6 zh~9(&4Fm*YhyTy3kIC_Z;DA&uF9#eMlrI|kLM~0_*{6G~^(2u^ia!imXTz)Skt4HM z`T;vrZ~{Fa>Vz&VkIzzRN2kAjb^5F6@{NpLxt6$v<+fz$d9Ui2t$zBlNgvU6$Icmb zE%QD%#3b9L^qV*fNNA{SZt@^d_Q^=g z2jzlU;*H|qUxT@N+5jl?wk$~hW0d@XE$F|fPh@mFwI^r&RV2}xc1w^+I7henI@Q`S2^A*`Gx*6v4uwY6$DYk<*d!K%wl1 z>6IIaw7w&D+sR+@o6H5}IC)(~Ns8P(SY|>?zPMBTkqS9v<@N|J)E|SFw#M|r6m?<7Js%C#7hBm#4NnW?*nJc+VGR_bB{+hpsqhIc{WRojy&U~(i$+femr1HvPaG7tF z6en-#8P+!cw-s2evhw?Iv--`)P=-(~-+5Npy*87(uSMOMRV#*Qd%%MmDev#;N?537YGnn?fKh85$ z(no(rs8k{W3GsjaCP{*Ew@=ku-)4~c7aOP_r+5E~UN_Ad=H0UlQa%FW!wQB(5gR$N z%5vwW!qHZhCj7BYb_sP)9{EVSz&F;8(x?$uQ{^J=Th4mqty*#8{85Za6@4irMyeV) z-A9Jz>JTX+zt9 zq5DN*$N+A{F3!+Yor=vZKQvMqD6>G4z;WX!spKyBxZ0YVKtu=)l~9?x$Q8uaGt);X z?cF{G&^ru#0!Gj?0$SIA1s&nA;q5r&CANdE8{-@vxJ~UuGfP@`11>7O9yCc0f}7M6 zHrsyqNDEGx*vVH6m9tlZp&`UJ-f<3IKF*PEs z;-+>+hYIhYuxcxHc@e52feuHd$0@2-@+8y&Qq#hRc*2a^%6$prW%5~7fzi5TaDtW2 zP9B5eoKTn!uZ6L^5nTy>QSqx=m9TNngg*?G$WT)DU$;+@ik3=n9p7A><=tG!n7WZcx%;R$q$s0M9T7V?AG=sGO>G@pvu{j}Es zfKF2mz37l`eb12*GXYiypE%$3i5jz|A*2p!bVvgPh8y^eqr1Fm+~e!%%3*v z!i^>Tj2xFom_jC#jGRj`T)LSSbQ^qVIV6JG8sm0vE=QxA0~b7pLpd~7l@bCCxE6Rz z+xFh!%jd3W(YDf@<213M)Awlw(f3a?05rTw_G&Ue;WRKX=}{{zrJV($)TwR;B77M2 z<&;EIxEy3}jzAXND_sSck;B4BL$wVB-&-xm8y>bhEXOB zV==QA^K6iN9|B~lVouqgdBaSVO2x6rxIgJ%vcn0v8WDthk`dT-g0QoZ0K(R6HghF2 zJg;;rU07Pd4wf2ruTr9mfAPD-^n5&t1Xkf|e{ z%C_9t0~WB%GW5lcMXW{c08n*vgzYjbj9y2&yEQ}M!1*7)l!OE(n;q~vX@anAJdEcq zh7OvdU1LgVtc5-t%j3k$0X0lz(bmaO+Wd|EBzo`--8i<9cp;$dKzgX~Q?41%o;zNa z-q3KVNJ*W!X@;D|DJ$as5^E+%*?r-7+en{C2-D10Ll8@7UFzjFADS5+RBDqySO?y9 zF|w(0fh9jDZi1k&LE_j}TISTUT8WT&+Gl_wF#SASiW6MqnP{>U0TdZI`gpvh$d6ZB zh?n=t9^0O|Xu5kN>mUSSlj>@M^DG<2SvF*jiiu5*K6NElqzPi|aRnn>h1l`v8QU-r za`(pWgl69ED@VHQtA-*VLQ8I7u~wlk;+A3{6VwAD1DomLKgeCK)tNLn{3zi*2Qpv{ zIA35vc1yKWIv0q704z0L=9o8o3`b4nz4~ju&n^o=>#woaRuUmSFdgl2RXwTJ>pWII zE^1Sq%W1xU_`Apt?$cZmb^0SU*`K=_`jrinmTdq0nD_A}hd=5-Auxj4rQs`L@vKj2 z>T5}MK8JW=BfRVZLeR&~j-%-f%^)Nxo3*X>pk6yBuO1#00^BR^`mehnYIU^={`xzt zh2d~5}e>LqK)|kFCT!{c7&|;Y}FiAc2VySgitB<W36K9i?r30~XSl8L92b5*8B`7B1WbM-FJ* zYuluWhDKy$N~Sc+{vaKiQnHdD03#Ztsyr-%yF4FAzcNe(h}~~iB15u=92G=8#9ixi zw62u#S9gMqr$X#aEtRU!0`8ydGa|`U9ZD;hL&Kq#dpwl#Oj)X`Hl^e3Cz>(n0*c^r zZ{`{qedDcTbQY%KB`4aFQfKB9!!D}}iz-7R8XOyPMKChh&RBcq8> zyi0T3D2C#fUWAd3!9IcP@X590sJoC@FByc@b#|LNdZ3y*X6tMj_L=vC4 zkGop#!BJb%A66#9RCZs>0C8*g8qMj}mfk5Hke?4m7Jdt@_WMnhLzl|eYFbQ~?|%?1 zP&-D!Z7QB~ZXhOHt-xdxx6`Pn()po_EwczyLyoWJLzMlV$SzK=U2wWZDXTLJsYXP-^kbsd8ARmoGEiR5lcb|>JyNVRpSf-H!&07Ji!JQ98Cg1<9i z++m5^t%Zy5>!%;9@7(0irTYViic7pN_}OpTRzRFZ)Gr7gG=JJ*F>o2!7_&ArEwE_` z@xEfb}4h_BbA`N2ttWjPb_H zq%MMAZcI?_ZocC!CA3jZ@%e=bqTySN(d(nuFegy3Lr6Y;`sHWaJ@i{od_$L4%}~R{ z-P8!Ns3RP<&n{pRsrSa%7ho_BSj~$7KrdlAwID^5p*pj@#0ty^yHt=>KE?+-&Zwlj zvFH0;a7_}GG)gaot3QNNh?ODxE=~-vV;`H6=~KVYn^N0W*}x@XQR9E^$YeyWL#Thc zyIQo$+mb)H@BuUS^Fl}?2E)~K5287}=&`$!5isRKNPp&Rn(Id&%Fpy{Jd#s#y{q!U zAA;sUi5>!mN`T4QknFppO+4Y2I3QYoAPbLM@v|;HeVDU#UKAvB@)Nt)bVoQHBq^S? z9dM1q5*J!)4fV;9Be4{&g*cBY)q|Z@Aqh!LR;ba?$^%3bljS>SPo>N7px|@G`{h{V zY>D9(^Sp7iK~+~o!d@d~8XO>oq7qC(-nKi7h3U|vcJzpy3>@WF!zjT`7%C1!=@W3e zGw#J<+NFGGCM?%{MByW3)M><-geqJgB}x_G_kbp6ycU*sIauJGW3Agr#RSEBlE;5C zWGmyY;sK&q35{hwC$Pkj@*!;o&LsNW1SFv8!1rl}G?Am{p>&gc`D76&mXkFgF%1+T zRY?wb>}H;8-n^>Tn9?99bu3|=|IS!qO1i(Zd3kX7x!l_|txhq^7&dboHiriL5yqDu zQL-vNa4SB5i*&yy@DWnhZc!M>Gf*U-G&Z7Wm;ewoOzL&#@03_R-}gSs_V2|a%elkL zGyVn{Klni<1b_9F>&5wF+g>pf&^vtf$Tvx(Dw&XU*M;V^yAZz$T)P(W$^dj;OKcwH zI@cNq*B@~0%J%QJN*8HM8{c$2Hybr-HF?t4IU4jev`-SLmvN>}E4@r#WB#(Vhrjpa z`=j0Xqd6EHQT~VBPNv~~2mRKPMruAC;D3H3hx7CPt2Y1iOD2!=bN+i1OhDj&cM2*B za1s5ZUjJX^{@;Awx9$J;6A&0llzdHvp4=^f{4emoiyr?k@%KMKKgxfBpo0Gw_`frR z{{xJo`v&49%L~H&JK}#u?{@hAHt+iCWx)~U)u)!yH^wd4g`ewUk`j=1AsRK{&jC24$d~l4h}Z|I5aN1r1{{q;>+?d;9poWq&=92%o(c49GuEl<&h(V+Ken;&Wp z=aQ0b<#(}BBYM&ih)V8Eq;^TD!ej`%q;h*WnHP!BAZmVj1tt!8J_KxfP?%9<)n&>) zYaTH+qZb8s=O=<#I1MZ}AWuXN^`rr6X&U|A8;`dY)B?wZ`MM*}hUD<%=gJ1Chm4{M zQ@Jz2c>QEslNh3-Qpb-@hkSc-6N(Zo6<1zg{l?S%Wslp|t%mOMObr!*qQ+Xi0Hf>X zfS(3{r$3=_YA<=8_17A8m)pxde|tdw!xRP?`q_;)puA8>{}?1L`E*kU69nWc6PQno z3t(aNvbU=?uyw_6Nq(oJw?Ef>2qEqas<+enL;k3%tKt{BF)4*3X>wnPq)*Ylv|X=&2>j zJ&cLiZ0LS=xs%M37A~Y zxz~&t3Gi=xDemro2R%@6d@$$uG)w?H8<;&Bvia4T(1Xmb)u{1QnRT%Hc@p9N^WK24 zE@*-bObB`vB5wKVDEw;n!iTi{2Vj%+bJ@puK-fQ5V6lh(BH*V{t%DH3bK$1DKijKE z%ZvT>UezW-TGVwn{YCE5=lQ$WFd&@e*#G$fW5o^+8tU;j;^=ewV-4YE9I7)vXoEd5 zFUamh#2$}&rk8X39;(&^S^;sQfp0ooHKSZ|dTB*I;&(!Ok>KO0XF*}w!a9K4_SgnE_|;p0wM<@>iaKFPsy(^I(G99M_7RDo9E^Ks!7$b*!O7NAW#Ipd0jH0lW zOWCO|4=IsXwQiwu1*4^IK+xHJ`ak=@RxY_YL~1%jOp{iHN_)jVryjQw;%~JRAWTP< zLt=K0MX}dKON>(;SG#vnnHMqq5(DSF7+OgM2tbvbk8eviK5ZX><7cqe1;JGnq4n8H zde9U)(Q9i{Ya&hzaP_CJwFmr6w}gQriHb{ zc=v~vllQy(!&=#w&1d5i-=0^1mx}=P9y_5uw@PKdE6gtd66zz#me9!~)s|5W=&|C* z+urNqp38Ml%ER7De>c(xtXNU~8^_<+?ca1L$NUPeT=6MRLR^bXyS`K6_w6C@vB~i? z!i7IeuFa|^wfOCPyB88FUtY-N=QN&X^zBEkX(tv&Z)K+(juKMJlEt%A0>=Uez3B`G z!XaXx;Uw_^0s|A{qj#7wqVIUz0Z?nhzG_a2wOBpw4m>2kWhh%+YTFd%8`0GVzPSSE z?Unv|mdO4MUONufjc{`;fIrqgXjdMlDTkjX@e{?K%b~~90F{!g5O#}qcw$3H*7wbW zvrd-L@N&@YigaXe1oJCmWR}OV2xS$mtzc|CRk1{ zGfDb2;41mWni4-JQJX4KgaHLHITh&!Qaogs2R^xoWLT$!7Fyi3Xx7JGeJC3U!o8FO zjGAJgO3${e*)=XGIgYZ)$F?ZNK16hl`Vyn-A2Hy8lN56~c7kimF+|95ra`a5rXn-F zI*?E_$Sb*}tCU0ic@i2>^?~Y4%vGlS)&Sw6pOg)=aEoP_9iR*-aLGw`+aE;Ab#>*? zgAs$w%!QaMvatyD9ueg7u!vB0cI`)4|Jp(Qf=gjq4ZggNue@&6+>ZhM8vTPF?|xpE z0MOmmt&*+gc|3g_c@Ugr?twYDd&0`vvBpyPeZXQq)=jH?4gwY5x{nHhG#VX1ku)_g z4vaRre$aQd@1KNb2M5oopO67ZD+tJxmx&@ZG?8SIT}6P&JCMggRt+M7qD57O0v|T{ zK6_K$>N+8f1cYDbY5Tk361)nF`lx|tBk7aokxAma(HR3TR%p?hW6^r1x~a)FhdRzd zHeRL9rq8)3?YXGmOiXGvL=6haF+ovjm7bTU4IjXN8}sd>MU>0LIc_uU4vtQu1lAg0 z{FNX{1r38YEmMvT3Q}b&HP-Rmk9-M(FNz2sQNh6E>o z3g=@I80$9Z()XK{4in+00_&Us<`Qe9L!FN?N{vMd1h0~sVsyj^zSk*$ANZ{VABUR4 z5~+mZo&87JeJmIW%|1t-Q!ws9uQ3vfvNL5|i0JO@7$co^`Yq-JpWjhuq3bxT1(o>k z5robSYoBi2-_pN;Nhg!H@n2KojB#6(^i0W7Bq~>{ zN%ASY7z~e)BFaY8D+~~y`@V+33x`pZvKR;!t}m{gIF1ZUY+0dtr6Vh`z(=dWQfzW; zsqd~N+0K=N0--QO3(Yoe?bEMY7P-xb8}$cEK=B%%0d=JFCFJE`km_Nz`yY zNvPZJP-5+tTX(f@?Q>dQs@w+}H3tP5jG`z4Goe8H2)987bQRFFSbG^lOfnWy^cEaw zgL;Dr>JR&lkYCas;i&aU{8ZK}g%3?st}Jbh8vz+d0@{@AJ#KPf2JUySU3^1APRpFY zHN(gw{aAt?oTn_J%OGD;k}+wl5F{(1 zaoD$#!syUZ>he1cG(qgy&S40K_}i6323q~5~#ODf|6 z&jj)c79ez~i5(BX8TM1rQO+90!ICe^>TpEe#LHnN0r>`FIx1zi2om||<=P9AZ(wHOi)ke` zRlz6##urYxJK36;{b8dysS~73Fp%!Su$Os+eduN$$jvIXsz7D zCVbEbp;vC<&hsYnc>?a8LmsjkNOd`?1gU22F4D?yaix&K4W7BtO?>A8AIv>J-+y6a zqEZZbPZq}_L5r%+fij8pj-y;PXzPr@O1kR+(8+&6uJa`|7j?*{DZufUVT4m>8mC~^ zRe0v+XQp9{6^@EIoaN^k=fUX+Ml3x9gd^6Z+gTv0D>Q7fW+s*;YK2149w()s^hSn2 zu?2PIs)iwAN38eM|Gf}D-*gkba-C&IpeUM&Iq~i9ikaV_C-G&gknULGm6ql;_JySZ zP^G7#oIQ)9C12P)>iiVYH<~1>Iqm89zdj-=D!{M%g*wK_^eI`@qxs3a|!WbfE#gvaO%^0;UWc4Mglz!a+@RrGc36e@fm9B|vhao`=yr5Od9jdP{Ge$^@ zVWUM;$rk9HriCFYwOqm}qrt&$lF~t*w8mv^O}kN5i{7d!Jm!8=kLZ;tq3}CR!-Jv} z@FAzKGf^MY<`z+MYwytM_}aC1p=O}STb+{=s`%F{__|-4a!G`L4s<<6iU-iwmzy`m4f<9d*Z`-HR#35oDi<$K$O<;wQUs2s)d!w2D{ zLRc5_0w$UjchyfM*gSK6lB`bR*y8X?<^bi35k0HYiG7e$I*C~}syuAx%#2pf-3G3n z=qB`!Pd@wxvBtiMhVIlH*c)R;8aMnG6KlLI)8=%?rQo_E_D`A$9#CO zBOUWRwvL}Wszr46n~P*#637`d%4*DtPLu>y9SG&1QDGD*RVa*V=jl?%)7oy2y&nJa z@o?AvD?r)0cm`RIn88=9N_u^da~|-y>%Mg^_LJ#m_*6$?OpezI;Ok98>fJ`!)-uW| zEx$>Wb18q2WV z2@19df>l{BCAuTKRy7sYko@an9J1MOv|`j6rNxO&5YRoq^1Oy8^VkeCN zeY8Z;W=)B}U*<9s*d*K6z|6ren7bFwihqN$PT5(u2;&xI_+py6<|TVo*e*11y+oWS>F`$LhC%fxekj~+x?)d3Mb zW`R_9A#10#cnut*zw0(qR<`U{h}iU;LZNlPOtXR?l3|u7y-IVP++_AKP|4 zj@r|WW4(qj6#1b$($Mjv#djW7L>(e`X`ryO1dx%j4lBR7can2Q6STVSZJwmpT;1DQ zU)&P=J->I~zANKmEhShE>BqZxUfCbDq^>_ zrkPKzZWv}sf-7O7GJi=&zVyws7l}>@i#U`! z`?Z*=IYFv)r|rQ7?)k+JFmOYAW}IG{<^YTCwaBqeMEaPCW_yW)IKn=2a(D@fOSmaF zEnR#@-nTSi6C-BOg5)f4zdkg4z5z^h zzW3SF@i?BLGkz(|<6Y}i(oq2BJnP){^|Y;IH~Mj>th@3RlQjg#VD4^FXS{ye+~s}` zSJE4Cids=#fY>A0ln#L+e_4tl0d*7U?rfR#rLvoG?}U3r^Rnsx(ii0)eV=qjXCfRj zGkQ2p8$A)no5Ze8|8()BNm~PK_<*_7zs=m(P=)T95P`+CK+BQuNrT4FjLGS4MqFFD z{YOHYXcm)bYWuj9%5(kaGhfcoB@>)~0IkEUx#fX9pMkuB6IhxaUC$xf3>f!e$821G_OgIZoXB{P4zIU zCdaQR%HEv86_W$D9ZG8Jf+?Gde&5UG^$UQoVSt4p74uIgD#*cdwym{VMF8@XQ6g@K zLuR+LwBHeOEJ#-4Y%-FS{n%Zh1k(uFi3cc1DM+R^5ZmxJ0hiSMingstdzyVCHfHXD z2akcx=+S2UP``1ax<%V2ZuAN$eaCBM`Ci-0WOz;aI7S=k)PZlu9U0wdMs0@s=M>7~ z0%5``)}x@ChGBmyCHb(I?5wali1TJMwPd`EJ-&<}M6y;P6H_Wijz7R3!PJf{{k^Fr z&ylDg@l=ZjunjK*$7gQf_pHl<<670O=lxH>0I`<^UWUUpDX)Q#xvJ-_vDUOKAu)%# zUM`($kRQFDur_(8@gyDjH!>L`R@8e07nF323U|#~qEAwTrf7B|ua>+ZPmv|zTl?MD zf{62|q&D~oowPvidIgg%0$x{uw|`e`P(faWc$hR zr`fXSMa8oFP6>`?^apTu#_t%1=}BH ztgiYWX7r69OFY|qANi;9Pu)D<6uD|?#I$s6-n>jr&r#4|1t4w7$GD_vI_Ygbq{YmY za&cItJ#rVfbICP&mjd*KvQpmnb^+aEjcq?uMsET*<;F`N1)07n@>boxZM%B-YK1{} zeO!}v-D7vZ*s{><)U-Uf04Lx4GB9t8?&g70d=DPqL;UTpJP(P|@RwzN@7t>Q!E+5{ z0g=ujrd@yX8_?fW#iyzd?E-&l68YtZ2yM#DozlKuP#Js7J?pA#u)uE(&;lx(f~Dpg zo|dOi(*+-m;ca!!aB8#J6~&@I*-y`_=3!&jIs*)!b-HTnl#J$!Kb7~58nR9vrmo?j9jL#>WWs@nqscseSUG)Hzq+RoxBhLF2jPUe5&&5S*~kZx{5c)B z^rl1THJO%Ro%)b4zDq(ABJc!{&2gZK6hyJguf8dv(Z7rfQu*Q7<22C37p%mH%jW}* zoznOMY9>qY9T(o{oleW;*BQ3J>2p%Y%St@sOWKX?!4TI#iMb7Kf=+Z2nJO$4t`WC47A;gN) z84!~h7%YeB@l(g6Nt_?q==k|&dK7Zk;_d~KR^t-H5^UxDw0Zhl%4EC8)7MEGxq`m` zHm{AE+u4>Q$m;+E>=6Cnd^X+EY5K0~_veu9YB6kTwJUq&LR+=81EDgPK`OsK;BtL$ z^KnvFC&NznuYJt?aMT7n%b*oy(SjnJ6ybV!HVMj5q}!AVRH?{HWxM?I0G{U?#rl zf#^BUbRUpA5r-lEB3FC>ZZ(JH&jAiqvmbj0mb0n3>7!mYr{Z0B6tO0?WnN0_q04KQp_Ki&Fd#?8L0F6>5MU;7@mkJw*(I@X*b=y=D6#}rc_q3| z6$avqsA*A{00^ak7&I3J*k_k0BE={yDIx0X0_EQx`H6g@s6bItiZd8?Wb5)eeKs)( zaX5$q>U=OfR%*!9_#xgP;`Ee3T@oBKNjQhX0wQ9RDIQSU8Rla0Vs=VMtT0y6suD@= zr}6^%ly{i%ZxgVY?^s{_hIwKMD-lCxJcJoJs4*D{sO(J`W1>sJtuSCzuhge5GG%93 z3OaP5!qnoofP%_be$Q*~S4AqGAS)J>BF-noft0AC4trm&HzY>+s~Rc`3W7Tfigmy$ zPOKrR z6M6~}pxuDyF5X3)%Co}L7W!_%28yhlbpnUVG=LaIv8Wy`oO6$$tS)NFIbN9EDlWJl znG!vny(tolv$w#hKMsaGG@SHTEU_5GuJqL;IR`U6PPyrEZvnO3)m_Vz$M@<+`wE}phwgAAZl8-TZ&y#t#kYe zht!$UQX&>WkKlRM(J6k#fH3Mc)_xH+1_LcNHky_@?#fm8!U-aX8UIlA*tk8SPi4#s zkSriY+#mGdG-M2e%1;SnitHnneK6O12XA&5A8Cvtlg>HAEbO%{6jzdsn%039o!3?i zGq>)Ao3g1jq?4xA9c!2z{-a{AsdMshoc!@C#T^x-%BaPJuEnKMUuMK#y=k%Z!&9*9 z+MGG?vQ$DvU@O#9<+Nq!{qz4-^3eXLs%a1hi+eBv0Rj>Q%%{f#u>DiKJJ4QtSeHb3 z#|#F&QBU&iAx{n*#%>ahgR5T`glqbhtgTigTEo!sAwv93sWhTZ=z?A8q@|tIqBuGYveu`SQr-p;`*PJ>Twq}6! zDOs;*KBgEqvm085n>_Y$nJ2Ojr}CUHzu;@*vAF5K&DzEg?fZCjYl>Dd<2|Kt<<~Vi z$E#@^bIxR8JuROWB!mVvnxjB8i7-LAV{c+#@4Uw>mj%%F`b8hpBX2muo&lj?vR}i`e0|QS@+WWD%fq3&;P5GL?s}L1-?_KuHu63cgYP8|!P^%KLZSn$}xS zt?wG|_-A-}{r%e)`N6AA#_4^OlUcSm3of0|y?n?QEK|DuA}gWmlF(}p(|3?u-( z!s-HSb-KCc9T?EyztkLy6IzeM7J# ze!qU_6D`Le8mWhDaiH(EHSoi86G_zUZ<_&I)*6NJ)T#;N-M_J0p%`e$#mtXwryI`h zO@%AAT~;O5>^X`6%SMJq*{(p!``rWMbUlpV5Rd~fAaZi$>EzEQ9-)zlP4qF;W_Q3H zK7Vttn7lu655*arp$T@2CRUL4A>Y5rXN`IO@jBwox|KHcGXi6_)r!Bh`-yvK@5vbu zgb&|rNn#z!K^99U4XY+^L^ib*+xnM`Su9B%PmO{5NIrQvay)23Yc*WzJ47|WjZLkP zF1!hVvx9T7_woVJ)l~R{ljSK1r}y++#Sp)Wx6jrr|B;?MK79$#L>6b4Kqg_Ci;VFF zCf0e7#|@?km1HxU5t{V_N>gWqmaofYNJUmhslu?`vWkjqjC3evUtd^BS(+tbPEa&C zQic|^zjYA9yT9Wv8GBS6m^)cPRN9QNfGRWq?HlQRgz>xlx(Vl+F$*d4?_Ih#o4+_N zNX@q|&KC&P<@63$aj4 zHoa=GLLq#(SP}7iq)vs$ zK5wIw|ICdCMce!c$lZ_e&SaVZ((Lp)Ny9d18E;YsQ2OpdQ%KX`NG7u)GA!y9-wKMB zMY0tY1yhFu%U<<<`O}DB7Z111am;?rW2Gpy!XrDA$Rl2M8dpoF{-4xze zKs`#kml;x|(7o{uJB81z*=8U4hYq4Pu|}b~8=y=rZ_?hQI%S5iv775l4~5>OI`JgF zc=kx0D3ZT4q1hfHuNU&pcG*0*7raI&d}ASjMVEFhLAiPLAqv7J=t()XKh|cI zO{oP6ijGN}`tJ_1TtcScP-%%~BY%8n|qI(qW`+J94Kz zCExST!Qd(L;&<=vzeZHM=H1>3y*v%E&$DQtkCyu7+L6oKU6|sEWOy;gKD!K7K;3hz zt$)Lw>hl|PR+e1Jk7S@^ssW|o_k#Das<`O4=*nHL>3a_EG4rZB)eqE-$R7pjm3{EXWLgHxo=w0v{*WJK6Q_#G`s zch4?7cuBvaawQzNyMnsuyQI##EMbms8@-O7!g`St?s6q|)dNHTBTRK#Z5Mo9l9}YS zkZwP&d560lJ7aXoBiNdJeG~sY<~<9zZORjX`x32vsSv;$5A)QQj)E=~GgwYwJ(3il zPkghbLHeoFbx*3EgTuN@Chm_e5wooiH&wjtM?Ha#P*Jh~r9fTYMS(~nf?PbL5RnXu z6K`TAYMsQ5X}$u;M-Zw>@+fWyiP$}ja}P6>B%KG}?kDwOoid?3THDwAk(m_!aKRpW zm!wPJF3l2XCl7|M`Zc+$cJnE$d!padi!PTSr!i~H-H8gHu! zC_kU)DelA%Zn(C6z(igIbWT4WwzY5C!0RYg!!VHt&#jp@pH%$w^o<(1q8NSh9 z{^I;=-8D8gKikEU1tg|Edi2Rj8F`kpyt2r^J{|0}cg9Z15~YIYvhIST9%j|1vgXW; z{nI#Z$~x6zk!k^WX%If}=u(V6Qu0X^%8mbB7I4MT)xq^3!i?*f%=d1PbvkJd>M3Go zo>b$mS>hcI*TqkmlLVx3M&F18sJ?4Ae~_Jy+2?*XaT#HilG%9{Y56!*ML{qk`&4Mr z*}^_t8=1Q;R9RCse=aY5I!%SJwaAp0KA%XnGe4G+afkxoyX2ecZW#b?Rdf-dzqqEg zieDuX0_YFO*tgOsjkJI!(5LLT%lqYE1so!~T9h+3VCV8MMp?ZTuoFBdo`)N}O7g6@ z*w19NXI6C1+)R5_^F{|UIaMIwp)<5V}qq4 z=;|(_eGq^>)S)GHoaEnzZQn^HW1b%lOBq?a+GUz4dq%mS`sC!_iA5no=%NZ{{=k!# zts}#WSwg^DrZ`i+`%e6EE!Zefl~^%y{(B^Iid?AQdX0nbIjyljEG63hOiIcvNUdPn zd|=&{GwrVPE#EwoqB2X=@ur{PQ}1wI!+v zNbr}0W{$k^x;$rd04Udk2JV;xoY?E`n7&Za}(?m6rI1V zwZx*kQXPz7IdK5ZDPS_H#dyh|!f1c;=L}gjVw}Ajh#+$d5Xj2Or<)n<6rS&#iIB1~ z$p!tz&Ib2Ijur%#;lCZg%KvPgY zf$bf@!I8#@8XpjJgQnNjQy-%Z2_z4IbyFeF@?9_(Rf}q)8;C#2PNH4RDE+2EYgnS> z6D02fj{;einv;)-;dT@&CR&?pDMVqTIzNl9UX-kxz;o?AQ1%B5JzsfJ75m4sw9sF` z1oyX){E;GUN3MGVUz(;R_}@i7L*3f-ecIQQntmucBz0~pBAYKwVQz0bPaBgTTxQ&z zstlbPlAc-?6~jiM6LKh;EH~u`1rxu!QP#`C4a+2`bFQ2cskw->PCDqM^Q6GgisBlh z(G#!ZKm|KjD#b2Rp>rd+Z^A%Lw-^xsIDCr_9_PNIFA#Mdj?rQ3S_(8{hXUtW! zWR7U|UZaW!-_}IITL)vC16!sCG)IK%cbUYkXcryGc4!$ABFVx{Tg&~39^~=>$m;Xd z>M2d2h4IH1!4I$GE9sZm1!bmrmaPSDM01+f7d6OxQ9tA^srs(k6HrYPQ5#G`2^>`X z3N4&(`F{vcgHKOxMsiV7?HxeeFJm+9I7+S!@FArEuffv+R=(8G0tooX14c9IThVCv zr6HbOieY?;gm=D}Z<$JItR(UPvHiN5Pl!FwYRbxalO;6JcdD{30g|HA^A}13$LKea z0q^k*J2#eyQyRX9@G4o|Bs795vEng9wC&XR+I7;jGQHoAa-IhFFQyns!_piqsv!b- zFwcs69YB0&Er3zLC1TbZEPgewIpA6MKA=|uK(La=bTeg#J!iZb5z;D&STjpp!y5ZmuN>UbXKS+p zeI95J#PD3(sQSf&EAMV}CqA*6@4)LZYD6_kNh8PCHE7yZZSL48p*`g`JEw-I+Q_ z%BeE(6~7Qy`ZvAz3wNc|FUbXTQ@877d4hT1k0^m5qzZazG_x{6^zKW7^scKWywaTZ zvr`@S@S%GaN2#~GjSjD1P)%zcxgp+&+l$fGDSltDvZ-0cHtsY42c5BCDN&_C@Gi?% zv-8<%2yV>Q$)zXREYa&yMCOhoSe@E(-jb1U(K6I;@T;O9@Zg-FWY4Hl)eXdN);ZC# z;SkQ{Aj;!ci^^vJc9CjMJSS4eJ#oVk8Y}c8xoLo)?u7FEA_lB7;LXgWJ-VP`DuaMQxro7KY|LHVoCFb32!9hTFfB;@Bz>dQj zGfF_}Z{!(}V^&sB)gPGFbDSDBfU{Z>ByE@72^PY;i z{U4cf&)`2bxbvHw=-xaOw$(98-A0Vd^o0ju^x6*WvDOvhTqjgx`fQxGmmj0_`uPJ?cbWVzp7|?4=^d7Kj%JK<(d{AdF zvnY#+j#5)uQ`#p{XW?S@J=3E>G8Fpa#IgBsesC~JRYC6inS0`zF~wom@+!xpzIVoo zoj8tPx;U93ZSMqG;W#(`!!-qcNYAw8N~wBdH*PPY zoI>=1G!TKc6zIEHv2XIo0L>Ln`|29ds_IUMOnEH@%VGCkmG#WVKb-hoziYIve9MI# z+KfkKV4j6LJOAjs93LuSwc2x;$OL5s)VJil(-D7Nr!I$SwocX@lEosq0IF5Dl zKO}oGt}im?{8p2NqnoX3$sqtgDN{~5sI0Z5xJqaF_SMhDE@Ky00T!3f6dX_3Cm)E? zf*lvry$>?T z$gczFbT{g&H@D~v8~ip?vW*C-^-7mQ*lZc@w>7yagti5#@jN$82f_}$7xiw*O`d0G z7t@#BOfG7nA10qd0mW`)cLvqj$tCYO6n;R7`!alK5snILK#1oea}`t2F!nThv$>4E z`{d61_S&8xh*y<0WBdpn!k*h7ycv@KW+0mRaeN$R7;KU(NQ=c$ z=h(ONaZwYcd~?)r!OVlKcq2Ioru2>`Fn~Wx%npIQZ-J#pfCIdr_MM`+HQ;uNKPyn^ zRXfq@rLXlvO86WDBMUB8hu=WJvLLHGdI2p&W#ta?D7{st8+DkmwgoWr!VDhERyd@q zusiDlLMybe!PF;9fi+noD8_i&ajj5hAOy?`C@cO~vjO{;k2F2cr59)a6~F$727(Lj z{oBxBpHUwSybvS=C^*gwqlAA(>*$wFl}sa=7f(yQk1>KX4ZAdR=8@P5V~<2y3+VyHk>L) zTK&o}q7neXqET6ZyrC{iVpgF*?Mb-!Ll_(r^lv)PFIn?RKopJ3oHe$t!Ep+Be%~^6 zt4v9heQ45)nv$4pB%HwaGNKOhR}0}P9G+n>kRUnQLDeLjKj>)45X&j{&#KIa$u@>7 zQ022f1y0K83(H$%5F_Q5XeF7&%iSKNN|X@yP$s6m*=KfG zQdtbRi#Z#302r2ngK2L+SfZK5iI&2IB7;u-;?cC>ertsKOVBHtImz5yQ}bjSDj5q8t}+SEaFT-8>prqB1WUy_YJ^Zcyf!wy*+ zWFNvac^FNglKzxxm@UJe9Mm$kH(0_gay~Ozbw+{x`A;$IZpZ8*d|J@5B zBF;_xPkYG!6&^uAaK6&c|8oQe)e{1@#4&+2;;8=u|2Gqj{@=i5vM(Uzzo7ri-uyRc zf%@OO@k;!^yZzq_+J6I?8U6(Z{{O)LbxL0?EQtRj+}AY_)Q%lkFMuH_=9C)*#LJG!)6vD&)X~xQpLDvi94HtD`2Y418HDjj s=Zt&};{QrQ;r+*8LOvj^2mvra65-!T<0SFH2>5{&k`z#00{;O2KfudN`v3p{ diff --git a/metainfer/tasks/dcu_kernel_auto_opt/server/routes.py b/metainfer/tasks/dcu_kernel_auto_opt/server/routes.py index 10404739..fb56ba0d 100644 --- a/metainfer/tasks/dcu_kernel_auto_opt/server/routes.py +++ b/metainfer/tasks/dcu_kernel_auto_opt/server/routes.py @@ -71,6 +71,132 @@ def _read_jsonl(path: Path) -> list[Dict[str, Any]]: return rows +def _execution_profile(state_dir: Path) -> str: + """``production`` or ``default`` — the queue rank this task runs at.""" + requirements = read_requirements(state_dir) or {} + answers = requirements.get("answers") + raw = "" + if isinstance(answers, dict): + raw = str(answers.get("execution_profile") or "") + raw = raw.strip().lower() + return "production" if raw in {"production", "prod"} else "default" + + +# --------------------------------------------------------------------------- # +# Embedding a harness_evolve child (AHE iteration -> DKAO view) +# --------------------------------------------------------------------------- # +# Every harness_evolve question is a real DKAO run, but it is spawned straight +# from the orchestrator CLI and is therefore NOT in the WebUI task registry -- +# so ``task_or_404`` cannot find it and the DKAO page could not render it. The +# AHE page passes the child's artifact directory instead. Only genuine AHE +# children are accepted, and they are exposed read-only: the roster of a round +# belongs to the HE orchestrator, not to whoever happens to be looking at it. +AHE_CHILD_PREFIX = "ahe-child:" + + +def _ahe_child_entry(state_dir_raw: str): + """Build a read-only entry for a harness_evolve child's artifact dir. + + Raises 404 unless ``state_dir`` is the ``state/`` of a question inside a + *registered* harness_evolve task's ``children/iteration_NNN//`` + tree. The registry is what makes the caller trustworthy: without it any + path on the box could be read through these routes. + """ + from metainfer.server import tasks as _tasks + + state_dir = Path(state_dir_raw) + parts = state_dir.parts + # .../children/iteration_NNN//state + # parts[-1] = "state", parts[-2] = , parts[-3] = iteration_NNN + if state_dir.name != "state" or len(parts) < 3 \ + or not re.fullmatch(r"iteration_\d+", parts[-3]): + raise HTTPException(404, f"not an AHE child directory: {state_dir}") + question_dir = state_dir.parent + iteration_dir = question_dir.parent + children_dir = iteration_dir.parent + if children_dir.name != "children": + raise HTTPException(404, f"not an AHE child directory: {state_dir}") + workspace_dir = children_dir.parent + owner = next( + (e for e in _tasks.list_tasks() + if e.type == "harness-evolve" + and Path(e.workspace_dir or "") == workspace_dir), + None, + ) + if owner is None: + raise HTTPException( + 404, f"no harness-evolve task owns {workspace_dir}") + return _tasks.TaskEntry( + id=f"{AHE_CHILD_PREFIX}{state_dir}", + type=PLUGIN_TYPE, + label=f"{owner.label or owner.id} · {question_dir.name}", + state_dir=str(state_dir), + workspace_dir=str(question_dir / "workspace"), + created_at=owner.created_at, + launcher=owner.launcher, + ) + + +def _entry_for(request: Request, task_id: str): + """``task_or_404``, plus the embed path used by the AHE page.""" + state_dir_raw = str(request.query_params.get("state_dir") or "").strip() + if state_dir_raw: + return _ahe_child_entry(state_dir_raw) + return task_or_404(task_id) + + +def _is_mirrored_child(entry) -> bool: + """True for the read-only AHE-child view (see ``_ahe_child_entry``).""" + return str(getattr(entry, "id", "")).startswith(AHE_CHILD_PREFIX) + + +def _require_writable(entry) -> None: + """Refuse writes through the embedded mirror. + + An AHE round's workers are steered by the HE orchestrator that spawned + them; letting the drill-down mutate them would let a read-only view change + a run's roster mid-round. The UI hides these controls too -- this is the + half that a hand-made request cannot bypass. + """ + if _is_mirrored_child(entry): + raise HTTPException( + 403, "this DKAO run is embedded read-only from a harness_evolve " + "iteration; steer it from its own task page") + + + + +def _latest_gpu_lease(state_dir: Path) -> Dict[str, Any]: + """The last ``gpu_leases_acquired`` row, or ``{}`` when nothing is leased.""" + latest: Dict[str, Any] = {} + for row in _read_jsonl(state_dir / "timeline.jsonl"): + if row.get("type") == "gpu_leases_acquired": + payload = row.get("payload") + if isinstance(payload, dict): + latest = payload + return latest + + +def _gate_status(state_dir: Path, limit: int = 200) -> Dict[str, Any]: + """Compact view of the admission-gate audit trail. + + Answers "why is nothing happening": a task waiting for VRAM<=90%/HCU==0 on + every candidate device leaves the blocked checks and their reasons here. + """ + rows = _read_jsonl(state_dir / "measurement_gate.jsonl")[-limit:] + blocked = [r for r in rows if r.get("event") == "gate_blocked"] + last = rows[-1] if rows else {} + return { + "checks": len(rows), + "blocked": len(blocked), + "last_event": last.get("event"), + "last_ts": last.get("ts"), + "last_site": last.get("site"), + "last_gpu": last.get("gpu"), + "last_reasons": list(last.get("reasons") or []), + } + + def _running_pid(path: Path) -> int | None: record = _load(path, {}) or {} try: @@ -473,8 +599,8 @@ def build_router(plugin) -> APIRouter: router = APIRouter() @router.get("/summary") - def summary(task_id: str) -> Dict[str, Any]: - entry = task_or_404(task_id) + def summary(task_id: str, request: Request) -> Dict[str, Any]: + entry = _entry_for(request, task_id) require_task_type(entry, PLUGIN_TYPE) state_dir = state_dir_for(entry) workspace_dir = workspace_dir_for(entry) @@ -488,11 +614,16 @@ def summary(task_id: str) -> Dict[str, Any]: "plan": _load(workspace_dir / "plan.json", None), "workers": workers, "report": _load(workspace_dir / "final_report.json", None), + # production mode: which queue rank this task took and which devices + # it holds, so the operator can tell a real run from a harness round + "execution_profile": _execution_profile(state_dir), + "gpu_leases": _latest_gpu_lease(state_dir), + "gate": _gate_status(state_dir), } @router.get("/iterations") - def iterations(task_id: str) -> Dict[str, Any]: - entry = task_or_404(task_id) + def iterations(task_id: str, request: Request) -> Dict[str, Any]: + entry = _entry_for(request, task_id) require_task_type(entry, PLUGIN_TYPE) return read_worker_lanes(workspace_dir_for(entry), state_dir_for(entry)) @@ -500,8 +631,9 @@ def iterations(task_id: str) -> Dict[str, Any]: async def submit_guidance( task_id: str, worker_id: str, request: Request ) -> Dict[str, Any]: - entry = task_or_404(task_id) + entry = _entry_for(request, task_id) require_task_type(entry, PLUGIN_TYPE) + _require_writable(entry) plan = _load(workspace_dir_for(entry) / "plan.json", {}) or {} assigned = { str(item.get("worker_id")) @@ -521,9 +653,10 @@ async def submit_guidance( return {"guidance": guidance} @router.post("/workers/{worker_id}/restart") - def restart_worker(task_id: str, worker_id: str) -> Dict[str, Any]: - entry = task_or_404(task_id) + def restart_worker(task_id: str, worker_id: str, request: Request) -> Dict[str, Any]: + entry = _entry_for(request, task_id) require_task_type(entry, PLUGIN_TYPE) + _require_writable(entry) if not re.fullmatch(r"worker_[0-3]", worker_id): raise HTTPException(status_code=400, detail="invalid worker id") state_dir = state_dir_for(entry) @@ -604,8 +737,9 @@ async def rename_repo(task_id: str, request: Request) -> Dict[str, Any]: Body: ``{"new_name": ""}``. Refuses while the task's orchestrator is still running. """ - entry = task_or_404(task_id) + entry = _entry_for(request, task_id) require_task_type(entry, PLUGIN_TYPE) + _require_writable(entry) state_dir = state_dir_for(entry) workspace_dir = workspace_dir_for(entry) try: @@ -625,8 +759,8 @@ async def rename_repo(task_id: str, request: Request) -> Dict[str, Any]: raise HTTPException(status_code=400, detail=str(exc)) from exc @router.get("/state-graph") - def state_graph(task_id: str) -> Dict[str, Any]: - entry = task_or_404(task_id) + def state_graph(task_id: str, request: Request) -> Dict[str, Any]: + entry = _entry_for(request, task_id) require_task_type(entry, PLUGIN_TYPE) state_dir = state_dir_for(entry) run = read_run(state_dir) @@ -643,8 +777,8 @@ def state_graph(task_id: str) -> Dict[str, Any]: ) @router.get("/skills") - def skills(task_id: str) -> Dict[str, Any]: - entry = task_or_404(task_id) + def skills(task_id: str, request: Request) -> Dict[str, Any]: + entry = _entry_for(request, task_id) require_task_type(entry, PLUGIN_TYPE) workspace_dir = workspace_dir_for(entry) library = list_skill_library(workspace_dir) @@ -660,9 +794,10 @@ def skills(task_id: str) -> Dict[str, Any]: return library @router.post("/skills/{skill_name}/publish") - def publish(task_id: str, skill_name: str) -> Dict[str, Any]: - entry = task_or_404(task_id) + def publish(task_id: str, skill_name: str, request: Request) -> Dict[str, Any]: + entry = _entry_for(request, task_id) require_task_type(entry, PLUGIN_TYPE) + _require_writable(entry) workspace_dir = workspace_dir_for(entry) plan = _load(workspace_dir / "plan.json", {}) or {} if plan.get("execution_mode") in {LEGACY_SMOKE_MODE, SMOKE_MODE}: @@ -685,10 +820,11 @@ def publish(task_id: str, skill_name: str) -> Dict[str, Any]: raise HTTPException(status_code=409, detail=str(exc)) from exc @router.post("/skills/sync") - def sync_skills(task_id: str) -> Dict[str, Any]: + def sync_skills(task_id: str, request: Request) -> Dict[str, Any]: """Manually mirror the authoritative dsh library into the ccb library.""" - entry = task_or_404(task_id) + entry = _entry_for(request, task_id) require_task_type(entry, PLUGIN_TYPE) + _require_writable(entry) workspace_dir = workspace_dir_for(entry) return {"sync": sync_skill_libraries(workspace_dir=workspace_dir)} @@ -697,8 +833,9 @@ async def fuse_skill_route(task_id: str, request: Request) -> Dict[str, Any]: """Trigger the main-agent fusion of one pending skill into the dsh library (mirrored to ccb afterwards). Runs in the background; poll ``GET /skills`` ``fuse_status`` for progress.""" - entry = task_or_404(task_id) + entry = _entry_for(request, task_id) require_task_type(entry, PLUGIN_TYPE) + _require_writable(entry) workspace_dir = workspace_dir_for(entry) state_dir = state_dir_for(entry) plan = _load(workspace_dir / "plan.json", {}) or {} @@ -752,11 +889,12 @@ def _run_fusion() -> None: } @router.post("/skills/{skill_name}/rollback") - def rollback(task_id: str, skill_name: str) -> Dict[str, Any]: + def rollback(task_id: str, skill_name: str, request: Request) -> Dict[str, Any]: """Restore the latest SKILL.md backup of a fused skill in the dsh library and re-mirror to ccb.""" - entry = task_or_404(task_id) + entry = _entry_for(request, task_id) require_task_type(entry, PLUGIN_TYPE) + _require_writable(entry) workspace_dir = workspace_dir_for(entry) try: return {"result": rollback_skill(skill_name, workspace_dir=workspace_dir)} @@ -766,10 +904,10 @@ def rollback(task_id: str, skill_name: str) -> Dict[str, Any]: raise HTTPException(status_code=404, detail=str(exc)) from exc @router.get("/variants") - def variants_index(task_id: str) -> Dict[str, Any]: + def variants_index(task_id: str, request: Request) -> Dict[str, Any]: """Return the fine-grained variant index (which shapes are already captured in the shared variant library).""" - entry = task_or_404(task_id) + entry = _entry_for(request, task_id) require_task_type(entry, PLUGIN_TYPE) return {"variants": list_variant_index()} @@ -778,8 +916,9 @@ async def add_variant_route(task_id: str, request: Request) -> Dict[str, Any]: """Add one optimized shape's accepted kernel into the shared variant library, organized by operator type / model / TP / specific operator. Replaces an existing section for the same shape (with a file backup).""" - entry = task_or_404(task_id) + entry = _entry_for(request, task_id) require_task_type(entry, PLUGIN_TYPE) + _require_writable(entry) state_dir = state_dir_for(entry) workspace_dir = workspace_dir_for(entry) try: diff --git a/metainfer/tasks/dcu_kernel_auto_opt/static/dkao-agent-fields.js b/metainfer/tasks/dcu_kernel_auto_opt/static/dkao-agent-fields.js index 00929b32..9a3f4788 100644 --- a/metainfer/tasks/dcu_kernel_auto_opt/static/dkao-agent-fields.js +++ b/metainfer/tasks/dcu_kernel_auto_opt/static/dkao-agent-fields.js @@ -4,10 +4,11 @@ // globalThis.__metainferOverrides. The agent_framework field is a plain // select; this component reads its current value from allValues and shows // only the models valid for that framework (ccb: Sonnet/Opus; dsh: -// deepseek-v4-flash), auto-correcting the value when the framework changes. +// deepseek-flash-4.1 / deepseek-v4-flash), auto-correcting the value when the +// framework changes. // // The label→model mapping must stay in sync with -// orchestrator/config.py (AGENT_FRAMEWORKS). +// orchestrator/config.py (CLAUDE_MODELS / DSH_MODEL_IDS). import { html } from "htm/preact"; import { useEffect } from "preact/hooks"; @@ -15,11 +16,12 @@ import { useEffect } from "preact/hooks"; // Framework label → allowed model labels (mirror of config.py). const FRAMEWORK_MODELS = { ccb: ["Opus", "Sonnet"], - dsh: ["deepseek-v4-flash"], + // deepseek-flash-4.1 first: it is the framework default. + dsh: ["deepseek-flash-4.1", "deepseek-v4-flash"], }; const DEFAULT_MODEL = { ccb: "Opus", - dsh: "deepseek-v4-flash", + dsh: "deepseek-flash-4.1", }; function AgentModelField({ field, value, onChange, allValues }) { diff --git a/metainfer/tasks/dcu_kernel_auto_opt/static/dkao-detail.js b/metainfer/tasks/dcu_kernel_auto_opt/static/dkao-detail.js index 43b942bb..49e378f7 100644 --- a/metainfer/tasks/dcu_kernel_auto_opt/static/dkao-detail.js +++ b/metainfer/tasks/dcu_kernel_auto_opt/static/dkao-detail.js @@ -20,6 +20,31 @@ async function postJson(url, body) { return payload; } +// --------------------------------------------------------------------------- +// Embedding context +// --------------------------------------------------------------------------- +// This view renders a DKAO run. Usually that is a task of this type, resolved +// by task_id. The AHE page also embeds it, pointing at one question of one +// iteration: those runs are spawned by the HE orchestrator and are not in the +// task registry, so the caller passes their artifact directory instead and the +// routes accept it (see routes.py -> _ahe_child_entry). Such an embedded view +// is read-only: the round's workers belong to the HE orchestrator. +// +// Every component below takes `ctx = { taskId, stateDir, readOnly }` so both +// the standalone page and the embedded one share one code path. +export function apiBase({ taskId, stateDir }) { + const base = `/api/dcu-kernel-auto-opt/${encodeURIComponent(taskId || "")}`; + return stateDir ? `${base}?state_dir=${encodeURIComponent(stateDir)}` : base; +} + +function ctxOf(props) { + return { + taskId: props.taskId, + stateDir: props.stateDir || "", + readOnly: Boolean(props.readOnly), + }; +} + function StateMachine({ graph }) { const nodes = graph?.nodes || []; if (!nodes.length) return html`

Waiting for the control plane.

`; @@ -324,7 +349,7 @@ function GuidanceCard({ item }) { `; } -function WorkerLane({ lane, taskId, onSaved }) { +function WorkerLane({ lane, ctx, onSaved }) { const [text, setText] = useState(""); const [saving, setSaving] = useState(false); const [restarting, setRestarting] = useState(false); @@ -335,7 +360,7 @@ function WorkerLane({ lane, taskId, onSaved }) { setSaving(true); setMessage(""); try { - const base = `/api/dcu-kernel-auto-opt/${encodeURIComponent(taskId)}`; + const base = apiBase(ctx); await postJson( `${base}/workers/${encodeURIComponent(lane.worker_id)}/guidance`, { text: text.trim() }, @@ -357,7 +382,7 @@ function WorkerLane({ lane, taskId, onSaved }) { setRestarting(true); setMessage(""); try { - const base = `/api/dcu-kernel-auto-opt/${encodeURIComponent(taskId)}`; + const base = apiBase(ctx); await postJson( `${base}/workers/${encodeURIComponent(lane.worker_id)}/restart`, {}, ); @@ -412,7 +437,7 @@ function WorkerLane({ lane, taskId, onSaved }) { ${lane.state} ${lane.assigned_shapes?.join(", ") || "No shapes assigned"} - ${["failed", "timed_out"].includes(lane.state) ? html` + ${!ctx.readOnly && ["failed", "timed_out"].includes(lane.state) ? html` - + `} ${message ? html`${message}` : null} @@ -523,7 +549,7 @@ function SkillFuseStatus({ status, onRollback }) { ${synced && (synced.added?.length || synced.updated?.length) ? ` · 已同步到 ccb 库${synced.added?.length ? `(新增 ${synced.added.length})` : ""}${synced.updated?.length ? `(更新 ${synced.updated.length})` : ""}` : ""} - ${status.action === "merge" ? html` + ${status.action === "merge" && onRollback ? html` ` : null} @@ -534,19 +560,19 @@ function SkillFuseStatus({ status, onRollback }) { `; } -function SkillLibrary({ taskId, onClose }) { +function SkillLibrary({ ctx, onClose }) { const [library, setLibrary] = useState(null); const [error, setError] = useState(""); const [busy, setBusy] = useState(""); const load = useCallback(async () => { try { - const base = `/api/dcu-kernel-auto-opt/${encodeURIComponent(taskId)}`; + const base = apiBase(ctx); setLibrary(await fetchJson(`${base}/skills`)); setError(""); } catch (err) { setError(String(err)); } - }, [taskId]); + }, [ctx.taskId, ctx.stateDir]); useEffect(() => { load(); }, [load]); // While a fusion job is running, poll the library so the status flips to // done/error without a manual refresh. @@ -559,7 +585,7 @@ function SkillLibrary({ taskId, onClose }) { if (!window.confirm(`Add "${skill.name}" to the existing skill library (dsh, mirrored to Claude)?`)) return; setBusy(`publish-${skill.name}`); try { - const base = `/api/dcu-kernel-auto-opt/${encodeURIComponent(taskId)}`; + const base = apiBase(ctx); await postJson( `${base}/skills/${encodeURIComponent(skill.name)}/publish`, {}, ); @@ -576,7 +602,7 @@ function SkillLibrary({ taskId, onClose }) { )) return; setBusy(`fuse-${skill.name}`); try { - const base = `/api/dcu-kernel-auto-opt/${encodeURIComponent(taskId)}`; + const base = apiBase(ctx); await postJson(`${base}/skills/fuse`, { skill_name: skill.name }); await load(); } catch (err) { @@ -588,7 +614,7 @@ function SkillLibrary({ taskId, onClose }) { const syncNow = async () => { setBusy("sync"); try { - const base = `/api/dcu-kernel-auto-opt/${encodeURIComponent(taskId)}`; + const base = apiBase(ctx); await postJson(`${base}/skills/sync`, {}); await load(); } catch (err) { @@ -601,7 +627,7 @@ function SkillLibrary({ taskId, onClose }) { if (!window.confirm(`Restore the last backup of skill "${name}"?`)) return; setBusy(`rollback-${name}`); try { - const base = `/api/dcu-kernel-auto-opt/${encodeURIComponent(taskId)}`; + const base = apiBase(ctx); await postJson(`${base}/skills/${encodeURIComponent(name)}/rollback`, {}); await load(); } catch (err) { @@ -617,11 +643,18 @@ function SkillLibrary({ taskId, onClose }) {

Skill library

dsh 库为权威,融合/发布后自动同步到 Claude (ccb) 库。

+ ${ctx.readOnly ? null : html` + onClick=${syncNow}>${busy === "sync" ? "Syncing…" : "同步 Skill 库"}`} ${error ? html`
${error}
` : null} + ${readOnly ? html` +
+ 只读镜像 —— 该 DKAO 运行是 harness_evolve 的一轮考题,由 HE 编排器调度; + 本页不提供引导 / 重启 / 发布等写操作。 +
+ ` : null} ${library?.publish_disabled_reason ? html`
${library.publish_disabled_reason} @@ -629,7 +662,8 @@ function SkillLibrary({ taskId, onClose }) { ? ` (${library.quarantined_count} pending files quarantined)` : ""}
` : null} - <${SkillFuseStatus} status=${library?.fuse_status} onRollback=${rollback} /> + <${SkillFuseStatus} status=${library?.fuse_status} + onRollback=${ctx.readOnly ? null : rollback} /> ${!library ? html`

Loading skills…

` : html`
@@ -648,8 +682,8 @@ function SkillLibrary({ taskId, onClose }) { ${(library.pending || []).map((skill) => html` <${SkillFile} skill=${skill} - canPublish=${true} - canFuse=${true} + canPublish=${!ctx.readOnly} + canFuse=${!ctx.readOnly} busy=${busy} onPublish=${publish} onFuse=${fuse} @@ -665,14 +699,17 @@ function SkillLibrary({ taskId, onClose }) { `; } -export default function DcuKernelAutoOptDetail({ taskId, data }) { +export default function DcuKernelAutoOptDetail(props) { + const { taskId, data } = props; + const ctx = ctxOf(props); + const readOnly = ctx.readOnly; const [runtime, setRuntime] = useState({ summary: null, graph: null, lanes: [] }); const [error, setError] = useState(null); const [showSkills, setShowSkills] = useState(false); const refresh = useCallback(async () => { if (!taskId) return; try { - const base = `/api/dcu-kernel-auto-opt/${encodeURIComponent(taskId)}`; + const base = apiBase(ctx); const [summary, graph, iterations] = await Promise.all([ fetchJson(`${base}/summary`), fetchJson(`${base}/state-graph`), @@ -683,7 +720,7 @@ export default function DcuKernelAutoOptDetail({ taskId, data }) { } catch (err) { setError(String(err)); } - }, [taskId]); + }, [ctx.taskId, ctx.stateDir]); useEffect(() => { refresh(); }, [refresh]); useEffect(() => { @@ -698,13 +735,13 @@ export default function DcuKernelAutoOptDetail({ taskId, data }) { const loadVariants = useCallback(async () => { if (!taskId) return; try { - const base = `/api/dcu-kernel-auto-opt/${encodeURIComponent(taskId)}`; + const base = apiBase(ctx); const data = await fetchJson(`${base}/variants`); // Map shape -> variant record (incl. median_us/speedup/source) so the // update flow can compare the existing variant against this task. setVariantIndex(new Map((data.variants || []).map((v) => [v.shape, v]))); } catch (err) { /* non-fatal */ } - }, [taskId]); + }, [ctx.taskId, ctx.stateDir]); useEffect(() => { loadVariants(); }, [loadVariants]); const addVariant = async (shape) => { const existing = variantIndex ? variantIndex.get(shape) : null; @@ -729,7 +766,7 @@ export default function DcuKernelAutoOptDetail({ taskId, data }) { setVariantBusy(shape); setVariantMsg(""); try { - const base = `/api/dcu-kernel-auto-opt/${encodeURIComponent(taskId)}`; + const base = apiBase(ctx); const res = await postJson(`${base}/variants`, { shape_id: shape }); const meta = res.meta || {}; const tp = meta.tp != null ? `tp${meta.tp}` : "tp?"; @@ -766,7 +803,7 @@ export default function DcuKernelAutoOptDetail({ taskId, data }) { setRenameBusy(true); setRenameMsg(""); try { - const base = `/api/dcu-kernel-auto-opt/${encodeURIComponent(taskId)}`; + const base = apiBase(ctx); const res = await postJson(`${base}/rename-repo`, { new_name: target.trim(), }); @@ -787,6 +824,12 @@ export default function DcuKernelAutoOptDetail({ taskId, data }) { return html`
${error ? html`
${error}
` : null} + ${readOnly ? html` +
+ 只读镜像 —— 该 DKAO 运行是 harness_evolve 的一轮考题,由 HE 编排器调度; + 本页不提供引导 / 重启 / 发布等写操作。 +
+ ` : null} ${smokeOnly ? html`
Infrastructure smoke only: this task did not execute the selected @@ -807,6 +850,7 @@ export default function DcuKernelAutoOptDetail({ taskId, data }) { Kernel repository — ${repoName} + ${readOnly ? null : html`
${renameMsg ? html`${renameMsg}` : null} -
+
`} ` : null} +
+

+ 执行画像 + + — ${runtime.summary?.execution_profile === "production" + ? "production(优先级 100,排在 harness_evolve 之前;仍过 VRAM≤90%/HCU==0 闸门)" + : "default(调控/占据模式按 gpu_mode 决定)"} + +

+ ${(() => { + const lease = runtime.summary?.gpu_leases || {}; + const gpus = lease.gpus || []; + const gate = runtime.summary?.gate || {}; + return html` + ${gpus.length ? html` +
+ 已租设备:${gpus.map((g) => `GPU${g}`).join(", ")} + · 模式 ${lease.mode || "-"} + · 优先级 ${lease.priority ?? "-"} + · holder ${lease.holder || "-"} +
+ ` : html`
尚未取得设备租约
`} + ${gate.blocked ? html` +
+ 准入闸门:共 ${gate.checks} 次检查,其中 ${gate.blocked} 次因设备被占用而等待 + — 最近一次 ${gate.last_event || "-"}@${gate.last_site || "-"} + GPU${gate.last_gpu ?? "-"}:${(gate.last_reasons || []).join("; ") || "-"} +
+ ` : html`
准入闸门:暂无阻塞记录(VRAM≤90% 且 HCU==0)
`} + `; + })()} +
+

State machine @@ -841,7 +918,7 @@ export default function DcuKernelAutoOptDetail({ taskId, data }) {

Iterations four isolated worker lanes

${runtime.lanes.map((lane) => html` - <${WorkerLane} lane=${lane} taskId=${taskId} onSaved=${refresh} /> + <${WorkerLane} lane=${lane} ctx=${ctx} onSaved=${refresh} /> `)}
@@ -868,6 +945,7 @@ export default function DcuKernelAutoOptDetail({ taskId, data }) { ${metric(item.metrics.logical_tops ?? item.metrics.tflops, 3)} ${metric(item.metrics.algorithmic_bandwidth_gb_s ?? item.metrics.bandwidth_gb_s, 2)} GB/s + ${readOnly ? null : html` + `} ${(() => { const v = variantIndex ? variantIndex.get(shape) : null; if (!v) { @@ -940,10 +1018,11 @@ export default function DcuKernelAutoOptDetail({ taskId, data }) { + ${readOnly ? html`

以只读方式嵌入:不提供发布/同步操作。

` : null} ${showSkills ? html` - <${SkillLibrary} taskId=${taskId} onClose=${() => setShowSkills(false)} /> + <${SkillLibrary} ctx=${ctx} onClose=${() => setShowSkills(false)} /> ` : null}
`; diff --git a/metainfer/tasks/dcu_kernel_auto_opt/static/dkao-shape-input.js b/metainfer/tasks/dcu_kernel_auto_opt/static/dkao-shape-input.js index 57fffc1a..a0240b49 100644 --- a/metainfer/tasks/dcu_kernel_auto_opt/static/dkao-shape-input.js +++ b/metainfer/tasks/dcu_kernel_auto_opt/static/dkao-shape-input.js @@ -24,9 +24,11 @@ function parseCsvInts(raw) { var W8A8_M_VALUES = [2, 16, 3072]; // Large-prefill boundary added on 2026-08-06 for TP4; extended to the // Hy3 / MiniMax M3 / GLM5.2 TP8 catalogs on 2026-08-27 via per-topology -// mValues. DeepSeek TP1/TP8 keep the three original M values. The backend -// validates shapes against the contract. +// mValues, plus the M=8 short-decode boundary on 2026-09-20. DeepSeek TP1/TP8 +// keep the three original M values. The backend validates shapes against the +// contract (MODEL_TP8_EXTRA_OPTIMIZATION_M_VALUES = (8, 4096)). var W8A8_TP4_M_VALUES = [2, 16, 3072, 4096]; +var W8A8_MODEL_TP8_M_VALUES = [2, 8, 16, 3072, 4096]; // Model label → W8A8 GEMM workload. (M, K) @ (K, N) per weight name and TP // size, mirroring the model cards in the operator docs. DeepSeek keeps the @@ -94,7 +96,7 @@ var MODEL_WORKLOADS = { }, { tp_size: 8, - mValues: [2, 16, 3072, 4096], + mValues: W8A8_MODEL_TP8_M_VALUES, operators: [ { operator: "qkv_proj", K: 4096, N: 1280 }, { operator: "o_proj", K: 1024, N: 4096 }, @@ -129,7 +131,7 @@ var MODEL_WORKLOADS = { }, { tp_size: 8, - mValues: [2, 16, 3072, 4096], + mValues: W8A8_MODEL_TP8_M_VALUES, operators: [ { operator: "qkv_proj", K: 6144, N: 1280 }, { operator: "qkv_proj_and_indexer_qk", K: 6144, N: 1536 }, @@ -167,7 +169,7 @@ var MODEL_WORKLOADS = { }, { tp_size: 8, - mValues: [2, 16, 3072, 4096], + mValues: W8A8_MODEL_TP8_M_VALUES, operators: [ { operator: "fused_qkv_a_proj", K: 6144, N: 2624 }, { operator: "q_b_proj", K: 2048, N: 2048 }, @@ -186,7 +188,8 @@ function modelCatalog(modelLabel) { var shapes = []; (model.topologies || []).forEach(function (topology) { // Per-topology M values override the defaults; TP4 and the Hy3/MiniMax/ - // GLM TP8 catalogs cover the M=4096 large-prefill boundary. + // GLM TP8 catalogs cover the M=8 short-decode and M=4096 large-prefill + // boundaries. var mValues = topology.mValues || (topology.tp_size === 4 ? W8A8_TP4_M_VALUES : W8A8_M_VALUES); topology.operators.forEach(function (item) { @@ -468,8 +471,10 @@ function ApiDefaultsPreview(_a) {
${catalog.length} logical shapes. Workloads are grouped by TP size and kept as separate task identities. - Each operator is optimized at M=2, M=16 and the large-prefill boundary - M=3072; TP4 additionally covers M=4096. + Every operator covers the decode and prefill boundaries of its own + catalog (per TP size above), e.g. DeepSeek M=2/16/3072 with TP4 adding + M=4096, and the Hy3 / MiniMax M3 / GLM5.2 TP8 catalogs adding the short + decode boundary M=8 and M=4096.
${tpSizes.map(function (tp) { @@ -488,7 +493,16 @@ function ApiDefaultsPreview(_a) { }); } }); - var mValues = tp === 4 ? W8A8_TP4_M_VALUES : W8A8_M_VALUES; + // Derive the M list from the catalog itself so model-specific + // catalogs (Hy3 / MiniMax M3 / GLM5.2 TP8) show their real M + // values (including the M=8 and M=4096 boundaries) instead of the + // DeepSeek defaults. + var mValues = topoShapes + .map(function (shape) { return shape.M; }) + .filter(function (value, index, all) { + return all.indexOf(value) === index; + }) + .sort(function (a, b) { return a - b; }); return html`
diff --git a/metainfer/tasks/dcu_kernel_auto_opt/tests/conftest.py b/metainfer/tasks/dcu_kernel_auto_opt/tests/conftest.py index 31681ab9..2b693a7b 100644 --- a/metainfer/tasks/dcu_kernel_auto_opt/tests/conftest.py +++ b/metainfer/tasks/dcu_kernel_auto_opt/tests/conftest.py @@ -21,6 +21,20 @@ def client(app): return TestClient(app) +@pytest.fixture(autouse=True) +def _no_measurement_gate(monkeypatch): + """Offline tests never wait on a real device. + + Every timed benchmark and PMC profile now passes the admission gate + (VRAM <= 90% and HCU == 0), which on a busy machine means a 30-minute wait + per check. Unit tests exercise command construction and parsing, not + admission, so the gate is off unless a test turns it back on (see + ``test_gpu_preflight.py``). + """ + monkeypatch.setenv("METAINFER_GPU_PREFLIGHT", "0") + yield + + def register_dkao_task( state_dir, workspace_dir, task_id: str = "dkao-1" ) -> TaskEntry: diff --git a/metainfer/tasks/dcu_kernel_auto_opt/tests/test_ahe_child_embed.py b/metainfer/tasks/dcu_kernel_auto_opt/tests/test_ahe_child_embed.py new file mode 100644 index 00000000..fca34b5c --- /dev/null +++ b/metainfer/tasks/dcu_kernel_auto_opt/tests/test_ahe_child_embed.py @@ -0,0 +1,175 @@ +"""The AHE page embeds this view for one question of one iteration. + +Every harness_evolve question *is* a real DKAO run, but it is spawned by the HE +orchestrator's CLI and is therefore absent from the WebUI task registry -- so +``task_or_404`` cannot resolve it and the DKAO page could not render it. The AHE +page passes the child's artifact directory instead, which these routes accept +under two conditions, both checked here: + + * the directory really is a question of a *registered* harness_evolve task + (otherwise any path on the box could be read through these routes), and + * it is read-only: a round's roster belongs to the orchestrator that spawned + it, not to whoever is watching it. +""" + +from __future__ import annotations + +import json + +from metainfer.server import tasks as _tasks +from metainfer.server.tasks import TaskEntry + + +def _he_task_with_child(tmp_path, *, register_owner: bool = True, + task_id: str = "he-embed", question: str = "m16_wqkv_a"): + """One registered HE task with one evaluated question on disk.""" + state_dir = tmp_path / "he-state" + workspace = tmp_path / "he-workspace" + child_state = (workspace / "children" / "iteration_003" / question + / "state") + child_ws = child_state.parent / "workspace" + child_state.mkdir(parents=True) + (child_ws / "workers" / "worker_1").mkdir(parents=True) + (child_state / "requirements.json").write_text(json.dumps({ + "task_id": "ahe-it003-wqkv", "task_type": "dcu-kernel-auto-opt", + "answers": {"shapes": []}, + }), encoding="utf-8") + (child_state / "run.json").write_text(json.dumps({ + "task_id": "ahe-it003-wqkv", "task_type": "dcu-kernel-auto-opt", + "current_phase": "parallel_explore", "current_iteration": 2, + "last_update": 123.0, + }), encoding="utf-8") + (child_ws / "plan.json").write_text(json.dumps({ + "shapes": [{"id": "m16_wqkv_a"}], "execution_mode": "gen-and-opt", + }), encoding="utf-8") + (child_ws / "workers" / "worker_1" / "status.json").write_text( + json.dumps({"worker_id": "worker_1", "gpu": 1, "state": "optimizing"}), + encoding="utf-8") + + state_dir.mkdir(parents=True, exist_ok=True) + if register_owner: + _tasks.add_task(TaskEntry( + id=task_id, type="harness-evolve", label="AHE", + state_dir=str(state_dir), workspace_dir=str(workspace), + created_at=0.0)) + return child_state, child_ws, workspace + + +def test_a_registered_he_child_is_served_through_the_dkao_routes( + tmp_path, client): + """The embedded view gets the same payload the standalone page gets.""" + child_state, _, _ = _he_task_with_child(tmp_path) + + resp = client.get( + "/api/dcu-kernel-auto-opt/ignored/summary", + params={"state_dir": str(child_state)}) + assert resp.status_code == 200, resp.text + body = resp.json() + assert body["run"]["current_phase"] == "parallel_explore" + assert body["plan"]["shapes"][0]["id"] == "m16_wqkv_a" + assert body["workers"][0]["worker_id"] == "worker_1" + assert body["gate"] is not None + + lanes = client.get( + "/api/dcu-kernel-auto-opt/ignored/iterations", + params={"state_dir": str(child_state)}) + assert lanes.status_code == 200, lanes.text + # one lane per device, and the one this child wrote shows its real state + by_id = {w["worker_id"]: w for w in lanes.json()["workers"]} + assert "worker_1" in by_id + assert by_id["worker_1"]["gpu"] == 1 + assert by_id["worker_1"]["state"] == "optimizing" + + +def test_the_task_id_in_the_path_is_irrelevant_for_an_embedded_child( + tmp_path, client): + """The AHE page keys the URL by question; only state_dir resolves it.""" + child_state, _, _ = _he_task_with_child(tmp_path) + other = _he_task_with_child(tmp_path / "second", task_id="he-embed-2", + question="m16_other") + first = client.get("/api/dcu-kernel-auto-opt/aaa/summary", + params={"state_dir": str(child_state)}) + second = client.get("/api/dcu-kernel-auto-opt/aaa/summary", + params={"state_dir": str(other[0])}) + assert first.status_code == second.status_code == 200 + assert first.json() == second.json() # same content, different paths + + +def test_an_unregistered_directory_is_refused(tmp_path, client): + """Without a registered HE owner, any path could be read: refuse it.""" + child_state, _, _ = _he_task_with_child(tmp_path, register_owner=False) + resp = client.get("/api/dcu-kernel-auto-opt/x/summary", + params={"state_dir": str(child_state)}) + assert resp.status_code == 404 + assert "harness-evolve" in resp.json()["detail"] + + +def test_arbitrary_paths_are_refused(tmp_path, client): + """Not a child of an iteration -> 404, even if the task exists.""" + _, _, workspace = _he_task_with_child(tmp_path) + probes = [ + tmp_path / "he-state", # the HE task's own state + workspace / "children", # too shallow + workspace / "children" / "iteration_003", # the question's parent + workspace / "children" / "iteration_003" / "m16_wqkv_a", # not "state" + tmp_path / "elsewhere" / "state", # unrelated path + ] + for probe in probes: + probe.mkdir(parents=True, exist_ok=True) + resp = client.get("/api/dcu-kernel-auto-opt/x/summary", + params={"state_dir": str(probe)}) + assert resp.status_code == 404, f"{probe} was accepted" + + +def test_without_a_state_dir_the_routes_behave_exactly_as_before( + tmp_path, client): + """No query parameter -> the ordinary registry lookup.""" + resp = client.get("/api/dcu-kernel-auto-opt/nope/summary") + assert resp.status_code == 404 + assert resp.json()["detail"] == "no such task: nope" + + +def test_the_embedded_view_cannot_write(tmp_path, client): + """Read-only for real: the UI hides these, the server refuses them.""" + child_state, _, _ = _he_task_with_child(tmp_path) + writes = [ + ("post", "/workers/worker_1/guidance", {"text": "steer me"}), + ("post", "/workers/worker_1/restart", {}), + ("post", "/skills/some-skill/publish", {}), + ("post", "/skills/sync", {}), + ("post", "/skills/fuse", {"skill_name": "s"}), + ("post", "/skills/some-skill/rollback", {}), + ("post", "/variants", {"shape_id": "m16"}), + ("post", "/rename-repo", {"new_name": "nope"}), + ] + for method, path, payload in writes: + resp = getattr(client, method)( + f"/api/dcu-kernel-auto-opt/ignored{path}", + params={"state_dir": str(child_state)}, json=payload) + assert resp.status_code == 403, f"{path} -> {resp.status_code}" + assert "read-only" in resp.json()["detail"] + + +def test_reads_are_not_blocked_by_the_write_guard(tmp_path, client): + """The guard must not leak onto GETs, or the mirror would be blank.""" + child_state, _, _ = _he_task_with_child(tmp_path) + for path in ("/summary", "/iterations", "/state-graph", "/skills", + "/variants"): + resp = client.get(f"/api/dcu-kernel-auto-opt/ignored{path}", + params={"state_dir": str(child_state)}) + assert resp.status_code == 200, f"{path} -> {resp.status_code}" + + +def test_a_registered_task_of_another_type_is_still_rejected(tmp_path, client): + """The embed path must not become a way around the type check.""" + _he_task_with_child(tmp_path) # HE task exists + other_state = tmp_path / "dkao-state" + other_ws = tmp_path / "dkao-ws" + other_state.mkdir(parents=True) + other_ws.mkdir(parents=True) + _tasks.add_task(TaskEntry( + id="calc-1", type="calc-theoretical-value", label="calc", + state_dir=str(other_state), workspace_dir=str(other_ws), created_at=0.0)) + + resp = client.get("/api/dcu-kernel-auto-opt/calc-1/summary") + assert resp.status_code == 409 # wrong type, as before diff --git a/metainfer/tasks/dcu_kernel_auto_opt/tests/test_config.py b/metainfer/tasks/dcu_kernel_auto_opt/tests/test_config.py index c7ccec16..46bf00d6 100644 --- a/metainfer/tasks/dcu_kernel_auto_opt/tests/test_config.py +++ b/metainfer/tasks/dcu_kernel_auto_opt/tests/test_config.py @@ -81,7 +81,26 @@ def test_dsh_framework_resolves_wrapper_model(monkeypatch): assert cfg.claude_model == "deepseek/deepseek-v4-flash-0731" -def test_dsh_default_model_when_agent_model_missing(monkeypatch): +def test_dsh_flash_41_label_resolves_to_flash_model(monkeypatch): + """The new frontend label maps to deepseek/deepseek-flash (4.1).""" + monkeypatch.delenv("DSH_AGENT_MODEL", raising=False) + req = _req(""" +shapes: + - {id: m2, M: 2, N: 16, K: 32} +assignments: + worker_0: {gpu: 0, shapes: [m2]} +""") + req["answers"]["agent_framework"] = "dsh" + req["answers"]["agent_model"] = "deepseek-flash-4.1" + + assert load_config(req).claude_model == "deepseek/deepseek-flash" + # Un-pinned, the older label still resolves to its own pinned build. + req["answers"]["agent_model"] = "deepseek-v4-flash" + assert load_config(req).claude_model == "deepseek/deepseek-v4-flash-0731" + + +def test_dsh_model_pin_collapses_catalog(monkeypatch): + """DSH_AGENT_MODEL pins a mixed-version host to one build for every dsh label.""" monkeypatch.setenv("DSH_AGENT_MODEL", "deepseek/deepseek-v4-flash-0731") req = _req(""" shapes: @@ -90,8 +109,24 @@ def test_dsh_default_model_when_agent_model_missing(monkeypatch): worker_0: {gpu: 0, shapes: [m2]} """) req["answers"]["agent_framework"] = "dsh" + req["answers"]["agent_model"] = "deepseek-flash-4.1" - assert load_config(req).claude_model == DSH_DEFAULT_MODEL_ID + assert load_config(req).claude_model == "deepseek/deepseek-v4-flash-0731" + + +def test_dsh_default_model_when_agent_model_missing(monkeypatch): + monkeypatch.delenv("DSH_AGENT_MODEL", raising=False) + req = _req(""" +shapes: + - {id: m2, M: 2, N: 16, K: 32} +assignments: + worker_0: {gpu: 0, shapes: [m2]} +""") + req["answers"]["agent_framework"] = "dsh" + + cfg = load_config(req) + assert cfg.claude_model == DSH_DEFAULT_MODEL_ID + assert cfg.claude_model == "deepseek/deepseek-flash" def test_dsh_rejects_ccb_only_model(monkeypatch): diff --git a/metainfer/tasks/dcu_kernel_auto_opt/tests/test_contention.py b/metainfer/tasks/dcu_kernel_auto_opt/tests/test_contention.py new file mode 100644 index 00000000..a58b2fc3 --- /dev/null +++ b/metainfer/tasks/dcu_kernel_auto_opt/tests/test_contention.py @@ -0,0 +1,91 @@ +"""Discarding a worker round that was measured on a shared device.""" + +from __future__ import annotations + +import json +import os +import time +from pathlib import Path + +from ..orchestrator.contention import ( + discard_contaminated_round, iteration_is_contaminated, read_contention, +) + + +def _history(path: Path, rounds: int) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("w", encoding="utf-8") as fh: + for i in range(1, rounds + 1): + fh.write(json.dumps({ + "schema_version": 1, "worker_id": "worker_0", "iteration": i, + "shape_id": "op_a", "accepted": i % 2 == 1, + "metrics": {"median_us": 100.0 - i, "p90_us": 100.0 - i, + "passed": True, "mismatch_count": 0}, + }) + "\n") + + +def _window(path: Path, **fields) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(fields), encoding="utf-8") + + +def test_a_round_inside_the_window_is_discarded_and_quarantined(tmp_path): + state = tmp_path / "state" + history = tmp_path / "runs" / "op_a" / "experiments.jsonl" + iteration_dir = tmp_path / "runs" / "op_a" / "iteration3" + iteration_dir.mkdir(parents=True) + _history(history, 3) + now = time.time() + frozen_from, recovered_at = now - 60, now - 30 + # the round's work happened while the device was shared + stamp = frozen_from + 10 + os.utime(iteration_dir, (stamp, stamp)) + _window(state / "contention.json", question="op_a", gpu=0, active=True, + frozen_from=frozen_from, recovered_at=recovered_at) + + result = discard_contaminated_round( + experiments_path=history, iteration=3, state_dir=state, + iteration_dir=iteration_dir) + + assert result["discarded"] is True + assert result["rounds_removed"] == 1 + kept = [json.loads(l) for l in history.read_text().splitlines()] + assert [r["iteration"] for r in kept] == [1, 2] # round 3 removed + quarantined = json.loads(Path(result["quarantined"][0]).read_text()) + assert quarantined["iteration"] == 3 + assert quarantined["metrics"]["median_us"] == 97.0 # evidence preserved + assert quarantined["reason"] == "device_shared_during_measurement" + + +def test_a_round_outside_the_window_is_kept(tmp_path): + state = tmp_path / "state" + history = tmp_path / "runs" / "op_a" / "experiments.jsonl" + iteration_dir = tmp_path / "runs" / "op_a" / "iteration1" + iteration_dir.mkdir(parents=True) + _history(history, 2) + now = time.time() + # the device was taken over AFTER this round finished + _window(state / "contention.json", frozen_from=now + 30, recovered_at=None) + + result = discard_contaminated_round( + experiments_path=history, iteration=1, state_dir=state, + iteration_dir=iteration_dir) + assert result["discarded"] is False + assert len(history.read_text().splitlines()) == 2 + + +def test_no_contention_record_means_nothing_is_discarded(tmp_path): + history = tmp_path / "runs" / "op_a" / "experiments.jsonl" + _history(history, 2) + result = discard_contaminated_round(experiments_path=history, iteration=1, + state_dir=tmp_path / "state") + assert result["discarded"] is False + assert "no contention" in result["reason"] + + +def test_an_open_window_contaminates_the_running_round(tmp_path): + """Contention still active: the round that was running is not trusted.""" + window = {"frozen_from": time.time() - 10, "active": True} + contaminated, why = iteration_is_contaminated(1, window) + assert contaminated is True and "taken over" in why + assert read_contention(tmp_path / "missing") == {} diff --git a/metainfer/tasks/dcu_kernel_auto_opt/tests/test_contract_snapshot.py b/metainfer/tasks/dcu_kernel_auto_opt/tests/test_contract_snapshot.py index ca271898..9a0324ef 100644 --- a/metainfer/tasks/dcu_kernel_auto_opt/tests/test_contract_snapshot.py +++ b/metainfer/tasks/dcu_kernel_auto_opt/tests/test_contract_snapshot.py @@ -148,20 +148,90 @@ def test_live_contract_accepts_model_catalog_tp8_m4096_shapes(): def test_default_optimization_shapes_unchanged_by_model_tp8_boundary(): - # Adding the model-catalog TP8 M=4096 boundary must not change the + # Adding the model-catalog TP8 M=8/M=4096 boundaries must not change the # DeepSeek-only default workload or its serial-validation fallback scope. from ..api.int8w8a8gemm.int8_w8a8_gemm_api import ( DEFAULT_OPTIMIZATION_SHAPES, MODEL_TP8_EXTRA_OPTIMIZATION_M_VALUES, ) - assert MODEL_TP8_EXTRA_OPTIMIZATION_M_VALUES == (4096,) + assert MODEL_TP8_EXTRA_OPTIMIZATION_M_VALUES == (8, 4096) assert len(DEFAULT_OPTIMIZATION_SHAPES) == 42 assert all( str(shape["id"]).startswith("tp") for shape in DEFAULT_OPTIMIZATION_SHAPES ) +def test_live_contract_accepts_model_catalog_tp8_m8_shapes(): + # Model-catalog TP8 short-decode boundary (2026-09-20): Hy3 / MiniMax M3 / + # GLM5.2 TP8 operators are optimizable at M=8 ("Selected shapes only"); + # DeepSeek TP8 defaults keep the original three M values. + contract = resolve_operator_api("Quantized GEMM", "INT8 W8A8") + shapes = { + "hy3_tp8_qkv_proj_m8": { + "tp_size": 8, "operator": "qkv_proj", + "M": 8, "N": 1280, "K": 4096, + }, + "hy3_tp8_o_proj_m8": { + "tp_size": 8, "operator": "o_proj", + "M": 8, "N": 4096, "K": 1024, + }, + "hy3_tp8_shared_gate_up_proj_m8": { + "tp_size": 8, "operator": "shared_gate_up_proj", + "M": 8, "N": 384, "K": 4096, + }, + "hy3_tp8_shared_down_proj_m8": { + "tp_size": 8, "operator": "shared_down_proj", + "M": 8, "N": 4096, "K": 192, + }, + "minimax_tp8_qkv_proj_m8": { + "tp_size": 8, "operator": "qkv_proj", + "M": 8, "N": 1280, "K": 6144, + }, + "minimax_tp8_qkv_proj_and_indexer_qk_m8": { + "tp_size": 8, "operator": "qkv_proj_and_indexer_qk", + "M": 8, "N": 1536, "K": 6144, + }, + "minimax_tp8_o_proj_m8": { + "tp_size": 8, "operator": "o_proj", + "M": 8, "N": 6144, "K": 1024, + }, + "minimax_tp8_shared_gate_up_proj_m8": { + "tp_size": 8, "operator": "shared_gate_up_proj", + "M": 8, "N": 768, "K": 6144, + }, + "minimax_tp8_shared_down_proj_m8": { + "tp_size": 8, "operator": "shared_down_proj", + "M": 8, "N": 6144, "K": 384, + }, + "glm52_tp8_fused_qkv_a_proj_m8": { + "tp_size": 8, "operator": "fused_qkv_a_proj", + "M": 8, "N": 2624, "K": 6144, + }, + "glm52_tp8_q_b_proj_m8": { + "tp_size": 8, "operator": "q_b_proj", + "M": 8, "N": 2048, "K": 2048, + }, + "glm52_tp8_kv_b_proj_m8": { + "tp_size": 8, "operator": "kv_b_proj", + "M": 8, "N": 3584, "K": 512, + }, + "glm52_tp8_o_proj_m8": { + "tp_size": 8, "operator": "o_proj", + "M": 8, "N": 6144, "K": 2048, + }, + "glm52_tp8_shared_gate_up_proj_m8": { + "tp_size": 8, "operator": "shared_gate_up_proj", + "M": 8, "N": 512, "K": 6144, + }, + "glm52_tp8_shared_down_proj_m8": { + "tp_size": 8, "operator": "shared_down_proj", + "M": 8, "N": 6144, "K": 256, + }, + } + validate_contract_shapes(contract, shapes) + + def test_live_contract_accepts_minimax_m3_tp4_shapes(): # Regression for minimaxm3-dsh-tp4-m4096-1-0c2f84a9: the MiniMax M3 TP4 # M=4096 workload stopped in prepare because TP=4 qkv_proj (K, N) diff --git a/metainfer/tasks/dcu_kernel_auto_opt/tests/test_execution_profile.py b/metainfer/tasks/dcu_kernel_auto_opt/tests/test_execution_profile.py new file mode 100644 index 00000000..dd9996c1 --- /dev/null +++ b/metainfer/tasks/dcu_kernel_auto_opt/tests/test_execution_profile.py @@ -0,0 +1,266 @@ +"""Production profile: priority in the device queue, but never a gate bypass. + +The operator's decision (2026-09-16, option 1): a production DKAO task outranks +the evolving harness when devices are handed out, but it still only measures on +a device that passes VRAM<=90% / HCU==0 — a shared card yields stale numbers no +matter who asked for the run. +""" + +from __future__ import annotations + +import json + +from ..orchestrator import config as cfg + +import importlib.util + +import pytest +_GPU_BROKER_AVAILABLE = ( + importlib.util.find_spec("metainfer.orchestrator.gpu_broker") is not None +) +requires_gpu_broker = pytest.mark.skipif( + not _GPU_BROKER_AVAILABLE, + reason="metainfer.orchestrator.gpu_broker is not available in this tree", +) + + +def _lease(monkeypatch, *, grants, profile, answers=None): + """Run lease_gpus_for_task against a fake broker and report the call.""" + from metainfer.orchestrator import gpu_broker + + calls = [] + + class _FakeBroker: + def acquire(self, holder, **kwargs): + calls.append(kwargs) + return [{"gpu": g, "holder": holder} for g in grants] + + monkeypatch.setattr(gpu_broker, "GpuBroker", _FakeBroker) + assignments = [cfg.WorkerAssignment(worker_id="worker_0", gpu=0, + shape_ids=["s1"])] + merged = {"task_id": "t1"} + merged.update(answers or {}) + rebuilt, info = cfg.lease_gpus_for_task( + assignments, merged, production=(profile == "production")) + return calls, rebuilt, info + + +@requires_gpu_broker +def test_production_leases_at_the_highest_priority(monkeypatch): + calls, rebuilt, info = _lease(monkeypatch, grants=[2], profile="production") + assert calls and calls[0]["priority"] == 100 # PRIORITY_PRODUCTION + assert calls[0]["meta"]["profile"] == "production" + assert info["profile"] == "production" and info["gpus"] == [2] + assert rebuilt[0].gpu == 2 + + +@requires_gpu_broker +def test_default_profile_still_uses_production_priority_on_the_broker(monkeypatch): + """DKAO has always asked at priority 100; the profile makes it explicit.""" + calls, _, info = _lease(monkeypatch, grants=[0], profile="default") + assert calls[0]["priority"] == 100 + assert calls[0]["meta"]["profile"] == "default" + assert info["profile"] == "default" + + +@requires_gpu_broker +def test_production_waits_longer_for_a_device_that_passes_the_gate(monkeypatch): + """Nothing passing the gate: production keeps waiting instead of giving up.""" + calls, _, _ = _lease(monkeypatch, grants=[0], profile="production", + answers={"gpu_lease_max_waits": "2", + "production_max_waits": "48", + "gpu_lease_wait_seconds": "0"}) + assert calls[0]["priority"] == 100 + + +@requires_gpu_broker +def test_no_device_after_the_budget_reports_the_gate_honestly(monkeypatch): + import pytest + + class _EmptyBroker: + def acquire(self, holder, **kwargs): + return [] + + from metainfer.orchestrator import gpu_broker + monkeypatch.setattr(gpu_broker, "GpuBroker", _EmptyBroker) + assignments = [cfg.WorkerAssignment(worker_id="worker_0", gpu=0, + shape_ids=["s1"])] + with pytest.raises(RuntimeError) as excinfo: + cfg.lease_gpus_for_task(assignments, {"task_id": "t1", + "gpu_lease_max_waits": "2", + "gpu_lease_wait_seconds": "0"}, + production=True) + message = str(excinfo.value) + assert "production" in message + # and it must say the gate was NOT skipped + assert "not skipped" in message and "shared card" in message + + +@requires_gpu_broker +def test_load_config_accepts_the_profile_and_forces_the_broker(monkeypatch): + """``production`` implies broker-mediated scheduling (that is the queue).""" + from metainfer.orchestrator import gpu_broker + + seen = {} + + class _FakeBroker: + def acquire(self, holder, **kwargs): + seen.update(kwargs) + return [{"gpu": 1, "holder": holder}] + + monkeypatch.setattr(gpu_broker, "GpuBroker", _FakeBroker) + req = { + "task_id": "prod-1", + "answers": { + "execution_mode": "Generate & optimize (auto-create kernel repo)", + "target_repo_path": "11111", + "shape_config": ("model: m\nassignment_mode: ai\nshape_scope: subset\n" + "shapes:\n - {id: s1, M: 16, N: 64, K: 32, tp_size: 8," + " operator: o_proj}\n"), + "max_iterations": "1", + "execution_profile": "production", + # deliberately ask for occupy: production must override it + "gpu_mode": "occupy", + }, + } + config = cfg.load_config(req) + assert config.execution_profile == "production" + assert config.gpu_mode == "scheduled" # broker carries the priority + assert seen["priority"] == 100 + assert config.gpu_leases.get("profile") == "production" + + +def test_an_unknown_profile_is_rejected(): + import pytest + + req = { + "task_id": "bad-1", + "answers": { + "execution_mode": "Generate & optimize (auto-create kernel repo)", + "target_repo_path": "11111", + "shape_config": ("model: m\nassignment_mode: ai\nshape_scope: subset\n" + "shapes:\n - {id: s1, M: 16, N: 64, K: 32, tp_size: 8," + " operator: o_proj}\n"), + "max_iterations": "1", + "execution_profile": "fastest", + }, + } + with pytest.raises(ValueError): + cfg.load_config(req) + + +@requires_gpu_broker +def test_replace_assignments_keeps_the_profile(monkeypatch): + """Re-planning the workers must not silently downgrade a production task.""" + from metainfer.orchestrator import gpu_broker + + class _FakeBroker: + def acquire(self, holder, **kwargs): + return [{"gpu": 3, "holder": holder}] + + monkeypatch.setattr(gpu_broker, "GpuBroker", _FakeBroker) + req = { + "task_id": "prod-2", + "answers": { + "execution_mode": "Generate & optimize (auto-create kernel repo)", + "target_repo_path": "11111", + "shape_config": ("model: m\nassignment_mode: ai\nshape_scope: subset\n" + "shapes:\n - {id: s1, M: 16, N: 64, K: 32, tp_size: 8," + " operator: o_proj}\n"), + "max_iterations": "1", + "execution_profile": "production", + }, + } + config = cfg.load_config(req) + replanned = cfg.replace_assignments(config, config.assignments) + assert replanned.execution_profile == "production" + assert replanned.gpu_leases.get("priority") == 100 + + +def test_gpu_leases_acquired_event_names_the_profile(): + """The timeline event carries the rank, so the GPU view can label a holder.""" + from ..orchestrator.gen_and_opt_pipeline import GenAndOptPipeline + + events = [] + + class _Store: + task_dir = None + + def append_timeline(self, name, payload): + events.append((name, payload)) + + pipeline = GenAndOptPipeline.__new__(GenAndOptPipeline) + pipeline.store = _Store() + pipeline._register_gpu_leases(type("C", (), { + "gpu_leases": {"holder": "dkao:prod-2", "gpus": [3], "mode": "scheduled", + "profile": "production", "priority": 100}, + })()) + acquired = dict(events)["gpu_leases_acquired"] + assert acquired["profile"] == "production" + assert acquired["priority"] == 100 + + +def test_summary_reports_the_profile_and_the_held_devices(tmp_path): + """The task view shows production mode + which devices the task leased.""" + from ..server.routes import _execution_profile, _latest_gpu_lease + + state_dir = tmp_path / "state" + state_dir.mkdir() + (state_dir / "requirements.json").write_text(json.dumps({ + "task_id": "prod-3", + "answers": {"execution_profile": "production"}, + }), encoding="utf-8") + (state_dir / "timeline.jsonl").write_text("\n".join([ + json.dumps({"ts": 1.0, "type": "phase_start", + "payload": {"phase": "prepare"}}), + json.dumps({"ts": 2.0, "type": "gpu_leases_acquired", "payload": { + "holder": "dkao:prod-3", "gpus": [1, 2], "mode": "scheduled", + "profile": "production", "priority": 100}}), + json.dumps({"ts": 3.0, "type": "gpu_leases_released", + "payload": {"holder": "dkao:prod-3", "gpus": [1, 2]}}), + ]), encoding="utf-8") + + assert _execution_profile(state_dir) == "production" + leases = _latest_gpu_lease(state_dir) + assert leases["gpus"] == [1, 2] and leases["priority"] == 100 + + +def test_summary_defaults_to_the_default_profile(tmp_path): + from ..server.routes import _execution_profile, _latest_gpu_lease + + state_dir = tmp_path / "state" + state_dir.mkdir() + (state_dir / "requirements.json").write_text(json.dumps({ + "task_id": "plain-1", "answers": {}}), encoding="utf-8") + assert _execution_profile(state_dir) == "default" + assert _latest_gpu_lease(state_dir) == {} + + +def test_gate_status_summarises_the_blocked_checks(tmp_path): + """A task parked on the gate must say why, in its own state dir.""" + from ..server.routes import _gate_status + + state_dir = tmp_path / "state" + state_dir.mkdir() + rows = [ + {"ts": 1.0, "event": "gate_blocked", "site": "generate_probe", + "gpu": 0, "attempt": 1, "max_waits": 48, + "reasons": ["device busy: HCU 98.0% > 0.0%"]}, + {"ts": 1801.0, "event": "gate_ok", "site": "generate_probe", "gpu": 1, + "passing": [1], "reasons": []}, + ] + (state_dir / "measurement_gate.jsonl").write_text( + "\n".join(json.dumps(r) for r in rows), encoding="utf-8") + + gate = _gate_status(state_dir) + assert gate["checks"] == 2 and gate["blocked"] == 1 + assert gate["last_event"] == "gate_ok" and gate["last_gpu"] == 1 + assert gate["last_reasons"] == [] + + +def test_gate_status_is_empty_without_an_audit_trail(tmp_path): + from ..server.routes import _gate_status + + state_dir = tmp_path / "state" + state_dir.mkdir() + assert _gate_status(state_dir)["checks"] == 0 diff --git a/metainfer/tasks/dcu_kernel_auto_opt/tests/test_gpu_mode.py b/metainfer/tasks/dcu_kernel_auto_opt/tests/test_gpu_mode.py new file mode 100644 index 00000000..213135c2 --- /dev/null +++ b/metainfer/tasks/dcu_kernel_auto_opt/tests/test_gpu_mode.py @@ -0,0 +1,108 @@ +"""DKAO GPU modes: occupy (default, pins devices) vs scheduled (broker leases).""" + +from __future__ import annotations + +import pytest + +import importlib.util + +_GPU_BROKER_AVAILABLE = ( + importlib.util.find_spec("metainfer.orchestrator.gpu_broker") is not None +) +requires_gpu_broker = pytest.mark.skipif( + not _GPU_BROKER_AVAILABLE, + reason="metainfer.orchestrator.gpu_broker is not available in this tree", +) + + +def _assignments(spec): + from ..orchestrator.config import WorkerAssignment + return [WorkerAssignment(worker_id=w, gpu=g, shape_ids=list(s)) + for w, g, s in spec] + + +def test_rebalance_merges_workers_when_leases_are_fewer(): + """2 leases + 4 planned workers -> 2 lanes, every shape kept.""" + from ..orchestrator.config import rebalance_for_gpus + planned = _assignments([ + ("worker_0", 0, ["a"]), ("worker_1", 1, ["b"]), + ("worker_2", 2, ["c"]), ("worker_3", 3, ["d"]), + ]) + lanes = rebalance_for_gpus(planned, [2, 3]) + assert len(lanes) == 2 # one lane per device + assert sorted(l.gpu for l in lanes) == [2, 3] + assert {l.worker_id for l in lanes} == {"worker_2", "worker_3"} + assert sorted(s for l in lanes for s in l.shape_ids) == ["a", "b", "c", "d"] + + +def test_rebalance_keeps_one_lane_per_worker_when_leases_are_plentiful(): + from ..orchestrator.config import rebalance_for_gpus + planned = _assignments([("worker_0", 0, ["a"]), ("worker_1", 1, ["b"])]) + lanes = rebalance_for_gpus(planned, [0, 1, 2, 3]) + assert len(lanes) == 2 # no empty lanes + assert sorted(l.gpu for l in lanes) == [0, 1] + + +@requires_gpu_broker +def test_lease_gpus_for_task_layouts_workers_on_leases(monkeypatch): + from ..orchestrator import config as C + from metainfer.orchestrator import gpu_broker + + class FakeBroker: + def __init__(self, *a, **k): + self.calls = [] + + def acquire(self, holder, **kwargs): + self.calls.append((holder, kwargs)) + return [{"gpu": 2}, {"gpu": 3}] + + monkeypatch.setattr(gpu_broker, "GpuBroker", FakeBroker) + planned = _assignments([ + ("worker_0", 0, ["a"]), ("worker_1", 1, ["b"]), + ("worker_2", 2, ["c"]), ("worker_3", 3, ["d"]), + ]) + lanes, info = C.lease_gpus_for_task(planned, {"task_id": "t-1"}) + assert info["mode"] == "scheduled" and info["gpus"] == [2, 3] + assert info["holder"] == "dkao:t-1" + assert info["requested_workers"] == 4 + assert info["mapping"] == {"worker_2": 2, "worker_3": 3} + assert len(lanes) == 2 + + +@requires_gpu_broker +def test_lease_gpus_for_task_fails_loudly_when_nothing_is_free(monkeypatch): + from ..orchestrator import config as C + from metainfer.orchestrator import gpu_broker + + class EmptyBroker: + def __init__(self, *a, **k): + pass + + def acquire(self, holder, **kwargs): + return [] + + monkeypatch.setattr(gpu_broker, "GpuBroker", EmptyBroker) + with pytest.raises(RuntimeError, match="no GPU lease"): + C.lease_gpus_for_task(_assignments([("worker_0", 0, ["a"])]), + {"task_id": "t-2", "gpu_lease_max_waits": 2}) + + +def test_scheduled_mode_skips_manual_pin_validation(): + """manual (worker_N -> GPU N) is only enforced in occupy mode.""" + from ..orchestrator.config import OptimizerConfig, WorkerAssignment + import dataclasses + base = dict(operator="Quantized GEMM", dtype="INT8 W8A8", hardware="dcu", + kernel_language="HIP C++", claude_model="m", + execution_mode="gen_and_opt", target_repo_path=None, + shapes={}, assignments=[WorkerAssignment("worker_2", 3, ["a"])], + shape_scope="all", mock_iterations=1, + minimum_improvement_percent=1.0) + occupy = OptimizerConfig(assignment_mode="manual", **base) + scheduled = OptimizerConfig(assignment_mode="scheduled", gpu_mode="scheduled", + **base) + assert occupy.gpu_mode == "occupy" + assert scheduled.gpu_mode == "scheduled" + # the pipeline only validates worker_N -> GPU N for occupy + assert (occupy.assignment_mode == "manual" and occupy.gpu_mode == "occupy") + assert not (scheduled.assignment_mode == "manual" + and scheduled.gpu_mode == "occupy") diff --git a/metainfer/tasks/dcu_kernel_auto_opt/tests/test_gpu_preflight.py b/metainfer/tasks/dcu_kernel_auto_opt/tests/test_gpu_preflight.py index f3f0b80c..faa21706 100644 --- a/metainfer/tasks/dcu_kernel_auto_opt/tests/test_gpu_preflight.py +++ b/metainfer/tasks/dcu_kernel_auto_opt/tests/test_gpu_preflight.py @@ -2,10 +2,28 @@ from __future__ import annotations +import json + import pytest from ..orchestrator import gpu_preflight as gp +import importlib.util +from pathlib import Path + +_GPU_BROKER_AVAILABLE = ( + importlib.util.find_spec("metainfer.orchestrator.gpu_broker") is not None +) +requires_gpu_broker = pytest.mark.skipif( + not _GPU_BROKER_AVAILABLE, + reason="metainfer.orchestrator.gpu_broker is not available in this tree", +) +_DTK_HIPPROF_AVAILABLE = Path("/opt/dtk/bin/hipprof").is_file() +requires_dtk_hipprof = pytest.mark.skipif( + not _DTK_HIPPROF_AVAILABLE, + reason="/opt/dtk/bin/hipprof is required (DCU toolchain is absent)", +) + SMI_SAMPLE = """ ================================= System Management Interface ================================== ================================================================================================ @@ -17,6 +35,20 @@ ======================================== End of SMI Log ========================================= """ +#: What hy-smi actually prints inside a container that cannot open the +#: management interface: the HCU% column (and every other utilisation value) +#: is a placeholder, NOT a zero. +SMI_UNAVAILABLE = """ +================================= System Management Interface ================================== +================================================================================================ +HCU Temp AvgPwr Perf PwrCap VRAM% HCU% Mode +0 52.0C 146.0W high 300.0W 0% N/A Normal +1 50.0C 149.0W high 300.0W 0% N/A Normal +2 50.0C 148.0W high 300.0W 0% N/A Normal +3 51.0C 148.0W high 300.0W 0% N/A Normal +======================================== End of SMI Log ========================================= +""" + def test_parse_hy_smi_rows(): parsed = gp.parse_hy_smi(SMI_SAMPLE) @@ -27,28 +59,72 @@ def test_parse_hy_smi_rows(): assert parsed[1]["temp_c"] == 49.0 -def test_usability_is_free_vram_not_percentage(): - """The rule is `free >= 4GB`, so a 91%-used card is still usable.""" - ok = gp.check_gpu(0, {"vram_percent": 91.0, "util_percent": 0.0}, +def test_na_utilisation_is_unknown_not_zero(): + """``N/A`` must not become ``0.0``: that is what admitted busy cards.""" + parsed = gp.parse_hy_smi(SMI_UNAVAILABLE) + assert set(parsed) == {0, 1, 2, 3} + assert "util_percent" not in parsed[0] # unknown, not idle + assert parsed[0]["power_w"] == 146.0 # real fields are kept + assert gp.smi_is_incomplete(SMI_UNAVAILABLE) is True + assert gp.smi_is_incomplete(SMI_SAMPLE) is False + + +def test_sample_falls_back_to_sysfs_and_gates_busy_cards(): + """hy-smi cannot see HCU%: sysfs decides, and a busy card stays blocked.""" + def sysfs(): + return {0: {"util_percent": 0.0, "vram_percent": 2.0}, + 1: {"util_percent": 0.0, "vram_percent": 2.0}, + 2: {"util_percent": 74.0, "vram_percent": 20.0}, + 3: {"util_percent": 0.0, "vram_percent": 95.0}} + + states = gp.sample_gpu_state([0, 1, 2, 3], samples=1, interval_s=0, + smi_reader=lambda: SMI_UNAVAILABLE, + sysfs_reader=sysfs) + assert states[2]["util_percent"] == 74.0 + assert states[3]["vram_percent"] == 95.0 + assert not any(row.get("unavailable") for row in states.values()) + checks = {g: gp.check_gpu(g, states[g], total_gb=64.0) for g in range(4)} + assert [g for g in range(4) if checks[g]["usable"]] == [0, 1] + assert checks[2]["usable"] is False and checks[3]["usable"] is False + + +def test_no_source_at_all_means_unavailable(monkeypatch): + states = gp.sample_gpu_state([0, 1], samples=1, interval_s=0, + smi_reader=lambda: "", + sysfs_reader=lambda: {}) + assert states[0]["unavailable"] == 1.0 + assert gp.check_gpu(0, states[0])["usable"] is False + + +def test_usability_requires_vram_limit_and_idle_device(): + """Operator rule: VRAM <= 90% *and* HCU == 0, nothing else counts.""" + ok = gp.check_gpu(0, {"vram_percent": 89.0, "util_percent": 0.0}, total_gb=64.0) - too_full = gp.check_gpu(1, {"vram_percent": 94.0, "util_percent": 0.0}, + assert ok["usable"] is True and ok["measurement_suspect"] is False + + too_full = gp.check_gpu(1, {"vram_percent": 91.0, "util_percent": 0.0}, total_gb=64.0) - assert ok["usable"] is True and ok["free_gb"] == 5.76 - assert ok["measurement_suspect"] is False - assert too_full["usable"] is False and too_full["free_gb"] == 3.84 - assert any("VRAM free" in r for r in too_full["reasons"]) - # a full device is a capacity problem, not a pollution problem - assert too_full["measurement_suspect"] is False - assert gp.DEFAULT_MIN_FREE_GB == 4.0 + assert too_full["usable"] is False + assert any("VRAM 91%" in r for r in too_full["reasons"]) + + busy = gp.check_gpu(2, {"vram_percent": 10.0, "util_percent": 3.0}, + total_gb=64.0) + assert busy["usable"] is False # HCU must be 0 + assert any("HCU 3.0%" in r for r in busy["reasons"]) + + assert gp.DEFAULT_VRAM_LIMIT_PERCENT == 90.0 + assert gp.DEFAULT_UTIL_TOLERANCE == 0.0 -def test_busy_marks_suspect_without_blocking(): +def test_busy_device_is_unusable_and_suspect(): busy = gp.check_gpu(2, {"vram_percent": 40.0, "util_percent": 97.0, "power_w": 251.0}, foreign_pids=[12345]) - assert busy["usable"] is True # usage is allowed (VRAM ok) + assert busy["usable"] is False # a busy device is never used assert busy["measurement_suspect"] is True assert any("busy" in r for r in busy["reasons"]) - assert any("power" in r for r in busy["reasons"]) + # power draw is recorded for diagnosis but is not part of the gate + assert busy["power_w"] == 251.0 + assert not any("power" in r for r in busy["reasons"]) # foreign pids are informational (idle holders are harmless) assert any("foreign" in n for n in busy["notes"]) @@ -63,28 +139,41 @@ def test_idle_foreign_holders_are_not_suspicion(): assert any("foreign" in n for n in idle["notes"]) -def test_preflight_prefers_clean_then_suspect_then_over_limit(monkeypatch): +def test_an_idle_card_passes_even_at_high_board_power(): + """The gate is exactly "VRAM <= 90% and HCU == 0" — nothing else blocks.""" + check = gp.check_gpu(1, {"vram_percent": 5.0, "util_percent": 0.0, + "power_w": 250.0}) + assert check["usable"] is True + assert check["reasons"] == [] + + +def test_preflight_gate_lists_all_and_only_passing_devices(monkeypatch): monkeypatch.setattr(gp, "sample_gpu_state", lambda ids=None, **kw: { 0: {"vram_percent": 1.0, "util_percent": 0.0}, - 1: {"vram_percent": 5.0, "util_percent": 55.0, + 1: {"vram_percent": 5.0, "util_percent": 0.0, "power_w": 250.0}, - 2: {"vram_percent": 30.0, "util_percent": 3.0}, - 3: {"vram_percent": 97.0, "util_percent": 10.0}, + 2: {"vram_percent": 30.0, "util_percent": 4.0}, + 3: {"vram_percent": 97.0, "util_percent": 0.0}, }) monkeypatch.setattr(gp, "foreign_kfd_pids", lambda *a, **k: []) monkeypatch.setattr(gp, "total_vram_gb", lambda *a, **k: 64.0) plan = gp.preflight_gpus([0, 1, 2, 3]) - assert plan["clean_ids"] == [0, 2] - assert plan["suspect_ids"] == [1] + assert plan["clean_ids"] == [0, 1] + assert plan["suspect_ids"] == [] + assert plan["blocked_ids"] == [2, 3] assert plan["over_limit_ids"] == [3] - # usable devices first (clean before suspect); the over-limit device drops - # out while any device is within the limit - assert plan["preferred"] == [0, 2, 1] - assert 3 not in plan["preferred"] + # only gate-passing devices are ever offered + assert plan["preferred"] == [0, 1] + assert "HCU 4.0%" in plan["gate_reasons"]["2"][0] -def test_over_limit_devices_return_when_nothing_else_is_usable(monkeypatch): +def test_blocked_devices_are_never_offered_for_a_measurement(monkeypatch): + """Both cards full: the gate offers nothing so the caller waits and re-checks. + + Waiting 30 min and retrying (up to 48 times) is the operator rule; silently + measuring on a device that failed the gate is what this prevents. + """ monkeypatch.setattr(gp, "sample_gpu_state", lambda ids=None, **kw: { 0: {"vram_percent": 97.0, "util_percent": 5.0}, @@ -93,7 +182,20 @@ def test_over_limit_devices_return_when_nothing_else_is_usable(monkeypatch): monkeypatch.setattr(gp, "foreign_kfd_pids", lambda *a, **k: []) monkeypatch.setattr(gp, "total_vram_gb", lambda *a, **k: 64.0) plan = gp.preflight_gpus([0, 1]) - assert plan["preferred"] == [0, 1] + assert plan["preferred"] == [] + assert plan["clean_ids"] == [] + assert plan["blocked_ids"] == [0, 1] + assert "VRAM 97% > 90% limit" in plan["gate_reasons"]["0"] + + +def test_unreadable_device_state_is_not_usable(monkeypatch): + """No HCU/VRAM reading must never be treated as "idle".""" + monkeypatch.setattr(gp, "foreign_kfd_pids", lambda *a, **k: []) + check = gp.check_gpu(0, {"unavailable": 1.0, + "missing_fields": ["vram_percent", "util_percent"]}) + assert check["usable"] is False + assert check["unavailable"] is True + assert any("state unavailable" in r for r in check["reasons"]) def test_foreign_kfd_pids_detects_invisible_pids(tmp_path): @@ -119,3 +221,256 @@ def test_disabled_preflight_returns_default_order(): assert plan["enabled"] is False assert plan["preferred"] == [0, 1, 2, 3] assert plan["gpus"] == {} + + +# ---------------------------------------------------------------- sub-task gate +# A DKAO child iterates for hours, so the device can be taken over long after +# the parent admitted it. These tests pin the *child-side* rule: no timed +# benchmark and no PMC profile may start on a device that fails the gate. + +def test_measurement_gate_waits_then_admits(monkeypatch, tmp_path): + from ..orchestrator import w8a8_pipeline as wp + + blocked = {"gpus": {2: {"usable": False, "util_percent": 91.0, + "vram_percent": 10.0, + "reasons": ["device busy: HCU 91.0% > 0.0%"]}}} + ok = {"gpus": {2: {"usable": True, "util_percent": 0.0, + "vram_percent": 5.0, "state_source": "hy-smi"}}} + plans = [blocked, ok] + calls = [] + + def fake_preflight(ids, **kw): + calls.append((list(ids), kw.get("vram_limit_percent"), + kw.get("util_tolerance"))) + return plans.pop(0) + + sleeps = [] + monkeypatch.setattr("metainfer.tasks.dcu_kernel_auto_opt.orchestrator" + ".gpu_preflight.preflight_gpus", fake_preflight) + monkeypatch.setattr(wp, "_env_flag_off", lambda env: False) + monkeypatch.setattr(wp.time, "sleep", lambda s: sleeps.append(s)) + + check = wp.ensure_measurement_gate(2, state_dir=tmp_path, wait_seconds=1800, + max_waits=48, site="benchmark") + assert check["usable"] is True + assert sleeps == [1800] # exactly one 30-minute re-check + assert calls[0][0] == [2] # only the assigned device + assert calls[0][1] == 90.0 and calls[0][2] == 0.0 # the operator's gate + + rows = [json.loads(l) for l in + (tmp_path / "measurement_gate.jsonl").read_text().splitlines()] + assert [r["event"] for r in rows] == ["gate_blocked", "gate_ok"] + assert rows[0]["site"] == "benchmark" and rows[0]["gpu"] == 2 + assert rows[1]["util_percent"] == 0.0 + + +def test_measurement_gate_gives_up_after_max_waits(monkeypatch, tmp_path): + from ..orchestrator import w8a8_pipeline as wp + + always = {"gpus": {3: {"usable": False, "util_percent": 0.0, + "vram_percent": 96.0, + "reasons": ["VRAM 96% > 90% limit"]}}} + monkeypatch.setattr("metainfer.tasks.dcu_kernel_auto_opt.orchestrator" + ".gpu_preflight.preflight_gpus", + lambda ids, **kw: always) + monkeypatch.setattr(wp, "_env_flag_off", lambda env: False) + sleeps = [] + monkeypatch.setattr(wp.time, "sleep", lambda s: sleeps.append(s)) + + with pytest.raises(wp.MeasurementGateBlocked) as excinfo: + wp.ensure_measurement_gate(3, state_dir=tmp_path, wait_seconds=1800, + max_waits=3) + assert excinfo.value.gpu == 3 + assert excinfo.value.checks == 3 # 3 re-checks, then give up + assert "VRAM 96%" in excinfo.value.gate_reason + assert sleeps == [1800, 1800, 1800] + rows = (tmp_path / "measurement_gate.jsonl").read_text().splitlines() + assert len(rows) == 4 # 3 blocked + the final blocked + + +def test_measurement_gate_fails_closed_when_the_probe_breaks(monkeypatch, tmp_path): + from ..orchestrator import w8a8_pipeline as wp + + def boom(*_a, **_k): + raise RuntimeError("hy-smi exploded") + + monkeypatch.setattr("metainfer.tasks.dcu_kernel_auto_opt.orchestrator" + ".gpu_preflight.preflight_gpus", boom) + monkeypatch.setattr(wp, "_env_flag_off", lambda env: False) + monkeypatch.setattr(wp.time, "sleep", lambda s: None) + with pytest.raises(wp.MeasurementGateBlocked): + wp.ensure_measurement_gate(0, state_dir=tmp_path, wait_seconds=0, + max_waits=1) + + +def test_runner_gates_every_measurement(monkeypatch, tmp_path): + """benchmark() and profile_pmc() both pass the gate before launching.""" + from ..orchestrator import w8a8_pipeline as wp + + gated = [] + monkeypatch.setattr(wp.W8A8Runner, "_ensure_gate", + lambda self, site: gated.append((self.gpu, site))) + monkeypatch.setattr(wp.time, "sleep", lambda s: None) # no real gate waits + + runner = object.__new__(wp.W8A8Runner) + runner.worker_root = tmp_path + runner.source = tmp_path + runner.harness = tmp_path / "w8a8_bench.py" + runner.env = {"METAINFER_GPU_PREFLIGHT": "1"} + runner.gpu = 1 + monkeypatch.setattr(wp.W8A8Runner, "_prepare_compile_cache", + lambda self: {"build_key": "k"}) + monkeypatch.setattr(wp.W8A8Runner, "_ensure_reference_cache", + lambda self, m, n, k: None) + monkeypatch.setattr(wp, "_run", + lambda *a, **k: type("R", (), {"stdout": "{}"})()) + monkeypatch.setattr(wp, "_last_json", lambda out: {}) + + runner.benchmark({"M": 16, "N": 64, "K": 32}) + assert gated == [(1, "benchmark")] + + (tmp_path / "profile_pmc.sh").write_text("#!/bin/bash\n", encoding="utf-8") + monkeypatch.setattr(wp, "parse_pmc_csv", + lambda path: {"available": False, "counters": {}}) + runner.profile_pmc({"M": 16, "N": 64, "K": 32}, tmp_path / "out") + assert gated == [(1, "benchmark"), (1, "profile_pmc")] + + +def test_any_measurement_gate_picks_an_idle_sibling(monkeypatch, tmp_path): + """A busy GPU 0 must not park the task while GPU 1 is free.""" + from ..orchestrator import w8a8_pipeline as wp + + plan = {"gpus": { + 0: {"usable": False, "util_percent": 98.0, "vram_percent": 97.0, + "reasons": ["device busy: HCU 98.0% > 0.0%"]}, + 1: {"usable": True, "util_percent": 0.0, "vram_percent": 5.0, + "state_source": "sysfs"}, + }} + calls = [] + monkeypatch.setattr( + "metainfer.tasks.dcu_kernel_auto_opt.orchestrator.gpu_preflight" + ".preflight_gpus", + lambda ids, **kw: (calls.append(list(ids)), plan)[1]) + monkeypatch.setattr(wp, "_env_flag_off", lambda env: False) + sleeps = [] + monkeypatch.setattr(wp.time, "sleep", lambda s: sleeps.append(s)) + + chosen = wp.ensure_any_measurement_gate([0, 1, 2, 3], state_dir=tmp_path, + wait_seconds=1800, site="probe") + assert chosen == 1 # an idle sibling, not the busy GPU 0 + assert sleeps == [] # and no waiting at all + assert calls == [[0, 1, 2, 3]] # the whole candidate set is checked + rows = [json.loads(l) for l in + (tmp_path / "measurement_gate.jsonl").read_text().splitlines()] + assert rows[0]["event"] == "gate_ok" and rows[0]["passing"] == [1] + + +def test_any_measurement_gate_waits_when_no_candidate_passes(monkeypatch, tmp_path): + from ..orchestrator import w8a8_pipeline as wp + + busy = {"gpus": {d: {"usable": False, "util_percent": 99.0, + "vram_percent": 96.0, + "reasons": [f"gpu{d} VRAM 96% > 90% limit"]} + for d in (0, 1)}} + monkeypatch.setattr( + "metainfer.tasks.dcu_kernel_auto_opt.orchestrator.gpu_preflight" + ".preflight_gpus", lambda ids, **kw: busy) + monkeypatch.setattr(wp, "_env_flag_off", lambda env: False) + sleeps = [] + monkeypatch.setattr(wp.time, "sleep", lambda s: sleeps.append(s)) + + with pytest.raises(wp.MeasurementGateBlocked) as excinfo: + wp.ensure_any_measurement_gate([0, 1], state_dir=tmp_path, + wait_seconds=1800, max_waits=2) + assert sleeps == [1800, 1800] # 30-minute re-checks, then give up + assert "gpu0" in excinfo.value.gate_reason + assert "gpu1" in excinfo.value.gate_reason + + +def test_any_measurement_gate_prefers_the_tasks_own_device(monkeypatch, tmp_path): + from ..orchestrator import w8a8_pipeline as wp + + free = {"gpus": {d: {"usable": True, "util_percent": 0.0, + "vram_percent": 1.0} for d in (0, 3)}} + monkeypatch.setattr( + "metainfer.tasks.dcu_kernel_auto_opt.orchestrator.gpu_preflight" + ".preflight_gpus", lambda ids, **kw: free) + monkeypatch.setattr(wp, "_env_flag_off", lambda env: False) + # 3 is the task's own card and is free: the probe stays on it + assert wp.ensure_any_measurement_gate([3, 0], env={"METAINFER_GPU_PREFLIGHT": "1"}) == 3 + + +def _fake_scaffold(root): + """A scaffold that satisfies the trusted Generate preflight file checks.""" + from ..orchestrator import gen_and_opt_pipeline as gp + + root.mkdir(parents=True, exist_ok=True) + for name in (gp._HARNESS_FILENAME, gp.W8A8_API_FILENAME, + gp._GIT_ATTRIBUTES_FILE, "scaffold_manifest.json", + *gp._SCAFFOLD_FILES): + path = root / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text("", encoding="utf-8") + (root / "profile_pmc.sh").write_text( + "#!/bin/bash\n/opt/dtk/bin/hipprof --pmc --pmc-type 3 " + '"$source_dir" "$output_dir"\n', encoding="utf-8") + return root + + +@requires_dtk_hipprof +def test_generate_probe_runs_on_a_device_that_passed_the_gate(monkeypatch, tmp_path): + """The probe is gated, but not pinned to GPU 0 (a busy card must not park it).""" + from ..orchestrator import gen_and_opt_pipeline as gp + + source = _fake_scaffold(tmp_path / "scaffold") + contract = gp.file_digest(source / gp.W8A8_API_FILENAME) + seen = {} + + def fake_run(cmd, **kwargs): + if cmd[-1] == "--self-test": + return type("R", (), {"stdout": '{"passed": true}'})() + if cmd[-1] == "--probe": + env = kwargs.get("env") or {} + seen["probe_gpu"] = env.get("HIP_VISIBLE_DEVICES") + # DTK filters twice when both variables name the same non-zero + # index, which hides the device ("No HIP GPUs are available" on + # GPU 1 while GPU 0 survived): binding must set HIP only. + seen["probe_env"] = dict(env) + return type("R", (), {"stdout": json.dumps({ + "visible_devices": 1, "cudagraph_available": True})})() + return type("R", (), {"stdout": ""})() + + monkeypatch.setattr(gp, "_run", fake_run) + monkeypatch.setattr(gp, "_gate_preflight_enabled", lambda: True) + + calls = [] + + def fake_gate(devices, **kwargs): + calls.append((list(devices), kwargs)) + return 1 + + monkeypatch.setattr(gp, "ensure_any_measurement_gate", fake_gate) + out = gp._validate_generate_scaffold( + source, contract_sha256=contract, shapes={"s1": {"M": 16}}, + probe_devices=[0, 1, 2, 3], state_dir=tmp_path / "state") + + assert calls[0][0] == [0, 1, 2, 3] # every candidate may answer + # the wait is auditable in the task's own state dir, not silently invisible + assert calls[0][1]["state_dir"] == tmp_path / "state" + assert calls[0][1]["site"] == "generate_probe" + assert seen["probe_gpu"] == "1" # and the probe honours the choice + assert "ROCR_VISIBLE_DEVICES" not in seen["probe_env"] + assert out["gpu_probe"]["visible_devices"] == 1 + + +@requires_gpu_broker +def test_probe_candidates_put_the_tasks_own_devices_first(): + from ..orchestrator import gen_and_opt_pipeline as gp + + class _Cfg: + assignments = [type("A", (), {"gpu": 2})(), type("A", (), {"gpu": 0})()] + + devices = gp._probe_gate_devices(_Cfg()) + assert devices[:2] == [2, 0] # the task's own cards, in order + assert 1 in devices and 3 in devices # then the idle siblings + assert len(devices) == len(set(devices)) diff --git a/metainfer/tasks/dcu_kernel_auto_opt/tests/test_plugin.py b/metainfer/tasks/dcu_kernel_auto_opt/tests/test_plugin.py index 176eb25d..db897b5b 100644 --- a/metainfer/tasks/dcu_kernel_auto_opt/tests/test_plugin.py +++ b/metainfer/tasks/dcu_kernel_auto_opt/tests/test_plugin.py @@ -49,7 +49,7 @@ def test_form_schema_is_available(): assert model["default"] == "Opus" assert model["override_component"] == "agent-model" assert [option["label"] for option in model["options"]] == [ - "Opus", "Sonnet", "deepseek-v4-flash" + "Opus", "Sonnet", "deepseek-flash-4.1", "deepseek-v4-flash" ] diff --git a/metainfer/tasks/dcu_kernel_auto_opt/tests/test_retest_tool.py b/metainfer/tasks/dcu_kernel_auto_opt/tests/test_retest_tool.py new file mode 100644 index 00000000..18ed4186 --- /dev/null +++ b/metainfer/tasks/dcu_kernel_auto_opt/tests/test_retest_tool.py @@ -0,0 +1,52 @@ +"""A/B retest tool: variant lookup, header parsing, verdict and environment.""" + +from __future__ import annotations + +from pathlib import Path + +from ..tools.retest_variant import ( + BENCH_ENV, bench_env, decide, find_variant, parse_variant_header_median, +) + + +def test_find_variant_tolerates_model_label_drift(tmp_path): + """glm should find the shipped glm52 directory.""" + root = tmp_path / "variant" / "int8w8a8-gemm" + glm = root / "glm52" / "TP8" / "M4096" + glm.mkdir(parents=True) + (glm / "shared_down_proj.hip").write_text("// kernel\n", encoding="utf-8") + assert find_variant("glm", 8, 4096, "shared_down_proj", root) == ( + glm / "shared_down_proj.hip") + assert find_variant("glm", 8, 16, "shared_down_proj", root) is None + assert find_variant("hy3", 4, 4096, "o_proj", root) is None + + +def test_parse_variant_header_median_reads_the_record(): + text = ("// @@variant shape=hy3_tp4_o_proj_m4096 commit=abc added=2026-09-14\n" + "// median_us=453.2 p90_us=454.2 speedup=43.87 baseline_us=1.988e+04\n" + "// code follows\n") + path = Path("/tmp/_variant_header_test.hip") + path.write_text(text, encoding="utf-8") + try: + assert parse_variant_header_median(path) == 453.2 + finally: + path.unlink(missing_ok=True) + assert parse_variant_header_median(Path("/tmp/does-not-exist.hip")) is None + + +def test_bench_env_pins_gfx928_and_the_device(): + env = bench_env(2, base={}) + assert env["PYTORCH_ROCM_ARCH"] == BENCH_ENV["PYTORCH_ROCM_ARCH"] == "gfx928" + assert env["HIP_VISIBLE_DEVICES"] == "2" + assert env["CUDA_VISIBLE_DEVICES"] == "2" + + +def test_decide_requires_margin_and_reproducibility(): + # a clear, reproducible win + assert decide(140.0, 280.0, recorded_us=140.0)["verdict"] == "promote" + # too small a margin + assert decide(275.0, 280.0, recorded_us=275.0)["verdict"] == "keep" + # fast but the re-measurement disagrees with the record -> review + assert decide(140.0, 280.0, recorded_us=100.0)["verdict"] == "needs_review" + # missing measurement + assert decide(None, 280.0)["verdict"] == "measurement_failed" diff --git a/metainfer/tasks/dcu_kernel_auto_opt/tests/test_round_strategy.py b/metainfer/tasks/dcu_kernel_auto_opt/tests/test_round_strategy.py new file mode 100644 index 00000000..0c3cd43d --- /dev/null +++ b/metainfer/tasks/dcu_kernel_auto_opt/tests/test_round_strategy.py @@ -0,0 +1,490 @@ +"""systemprompt/round_strategy.yaml is wired: the worker-facing mandate is data. + +What these tests pin down: + +* the built-in texts are byte-identical to the historical Python literals, so an + un-evolved harness tells the worker exactly what it always did; +* the seed file reproduces those defaults (drift guard, like gates.yaml); +* editing the seed changes the runtime mandate, including inside the pipeline's + own call path; +* ``wired: false`` in the manifest makes the component inert; +* a malformed / partial harness file degrades to built-in text instead of + raising (a broken harness is a performance problem, not a crash). +""" + +from __future__ import annotations + +import shutil + +import pytest +import yaml + +from ..orchestrator import round_strategy as rs +from ..orchestrator.harness_io import default_harness_dir +from ..orchestrator.prompts import w8a8_round_strategy + +import importlib.util + +import pytest +_HARNESS_EVOLVE_AVAILABLE = ( + importlib.util.find_spec( + "metainfer.tasks.harness_evolve.orchestrator.pipeline" + ) + is not None +) +requires_harness_evolve = pytest.mark.skipif( + not _HARNESS_EVOLVE_AVAILABLE, + reason="metainfer.tasks.harness_evolve is not available in this tree", +) + +M16 = {"model": "hy3", "M": 16, "N": 2048, "K": 256} +SMALL = {"model": "hy3", "M": 2, "N": 2048, "K": 256} +PREFILL = {"model": "hy3", "M": 3072, "N": 2048, "K": 256} + + +@pytest.fixture(autouse=True) +def _clear_cache(): + rs.reset_cache() + yield + rs.reset_cache() + + +def _workspace(tmp_path, *, wired=True): + root = tmp_path / "harness" + shutil.copytree(default_harness_dir(), root) + manifest = yaml.safe_load((root / "manifest.yaml").read_text(encoding="utf-8")) + manifest["components"]["systemprompt"]["wired"] = wired + (root / "manifest.yaml").write_text( + yaml.safe_dump(manifest, sort_keys=False), encoding="utf-8") + return root + + +def _strategy_file(root): + return root / rs.RELATIVE_PATH + + +def _edit(root, mutate): + data = yaml.safe_load(_strategy_file(root).read_text(encoding="utf-8")) + mutate(data) + _strategy_file(root).write_text( + yaml.safe_dump(data, sort_keys=False, allow_unicode=True), + encoding="utf-8") + + +# --------------------------------------------------------------------------- # +# Built-in defaults and the seed file agree +# --------------------------------------------------------------------------- # + +def test_seed_matches_builtin_defaults(): + """The seed file mirrors the built-in texts (so nothing drifts silently).""" + seed = yaml.safe_load(_strategy_file(default_harness_dir()).read_text( + encoding="utf-8")) + defaults = rs.builtin_strategy() + for section in ("repair", "phases", "append"): + for key, value in defaults[section].items(): + assert seed[section][key].strip() == value, f"{section}.{key}" + for regime, portfolio in defaults["portfolios"].items(): + for slot, value in portfolio.items(): + assert seed["portfolios"][regime][slot].strip() == value, ( + f"portfolios.{regime}.{slot}") + + +def test_seed_round_trips_through_the_loader(tmp_path, monkeypatch): + """Loading the seed yields exactly the built-in texts.""" + root = _workspace(tmp_path) + monkeypatch.setenv("METAINFER_HARNESS_ROOT", str(root)) + rs.reset_cache() + loaded = rs.load_round_strategy() + assert loaded.source.endswith("round_strategy.yaml") + assert loaded.repair == rs.builtin_strategy()["repair"] + assert loaded.phases == rs.builtin_strategy()["phases"] + assert loaded.append == rs.builtin_strategy()["append"] + assert loaded.portfolios == rs.builtin_strategy()["portfolios"] + + +def test_no_harness_root_uses_the_plugin_seed(monkeypatch): + """Without METAINFER_HARNESS_ROOT the plugin's own seed is the workspace. + + Resolution falls back to ``harness_default/`` (not to an in-code sentinel), + so the seed file is what production tasks read — and the drift test above + proves it carries the built-in texts. + """ + monkeypatch.delenv("METAINFER_HARNESS_ROOT", raising=False) + rs.reset_cache() + loaded = rs.load_round_strategy() + assert loaded.source.endswith("harness_default/systemprompt/round_strategy.yaml") + assert loaded.portfolios["m16"]["2"].startswith("Architecture round") + assert loaded.portfolios == rs.builtin_strategy()["portfolios"] + + +# --------------------------------------------------------------------------- # +# Editing the file changes the runtime mandate +# --------------------------------------------------------------------------- # + +def test_editing_the_seed_changes_runtime_mandate(tmp_path, monkeypatch): + root = _workspace(tmp_path) + + def mutate(data): + data["portfolios"]["m16"]["2"] = ( + "EVOLVED: benchmark 3-wave and 4-wave geometries before anything else.") + + _edit(root, mutate) + monkeypatch.setenv("METAINFER_HARNESS_ROOT", str(root)) + rs.reset_cache() + + rendered = w8a8_round_strategy(M16, 2, [], {}, 10, None) + assert rendered.startswith("EVOLVED: benchmark 3-wave") + # Untouched branches keep their historical text. + assert w8a8_round_strategy(M16, 1, [], {}, 10, None).startswith( + "Establish a minimal 16x16x32 DUMMA tile") + + +def test_edited_text_is_seen_by_the_pipeline_call_path(tmp_path, monkeypatch): + """w8a8_pipeline's non-planner path renders the harness text too.""" + root = _workspace(tmp_path) + + def mutate(data): + data["repair"]["build_failure"] = "EVOLVED BUILD REPAIR for {iteration}" + + _edit(root, mutate) + monkeypatch.setenv("METAINFER_HARNESS_ROOT", str(root)) + monkeypatch.delenv("METAINFER_PLANNER", raising=False) + rs.reset_cache() + + from ..orchestrator import w8a8_pipeline as W + + history = [{"iteration": 4, "build_success": False, + "failure_reason": "hipcc: undefined template", + "artifact_dir": "/tmp/a4"}] + text = W.w8a8_round_strategy(M16, 4, history, {}, max_iterations=10, + isa_policy={}) + assert text == "EVOLVED BUILD REPAIR for 4" + + +def test_repeated_scale_can_be_edited(tmp_path, monkeypatch): + root = _workspace(tmp_path) + + def mutate(data): + data["phases"]["isa_guided_hip"] = "EVOLVED ISA round {completed} of many" + + _edit(root, mutate) + monkeypatch.setenv("METAINFER_HARNESS_ROOT", str(root)) + rs.reset_cache() + text = w8a8_round_strategy( + M16, 3, [], {}, 10, + {"phase": "isa_guided_hip", "valid_isa_guided_rounds": 1}) + assert text == "EVOLVED ISA round 2 of many" + + +def test_append_suffixes_can_be_edited(tmp_path, monkeypatch): + root = _workspace(tmp_path) + + def mutate(data): + data["append"]["split_candidates"] = " SUSPECTS={split_candidates}." + + _edit(root, mutate) + monkeypatch.setenv("METAINFER_HARNESS_ROOT", str(root)) + rs.reset_cache() + text = w8a8_round_strategy( + M16, 3, [], {"grid_blocks": 500, "device_cu_count": 60}, 10, None) + assert "SUSPECTS=" in text + assert "Trusted occupancy-probe split candidates" not in text + + +# --------------------------------------------------------------------------- # +# The manifest decides whether the file applies at all +# --------------------------------------------------------------------------- # + +def test_unwired_component_falls_back_to_builtin(tmp_path, monkeypatch): + root = _workspace(tmp_path, wired=False) + + def mutate(data): + data["portfolios"]["m16"]["2"] = "EVOLVED: should never be used" + + _edit(root, mutate) + monkeypatch.setenv("METAINFER_HARNESS_ROOT", str(root)) + rs.reset_cache() + loaded = rs.load_round_strategy() + assert loaded.source == "builtin" + assert loaded.portfolios["m16"]["2"].startswith("Architecture round") + assert any("wired:false" in note for note in loaded.notes) + + +def test_missing_component_entry_still_applies(tmp_path, monkeypatch): + """A manifest without the component key must not disable the file.""" + root = _workspace(tmp_path) + manifest = yaml.safe_load((root / "manifest.yaml").read_text(encoding="utf-8")) + manifest["components"].pop("systemprompt") + (root / "manifest.yaml").write_text( + yaml.safe_dump(manifest, sort_keys=False), encoding="utf-8") + + def mutate(data): + data["portfolios"]["m16"]["2"] = "EVOLVED without a manifest entry" + + _edit(root, mutate) + monkeypatch.setenv("METAINFER_HARNESS_ROOT", str(root)) + rs.reset_cache() + assert w8a8_round_strategy(M16, 2, [], {}, 10, None).startswith("EVOLVED without") + + +# --------------------------------------------------------------------------- # +# Degradation: a broken harness must not break a round +# --------------------------------------------------------------------------- # + +def test_missing_file_uses_builtin(tmp_path, monkeypatch): + root = _workspace(tmp_path) + _strategy_file(root).unlink() + monkeypatch.setenv("METAINFER_HARNESS_ROOT", str(root)) + rs.reset_cache() + loaded = rs.load_round_strategy() + assert loaded.source == "builtin" + assert loaded.repair == rs.builtin_strategy()["repair"] + + +def test_malformed_yaml_is_reported_not_raised(tmp_path, monkeypatch): + root = _workspace(tmp_path) + _strategy_file(root).write_text("repair: [unclosed\n", encoding="utf-8") + monkeypatch.setenv("METAINFER_HARNESS_ROOT", str(root)) + rs.reset_cache() + loaded = rs.load_round_strategy() + assert loaded.source == "builtin" + assert loaded.notes and "round_strategy.yaml" in loaded.notes[0] + assert w8a8_round_strategy(M16, 2, [], {}, 10, None).startswith("Architecture") + + +def test_partial_file_keeps_unlisted_branches(tmp_path, monkeypatch): + """Omitting a key keeps its built-in text instead of blanking the branch.""" + root = _workspace(tmp_path) + # A file that only carries a reformatted large-M round 3. + _strategy_file(root).write_text( + "portfolios:\n large:\n \"3\": EVOLVED large round three\n", + encoding="utf-8") + monkeypatch.setenv("METAINFER_HARNESS_ROOT", str(root)) + rs.reset_cache() + loaded = rs.load_round_strategy() + assert loaded.portfolios["large"]["3"] == "EVOLVED large round three" + assert loaded.portfolios["large"]["2"] == ( + rs.builtin_strategy()["portfolios"]["large"]["2"]) + assert loaded.portfolios["m16"]["2"].startswith("Architecture round") + + +def test_unknown_fields_degrade_to_builtin_text(tmp_path, monkeypatch): + """A renamed format field must not raise mid-round.""" + root = _workspace(tmp_path) + + def mutate(data): + data["append"]["grid_warning"] = " warning for {cus} CUs" + + _edit(root, mutate) + monkeypatch.setenv("METAINFER_HARNESS_ROOT", str(root)) + rs.reset_cache() + text = w8a8_round_strategy( + M16, 3, [], {"grid_blocks": 4, "device_cu_count": 60}, 10, None) + # The suffix falls back to the built-in wording rather than raising. + assert "Trusted control-plane warning" in text + + +def test_unknown_regime_is_ignored(tmp_path, monkeypatch): + root = _workspace(tmp_path) + + def mutate(data): + data["portfolios"]["medium"] = {"2": "nonsense"} + + _edit(root, mutate) + monkeypatch.setenv("METAINFER_HARNESS_ROOT", str(root)) + rs.reset_cache() + loaded = rs.load_round_strategy() + assert "medium" not in loaded.portfolios + assert any("unknown regime" in note for note in loaded.notes) + + +def test_empty_string_is_ignored(tmp_path, monkeypatch): + """An empty value keeps the built-in text (a mandate is never blank).""" + root = _workspace(tmp_path) + + def mutate(data): + data["portfolios"]["m16"]["2"] = " " + + _edit(root, mutate) + monkeypatch.setenv("METAINFER_HARNESS_ROOT", str(root)) + rs.reset_cache() + loaded = rs.load_round_strategy() + assert loaded.portfolios["m16"]["2"].startswith("Architecture round") + + +# --------------------------------------------------------------------------- # +# Cache invalidation: a mid-run harness swap is picked up +# --------------------------------------------------------------------------- # + +def test_cache_is_invalidated_by_file_change(tmp_path, monkeypatch): + root = _workspace(tmp_path) + monkeypatch.setenv("METAINFER_HARNESS_ROOT", str(root)) + rs.reset_cache() + assert w8a8_round_strategy(M16, 2, [], {}, 10, None).startswith("Architecture") + + _edit(root, lambda data: data["portfolios"]["m16"].__setitem__( + "2", "EVOLVED after a mid-run swap")) + import os + stat = _strategy_file(root).stat() + os.utime(_strategy_file(root), (stat.st_atime, stat.st_mtime + 10)) + assert w8a8_round_strategy(M16, 2, [], {}, 10, None) == "EVOLVED after a mid-run swap" + + +def test_cache_is_invalidated_by_manifest_change(tmp_path, monkeypatch): + root = _workspace(tmp_path) + monkeypatch.setenv("METAINFER_HARNESS_ROOT", str(root)) + rs.reset_cache() + assert rs.load_round_strategy().source.endswith("round_strategy.yaml") + + manifest = yaml.safe_load((root / "manifest.yaml").read_text(encoding="utf-8")) + manifest["components"]["systemprompt"]["wired"] = False + (root / "manifest.yaml").write_text( + yaml.safe_dump(manifest, sort_keys=False), encoding="utf-8") + import os + stat = (root / "manifest.yaml").stat() + os.utime(root / "manifest.yaml", (stat.st_atime, stat.st_mtime + 10)) + assert rs.load_round_strategy().source == "builtin" + + +# --------------------------------------------------------------------------- # +# The small-M late-round crash is fixed +# --------------------------------------------------------------------------- # + +@pytest.mark.parametrize("max_iterations", [9, 10, 11, 20]) +@pytest.mark.parametrize("iteration", [8, 9, 10, 11, 12]) +def test_small_m_late_rounds_never_raise(max_iterations, iteration): + """M<16 has no slot 9/10; it used to raise KeyError for max_iterations 9..10.""" + text = w8a8_round_strategy(SMALL, iteration, [], {}, max_iterations, None) + assert text + assert isinstance(text, str) + + +def test_small_m_keeps_its_consolidation_round_late(): + text = w8a8_round_strategy(SMALL, 10, [], {}, 20, None) + assert text.startswith("HIP-only consolidation round") + + +def test_normal_regimes_are_unaffected_by_the_fix(): + # 16..127 and >=128 still reach the late slots. + assert w8a8_round_strategy(M16, 9, [], {}, 10, None).startswith( + "Late ISA-diagnosis round") + assert w8a8_round_strategy(M16, 10, [], {}, 10, None).startswith( + "Final conditional inline-asm round") + assert w8a8_round_strategy(PREFILL, 20, [], {}, 20, None).startswith( + "Final conditional inline-asm round") + + +# --------------------------------------------------------------------------- # +# End-to-end: seeding, and the AHE write-scope check reaches the new component +# --------------------------------------------------------------------------- # + +def test_seed_workspace_carries_the_systemprompt_component(tmp_path): + """HE seeds the experiment workspace from harness_default/, so the file ships.""" + from ..orchestrator.harness_io import seed_workspace + + dst = tmp_path / "seeded" + seed_workspace(dst) + assert (dst / rs.RELATIVE_PATH).is_file() + manifest = yaml.safe_load((dst / "manifest.yaml").read_text(encoding="utf-8")) + entry = manifest["components"]["systemprompt"] + assert entry["path"] == rs.RELATIVE_PATH + assert entry["wired"] is True + + +#: Rendered outputs of a 5400-case matrix (M regime x iteration x history x PMC +#: x ISA phase), frozen after the externalisation refactor. Any edit to a +#: mandate — in the built-in defaults *or* in the seed file — changes this +#: digest, which makes an accidental behaviour change impossible to merge +#: unnoticed. Regenerate deliberately when a wording change is intended. +_RENDERED_DIGEST = "8a3bb533a7779a45e6ad2a7b45128d0cc1b406155f83d04f1fc74fcb15255fea" + +_SHAPES = { + "m16": {"model": "hy3", "M": 16, "N": 2048, "K": 256}, + "m64": {"model": "hy3", "M": 64, "N": 4096, "K": 512}, + "large": {"model": "hy3", "M": 3072, "N": 2048, "K": 256}, + "prefill": {"model": "hy3", "M": 4096, "N": 1024, "K": 256}, + "small": {"model": "hy3", "M": 2, "N": 2048, "K": 256}, + "tiny": {"model": "hy3", "M": 1, "N": 2048, "K": 256}, +} +_HISTORIES = { + "empty": [], + "faster_wrong": [ + {"iteration": 2, "correctness_passed": False, "speedup": 1.4, + "artifact_dir": "/tmp/arch/iter2", "failure_reason": "mismatch_count 12"}, + {"iteration": 4, "correctness_passed": True, "speedup": 0.9, + "artifact_dir": "/tmp/arch/iter4", "failure_reason": ""}, + ], + "infra": [{"iteration": 3, "correctness_passed": True, "build_success": True, + "failure_reason": "agent timeout after 900s", + "artifact_dir": "/tmp/i3"}], + "build": [{"iteration": 3, "correctness_passed": True, "build_success": False, + "failure_reason": "hipcc: undefined template", + "artifact_dir": "/tmp/i3"}], + "assert": [{"iteration": 3, "correctness_passed": True, "build_success": True, + "failure_reason": "mismatch_count 3", "artifact_dir": "/tmp/i3"}], +} +_PMCS = [ + {}, + {"grid_blocks": 4, "device_cu_count": 60, "l2_hit_rate": 0.55}, + {"grid_blocks": 400, "device_cu_count": 60, "l2_hit_rate": 0.9}, +] +_ISA_POLICIES = [ + {}, + {"phase": "hip_only"}, + {"phase": "isa_guided_hip", "valid_isa_guided_rounds": 0}, + {"phase": "isa_guided_hip", "valid_isa_guided_rounds": 3}, + {"phase": "conditional_inline_asm"}, +] + + +def test_rendered_mandates_match_the_frozen_digest(): + """The worker-facing mandates are frozen: 5400 rendered strings, one hash.""" + import hashlib + import json + + payload = {} + for regime, shape in _SHAPES.items(): + for iteration in range(1, 13): + for hlabel, history in _HISTORIES.items(): + for pidx, pmc in enumerate(_PMCS): + for iidx, isa in enumerate(_ISA_POLICIES): + payload[ + f"{regime}-it{iteration:02d}-{hlabel}" + f"-pmc{pidx}-isa{iidx}" + ] = w8a8_round_strategy(shape, iteration, history, pmc, 10, isa) + blob = json.dumps(payload, ensure_ascii=False, sort_keys=True).encode("utf-8") + assert len(payload) == 5400 + assert hashlib.sha256(blob).hexdigest() == _RENDERED_DIGEST, ( + "a round mandate changed; if that was intended, regenerate the digest " + "intentionally and describe the new wording in the change manifest") + + +@requires_harness_evolve +def test_ahe_wired_scope_check_covers_the_systemprompt_component(tmp_path): + """An edit to the component while it is wired:false is reported by AHE. + + This is what makes the wider write scope auditable: harness_evolve compares + each component's declared path, so a new component is covered as soon as it + is registered in the manifest. + """ + from metainfer.tasks.harness_evolve.orchestrator.pipeline import ( + _wired_scope_violations, + ) + + before = tmp_path / "before" + after = tmp_path / "after" + shutil.copytree(default_harness_dir(), before) + shutil.copytree(default_harness_dir(), after) + manifest = yaml.safe_load((after / "manifest.yaml").read_text(encoding="utf-8")) + manifest["components"]["systemprompt"]["wired"] = False + (after / "manifest.yaml").write_text( + yaml.safe_dump(manifest, sort_keys=False), encoding="utf-8") + # Unchanged file: not a violation. + assert _wired_scope_violations(before, after) == [] + # Edited file while declared unwired: a violation, with the component named. + text = (after / rs.RELATIVE_PATH).read_text(encoding="utf-8") + (after / rs.RELATIVE_PATH).write_text( + text + "\n# evolved by hand\n", encoding="utf-8") + violations = _wired_scope_violations(before, after) + assert violations == [{"component": "systemprompt", + "path": rs.RELATIVE_PATH}] diff --git a/metainfer/tasks/dcu_kernel_auto_opt/tests/test_shape_catalog_panel.py b/metainfer/tasks/dcu_kernel_auto_opt/tests/test_shape_catalog_panel.py new file mode 100644 index 00000000..10be54fb --- /dev/null +++ b/metainfer/tasks/dcu_kernel_auto_opt/tests/test_shape_catalog_panel.py @@ -0,0 +1,126 @@ +"""Panel <-> contract <-> baseline integration for the model shape catalog. + +``static/dkao-shape-input.js`` is what the DKAO "New Task" panel offers the +operator, so it is the source of truth for which shapes a task can submit. +Every shape it can submit must + +* satisfy the frozen operator contract (``validate_optimization_shape``), and +* have a fixed Triton Graph baseline (``fixed_triton_graph_baseline``), + +otherwise the run dies in the ``baseline`` phase with a ValueError long after +the task was created. These tests read the *real* frontend catalog (by +evaluating its self-contained leading section under node, so the assertions +cannot drift from the shipped UI) and check that the three layers agree. +""" + +from __future__ import annotations + +import json +import shutil +import subprocess +from pathlib import Path + +import pytest + +from ..api.int8w8a8gemm.int8_w8a8_gemm_api import ( + MODEL_TP8_EXTRA_OPTIMIZATION_M_VALUES, + validate_optimization_shape, +) +from ..orchestrator.w8a8_baselines import fixed_triton_graph_baseline + + +STATIC_JS = Path(__file__).resolve().parent.parent / "static" / "dkao-shape-input.js" +MODEL_CATALOG_MARKERS = ( + "var W8A8_M_VALUES", + 'var DEFAULT_W8A8_SHAPES = modelCatalog("DeepSeek V4 Flash");', +) +# Models whose TP8 catalog carries the M=8 short-decode boundary. +M8_MODELS = ("Hy3 (Hunyuan 3)", "MiniMax M3", "GLM5.2") + + +def _frontend_catalogs() -> dict[str, list[dict[str, object]]]: + """Return {model label: [shape, ...]} from the shipped panel catalog.""" + node = shutil.which("node") + if node is None: + pytest.skip("node is required to evaluate the panel shape catalog") + text = STATIC_JS.read_text(encoding="utf-8") + start, end = MODEL_CATALOG_MARKERS + head = text.index(start) + tail = text.index(end) + len(end) + harness = ( + text[head:tail] + + "\nvar out = {};\n" + + "Object.keys(MODEL_WORKLOADS).forEach(function (label) {\n" + + " out[label] = modelCatalog(label);\n" + + "});\n" + + "console.log(JSON.stringify(out));\n" + ) + proc = subprocess.run( + [node, "-e", harness], + capture_output=True, + text=True, + check=False, + ) + assert proc.returncode == 0, proc.stderr + return json.loads(proc.stdout) + + +@pytest.fixture(scope="module") +def catalogs() -> dict[str, list[dict[str, object]]]: + return _frontend_catalogs() + + +def test_panel_catalog_exposes_m8_for_model_tp8(catalogs): + # The M=8 short-decode boundary added on 2026-09-20 must be offered for + # Hy3 / MiniMax M3 / GLM5.2 TP8 operators. + for model in M8_MODELS: + tp8 = [s for s in catalogs[model] if s["tp_size"] == 8] + assert tp8, model + assert all(s["M"] in (8, 2, 16, 3072, 4096) for s in tp8), model + assert any(s["M"] == 8 for s in tp8), model + assert sorted({int(s["M"]) for s in tp8}) == [2, 8, 16, 3072, 4096] + + +def test_panel_catalog_keeps_deepseek_tp8_defaults(catalogs): + # DeepSeek TP8 intentionally keeps the three original M values, so the + # DeepSeek-only default workload and its validation scope are unchanged. + tp8 = [s for s in catalogs["DeepSeek V4 Flash"] if s["tp_size"] == 8] + assert sorted({int(s["M"]) for s in tp8}) == [2, 16, 3072] + + +def test_panel_m8_shapes_match_the_extra_m_value_contract(catalogs): + assert 8 in MODEL_TP8_EXTRA_OPTIMIZATION_M_VALUES + for model in M8_MODELS: + m8 = [s for s in catalogs[model] if s["tp_size"] == 8 and s["M"] == 8] + # One M=8 shape per catalog operator. + assert len(m8) == len( + {s["operator"] for s in catalogs[model] if s["tp_size"] == 8} + ) + for shape in m8: + validate_optimization_shape(shape) + + +def test_every_panel_tp4_tp8_shape_is_submittable(catalogs): + # A shape the panel offers must both pass the frozen contract and resolve a + # fixed baseline; otherwise the task fails in the baseline phase. + checked = 0 + for model, shapes in catalogs.items(): + for shape in shapes: + if shape["tp_size"] not in (4, 8): + continue + validate_optimization_shape(shape) + record = fixed_triton_graph_baseline(str(shape["id"]), shape) + assert record["median_us"] > 0, (model, shape) + checked += 1 + assert checked >= 70 + + +def test_panel_m8_shapes_resolve_decode_baselines(catalogs): + for model in M8_MODELS: + for shape in catalogs[model]: + if shape["tp_size"] != 8 or shape["M"] != 8: + continue + record = fixed_triton_graph_baseline(str(shape["id"]), shape) + assert record["timing_scope"] == "decode_graph_replay" + assert record["baseline_kind"] == "triton_graph" + assert record["case"], shape diff --git a/metainfer/tasks/dcu_kernel_auto_opt/tests/test_w8a8_baselines.py b/metainfer/tasks/dcu_kernel_auto_opt/tests/test_w8a8_baselines.py index 0eb9d65a..b3a56910 100644 --- a/metainfer/tasks/dcu_kernel_auto_opt/tests/test_w8a8_baselines.py +++ b/metainfer/tasks/dcu_kernel_auto_opt/tests/test_w8a8_baselines.py @@ -86,6 +86,37 @@ def test_model_catalog_tp8_m4096_baselines(shape, expected): assert record["timing_scope"] == "prefill_graph_replay" +@pytest.mark.parametrize( + ("shape", "expected"), + [ + # Hy3 TP8 M=8 (measured 2026-09-20, CUDA-graph replay). + ({"tp_size": 8, "M": 8, "N": 1280, "K": 4096}, 66.432), + ({"tp_size": 8, "M": 8, "N": 4096, "K": 1024}, 31.472), + ({"tp_size": 8, "M": 8, "N": 384, "K": 4096}, 57.008), + ({"tp_size": 8, "M": 8, "N": 4096, "K": 192}, 14.592), + # MiniMax M3 TP8 M=8. + ({"tp_size": 8, "M": 8, "N": 1280, "K": 6144}, 98.865), + ({"tp_size": 8, "M": 8, "N": 1536, "K": 6144}, 99.473), + ({"tp_size": 8, "M": 8, "N": 6144, "K": 1024}, 32.960), + ({"tp_size": 8, "M": 8, "N": 768, "K": 6144}, 92.928), + ({"tp_size": 8, "M": 8, "N": 6144, "K": 384}, 19.984), + # GLM5.2 TP8 M=8. + ({"tp_size": 8, "M": 8, "N": 2624, "K": 6144}, 87.200), + ({"tp_size": 8, "M": 8, "N": 2048, "K": 2048}, 36.704), + ({"tp_size": 8, "M": 8, "N": 3584, "K": 512}, 15.808), + ({"tp_size": 8, "M": 8, "N": 6144, "K": 2048}, 58.864), + ({"tp_size": 8, "M": 8, "N": 512, "K": 6144}, 88.448), + ({"tp_size": 8, "M": 8, "N": 6144, "K": 256}, 15.136), + ], +) +def test_model_catalog_tp8_m8_baselines(shape, expected): + # M=8 is the model-catalog short-decode boundary added on 2026-09-20 and + # resolves as a decode baseline (M <= 16). + record = fixed_triton_graph_baseline("shape", shape) + assert record["median_us"] == expected + assert record["timing_scope"] == "decode_graph_replay" + + def test_bootstrap_metrics_are_kept_separate(): bootstrap = {"passed": True, "median_us": 123.0} record = fixed_triton_graph_baseline( diff --git a/metainfer/tasks/dcu_kernel_auto_opt/tools/__init__.py b/metainfer/tasks/dcu_kernel_auto_opt/tools/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/metainfer/tasks/dcu_kernel_auto_opt/tools/baseline/bench_triton_tp8_m8.py b/metainfer/tasks/dcu_kernel_auto_opt/tools/baseline/bench_triton_tp8_m8.py new file mode 100644 index 00000000..57de33b6 --- /dev/null +++ b/metainfer/tasks/dcu_kernel_auto_opt/tools/baseline/bench_triton_tp8_m8.py @@ -0,0 +1,238 @@ +#!/usr/bin/env python3 +"""Fair Triton W8A8 GEMM baselines for the model-catalog TP8 shapes at M=8. + +Same protocol as bench_triton_tp8_m4096.py / the 2026-08-12 catalog entries: +lmslim-style int8_utils.matmul_kernel into a preallocated bf16 output (output +allocation/clear excluded), GPU events, hot cache, CUDA-Graph replay, +warmups=10 / samples=20 / launches_per_sample=5. Reports median/P90/min in +microseconds. + +Triton config replicates matmul_int8's built-in default for M <= 32: + BLOCK_SIZE_M=16, BLOCK_SIZE_N=32, BLOCK_SIZE_K=256, + GROUP_SIZE_M=4, SPLIT_K=1, num_stages=0, num_warps=4 + +``--m`` defaults to 8 (the new decode boundary). Passing ``--m 16`` re-measures +the already-published M=16 catalog entries as a protocol cross-check. +""" + +from __future__ import annotations + +import argparse +import importlib.util +import json +import math +import statistics +import sys +from pathlib import Path + +import torch +import triton + + +UTILS_PATH = str(Path(__file__).resolve().parent / "int8_utils.py") + +# (shape_id stem, operator, K, N) -- Hy3 / MiniMax M3 / GLM5.2 TP8 catalog. +TP8_CASES = [ + ("hy3_tp8_qkv_proj", "qkv_proj", 4096, 1280), + ("hy3_tp8_o_proj", "o_proj", 1024, 4096), + ("hy3_tp8_shared_gate_up_proj", "shared_gate_up_proj", 4096, 384), + ("hy3_tp8_shared_down_proj", "shared_down_proj", 192, 4096), + ("minimax_tp8_qkv_proj", "qkv_proj", 6144, 1280), + ("minimax_tp8_qkv_proj_and_indexer_qk", "qkv_proj_and_indexer_qk", 6144, 1536), + ("minimax_tp8_o_proj", "o_proj", 1024, 6144), + ("minimax_tp8_shared_gate_up_proj", "shared_gate_up_proj", 6144, 768), + ("minimax_tp8_shared_down_proj", "shared_down_proj", 384, 6144), + ("glm52_tp8_fused_qkv_a_proj", "fused_qkv_a_proj", 6144, 2624), + ("glm52_tp8_q_b_proj", "q_b_proj", 2048, 2048), + ("glm52_tp8_kv_b_proj", "kv_b_proj", 512, 3584), + ("glm52_tp8_o_proj", "o_proj", 2048, 6144), + ("glm52_tp8_shared_gate_up_proj", "shared_gate_up_proj", 6144, 512), + ("glm52_tp8_shared_down_proj", "shared_down_proj", 256, 6144), +] + +# Exact default config used by matmul_int8 when M <= 32. +CONFIG = { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 4, + "SPLIT_K": 1, + "num_stages": 0, + "num_warps": 4, +} + + +def load_int8_utils(path: str): + spec = importlib.util.spec_from_file_location("baseline_int8_utils", path) + if spec is None or spec.loader is None: + raise RuntimeError(f"cannot load {path}") + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +def launch(out, a, a_scale, b, b_scale, m, n, k, utils) -> None: + grid = ( + triton.cdiv(m, CONFIG["BLOCK_SIZE_M"]) + * triton.cdiv(n, CONFIG["BLOCK_SIZE_N"]), + CONFIG["SPLIT_K"], + ) + utils.matmul_kernel[grid]( + a, + a_scale, + b, + b_scale, + out, + m, + n, + k, + a.stride(0), + a.stride(1), + a_scale.stride(0), + b.stride(0), + b.stride(1), + b_scale.stride(0), + out.stride(0), + out.stride(1), + **CONFIG, + ) + + +def percentile(values, fraction): + ordered = sorted(values) + return ordered[max(0, math.ceil(fraction * len(ordered)) - 1)] + + +def measure(fn, warmups, samples, launches_per_sample): + for _ in range(warmups): + fn() + torch.cuda.synchronize() + + start = torch.cuda.Event(enable_timing=True) + end = torch.cuda.Event(enable_timing=True) + times_us = [] + for _ in range(samples): + start.record() + for _ in range(launches_per_sample): + fn() + end.record() + end.synchronize() + times_us.append(start.elapsed_time(end) * 1000.0 / launches_per_sample) + return { + "median_us": statistics.median(times_us), + "p90_us": percentile(times_us, 0.90), + "min_us": min(times_us), + "max_us": max(times_us), + "samples_us": [round(v, 3) for v in times_us], + } + + +def run_case(utils, case_stem, operator, k, n, m, warmups, samples, + launches_per_sample, mode): + device = torch.device("cuda:0") + torch.manual_seed(20260724 + m + n + k) + a = torch.randint(-127, 128, (m, k), device=device, dtype=torch.int8) + b = torch.randint(-127, 128, (k, n), device=device, dtype=torch.int8) + a_scale = torch.rand((m, 1), device=device, dtype=torch.float32) + 0.01 + b_scale = torch.rand((n, 1), device=device, dtype=torch.float32) + 0.01 + out = torch.empty((m, n), device=device, dtype=torch.bfloat16) + + def fn(): + launch(out, a, a_scale, b, b_scale, m, n, k, utils) + + # One untimed JIT compile pass (correctness is the production Triton path). + fn() + torch.cuda.synchronize() + + result = { + "shape_id": f"{case_stem}_m{m}", + "operator": operator, + "M": m, + "N": n, + "K": k, + "config": CONFIG, + } + if mode in ("eager", "both"): + result["eager"] = measure(fn, warmups, samples, launches_per_sample) + if mode in ("graph", "both"): + fn() + torch.cuda.synchronize() + graph = torch.cuda.CUDAGraph() + with torch.cuda.graph(graph): + fn() + torch.cuda.synchronize() + result["graph"] = measure( + graph.replay, warmups, samples, launches_per_sample + ) + del graph + + del a, b, a_scale, b_scale, out + return result + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--utils", default=UTILS_PATH) + parser.add_argument("--m", type=int, default=8) + parser.add_argument("--warmups", type=int, default=10) + parser.add_argument("--samples", type=int, default=20) + parser.add_argument("--launches-per-sample", type=int, default=5) + parser.add_argument("--mode", choices=["eager", "graph", "both"], + default="graph") + parser.add_argument("--json", type=str, default="") + parser.add_argument("--only", type=str, default="", + help="comma-separated case-stem substrings to keep") + args = parser.parse_args() + m = args.m + + utils = load_int8_utils(args.utils) + cases = TP8_CASES + if args.only: + needles = [s.strip() for s in args.only.split(",") if s.strip()] + cases = [c for c in cases if any(x in c[0] for x in needles)] + + print( + "BENCHMARK triton=int8_utils.matmul_kernel output=preallocated " + f"quantization=excluded allocation=excluded epilogue=included " + f"M={m} config={json.dumps(CONFIG)} mode={args.mode}", + flush=True, + ) + print( + f"{'shape_id':44s} {'N':5s} {'K':5s} {'graph_us':>10s} " + f"{'p90_us':>10s} {'eager_us':>10s}", + flush=True, + ) + results = [] + for case_stem, operator, k, n in cases: + res = run_case( + utils, + case_stem, + operator, + k, + n, + m, + args.warmups, + args.samples, + args.launches_per_sample, + args.mode, + ) + results.append(res) + graph_us = (res.get("graph") or {}).get("median_us") + p90_us = (res.get("graph") or {}).get("p90_us") + eager_us = (res.get("eager") or {}).get("median_us") + print( + f"{res['shape_id']:44s} {n:5d} {k:5d} " + f"{(f'{graph_us:.3f}' if graph_us else '—'):>10s} " + f"{(f'{p90_us:.3f}' if p90_us else '—'):>10s} " + f"{(f'{eager_us:.3f}' if eager_us else '—'):>10s}", + flush=True, + ) + if args.json: + with open(args.json, "w", encoding="utf-8") as fh: + json.dump(results, fh, indent=2) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/metainfer/tasks/dcu_kernel_auto_opt/tools/baseline/tp8_m8_graph_merged.json b/metainfer/tasks/dcu_kernel_auto_opt/tools/baseline/tp8_m8_graph_merged.json new file mode 100644 index 00000000..e5104ff8 --- /dev/null +++ b/metainfer/tasks/dcu_kernel_auto_opt/tools/baseline/tp8_m8_graph_merged.json @@ -0,0 +1,197 @@ +[ + { + "shape_id": "hy3_tp8_qkv_proj_m8", + "M": 8, + "N": 1280, + "K": 4096, + "operator": "qkv_proj", + "pass_medians_us": [ + 66.432, + 66.272, + 67.216 + ], + "published_median_us": 66.432 + }, + { + "shape_id": "hy3_tp8_o_proj_m8", + "M": 8, + "N": 4096, + "K": 1024, + "operator": "o_proj", + "pass_medians_us": [ + 31.472, + 31.488, + 31.424 + ], + "published_median_us": 31.472 + }, + { + "shape_id": "hy3_tp8_shared_gate_up_proj_m8", + "M": 8, + "N": 384, + "K": 4096, + "operator": "shared_gate_up_proj", + "pass_medians_us": [ + 56.896, + 57.008, + 57.792 + ], + "published_median_us": 57.008 + }, + { + "shape_id": "hy3_tp8_shared_down_proj_m8", + "M": 8, + "N": 4096, + "K": 192, + "operator": "shared_down_proj", + "pass_medians_us": [ + 14.592, + 14.592, + 14.592 + ], + "published_median_us": 14.592 + }, + { + "shape_id": "minimax_tp8_qkv_proj_m8", + "M": 8, + "N": 1280, + "K": 6144, + "operator": "qkv_proj", + "pass_medians_us": [ + 98.401, + 98.865, + 99.36 + ], + "published_median_us": 98.865 + }, + { + "shape_id": "minimax_tp8_qkv_proj_and_indexer_qk_m8", + "M": 8, + "N": 1536, + "K": 6144, + "operator": "qkv_proj_and_indexer_qk", + "pass_medians_us": [ + 99.456, + 99.473, + 99.968 + ], + "published_median_us": 99.473 + }, + { + "shape_id": "minimax_tp8_o_proj_m8", + "M": 8, + "N": 6144, + "K": 1024, + "operator": "o_proj", + "pass_medians_us": [ + 32.96, + 33.008, + 32.768 + ], + "published_median_us": 32.96 + }, + { + "shape_id": "minimax_tp8_shared_gate_up_proj_m8", + "M": 8, + "N": 768, + "K": 6144, + "operator": "shared_gate_up_proj", + "pass_medians_us": [ + 92.544, + 93.936, + 92.928 + ], + "published_median_us": 92.928 + }, + { + "shape_id": "minimax_tp8_shared_down_proj_m8", + "M": 8, + "N": 6144, + "K": 384, + "operator": "shared_down_proj", + "pass_medians_us": [ + 19.968, + 33.76, + 19.984 + ], + "published_median_us": 19.984 + }, + { + "shape_id": "glm52_tp8_fused_qkv_a_proj_m8", + "M": 8, + "N": 2624, + "K": 6144, + "operator": "fused_qkv_a_proj", + "pass_medians_us": [ + 87.2, + 88.032, + 86.976 + ], + "published_median_us": 87.2 + }, + { + "shape_id": "glm52_tp8_q_b_proj_m8", + "M": 8, + "N": 2048, + "K": 2048, + "operator": "q_b_proj", + "pass_medians_us": [ + 37.92, + 36.704, + 35.792 + ], + "published_median_us": 36.704 + }, + { + "shape_id": "glm52_tp8_kv_b_proj_m8", + "M": 8, + "N": 3584, + "K": 512, + "operator": "kv_b_proj", + "pass_medians_us": [ + 16.0, + 15.808, + 15.744 + ], + "published_median_us": 15.808 + }, + { + "shape_id": "glm52_tp8_o_proj_m8", + "M": 8, + "N": 6144, + "K": 2048, + "operator": "o_proj", + "pass_medians_us": [ + 58.864, + 58.864, + 58.064 + ], + "published_median_us": 58.864 + }, + { + "shape_id": "glm52_tp8_shared_gate_up_proj_m8", + "M": 8, + "N": 512, + "K": 6144, + "operator": "shared_gate_up_proj", + "pass_medians_us": [ + 89.216, + 88.336, + 88.448 + ], + "published_median_us": 88.448 + }, + { + "shape_id": "glm52_tp8_shared_down_proj_m8", + "M": 8, + "N": 6144, + "K": 256, + "operator": "shared_down_proj", + "pass_medians_us": [ + 16.096, + 15.136, + 15.072 + ], + "published_median_us": 15.136 + } +] \ No newline at end of file diff --git a/metainfer/tasks/dcu_kernel_auto_opt/tools/baseline/tp8_m8_graph_run1.json b/metainfer/tasks/dcu_kernel_auto_opt/tools/baseline/tp8_m8_graph_run1.json new file mode 100644 index 00000000..a5ec1aa7 --- /dev/null +++ b/metainfer/tasks/dcu_kernel_auto_opt/tools/baseline/tp8_m8_graph_run1.json @@ -0,0 +1,662 @@ +[ + { + "shape_id": "hy3_tp8_qkv_proj_m8", + "operator": "qkv_proj", + "M": 8, + "N": 1280, + "K": 4096, + "config": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 4, + "SPLIT_K": 1, + "num_stages": 0, + "num_warps": 4 + }, + "graph": { + "median_us": 66.4322018623352, + "p90_us": 67.2324001789093, + "min_us": 66.20839834213257, + "max_us": 67.4884021282196, + "samples_us": [ + 67.488, + 67.232, + 67.2, + 66.56, + 66.432, + 66.368, + 66.432, + 66.304, + 66.368, + 67.456, + 66.304, + 66.208, + 66.464, + 66.272, + 66.272, + 66.432, + 66.336, + 66.368, + 66.464, + 67.232 + ] + } + }, + { + "shape_id": "hy3_tp8_o_proj_m8", + "operator": "o_proj", + "M": 8, + "N": 4096, + "K": 1024, + "config": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 4, + "SPLIT_K": 1, + "num_stages": 0, + "num_warps": 4 + }, + "graph": { + "median_us": 31.47220015525818, + "p90_us": 31.584200263023376, + "min_us": 31.296199560165405, + "max_us": 32.41620063781738, + "samples_us": [ + 32.416, + 31.36, + 31.52, + 31.552, + 31.456, + 31.584, + 31.616, + 31.392, + 31.456, + 31.52, + 31.392, + 31.36, + 31.552, + 31.296, + 31.488, + 31.584, + 31.424, + 31.584, + 31.392, + 31.328 + ] + } + }, + { + "shape_id": "hy3_tp8_shared_gate_up_proj_m8", + "operator": "shared_gate_up_proj", + "M": 8, + "N": 384, + "K": 4096, + "config": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 4, + "SPLIT_K": 1, + "num_stages": 0, + "num_warps": 4 + }, + "graph": { + "median_us": 56.89629912376404, + "p90_us": 57.69619941711426, + "min_us": 56.64039850234985, + "max_us": 57.95239806175232, + "samples_us": [ + 56.672, + 57.696, + 56.864, + 56.96, + 56.8, + 57.184, + 56.704, + 56.928, + 57.728, + 57.056, + 56.8, + 56.992, + 56.704, + 57.12, + 56.64, + 56.96, + 56.704, + 56.864, + 56.768, + 57.952 + ] + } + }, + { + "shape_id": "hy3_tp8_shared_down_proj_m8", + "operator": "shared_down_proj", + "M": 8, + "N": 4096, + "K": 192, + "config": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 4, + "SPLIT_K": 1, + "num_stages": 0, + "num_warps": 4 + }, + "graph": { + "median_us": 14.592200517654419, + "p90_us": 14.688000082969666, + "min_us": 14.528200030326843, + "max_us": 17.952199280261993, + "samples_us": [ + 14.656, + 14.528, + 14.624, + 14.592, + 14.592, + 14.56, + 14.624, + 14.592, + 14.624, + 14.688, + 14.528, + 14.592, + 14.656, + 14.528, + 14.56, + 14.592, + 14.592, + 14.688, + 17.952, + 14.624 + ] + } + }, + { + "shape_id": "minimax_tp8_qkv_proj_m8", + "operator": "qkv_proj", + "M": 8, + "N": 1280, + "K": 6144, + "config": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 4, + "SPLIT_K": 1, + "num_stages": 0, + "num_warps": 4 + }, + "graph": { + "median_us": 98.40050041675568, + "p90_us": 98.68860244750977, + "min_us": 98.27240109443665, + "max_us": 99.52059984207153, + "samples_us": [ + 98.497, + 98.336, + 99.361, + 98.337, + 98.337, + 98.401, + 98.401, + 98.337, + 98.465, + 98.272, + 98.432, + 98.689, + 98.273, + 98.273, + 98.561, + 98.529, + 98.305, + 99.521, + 98.336, + 98.4 + ] + } + }, + { + "shape_id": "minimax_tp8_qkv_proj_and_indexer_qk_m8", + "operator": "qkv_proj_and_indexer_qk", + "M": 8, + "N": 1536, + "K": 6144, + "config": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 4, + "SPLIT_K": 1, + "num_stages": 0, + "num_warps": 4 + }, + "graph": { + "median_us": 99.45639967918396, + "p90_us": 100.32060146331787, + "min_us": 99.2326021194458, + "max_us": 100.41660070419312, + "samples_us": [ + 100.321, + 100.353, + 99.233, + 99.265, + 99.553, + 99.233, + 99.456, + 99.328, + 99.617, + 99.233, + 99.361, + 99.361, + 99.361, + 100.224, + 99.456, + 100.417, + 99.553, + 99.457, + 99.457, + 99.297 + ] + } + }, + { + "shape_id": "minimax_tp8_o_proj_m8", + "operator": "o_proj", + "M": 8, + "N": 6144, + "K": 1024, + "config": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 4, + "SPLIT_K": 1, + "num_stages": 0, + "num_warps": 4 + }, + "graph": { + "median_us": 32.96020030975342, + "p90_us": 33.024200797080994, + "min_us": 32.864201068878174, + "max_us": 33.05619955062866, + "samples_us": [ + 32.96, + 32.992, + 32.96, + 33.056, + 32.96, + 33.056, + 32.96, + 32.864, + 32.96, + 32.992, + 32.864, + 32.928, + 33.024, + 32.992, + 32.96, + 33.024, + 32.992, + 32.96, + 32.96, + 32.928 + ] + } + }, + { + "shape_id": "minimax_tp8_shared_gate_up_proj_m8", + "operator": "shared_gate_up_proj", + "M": 8, + "N": 768, + "K": 6144, + "config": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 4, + "SPLIT_K": 1, + "num_stages": 0, + "num_warps": 4 + }, + "graph": { + "median_us": 92.54440069198608, + "p90_us": 93.34459900856018, + "min_us": 92.06439852714539, + "max_us": 93.4723973274231, + "samples_us": [ + 93.345, + 92.545, + 92.129, + 92.577, + 92.385, + 92.481, + 92.193, + 92.417, + 92.449, + 93.377, + 92.288, + 92.544, + 93.152, + 92.576, + 92.064, + 93.472, + 92.256, + 92.544, + 92.896, + 92.609 + ] + } + }, + { + "shape_id": "minimax_tp8_shared_down_proj_m8", + "operator": "shared_down_proj", + "M": 8, + "N": 6144, + "K": 384, + "config": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 4, + "SPLIT_K": 1, + "num_stages": 0, + "num_warps": 4 + }, + "graph": { + "median_us": 19.968199729919434, + "p90_us": 20.03200054168701, + "min_us": 19.904200732707977, + "max_us": 20.064200460910797, + "samples_us": [ + 19.904, + 20.032, + 20.0, + 19.968, + 19.968, + 19.968, + 19.968, + 19.968, + 20.0, + 20.0, + 20.064, + 19.968, + 19.968, + 19.904, + 19.968, + 19.936, + 20.0, + 19.968, + 20.064, + 20.0 + ] + } + }, + { + "shape_id": "glm52_tp8_fused_qkv_a_proj_m8", + "operator": "fused_qkv_a_proj", + "M": 8, + "N": 2624, + "K": 6144, + "config": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 4, + "SPLIT_K": 1, + "num_stages": 0, + "num_warps": 4 + }, + "graph": { + "median_us": 87.20049858093262, + "p90_us": 89.47259783744812, + "min_us": 86.17640137672424, + "max_us": 97.60059714317322, + "samples_us": [ + 87.329, + 87.265, + 86.208, + 86.24, + 94.241, + 87.136, + 86.177, + 86.241, + 86.336, + 86.176, + 86.369, + 86.177, + 87.136, + 97.601, + 89.473, + 89.408, + 89.152, + 89.248, + 89.152, + 89.313 + ] + } + }, + { + "shape_id": "glm52_tp8_q_b_proj_m8", + "operator": "q_b_proj", + "M": 8, + "N": 2048, + "K": 2048, + "config": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 4, + "SPLIT_K": 1, + "num_stages": 0, + "num_warps": 4 + }, + "graph": { + "median_us": 37.92020082473755, + "p90_us": 38.24020028114319, + "min_us": 36.80019974708557, + "max_us": 38.624200224876404, + "samples_us": [ + 38.624, + 38.336, + 37.888, + 37.824, + 38.048, + 37.92, + 37.856, + 37.856, + 37.92, + 37.952, + 37.792, + 37.984, + 37.888, + 38.08, + 38.24, + 37.92, + 38.112, + 38.08, + 37.92, + 36.8 + ] + } + }, + { + "shape_id": "glm52_tp8_kv_b_proj_m8", + "operator": "kv_b_proj", + "M": 8, + "N": 3584, + "K": 512, + "config": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 4, + "SPLIT_K": 1, + "num_stages": 0, + "num_warps": 4 + }, + "graph": { + "median_us": 16.00010022521019, + "p90_us": 24.86419975757599, + "min_us": 15.71200042963028, + "max_us": 26.464200019836426, + "samples_us": [ + 26.464, + 23.808, + 24.448, + 23.84, + 24.864, + 24.896, + 24.832, + 17.024, + 15.776, + 15.744, + 15.84, + 15.872, + 16.032, + 15.872, + 15.808, + 15.872, + 15.968, + 15.936, + 15.712, + 16.736 + ] + } + }, + { + "shape_id": "glm52_tp8_o_proj_m8", + "operator": "o_proj", + "M": 8, + "N": 6144, + "K": 2048, + "config": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 4, + "SPLIT_K": 1, + "num_stages": 0, + "num_warps": 4 + }, + "graph": { + "median_us": 58.864399790763855, + "p90_us": 59.29639935493469, + "min_us": 57.79240131378174, + "max_us": 59.52039957046509, + "samples_us": [ + 59.136, + 59.104, + 58.88, + 59.2, + 59.52, + 58.432, + 58.848, + 58.432, + 59.136, + 58.848, + 59.296, + 57.792, + 59.04, + 57.984, + 58.848, + 59.456, + 58.016, + 58.304, + 58.912, + 57.92 + ] + } + }, + { + "shape_id": "glm52_tp8_shared_gate_up_proj_m8", + "operator": "shared_gate_up_proj", + "M": 8, + "N": 512, + "K": 6144, + "config": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 4, + "SPLIT_K": 1, + "num_stages": 0, + "num_warps": 4 + }, + "graph": { + "median_us": 89.21639919281006, + "p90_us": 89.60059881210327, + "min_us": 88.22460174560547, + "max_us": 89.60059881210327, + "samples_us": [ + 89.249, + 89.601, + 88.225, + 89.28, + 88.512, + 89.152, + 89.345, + 88.609, + 88.225, + 89.536, + 88.576, + 89.12, + 89.313, + 89.601, + 89.184, + 89.472, + 88.608, + 89.249, + 89.057, + 89.601 + ] + } + }, + { + "shape_id": "glm52_tp8_shared_down_proj_m8", + "operator": "shared_down_proj", + "M": 8, + "N": 6144, + "K": 256, + "config": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 4, + "SPLIT_K": 1, + "num_stages": 0, + "num_warps": 4 + }, + "graph": { + "median_us": 16.096200048923492, + "p90_us": 16.223999857902527, + "min_us": 15.072199702262878, + "max_us": 18.592000007629395, + "samples_us": [ + 16.096, + 16.096, + 16.096, + 15.104, + 16.096, + 16.192, + 16.096, + 16.064, + 16.128, + 15.072, + 16.224, + 15.2, + 16.192, + 18.592, + 16.128, + 16.064, + 15.136, + 16.224, + 16.192, + 16.224 + ] + } + } +] \ No newline at end of file diff --git a/metainfer/tasks/dcu_kernel_auto_opt/tools/baseline/tp8_m8_graph_run2.json b/metainfer/tasks/dcu_kernel_auto_opt/tools/baseline/tp8_m8_graph_run2.json new file mode 100644 index 00000000..2970f303 --- /dev/null +++ b/metainfer/tasks/dcu_kernel_auto_opt/tools/baseline/tp8_m8_graph_run2.json @@ -0,0 +1,662 @@ +[ + { + "shape_id": "hy3_tp8_qkv_proj_m8", + "operator": "qkv_proj", + "M": 8, + "N": 1280, + "K": 4096, + "config": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 4, + "SPLIT_K": 1, + "num_stages": 0, + "num_warps": 4 + }, + "graph": { + "median_us": 66.27230048179626, + "p90_us": 67.26440191268921, + "min_us": 66.11220240592957, + "max_us": 74.72040057182312, + "samples_us": [ + 67.264, + 66.24, + 66.272, + 66.304, + 66.24, + 74.72, + 66.336, + 67.328, + 66.112, + 66.368, + 66.208, + 66.24, + 66.208, + 66.112, + 66.304, + 67.168, + 66.112, + 66.304, + 66.272, + 66.24 + ] + } + }, + { + "shape_id": "hy3_tp8_o_proj_m8", + "operator": "o_proj", + "M": 8, + "N": 4096, + "K": 1024, + "config": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 4, + "SPLIT_K": 1, + "num_stages": 0, + "num_warps": 4 + }, + "graph": { + "median_us": 31.488101184368134, + "p90_us": 31.58400058746338, + "min_us": 31.391999125480652, + "max_us": 32.41620063781738, + "samples_us": [ + 32.416, + 31.456, + 31.424, + 31.488, + 31.584, + 31.392, + 31.552, + 31.424, + 31.488, + 31.424, + 31.616, + 31.52, + 31.552, + 31.424, + 31.488, + 31.52, + 31.552, + 31.456, + 31.456, + 31.456 + ] + } + }, + { + "shape_id": "hy3_tp8_shared_gate_up_proj_m8", + "operator": "shared_gate_up_proj", + "M": 8, + "N": 384, + "K": 4096, + "config": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 4, + "SPLIT_K": 1, + "num_stages": 0, + "num_warps": 4 + }, + "graph": { + "median_us": 57.00819790363312, + "p90_us": 57.856398820877075, + "min_us": 56.83220028877258, + "max_us": 57.98439979553223, + "samples_us": [ + 57.984, + 57.856, + 56.832, + 57.152, + 57.024, + 57.92, + 56.992, + 56.896, + 56.928, + 56.992, + 56.832, + 57.088, + 56.96, + 57.088, + 56.928, + 57.12, + 56.96, + 57.056, + 56.896, + 57.088 + ] + } + }, + { + "shape_id": "hy3_tp8_shared_down_proj_m8", + "operator": "shared_down_proj", + "M": 8, + "N": 4096, + "K": 192, + "config": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 4, + "SPLIT_K": 1, + "num_stages": 0, + "num_warps": 4 + }, + "graph": { + "median_us": 14.591999351978302, + "p90_us": 14.655999839305878, + "min_us": 14.496199786663055, + "max_us": 15.96820056438446, + "samples_us": [ + 14.624, + 14.56, + 14.592, + 14.592, + 14.56, + 14.592, + 14.56, + 15.968, + 14.56, + 14.496, + 14.592, + 14.56, + 14.592, + 14.592, + 14.56, + 14.656, + 14.592, + 14.592, + 14.592, + 14.656 + ] + } + }, + { + "shape_id": "minimax_tp8_qkv_proj_m8", + "operator": "qkv_proj", + "M": 8, + "N": 1280, + "K": 6144, + "config": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 4, + "SPLIT_K": 1, + "num_stages": 0, + "num_warps": 4 + }, + "graph": { + "median_us": 98.8646000623703, + "p90_us": 99.64860081672668, + "min_us": 98.56060147285461, + "max_us": 100.03260374069214, + "samples_us": [ + 99.617, + 99.745, + 98.561, + 99.649, + 98.913, + 98.656, + 98.624, + 98.976, + 98.72, + 98.593, + 100.033, + 98.625, + 98.721, + 98.817, + 99.52, + 98.72, + 98.752, + 99.552, + 99.616, + 99.041 + ] + } + }, + { + "shape_id": "minimax_tp8_qkv_proj_and_indexer_qk_m8", + "operator": "qkv_proj_and_indexer_qk", + "M": 8, + "N": 1536, + "K": 6144, + "config": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 4, + "SPLIT_K": 1, + "num_stages": 0, + "num_warps": 4 + }, + "graph": { + "median_us": 99.47250187397003, + "p90_us": 100.35239458084106, + "min_us": 99.29659962654114, + "max_us": 100.41639804840088, + "samples_us": [ + 100.352, + 100.416, + 99.393, + 99.521, + 100.321, + 99.393, + 99.457, + 99.297, + 100.416, + 99.328, + 99.648, + 99.328, + 99.488, + 99.361, + 99.553, + 99.489, + 99.457, + 99.329, + 99.361, + 99.52 + ] + } + }, + { + "shape_id": "minimax_tp8_o_proj_m8", + "operator": "o_proj", + "M": 8, + "N": 6144, + "K": 1024, + "config": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 4, + "SPLIT_K": 1, + "num_stages": 0, + "num_warps": 4 + }, + "graph": { + "median_us": 33.00819993019104, + "p90_us": 33.05619955062866, + "min_us": 32.92819857597351, + "max_us": 34.04819965362549, + "samples_us": [ + 34.048, + 32.992, + 32.928, + 33.024, + 33.024, + 33.056, + 32.928, + 33.056, + 32.992, + 32.992, + 32.992, + 32.96, + 32.992, + 32.992, + 33.056, + 32.992, + 33.024, + 33.088, + 33.024, + 33.024 + ] + } + }, + { + "shape_id": "minimax_tp8_shared_gate_up_proj_m8", + "operator": "shared_gate_up_proj", + "M": 8, + "N": 768, + "K": 6144, + "config": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 4, + "SPLIT_K": 1, + "num_stages": 0, + "num_warps": 4 + }, + "graph": { + "median_us": 93.93649995326996, + "p90_us": 95.87259888648987, + "min_us": 92.83260107040405, + "max_us": 99.07259941101074, + "samples_us": [ + 93.792, + 93.952, + 93.024, + 92.833, + 93.921, + 93.888, + 93.024, + 92.833, + 93.025, + 93.024, + 93.024, + 99.073, + 95.873, + 95.585, + 96.001, + 95.873, + 95.648, + 95.776, + 95.84, + 95.744 + ] + } + }, + { + "shape_id": "minimax_tp8_shared_down_proj_m8", + "operator": "shared_down_proj", + "M": 8, + "N": 6144, + "K": 384, + "config": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 4, + "SPLIT_K": 1, + "num_stages": 0, + "num_warps": 4 + }, + "graph": { + "median_us": 33.76020044088364, + "p90_us": 37.216201424598694, + "min_us": 21.312199532985687, + "max_us": 39.61620032787323, + "samples_us": [ + 39.04, + 34.944, + 33.824, + 34.112, + 33.792, + 33.6, + 37.216, + 34.016, + 32.992, + 33.248, + 33.088, + 33.728, + 39.616, + 32.96, + 35.424, + 33.216, + 33.792, + 22.08, + 21.6, + 21.312 + ] + } + }, + { + "shape_id": "glm52_tp8_fused_qkv_a_proj_m8", + "operator": "fused_qkv_a_proj", + "M": 8, + "N": 2624, + "K": 6144, + "config": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 4, + "SPLIT_K": 1, + "num_stages": 0, + "num_warps": 4 + }, + "graph": { + "median_us": 88.03249895572662, + "p90_us": 88.64039778709412, + "min_us": 87.07240223884583, + "max_us": 89.47259783744812, + "samples_us": [ + 89.473, + 87.841, + 88.32, + 88.064, + 88.096, + 88.64, + 87.52, + 87.84, + 88.225, + 88.832, + 87.36, + 87.808, + 88.032, + 88.256, + 87.232, + 88.384, + 88.033, + 88.0, + 87.648, + 87.072 + ] + } + }, + { + "shape_id": "glm52_tp8_q_b_proj_m8", + "operator": "q_b_proj", + "M": 8, + "N": 2048, + "K": 2048, + "config": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 4, + "SPLIT_K": 1, + "num_stages": 0, + "num_warps": 4 + }, + "graph": { + "median_us": 36.70420050621033, + "p90_us": 37.88819909095764, + "min_us": 35.84020137786865, + "max_us": 37.95219957828522, + "samples_us": [ + 37.568, + 37.888, + 35.904, + 36.576, + 37.952, + 35.84, + 36.608, + 37.888, + 35.968, + 36.576, + 37.952, + 35.968, + 36.768, + 37.088, + 35.904, + 36.64, + 37.056, + 36.032, + 37.344, + 37.056 + ] + } + }, + { + "shape_id": "glm52_tp8_kv_b_proj_m8", + "operator": "kv_b_proj", + "M": 8, + "N": 3584, + "K": 512, + "config": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 4, + "SPLIT_K": 1, + "num_stages": 0, + "num_warps": 4 + }, + "graph": { + "median_us": 15.808099508285522, + "p90_us": 17.728200554847717, + "min_us": 15.71200042963028, + "max_us": 17.85600036382675, + "samples_us": [ + 16.48, + 17.728, + 16.96, + 15.808, + 15.712, + 16.448, + 17.728, + 17.856, + 15.776, + 15.744, + 16.576, + 17.728, + 16.64, + 15.744, + 15.744, + 15.744, + 15.712, + 15.712, + 15.808, + 15.808 + ] + } + }, + { + "shape_id": "glm52_tp8_o_proj_m8", + "operator": "o_proj", + "M": 8, + "N": 6144, + "K": 2048, + "config": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 4, + "SPLIT_K": 1, + "num_stages": 0, + "num_warps": 4 + }, + "graph": { + "median_us": 58.86429846286774, + "p90_us": 59.456199407577515, + "min_us": 57.888197898864746, + "max_us": 64.06440138816833, + "samples_us": [ + 59.232, + 58.592, + 58.752, + 58.592, + 58.464, + 58.464, + 58.432, + 57.888, + 58.144, + 58.144, + 58.016, + 64.064, + 59.456, + 59.2, + 59.392, + 59.264, + 58.976, + 59.136, + 59.456, + 59.04 + ] + } + }, + { + "shape_id": "glm52_tp8_shared_gate_up_proj_m8", + "operator": "shared_gate_up_proj", + "M": 8, + "N": 512, + "K": 6144, + "config": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 4, + "SPLIT_K": 1, + "num_stages": 0, + "num_warps": 4 + }, + "graph": { + "median_us": 88.33639919757843, + "p90_us": 303.5536050796509, + "min_us": 87.90439963340759, + "max_us": 472.0344066619873, + "samples_us": [ + 89.088, + 88.256, + 88.448, + 88.16, + 88.128, + 88.48, + 88.129, + 88.128, + 88.352, + 88.128, + 88.128, + 88.416, + 87.904, + 88.289, + 88.32, + 162.305, + 303.554, + 450.85, + 295.938, + 472.034 + ] + } + }, + { + "shape_id": "glm52_tp8_shared_down_proj_m8", + "operator": "shared_down_proj", + "M": 8, + "N": 6144, + "K": 256, + "config": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 4, + "SPLIT_K": 1, + "num_stages": 0, + "num_warps": 4 + }, + "graph": { + "median_us": 15.136100351810455, + "p90_us": 15.168200433254242, + "min_us": 15.07200002670288, + "max_us": 16.128000617027283, + "samples_us": [ + 16.0, + 15.136, + 15.104, + 15.136, + 15.072, + 15.104, + 15.168, + 15.136, + 15.136, + 15.104, + 15.136, + 15.136, + 15.136, + 15.104, + 16.128, + 15.168, + 15.168, + 15.168, + 15.104, + 15.168 + ] + } + } +] \ No newline at end of file diff --git a/metainfer/tasks/dcu_kernel_auto_opt/tools/baseline/tp8_m8_graph_run3.json b/metainfer/tasks/dcu_kernel_auto_opt/tools/baseline/tp8_m8_graph_run3.json new file mode 100644 index 00000000..c7586f86 --- /dev/null +++ b/metainfer/tasks/dcu_kernel_auto_opt/tools/baseline/tp8_m8_graph_run3.json @@ -0,0 +1,662 @@ +[ + { + "shape_id": "hy3_tp8_qkv_proj_m8", + "operator": "qkv_proj", + "M": 8, + "N": 1280, + "K": 4096, + "config": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 4, + "SPLIT_K": 1, + "num_stages": 0, + "num_warps": 4 + }, + "graph": { + "median_us": 67.21619963645935, + "p90_us": 67.36019849777222, + "min_us": 66.27219915390015, + "max_us": 67.42439866065979, + "samples_us": [ + 67.072, + 67.104, + 67.072, + 67.232, + 67.36, + 67.168, + 67.264, + 67.232, + 67.424, + 67.04, + 67.296, + 66.272, + 67.136, + 67.232, + 67.168, + 67.392, + 66.272, + 67.264, + 67.2, + 67.296 + ] + } + }, + { + "shape_id": "hy3_tp8_o_proj_m8", + "operator": "o_proj", + "M": 8, + "N": 4096, + "K": 1024, + "config": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 4, + "SPLIT_K": 1, + "num_stages": 0, + "num_warps": 4 + }, + "graph": { + "median_us": 31.424200534820557, + "p90_us": 31.488201022148132, + "min_us": 31.327998638153076, + "max_us": 32.28819966316223, + "samples_us": [ + 32.288, + 31.424, + 31.488, + 31.424, + 31.584, + 31.424, + 31.424, + 31.488, + 31.392, + 31.36, + 31.456, + 31.424, + 31.456, + 31.424, + 31.392, + 31.328, + 31.392, + 31.456, + 31.328, + 31.424 + ] + } + }, + { + "shape_id": "hy3_tp8_shared_gate_up_proj_m8", + "operator": "shared_gate_up_proj", + "M": 8, + "N": 384, + "K": 4096, + "config": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 4, + "SPLIT_K": 1, + "num_stages": 0, + "num_warps": 4 + }, + "graph": { + "median_us": 57.79229998588562, + "p90_us": 57.95220136642456, + "min_us": 56.83220028877258, + "max_us": 57.95239806175232, + "samples_us": [ + 57.792, + 57.856, + 56.832, + 56.928, + 57.792, + 57.888, + 57.792, + 57.952, + 57.824, + 57.952, + 57.632, + 57.888, + 57.728, + 57.792, + 57.76, + 57.888, + 57.728, + 57.952, + 57.792, + 56.96 + ] + } + }, + { + "shape_id": "hy3_tp8_shared_down_proj_m8", + "operator": "shared_down_proj", + "M": 8, + "N": 4096, + "K": 192, + "config": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 4, + "SPLIT_K": 1, + "num_stages": 0, + "num_warps": 4 + }, + "graph": { + "median_us": 14.591999351978302, + "p90_us": 14.62399959564209, + "min_us": 14.496000111103058, + "max_us": 14.624199271202087, + "samples_us": [ + 14.56, + 14.528, + 14.624, + 14.496, + 14.56, + 14.592, + 14.56, + 14.592, + 14.592, + 14.56, + 14.592, + 14.528, + 14.528, + 14.592, + 14.624, + 14.592, + 14.624, + 14.624, + 14.56, + 14.592 + ] + } + }, + { + "shape_id": "minimax_tp8_qkv_proj_m8", + "operator": "qkv_proj", + "M": 8, + "N": 1280, + "K": 6144, + "config": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 4, + "SPLIT_K": 1, + "num_stages": 0, + "num_warps": 4 + }, + "graph": { + "median_us": 99.36040043830872, + "p90_us": 99.5523989200592, + "min_us": 98.43239784240723, + "max_us": 99.61640238761902, + "samples_us": [ + 99.392, + 99.552, + 99.52, + 99.424, + 98.561, + 99.488, + 98.432, + 99.489, + 98.592, + 99.425, + 98.784, + 99.552, + 99.297, + 98.688, + 98.528, + 99.328, + 99.616, + 99.457, + 99.232, + 98.56 + ] + } + }, + { + "shape_id": "minimax_tp8_qkv_proj_and_indexer_qk_m8", + "operator": "qkv_proj_and_indexer_qk", + "M": 8, + "N": 1536, + "K": 6144, + "config": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 4, + "SPLIT_K": 1, + "num_stages": 0, + "num_warps": 4 + }, + "graph": { + "median_us": 99.9684989452362, + "p90_us": 100.28860569000244, + "min_us": 99.16840195655823, + "max_us": 100.67239999771118, + "samples_us": [ + 99.872, + 100.385, + 99.456, + 100.224, + 99.553, + 100.672, + 100.289, + 100.16, + 100.256, + 99.425, + 100.192, + 99.328, + 100.065, + 99.328, + 100.192, + 99.52, + 100.256, + 99.489, + 99.168, + 99.328 + ] + } + }, + { + "shape_id": "minimax_tp8_o_proj_m8", + "operator": "o_proj", + "M": 8, + "N": 6144, + "K": 1024, + "config": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 4, + "SPLIT_K": 1, + "num_stages": 0, + "num_warps": 4 + }, + "graph": { + "median_us": 32.76819884777069, + "p90_us": 32.96020030975342, + "min_us": 32.70399868488312, + "max_us": 33.82419943809509, + "samples_us": [ + 33.824, + 32.864, + 32.896, + 32.704, + 32.768, + 32.896, + 32.736, + 32.8, + 32.832, + 32.736, + 32.704, + 33.024, + 32.736, + 32.736, + 32.896, + 32.736, + 32.736, + 32.768, + 32.96, + 32.736 + ] + } + }, + { + "shape_id": "minimax_tp8_shared_gate_up_proj_m8", + "operator": "shared_gate_up_proj", + "M": 8, + "N": 768, + "K": 6144, + "config": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 4, + "SPLIT_K": 1, + "num_stages": 0, + "num_warps": 4 + }, + "graph": { + "median_us": 92.92849898338318, + "p90_us": 93.15239787101746, + "min_us": 92.00040102005005, + "max_us": 93.34459900856018, + "samples_us": [ + 93.216, + 92.992, + 92.192, + 93.12, + 92.225, + 92.16, + 92.897, + 92.224, + 93.345, + 93.056, + 92.0, + 93.088, + 93.152, + 92.96, + 92.128, + 93.024, + 92.224, + 92.192, + 93.056, + 92.192 + ] + } + }, + { + "shape_id": "minimax_tp8_shared_down_proj_m8", + "operator": "shared_down_proj", + "M": 8, + "N": 6144, + "K": 384, + "config": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 4, + "SPLIT_K": 1, + "num_stages": 0, + "num_warps": 4 + }, + "graph": { + "median_us": 19.98410001397133, + "p90_us": 20.03220021724701, + "min_us": 19.904200732707977, + "max_us": 20.064200460910797, + "samples_us": [ + 20.0, + 19.968, + 19.968, + 19.936, + 20.032, + 19.968, + 19.904, + 20.032, + 20.064, + 19.968, + 20.0, + 19.968, + 20.032, + 19.968, + 20.032, + 20.032, + 20.064, + 19.936, + 19.968, + 20.032 + ] + } + }, + { + "shape_id": "glm52_tp8_fused_qkv_a_proj_m8", + "operator": "fused_qkv_a_proj", + "M": 8, + "N": 2624, + "K": 6144, + "config": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 4, + "SPLIT_K": 1, + "num_stages": 0, + "num_warps": 4 + }, + "graph": { + "median_us": 86.9763970375061, + "p90_us": 87.13639974594116, + "min_us": 86.01620197296143, + "max_us": 87.13639974594116, + "samples_us": [ + 87.04, + 86.944, + 86.144, + 87.136, + 86.176, + 87.136, + 87.04, + 86.912, + 86.976, + 87.04, + 86.912, + 87.072, + 86.016, + 87.136, + 87.008, + 86.24, + 86.976, + 87.136, + 86.112, + 86.976 + ] + } + }, + { + "shape_id": "glm52_tp8_q_b_proj_m8", + "operator": "q_b_proj", + "M": 8, + "N": 2048, + "K": 2048, + "config": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 4, + "SPLIT_K": 1, + "num_stages": 0, + "num_warps": 4 + }, + "graph": { + "median_us": 35.79210042953491, + "p90_us": 35.90399920940399, + "min_us": 35.74399948120117, + "max_us": 36.7682009935379, + "samples_us": [ + 36.768, + 35.808, + 35.808, + 35.808, + 35.776, + 35.744, + 35.776, + 35.808, + 35.808, + 35.904, + 35.744, + 35.872, + 36.704, + 35.776, + 35.776, + 35.744, + 35.776, + 35.744, + 35.84, + 35.776 + ] + } + }, + { + "shape_id": "glm52_tp8_kv_b_proj_m8", + "operator": "kv_b_proj", + "M": 8, + "N": 3584, + "K": 512, + "config": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 4, + "SPLIT_K": 1, + "num_stages": 0, + "num_warps": 4 + }, + "graph": { + "median_us": 15.744000673294067, + "p90_us": 15.776200592517853, + "min_us": 15.680000185966492, + "max_us": 21.376200020313263, + "samples_us": [ + 15.712, + 15.744, + 15.744, + 15.744, + 15.68, + 21.376, + 15.712, + 15.744, + 15.744, + 15.712, + 15.776, + 15.776, + 15.68, + 15.744, + 15.808, + 15.68, + 15.712, + 15.712, + 15.744, + 15.776 + ] + } + }, + { + "shape_id": "glm52_tp8_o_proj_m8", + "operator": "o_proj", + "M": 8, + "N": 6144, + "K": 2048, + "config": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 4, + "SPLIT_K": 1, + "num_stages": 0, + "num_warps": 4 + }, + "graph": { + "median_us": 58.06429982185364, + "p90_us": 58.56040120124817, + "min_us": 57.53620266914368, + "max_us": 59.296202659606934, + "samples_us": [ + 58.56, + 59.296, + 57.824, + 58.112, + 58.208, + 57.6, + 57.792, + 58.528, + 57.76, + 57.536, + 58.208, + 58.24, + 58.848, + 57.888, + 58.24, + 57.696, + 58.208, + 58.016, + 57.856, + 57.824 + ] + } + }, + { + "shape_id": "glm52_tp8_shared_gate_up_proj_m8", + "operator": "shared_gate_up_proj", + "M": 8, + "N": 512, + "K": 6144, + "config": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 4, + "SPLIT_K": 1, + "num_stages": 0, + "num_warps": 4 + }, + "graph": { + "median_us": 88.44839930534363, + "p90_us": 88.70440125465393, + "min_us": 87.52040266990662, + "max_us": 88.73639702796936, + "samples_us": [ + 88.736, + 88.448, + 88.608, + 88.64, + 88.384, + 88.416, + 87.712, + 87.744, + 87.584, + 88.64, + 87.52, + 88.224, + 87.68, + 88.704, + 88.448, + 88.609, + 87.52, + 88.576, + 88.672, + 88.704 + ] + } + }, + { + "shape_id": "glm52_tp8_shared_down_proj_m8", + "operator": "shared_down_proj", + "M": 8, + "N": 6144, + "K": 256, + "config": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 4, + "SPLIT_K": 1, + "num_stages": 0, + "num_warps": 4 + }, + "graph": { + "median_us": 15.07200002670288, + "p90_us": 15.136000514030457, + "min_us": 15.007999539375305, + "max_us": 15.136200189590454, + "samples_us": [ + 15.136, + 15.072, + 15.04, + 15.072, + 15.008, + 15.008, + 15.072, + 15.072, + 15.072, + 15.04, + 15.136, + 15.104, + 15.136, + 15.104, + 15.072, + 15.072, + 15.104, + 15.104, + 15.104, + 15.072 + ] + } + } +] \ No newline at end of file diff --git a/metainfer/tasks/dcu_kernel_auto_opt/tools/retest_variant.py b/metainfer/tasks/dcu_kernel_auto_opt/tools/retest_variant.py new file mode 100644 index 00000000..744f0207 --- /dev/null +++ b/metainfer/tasks/dcu_kernel_auto_opt/tools/retest_variant.py @@ -0,0 +1,331 @@ +"""A/B retest tool: compare a candidate kernel against the shipped variant. + +Why this exists +--------------- +DKAO records each candidate's median in its ``accepted//manifest.json`` +and each shipped variant's median in its header comment. Those are *records*, +not measurements: if the round that produced them ran while another workload +shared the device, the recorded number can be suppressed (we saw 884us -> 2043 +-> 3047us for one unchanged shape). Before promoting a kernel — especially one +copied out of an old workspace — re-measure both sides under identical +conditions. + +What "A/B" means here +--------------------- +* **A** = the *candidate* kernel (what we might promote). +* **B** = the *variant* currently shipped in DKAO (the incumbent). +* Both are compiled from the same repository skeleton with only the kernel + source swapped, benchmarked on the **same idle device**, with the **same + sampling parameters**, and measured **alternately** (A, B, A, B...). + +Alternating matters: GPU clock/thermal state and any background load drift over +time, so "measure A fully, then B fully" would mix that drift into the +difference. Interleaving makes both sides absorb the same conditions, so the +remaining difference is attributable to the kernel itself. Two rounds are run +so a single fluke cannot decide the outcome; medians are compared and a +candidate must win by ``--threshold`` percent (default 3, matching DKAO's +promotion rule) *and* reproduce its own recorded median (``--recorded-tol``) +before it is written. + +Usage +----- + python -m metainfer.tasks.dcu_kernel_auto_opt.tools.retest_variant \ + --candidate-workspace \ + --shape hy3_tp4_o_proj_m4096 --rounds 2 [--promote] + + # or straight from an accepted kernel file + python ...retest_variant --shape ... --candidate-kernel path/kernel.hip \ + --kernel-root +""" + +from __future__ import annotations + +import argparse +import json +import os +import shutil +import statistics +import subprocess +import sys +from pathlib import Path +from typing import Any, Dict, List, Optional, Tuple + +PLUGIN_DIR = Path(__file__).resolve().parents[1] +REPO_ROOT = Path(__file__).resolve().parents[4] +BENCH = PLUGIN_DIR / "assets" / "w8a8_bench.py" +VARIANT_ROOT = PLUGIN_DIR / "variant" / "int8w8a8-gemm" + +#: DKAO compiles for gfx928 only. The int8 DUMMA fragment specialisations in +#: /opt/dtk/include/du_mma.h sit behind ``#if defined(__gfx928__)``, so a +#: default multi-arch torch build fails with "undefined template DUFragment +#: <... signed char ...>". w8a8_pipeline sets this for the same reason. +BENCH_ENV = {"PYTORCH_ROCM_ARCH": "gfx928"} + + +# --------------------------------------------------------------- helpers ---- + +def find_variant(model: str, tp: int, m: int, operator: str, + root: Optional[Path] = None) -> Optional[Path]: + """Shipped variant for a shape, tolerating model-label drift (glm -> glm52).""" + root = Path(root or VARIANT_ROOT) + exact = root / model / f"TP{tp}" / f"M{m}" / f"{operator}.hip" + if exact.is_file(): + return exact + for model_dir in sorted(root.iterdir()) if root.is_dir() else []: + if not model_dir.name.startswith(model): + continue + candidate = model_dir / f"TP{tp}" / f"M{m}" / f"{operator}.hip" + if candidate.is_file(): + return candidate + hits = sorted(root.glob(f"*/TP{tp}/M{m}/{operator}.hip")) if root.is_dir() else [] + return hits[0] if hits else None + + +def parse_variant_header_median(path: Path) -> Optional[float]: + """Median recorded in a variant's header comment (recorded, not measured).""" + try: + for line in path.read_text(encoding="utf-8", + errors="replace").splitlines()[:8]: + if "median_us=" in line: + return float(line.split("median_us=")[1].split()[0]) + except (OSError, ValueError, IndexError): + return None + return None + + +def bench_env(device: int, base: Optional[Dict[str, str]] = None) -> Dict[str, str]: + """Environment for an independent measurement on one device.""" + env = dict(base if base is not None else os.environ) + env["HIP_VISIBLE_DEVICES"] = str(device) + env["CUDA_VISIBLE_DEVICES"] = str(device) + env.update(BENCH_ENV) + return env + + +def decide(candidate_us: Optional[float], variant_us: Optional[float], *, + threshold_percent: float = 3.0, + recorded_us: Optional[float] = None, + recorded_tol_percent: float = 5.0) -> Dict[str, Any]: + """Promotion verdict from measured medians (+ reproducibility check).""" + if not candidate_us or not variant_us: + return {"verdict": "measurement_failed", + "reason": "one side produced no median"} + improvement = (variant_us - candidate_us) / variant_us * 100.0 + recorded_delta = None + if recorded_us: + recorded_delta = (candidate_us - float(recorded_us)) / float(recorded_us) * 100.0 + if improvement < float(threshold_percent): + return {"verdict": "keep", "improvement_percent": improvement, + "recorded_delta_percent": recorded_delta, + "reason": (f"improvement {improvement:.2f}% < required " + f"{float(threshold_percent):.2f}%")} + if recorded_delta is not None and abs(recorded_delta) > float(recorded_tol_percent): + return {"verdict": "needs_review", "improvement_percent": improvement, + "recorded_delta_percent": recorded_delta, + "reason": (f"win {improvement:.2f}% but the re-measurement " + f"differs from the record by {recorded_delta:.2f}%")} + return {"verdict": "promote", "improvement_percent": improvement, + "recorded_delta_percent": recorded_delta, + "reason": f"candidate faster by {improvement:.2f}%"} + + +def accepted_kernel_for(workspace: Path, shape_id: str) -> Optional[Path]: + hits = sorted(Path(workspace).glob( + f"workers/*/accepted/{shape_id}/kernel.hip")) + return hits[0] if hits else None + + +def recorded_median_for(kernel: Path) -> Optional[float]: + try: + manifest = json.loads((kernel.parent / "manifest.json") + .read_text(encoding="utf-8")) + except (OSError, ValueError): + return None + value = (manifest.get("metrics") or {}).get("median_us") + try: + return float(value) + except (TypeError, ValueError): + return None + + +def idle_device(gpu_ids: Optional[List[int]] = None) -> Optional[int]: + """First device satisfying the operating gate (VRAM<=90 and HCU==0).""" + try: + sys.path.insert(0, str(REPO_ROOT)) + from metainfer.tasks.dcu_kernel_auto_opt.orchestrator.gpu_preflight import ( # noqa: E501 + preflight_gpus, + ) + except Exception: # noqa: BLE001 + return None + plan = preflight_gpus(gpu_ids or [0, 1, 2, 3], samples=3, interval_s=2) + clean = plan.get("clean_ids") or [] + return int(clean[0]) if clean else None + + +def prepare_repo(kernel_root: Path, kernel: Path, target: Path) -> Path: + """Copy a kernel repo skeleton and swap in one kernel source.""" + if target.exists(): + shutil.rmtree(target) + shutil.copytree(kernel_root, target) + (target / "csrc" / "w8a8_gemm_hip.hip").write_text( + Path(kernel).read_text(encoding="utf-8", errors="replace"), + encoding="utf-8") + return target + + +def measure(repo: Path, shape: Tuple[int, int, int], device: int, *, + warmups: int = 30, samples: int = 30, replays: int = 50, + reference_cache: Optional[Path] = None, + timeout_s: int = 3600) -> Optional[float]: + m, n, k = shape + cmd = [sys.executable, str(BENCH), "--source", str(repo), + "--m", str(m), "--n", str(n), "--k", str(k), + "--warmups", str(warmups), "--samples", str(samples), + "--replays-per-sample", str(replays), "--skip-correctness"] + if reference_cache is not None and Path(reference_cache).is_dir(): + cmd += ["--reference-cache-dir", str(reference_cache)] + proc = subprocess.run(cmd, capture_output=True, text=True, cwd=str(repo), + env=bench_env(device), timeout=timeout_s) + for line in reversed((proc.stdout or "").splitlines()): + line = line.strip() + if line.startswith("{") and line.endswith("}"): + try: + payload = json.loads(line) + except ValueError: + continue + value = payload.get("median_us") + return float(value) if value else None + return None + + +def ab_retest(*, shape_id: str, candidate_kernel: Path, kernel_root: Path, + model: str, tp: int, m: int, operator: str, + shape: Tuple[int, int, int], rounds: int = 2, + device: Optional[int] = None, warmups: int = 30, samples: int = 30, + replays: int = 50, threshold: float = 3.0, + recorded_tol: float = 5.0, + reference_cache: Optional[Path] = None, + build_root: Path = Path("/tmp/retest_build"), + variant_path: Optional[Path] = None) -> Dict[str, Any]: + """Interleaved A/B measurement of one candidate against the variant.""" + device = device if device is not None else idle_device() + if device is None: + return {"shape": shape_id, "verdict": "no_idle_device"} + variant = variant_path or find_variant(model, tp, m, operator) + if variant is None: + return {"shape": shape_id, "verdict": "no_variant", + "reason": f"no variant for {model}/TP{tp}/M{m}/{operator}"} + cand_repo = prepare_repo(kernel_root, candidate_kernel, + Path(build_root) / shape_id / "cand") + var_repo = prepare_repo(kernel_root, variant, + Path(build_root) / shape_id / "var") + measured: Dict[str, List[float]] = {"cand": [], "var": []} + for _ in range(max(1, int(rounds))): + for label, repo in (("cand", cand_repo), ("var", var_repo)): + value = measure(repo, shape, device, warmups=warmups, + samples=samples, replays=replays, + reference_cache=reference_cache) + if value: + measured[label].append(value) + cand_median = statistics.median(measured["cand"]) if measured["cand"] else None + var_median = statistics.median(measured["var"]) if measured["var"] else None + verdict = decide(cand_median, var_median, threshold_percent=threshold, + recorded_us=recorded_median_for(candidate_kernel), + recorded_tol_percent=recorded_tol) + return {"shape": shape_id, "device": device, + "candidate_medians_us": measured["cand"], + "variant_medians_us": measured["var"], + "candidate_median_us": cand_median, "variant_median_us": var_median, + "variant_path": str(variant), + "recorded_median_us": recorded_median_for(candidate_kernel), + **verdict} + + +# ------------------------------------------------------------------- CLI ---- + +def main(argv: Optional[List[str]] = None) -> int: + ap = argparse.ArgumentParser( + prog="retest-variant", + description="A/B re-measure a candidate kernel against the shipped variant") + ap.add_argument("--shape", required=True, help="shape id, e.g. hy3_tp4_o_proj_m4096") + ap.add_argument("--candidate-workspace", type=Path, + help="AHE child workspace (accepted kernel is looked up)") + ap.add_argument("--candidate-kernel", type=Path, help="explicit kernel.hip") + ap.add_argument("--kernel-root", type=Path, + help="repo skeleton containing csrc/ (defaults to the " + "candidate's worker source)") + ap.add_argument("--model", help="model label (default: parsed from the shape id)") + ap.add_argument("--tp", type=int, help="tensor-parallel size (default: parsed)") + ap.add_argument("--m", type=int, help="M (default: from the accepted manifest)") + ap.add_argument("--operator", help="operator name (default: parsed from the shape id)") + ap.add_argument("--shape-mnk", help="M,N,K of the problem, e.g. 4096,4096,2048") + ap.add_argument("--rounds", type=int, default=2) + ap.add_argument("--warmups", type=int, default=30) + ap.add_argument("--samples", type=int, default=30) + ap.add_argument("--replays", type=int, default=50) + ap.add_argument("--threshold", type=float, default=3.0) + ap.add_argument("--recorded-tol", type=float, default=5.0) + ap.add_argument("--device", type=int, help="GPU id (default: pick an idle one)") + ap.add_argument("--promote", action="store_true", + help="write the variant when the verdict is 'promote'") + ap.add_argument("--json", type=Path, help="write the report to this path") + args = ap.parse_args(argv) + + from metainfer.tasks.harness_evolve.orchestrator.pool import Pool + pool_file = Path("/root/zth_agent/ahe-kernel-repos/registered_pool.yaml") + pool = Pool.from_yaml(pool_file) if pool_file.is_file() else None + inst = pool.get(args.shape) if pool is not None else None + + candidate = args.candidate_kernel + if candidate is None: + if not args.candidate_workspace: + ap.error("--candidate-workspace or --candidate-kernel is required") + candidate = accepted_kernel_for(args.candidate_workspace, args.shape) + if candidate is None or not Path(candidate).is_file(): + ap.error(f"candidate kernel not found for {args.shape}") + + kernel_root = args.kernel_root or Path(candidate).parents[2] / "source" + model = args.model or (inst.model if inst else args.shape.split("_tp")[0]) + tp = args.tp or (inst.tp_size if inst else 4) + m = args.m or (inst.M if inst else 4096) + operator = args.operator or args.shape.split("_m")[0].split("_", 2)[-1] + if args.shape_mnk: + mnk = tuple(int(x) for x in args.shape_mnk.split(",")) + elif inst is not None: + mnk = (inst.M, inst.N, inst.K) + else: + ap.error("--shape-mnk is required when the shape is not in the pool") + + report = ab_retest( + shape_id=args.shape, candidate_kernel=Path(candidate), + kernel_root=Path(kernel_root), model=model, tp=int(tp), m=int(m), + operator=operator, shape=mnk, rounds=args.rounds, device=args.device, + warmups=args.warmups, samples=args.samples, replays=args.replays, + threshold=args.threshold, recorded_tol=args.recorded_tol, + ) + print(json.dumps(report, ensure_ascii=False, indent=2)) + if args.json: + Path(args.json).write_text(json.dumps(report, ensure_ascii=False, indent=2), + encoding="utf-8") + + if args.promote and report.get("verdict") == "promote": + from metainfer.tasks.dcu_kernel_auto_opt.orchestrator.variant_promote import ( # noqa: E501 + model_label_for, promote_variant, + ) + workspace = args.candidate_workspace or Path(candidate).parents[4] + outcome = promote_variant( + workspace_dir=Path(workspace), + answers={"operator": "Quantized GEMM", "dtype": "INT8 W8A8", + "model": model, "tp_size": tp}, + shape_id=args.shape, + source_task=f"{args.shape} (A/B retested)", + correctness_ok=True, min_improvement_percent=args.threshold, + tp=int(tp), m=int(m), + model_label=(model_label_for(args.shape, str(model)) or None), + ) + print(json.dumps({"promotion": outcome}, ensure_ascii=False, indent=2)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/metainfer/tasks/dcu_kernel_auto_opt/variant/int8w8a8-gemm/glm52/TP8/M4096/fused_qkv_a_proj.hip b/metainfer/tasks/dcu_kernel_auto_opt/variant/int8w8a8-gemm/glm52/TP8/M4096/fused_qkv_a_proj.hip index f5387ae1..b8032fe2 100644 --- a/metainfer/tasks/dcu_kernel_auto_opt/variant/int8w8a8-gemm/glm52/TP8/M4096/fused_qkv_a_proj.hip +++ b/metainfer/tasks/dcu_kernel_auto_opt/variant/int8w8a8-gemm/glm52/TP8/M4096/fused_qkv_a_proj.hip @@ -1,457 +1,78 @@ -// @@variant shape=glm_tp8_fused_qkv_a_proj_m4096 commit=99b9a733eddfc7196f3d186b80affe4324351ac4 added=2026-09-11 -// median_us=883.7 p90_us=885.5 speedup=77.7 baseline_us=6.866e+04 -// source=test-9-11-1-b01955b3/iteration_001/glm_tp8_fused_qkv_a_proj_m4096 +// @@variant shape=glm_tp8_fused_qkv_a_proj_m4096 commit=7cf8d67fe9e0c38f2b4c3c67aded1bf8cc894342 added=2026-09-14 +// median_us=693.8 p90_us=695.3 speedup=98.96 baseline_us=6.866e+04 +// source=test-9-11-1/iteration_002/glm_tp8_fused_qkv_a_proj_m4096 // MetaInfer W8A8 INT8 GEMM -- round-1 architecture establishment for // Hygon K500SM_AI / gfx928 (worker_0, physical GPU 0). // // Assigned shape: // glm_tp8_fused_qkv_a_proj_m4096 : M=4096, N=2624, K=6144 +// (GLM5.2 TP=8 fused_qkv_a_proj, logical (K, N) = (6144, 2624)) // -// Operator contract (fixed by the control plane): +// Operator contract (fixed by csrc/bindings.cpp and int8_w8a8_gemm_api.py): // out[m, n] = bf16( int32_dot(x_q[m, :], raw_weight[:, n]) -// * x_scale[m] * weight_scale[n] ) +// * x_scale[m, 0] * weight_scale[n, 0] ) // -// Round 1 (mandatory establish_arch) -- packed_weight architecture family: +// Mandatory decision this round: the tile-shape (M-tile vs N-tile aspect ratio) +// round. The packed_weight architecture, the exact-shape packed-B swizzle, the +// double-buffered 64-K stage pipeline, the bank-safe A LDS stride, the +// conflict-free swizzled B fragment reads and the fused coalesced bf16 epilogue +// are all RETAINED unchanged; only the macro-tile aspect ratio and its derived +// wave split are retuned for M=4096, N=2624, K=6144. No split-K, no workspace +// use, no allocation/compilation/packing/synchronization inside the timed API. // -// Bottleneck hypothesis, from the trusted PMC of the current best source -// (the correctness-first bootstrap: w8a8_dumma_prefill_tiled_kernel -// <128, 64, 128>, identity [K, N] weight, library du_load_matrix_sync -// loaders, LDS row strides 128 (A) / 64 (B), grid 41 x 32 = 1312 blocks, -// 256 threads, 2 blocks/CU): -// median 2920.88 us / p90 2962.82 us, lds_instructions 21,999,616, -// lds_bank_conflicts 274,071,552 (~12.5 per LDS instruction), -// lds_wait_instructions 50,181,819, valu_instructions 86,560,512, -// vmem_read_instructions 1,574,400, and a counter-derived operator HBM -// bandwidth of only 50.6 GB/s (147.8 MB per replay at a 90.7% L2 hit -// rate). The exact-shape stage loop is LDS-issue/conflict bound, not -// MMA- or HBM-bound: both LDS row strides alias every row onto one bank -// phase (128 mod 32 == 0, 64 mod 32 == 0), and the row_major matrix_b -// loader of du_load_matrix_sync lowers each m16n16k32 int8 B fragment to -// 8 ds_read_u8 at 16-byte spacing plus a byte-reassembly chain. +// (1) launch_pack_w8a8_weight packs the exact (k, n) == (6144, 2624) weight +// ONCE, outside the timed region and out of Graph capture, into a +// fragment-interleaved swizzle of 512-byte 32-k x 16-n DUMMA B fragment +// blocks laid out [k32][n16][k8][n][8]: +// packed[((kk>>5)*n16g + (n>>4))*512 + +// (((kk>>3)&3)*16 + (n&15))*8 + (kk&7)] = raw[kk*n + n], +// n16g = n>>4 = 164. Byte-exact bijection 192 x 164 x 512 = k*n = +// 16,121,856: same byte count and same buffer allocation, so every +// captured address is graph-stable. Every other (K, N) keeps the identity +// device-to-device copy. +// (2) the exact-shape guard dispatches the new +// w8a8_dumma_prefill_packedb_db_kernel<256, 128, 64, 512, true>: +// grid = ((N + 127)/128) x (M/256) = 21 x 16 = 336 blocks, +// block = 512 threads = 8 gfx928 wavefronts, +// a 64 x 64 quadrant of sixteen m16n16k32 int32 accumulator fragments +// per wave (4 wave rows x 2 wave columns, the balanced square panel), +// i.e. a 256 x 128 M-major operand-reuse macro-tile, 64-K +// DOUBLE-buffered stages with ONE __syncthreads per stage and the +// stage-(s+1) global loads issued into staging VGPRs before the stage-s +// consume, k0-outer over 96 ascending 64-K stages with kk-inner +// ascending m16n16k32 MMAs (exact int32 accumulation order of the +// reference), and a fused branch-free per-row/column scale + bf16 +// epilogue emitting one coalesced 8-byte store per lane. +// The A LDS tile uses the bank-safe 72-byte row stride (StageK + 8: an +// odd 8-byte count, so the 16 rows of each 16-lane ds_read_b64 phase land +// on 16 distinct bank pairs); the swizzled B tile is a flat BN x 64-byte +// copy of the packed stage, so every col_major B fragment read is one +// conflict-free ds_read_b64 (load_frag8_swz). +// LDS = 2 x (A[256, 72] + B[128, 64]) = 2 x 18,432 + 2 x 8,192 = +// 53,248 B per block, so ONE resident block/CU = 8 waves/CU (the +// two-operand global traffic falls from M*K*21 + N*K*32 = 1,044 MB to +// M*K*21 + N*K*16 = 786 MB, -24.7%, at unchanged wave occupancy). +// (3) the scalar int8/int32 fallback decodes the swizzled layout element-wise +// in ascending kk for (k, n) == (6144, 2624), so the paired M=2 / M=16 +// API shapes with the same (N, K) stay byte-exact; the generic +// single-buffered <64, 64, 128> identity arm covers every other large-M +// shape with n % 64 == 0 and k % 128 == 0. The fallback is never replaced +// by an unconditional shape-specialized launch. // -// Focused change (ONE architecture family -- packed_weight -- entirely -// inside csrc/w8a8_gemm_hip.hip): -// (1) launch_pack_w8a8_weight packs the exact (k, n) == (6144, 2624) -// weight ONCE, outside the timed region and out of Graph capture, -// into the n-major layout packed[n * k + kk] = raw[kk * n + n] -// (same byte count, same buffer allocation, so every captured -// address is unchanged). Every other (K, N) keeps the identity -// device-to-device copy. -// (2) the exact-shape guard dispatches the new -// w8a8_dumma_prefill_packedb_kernel<128, 64, 128>. It keeps the -// EXACT bootstrap geometry -- grid (N/64) x (M/128) = 41 x 32 = 1312 -// blocks, 256 threads = 4 gfx928 wavefronts, one 64 x 32 quadrant of -// eight m16n16k32 int32 accumulator fragments per wave, the same -// single-buffered 128-K LDS stage with two __syncthreads per stage, -// the same k0-outer / kk-inner ascending int32 accumulation order and -// the same coalesced bf16 epilogue -- and changes only the operand -// path to the packed-weight architecture: -// * B is staged n-major (b_tile[n_row][k]) from the packed [N, K] -// weight, so a col_major matrix_b fragment is 8 contiguous K -// bytes per lane and every staging vector is one coalesced -// 16-byte global read; -// * both LDS tiles use the bank-safe row stride StageK + 8 = 136 -// bytes (34 dwords): 8-byte aligned with an odd 8-byte count -// (136/8 = 17), so the 16 rows of each 16-lane ds_read_b64 phase -// land on 16 distinct bank pairs (the conflict-free 4-phase -// floor), and 136 % 16 == 8 keeps the two-half int64 staging -// form; -// * both fragment loaders are the explicit load_frag8: ONE aligned -// 8-byte load per lane per fragment (row = lane & 15, k-quarter = -// lane >> 4), replacing the byte-reassembly library path with a -// single ds_read_b64 while producing identical fragment values. -// (3) LDS/block grows 24,576 -> 26,112 B (A[128, 136] + B[64, 136]); -// 2 x 26,112 = 52,224 <= 65,536 B keeps TWO resident blocks/CU = -// 8 waves/CU, so grid_blocks (1312), waves_per_block (4) and the -// active CU count (120) are unchanged. -// (4) the scalar fallback decodes the n-major layout when -// (k, n) == (6144, 2624) with ascending kk, so the paired M=2 / -// M=16 API shapes with the same (N, K) stay byte-exact; the generic -// <64, 64, 128> identity arm is untouched. +// Correctness: the same weight elements reach the same fragment slots in the +// same ascending k-chunk sequence (k0-outer over 96 ascending 64-K stages, +// kk-inner ascending over two m16n16k32 steps, i.e. the identical (k0 + kk) +// k-chunk sequence 0, 32, 64, 96, ...), and the epilogue applies the exact +// reference multiply order float(dot) * x_scale[row] * weight_scale[col], so +// every bf16 output byte is bit-identical to the scalar reference. Widening BM +// only changes WHICH wave computes which output element; it never reorders the +// K stages or the int32 accumulation. // -// Correctness: the same weight elements reach the same fragment slots in -// the same ascending k-chunk sequence (k0 outer over 48 stages of 128 K, -// kk inner ascending over four m16n16k32 steps), so every int32 -// accumulator -- and therefore every bf16 output byte -- is identical to -// the bootstrap kernel and to the scalar reference. -// -// Round 2 (mandated operand-reuse round: cooperative A+B LDS staging vs direct -// loads, quantified A/B reuse per macro-tile, vectorized coalesced global loads -// with a bank-safe LDS layout): -// -// Operand-reuse accounting for the accepted <128, 64, 128> macro-tile -// (M=4096, N=2624, K=6144): -// * per block the unique operand bytes are A = 128 x 6144 = 786,432 and -// B = 6144 x 64 = 393,216; the 41 N-blocks re-read every A row-slab -// (M*K*41 = 1.03 GB/replay, 41x amplification of the 25.2 MB unique A) -// and the 32 M-blocks re-read every B column-slab (K*N*32 = 0.52 -// GB/replay, 32x of the 16.1 MB unique B): 1.55 GB/replay of staged -// global traffic; -// * inside one block each staged 16-B vector is re-read from LDS by the two -// wave columns (A) / the two wave rows (B), i.e. exactly 2x wave-level -// reuse per staged byte, so the LDS read traffic is 2x the staged bytes -// (4.72 GB/replay, ~23 B/cycle/CU at the measured 1.271 GHz -- far below -// the LDS bandwidth), and the counter-derived HBM rate is only -// 108.4 GB/s (147.8 MB/replay at a 90.7% L2 hit rate). The kernel is -// therefore NOT HBM- or LDS-bandwidth bound, so a direct per-wave global -// fragment path is rejected up front: it would issue 4x more vmem -// instructions (4 waves x 48 stages of 8-B fragment loads) and every 8-B -// fragment load would be a row-strided multi-line access instead of the -// coalesced 16-B staging vectors of the cooperative path. Cooperative -// A+B LDS staging is RETAINED -- the round improves it instead. -// -// Remaining exposed cost of the retained path: the VMEM round trip in front -// of every stage consume. The accepted source (PMC digest daf403b3..., -// profiles/.../iteration2/pmc.json: profiled 1368.793 us, grid 1312 blocks, -// 2 blocks/CU, 26,112 B LDS, lds_instructions 7,892,992 with -// lds_wait 4,535,391, lds_bank_conflicts 0, vmem_read 1,574,400, -// grbm_gui_active == grbm_count) issues per 128-K stage 24 wave-level -// global_load_dwordx4 + 24 ds_write2_b64 and then, per the exact-source ISA, -// strictly serializes global_load_dwordx4 -> s_waitcnt vmcnt(0) -> -// ds_write2_b64 -> s_barrier -> 24x ds_read_b64 -> 32x v_mmac -> s_barrier: -// with 8 resident waves/CU the GPU is 100% GRBM GUI-active yet issues only -// ~4.6% of its slots, i.e. the stage boundary -- not HBM, LDS bandwidth or -// MMA throughput -- sets the pace. -// -// Focused change (ONE mechanism -- the K stage pipeline -- entirely inside -// csrc/w8a8_gemm_hip.hip): the exact-shape guard now dispatches -// w8a8_dumma_prefill_packedb_db_kernel<128, 64, 64>. It keeps the SAME -// macro-tile (grid 41 x 32 = 1312 blocks, 256 threads = 4 wavefronts, 64 x 32 -// quadrant of eight m16n16k32 int32 accumulators per wave), the SAME packed -// n-major B (16-B coalesced staging vectors), the SAME explicit load_frag8 -// fragment reads, the SAME bank-safe LDS stride form and the SAME coalesced -// epilogue -- and changes only how the K stages are staged: -// * the single-buffered 128-K stage (TWO __syncthreads per stage, full -// VMEM latency exposed in front of every consume) becomes a -// DOUBLE-buffered 64-K stage with ONE __syncthreads per stage: the -// stage-(s+1) global loads (2 int4 A + 1 int4 B per thread) are issued -// into staging VGPRs BEFORE the stage-s consume and flushed into the idle -// LDS buffer after it, so the stage-(s+1) VMEM latency overlaps the -// stage-s LDS/v_mmac work. -// -// Resources: LDS = 2 x (A[128, 72] + B[64, 72]) = 2 x 13,824 = 27,648 B/block -// (26,112 -> 27,648 B, +5.9%); 2 x 27,648 = 55,296 <= 65,536 B keeps TWO -// resident blocks/CU = 8 waves/CU, occupancy UNCHANGED. kLdsStride = 72 = -// StageK + 8: 8-aligned with an odd 8-byte count (72/8 = 9) so the 16 rows of -// each 16-lane ds_read_b64 phase land on 16 distinct bank pairs (18r mod 32, -// r = 0..15) -- the same conflict-free 4-phase floor as the accepted kernel -- -// and 72 % 16 == 8 keeps the two-half ds_write2_b64 staging form. 12 extra -// VGPR hold the prefetch payload (~72 -> ~84, still <= the 128/thread budget -// of 2 blocks/CU; 0 spills expected). -// -// Correctness: k0-outer over 96 ASCENDING 64-K stages with kk-inner ascending -// over two m16n16k32 steps gives the identical (k0 + kk) k-chunk sequence -// 0, 32, 64, 96, ... as the accepted 48 x 128-K stages, with the identical -// element-to-slot fragment mapping, so every int32 accumulator -- and every -// bf16 output byte -- is bit-identical to the accepted kernel and to the -// scalar reference. The n-major pack, the scalar-fallback n-major decode, the -// generic <64, 64, 128> identity arm and every shape guard are untouched. -// -// Round 3 (mandated pipeline round: compare single and double buffering across -// K tiles; retain double buffering only on ISA/PMC evidence of reduced VMEM -// stalls without harmful LDS or occupancy growth): -// -// * Mandatory comparison (exact trusted PMC/ISA of the two accepted arms): -// arm median us LDS B/blk blocks/CU VGPR -// single 128-K <128,64,128> 1363.078 26,112 2/8 waves 72 -// double 64-K <128,64,64> 1153.970 27,648 2/8 waves 96 -// A single 64-K arm is not built: with NO prefetch it can only add -// barriers (2 per 64-K stage = 192 per block vs the 128-K arm's 96) while -// exposing the same VMEM round trip, so it is strictly dominated and -// rejected by construction. -// RETAINED -- double buffering. Evidence: median 1363.078 -> 1153.970 us -// (-15.3%) and p90 1364.825 -> 1164.522 us (-14.7%); the exact-source PMC -// of the double-buffered arm (digest ee609c3f..., iteration3/pmc.json) -// shows lds_wait_instructions 4,562,210 -> 3,590,796 (-21.3%) with -// grbm_gui_active == grbm_count (100% GUI-active), LDS 27,648 B/block -// (2 x 27,648 = 55,296 <= 65,536 so the trusted occupancy-probe split 2 -// is unchanged = 8 waves/CU), arch_vgpr 96 with 0 spills (<= the -// 128/thread budget of 2 blocks/CU) and sgpr 32. The ISA confirms the -// mechanism: the stage-(s+1) global_load_dwordx4s are issued BEFORE the -// stage-s ds_read_b64 / v_mmac burst and the vmcnt(0) wait lands on the -// ds_write2_b64 flush AFTER the MMACs, while the 128-K arm serializes -// global_load_dwordx4 -> s_waitcnt vmcnt(0) -> ds_write2_b64 -> s_barrier -// with zero compute overlap. So the prefetch converted exposed VMEM -// latency into overlapped work without trading away occupancy. -// LDS-growth verdict: the +1,536 B/block stays inside the 2-block budget -// and lds_wait FELL (the LDS path is not harmed in the net); the new -// lds_bank_conflicts 12,091,392 is exactly 6 x 4 x 4 x 96 x 1312 = one -// conflict per 16-lane phase of every merged ds_read2_b64, i.e. the -// bank-preserving alias of the +16-row second fragment offset -// (16 x 72 = 1152 B = 9 x 128 B; for ANY 8-aligned uniform row stride S, -// 16 rows = 4S dwords with 4S % 32 == 0). The unmerged ds_read_b64 -// control at the same 72/136-byte strides measured 0 conflicts, so this -// is an alias of the merged read form, not a layout defect. -// Every other K-tile pipeline is rejected inside the 65,536 B budget: -// StageK=96 double buffer = 39,936 B/block, StageK=128 double buffer = -// 52,224 B/block and 3 x 64-K = 41,472 B/block all exceed the 32,768 -// B/block that 2 resident blocks/CU permit (each would drop to 1 -// block/CU = 4 waves/CU, a 2x occupancy loss, explicitly excluded), and a -// 32-K stage leaves only 128 B staging vectors per block (half the 256 -// threads idle) while carrying a single m16n16k32 step per stage (too -// little consume work to amortize the barrier). StageK=64 x 2 buffers is -// therefore the unique 2-resident-block K pipeline and is retained. -// -// * Focused change (ONE mechanism -- the B operand's global-memory layout -- -// entirely inside csrc/w8a8_gemm_hip.hip). The pipeline round's own PMC -// identifies the next exposed cost: the accepted source reads -// 170,221,184 B/replay at only an 86.938% L2 hit rate (3,250,848 misses) -// and 166.147 GB/s counter-derived HBM, while the unique operands are only -// 25.2 MB (A) + 16.1 MB (B). The B side causes it: the n-major pack -// packed[n*K + kk] lays a [64, 64] B tile out as 64 separate 64-byte -// column chunks at a 6,144-byte stride, so every one of the 8 warp-level -// 16-byte staging loads touches 32 half-used 128-byte lines. -// launch_pack_w8a8_weight now packs the exact (k, n) == (6144, 2624) -// weight once, outside the timed region and out of Graph capture, into 41 -// B panels of [K, 64] int8 with 16-byte k-runs per column: -// packed[(n>>6)*(k*64) + (kk>>4)*1024 + (n&63)*16 + (kk&15)] -// = raw[kk*n + n] -// (byte-exact bijection 41 x 384 x 64 x 16 = k*n = 16,121,856; same byte -// count, same buffer and same captured addresses; every other (K, N) -// keeps the identity copy). Each [64, 64] B tile of a 64-K stage is then -// FOUR contiguous 1-KiB slabs (one per 16-k-run) and every staging vector -// is a 16-byte-aligned contiguous k-run of one column, so a warp's 32 -// x 16-byte B staging loads touch 4 FULLY-USED 128-byte lines instead of -// 32 half-used ones (8x fewer line requests per warp-load; the tile's line -// footprint is 32 fully-used lines instead of 64 half-used ones). -// The ONLY kernel change is the two B global-load sites (prologue + -// steady-state prefetch) of the dispatched -// w8a8_dumma_prefill_packedb_db_kernel<128, 64, 64>: byte -// (n0>>6)*(k*64) + ((k0>>4)+k16)*1024 + n_row*16 replaces -// (n0+n_row)*k + k0 + k16*16. The LDS staging layout b_tile[2][64][72] -// (two-half ds_write2_b64), the col_major load_frag8 consumes, the -// k0-outer/kk-inner ascending int32 accumulation order, the coalesced -// epilogue, the grid (41 x 32 = 1312 blocks), the block size (256 = 4 -// wavefronts), the retained double-buffered 64-K pipeline (one -// __syncthreads per stage), LDS/block 27,648 B (2 blocks/CU = 8 waves/CU) -// and the VGPR budget are ALL unchanged, so this is a pure global-layout -// A/B. -// * Correctness: the same weight elements reach the same fragment slots in -// the same ascending k-chunk sequence, so every int32 accumulator -- and -// every bf16 output byte -- is bit-identical to the accepted kernel and to -// the scalar reference. The scalar fallback for (k, n) == (6144, 2624) -// decodes the panel layout in ascending 16-byte k-runs (the paired M=2 / -// M=16 API shapes with the same (N, K) stay byte-exact); the generic -// identity arm, the round-2 n-major pack kernel (kept compiled as the -// layout control) and every shape guard are untouched. Packing stays -// outside the timed region and out of Graph capture. -// * Predicted deltas: vmem_read_instructions unchanged (1,574,400), LDS -// counters unchanged (lds_instructions 4,901,632, lds_wait 3,590,796, -// conflicts at the merged-read alias floor 12,091,392), L2 misses strictly -// below 3,250,848 with the hit rate up, operator read bytes strictly below -// 170,221,184 (B line fetches halve), median < 1153.970 us. -// * Superseded by Round 5: the B-panel pack and the b_tile[2][64][72] LDS -// form below are replaced by the fragment-interleaved swizzle. -// -// Round 5 (mandated packing round: one weight packing/swizzle that makes each -// DUMMA B tile vector-loadable and LDS-bank-safe): -// -// * Baseline: the accepted double-buffered <128, 64, 64> kernel (official -// median 937.431 us / p90 939.452 us; exact-source PMC -// profiles/.../iteration4/pmc.json: profiled 889.453 us, grid 41x32 = -// 1312 blocks, 2 blocks/CU = 8 waves/CU, 27,648 B LDS, arch_vgpr 96 with -// 0 spills, lds_instructions 4,901,632 = 9.7 per wave-stage (6 merged -// ds_read2_b64 + 3 ds_write2_b64), lds_wait 3,828,676, -// lds_bank_conflicts 12,091,392 = 6 x 4 x 4 x 96 x 1312, vmem_read -// 1,574,400, L2 hit 90.39%). The surviving exposed LDS cost is the -// b_tile row-major layout: each col_major B fragment is one ds_read_b64 -// at a 72-byte row stride, and the compiler merges the fragment pairs -// into ds_read2_b64 whose +16-row second offset (16 x 72 = 1152 B = -// 9 x 128 B) aliases the same bank pairs -- one conflict per 16-lane -// phase, exactly the measured 12,091,392 conflicts. -// * Mechanism (ONE packing/swizzle, entirely in this file): replace the -// Round-3 B-panel pack with a fragment-interleaved swizzle. For each -// 32-k x 16-n DUMMA B fragment block the 4 k8 groups x 16 n-rows x 8 B -// are stored n-interleaved as [k8][n][8] (512 B per fragment block), -// 32-k major inside the 64-K stage: -// packed[((kk>>5)*n16g + (col>>4))*512 + -// (((kk>>3)&3)*16 + (col&15))*8 + (kk&7)] = raw[kk*n + col], -// with n16g = n>>4 (164 for the exact shape). Byte-exact bijection -// 192 x 164 x 512 = k*n = 16,121,856: same byte count, same buffer and -// same captured addresses (graph-stable), and packing still runs once, -// outside the timed region and out of Graph capture. -// * Kernel-side effect: every 16-byte B staging vector is now 2 n-rows x -// 8 B of one k8 of one fragment block, so the staging store is ONE -// 16-byte-aligned int4 (ds_write_b128) flat copy per thread (256 chunks -// = one per thread per 64-K stage; a warp reads 8 fully-used 128-byte -// lines) and every col_major B fragment is load_frag8_swz: lane -// (n = lane&15, k8 = lane>>4) reads p + k8*128 + n*8, i.e. one -// ds_read_b64 whose 16-lane phase spans exactly one 128-byte k8 block -- -// all 32 banks exactly once per phase, conflict-free by construction. -// The A tile keeps its bank-safe 72-byte stride, and the double-buffered -// 64-K pipeline, the k0-outer/kk-inner ascending int32 order, the -// coalesced epilogue, the grid (41x32 = 1312 blocks), the block -// (256 threads = 4 wavefronts) and the occupancy (2 blocks/CU = 8 -// waves/CU) are all unchanged, so this is a pure packing/swizzle A/B. -// * LDS: b_tile shrinks from 2 x [64, 72] = 9,216 B to 2 x 4,096 B; the -// block total is 18,432 (A) + 8,192 (B) = 26,624 B, so 2 x 26,624 = -// 53,248 <= 65,536 B keeps 2 resident blocks/CU = 8 waves/CU. -// * Correctness: the same weight elements reach the same fragment slots in -// the same k0-outer/kk-inner ascending k-chunk sequence, so every int32 -// accumulator -- and every bf16 output byte -- is bit-identical to the -// accepted kernel and to the scalar reference. The scalar fallback for -// (k, n) == (6144, 2624) decodes the swizzle element-wise in ascending -// kk (the paired M=2 / M=16 API shapes stay byte-exact); the generic -// identity arm, the Round-2 n-major pack control and every shape guard -// are untouched. -// * Predicted deltas: vmem_read_instructions unchanged (~1,574,400), -// LDS/block 27,648 -> 26,624 B, lds_bank_conflicts strictly below -// 12,091,392, lds_wait below 3,828,676, median < 937.431 us. -// Falsifiable: retained only if median_us < 937.4308013916016, -// mismatch_count == 0, max_abs_error == 0.0, -// graph_capture_passed == true, the exact code object shows 0 spills / -// LDS == 26,624 B / VGPR <= 128 (2 blocks/CU), and the PMC shows -// lds_bank_conflicts and lds_wait strictly below the baseline with -// vmem_read not growing; any regression restores the iteration-3 source. -// -// Round 6 (mandated epilogue round: fuse per-row/per-column scales, bf16 -// conversion and the final coalesced store into the compute kernel; remove any -// unnecessary workspace/combine pass): -// -// * Fusion audit (nothing to remove, so the fused epilogue is made cheaper). -// The accepted source ALREADY fuses everything: store_prefill_fragment_ -// coalesced applies x_scale[row](per-row) * weight_scale[col](per-column), -// converts to bf16 and writes one coalesced 8-byte store per lane, and the -// exact-shape operator replay is a SINGLE dispatch -- the trusted PMC -// (profiles/glm_tp8_fused_qkv_a_proj_m4096/iteration6/pmc.json, digest -// 7e5731dc..., compile key 120c807e...) reports operator_aggregate -// kernel_count == 1, scratch_bytes == 0, vmem_write_instructions 41,984 = -// 8 x 4 x 1312 and write_bytes_per_operator_replay 21,500,384 = m*n*2, i.e. -// there is no workspace, no split-K combine pass and no second Graph node -// to remove; workspace_bytes is unused and workspace_split_k_capacity is -// zero for this shape. The remaining epilogue cost is therefore the -// conversion itself. -// * Measured cost of the fused epilogue (exact gfx928 code object of the -// current best, iteration6/current-best-isa/isa.txt). The dispatched -// kernel is ~1,920 instructions; the steady-state K-stage loop body is 107 -// instructions (one `s_branch 65428` back-edge at 0x6BB8 -> 0x6A0C) and -// the prologue is ~259, so the fused epilogue is ~1,490 static -// instructions -- roughly 13% of the dynamic instruction stream over -// 96 stages. The dominant term is that this DTK lowers EACH of the 32 -// per-lane __float2bfloat16 calls to a ~13-instruction EXEC-masked -// Inf/NaN sequence: -// v_and_b32 (b & 0x7f800000) / v_cmp_ne_u32 / s_and_saveexec_b64 / -// s_xor_b64 / v_bfe_u32 (bit 16) / v_add3_u32 (b + lsb + 0x7fff) / -// s_andn2_saveexec_b64 / v_mov_b32 0 / v_or_b32 (b | 0x10000) / -// v_cmp_eq_u32_sdwa / v_cndmask_b64 / s_or_b64 (restore EXEC) -// so ~420 instructions per lane plus 4 x (3 shifts + 3 ors) of packing, -// with 32 dependent SALU EXEC save/restore pairs that cannot be software- -// pipelined and serialise the wavefront (the epilogue is the only place -// VALU issues drop while the exact-shape kernel is 100% GRBM GUI-active). -// * Focused change (ONE mechanism -- the bf16 conversion of the fused -// epilogue -- entirely inside csrc/w8a8_gemm_hip.hip). Replace the library -// conversion + shift/or pack in store_prefill_fragment_coalesced with the -// branch-free round-to-nearest-even integer identity -// bf16_rne_high16(v) = (b + 0x7fff + ((b >> 16) & 1)) >> 16, -// b = __float_as_uint(v), -// which the disassembly proves is exactly what the normal (finite) path of -// the library conversion computes (v_bfe bit 16, v_add3 b + lsb + 0x7fff). -// Adding 0x7fff + the retained bit-16 LSB cannot carry out of the sign, the -// +((b>>16)&1) term rounds exact ties to an even LSB (RNE), max-float still -// rounds to +/-Inf and 0.0/-0.0 and subnormal encodings are preserved, so -// every 16-bit payload is identical. Every value converted here is finite -// by construction (|int32 dot| < 2^31 times finite dequant scales), so the -// removed Inf/NaN special case is unreachable. Four values now pack into -// one 8-byte word with three VALU ops (v_bfe/v_and + v_add3 + v_lshrrev) -// instead of ~13 EXEC-bracketed instructions each, and the 32 EXEC -// save/restore chains disappear. The epilogue structure, the per-row -// x_scale gather, the per-column float4 weight_scale gather, the register -// 4x4 shuffle transpose, the multiply order float(dot) * x_scale * -// weight_scale, the 8-byte-aligned coalesced store address, the grid -// (41 x 32 = 1312), the block (256 = 4 wavefronts), the double-buffered -// 64-K pipeline, the swizzled packed B, LDS/block 26,624 B (2 blocks/CU = -// 8 waves/CU) and the 65,536 B workspace budget are ALL unchanged, so this -// is a pure conversion A/B. The scalar fallback keeps __float2bfloat16 (it -// is not on the timed path); the generic identity arm and every shape -// guard are untouched. -// * Predicted deltas: grid_blocks 1312, LDS/block 26,624 B, 2 blocks/CU = -// 8 waves/CU, vmem_read 1,574,400 and vmem_write 41,984 unchanged; -// valu_instructions strictly below 23,920,384 and the epilogue's static -// EXEC/SALU count down by >1,000; median < 918.191 us with no p90 -// regression. Falsifiable: retained only if median_us <= 909.0 us -// (>= 1% round acceptance) and p90_us <= 919.530 us, mismatch_count == 0, -// max_abs_error == 0.0, graph_capture_passed == true, the exact code -// object shows 0 spills / LDS == 26,624 B / VGPR <= 128 (2 blocks/CU) and -// no __float2bfloat16 EXEC sequence remains in the epilogue; otherwise the -// iteration-5 source is restored. -// -// Round 9 (late ISA-diagnosis round; the control-plane plateau gate is CLOSED -// for this round -- phase hip_only, plateau false, raw inline asm and ISA -// Skills forbidden -- so this stays a HIP-only round): -// -// * Bottleneck evidence (exact trusted gfx928 ISA of the current best, -// profiles/glm_tp8_fused_qkv_a_proj_m4096/iteration6/current-best-isa/ -// isa.txt, digest 7e5731dc...; the round-6 source differs from that -// profiled official best only in the bf16 conversion, so the steady-state -// loop analysed here is the dispatched loop). The steady-state stage body -// is 107 instructions (0x6A0C..0x6BB8, one back-edge `s_branch 65428` at -// 0x6BB8 -> 0x6A0C) and roughly 14 of them are pure A-row tail -// predication: -// 0x6A0C..0x6A20 six unconditional `v_mov_b32` zero-fills of the two -// int4 staging vectors (v33/v34 and v37..v40), -// 0x6A24..0x6A34 `s_and_saveexec_b64` / `s_cbranch_execz` / -// `global_load_dwordx4` / `s_or_b64` for A vector 0, -// 0x6A38..0x6A50 two more `v_mov_b32` zero-fills (v35/v36) plus the -// same EXEC save/restore + uniform branch for A -// vector 1. -// The guard source `a_pre[v] = (g_row < m) ? load : int4{0,0,0,0}` is -// never false for the dispatched M=4096 (M / BM = 4096 / 128 = 32 exact), -// yet its zero-fills execute unconditionally in every one of the 96 -// stages and its two EXEC read-modify-writes serialize the wavefront in -// front of the very global loads that prefetch the next tile. That is -// ~14/107 = 13% of the hot loop's instruction stream, on top of the -// epilogue's per-fragment `row >= m` EXEC bracket (s_and_saveexec / -// s_xor_b64 / s_andn2_saveexec_b64 / s_cbranch_execz / s_or_b64 around -// each of the eight unrolled fragment stores), for a kernel the PMC shows -// 100% GRBM GUI-active yet issuing only ~0.42 instructions per cycle per -// CU (lds_wait_instructions 5,302,705 against lds_instructions -// 4,901,632). -// * Focused change (ONE mechanism -- exact-shape full-tile specialization, -// entirely inside csrc/w8a8_gemm_hip.hip): the dispatched kernel gains a -// compile-time `bool kFullTile` template parameter. The exact-shape guard -// keeps `(m >= 128, n == 2624, k == 6144)` and the same grid (N/64 x -// ceil(M/128)) and block (256 = 4 wavefronts), and selects -// `kFullTile == true` exactly when the caller's m is a multiple of -// BM = 128 -- i.e. when every block tile lies fully inside the output (the -// assigned M=4096 case). In that instantiation the A staging loads -// (prologue AND steady state) drop the `(g_row < m)` select / zero-fill -// and the epilogue drops the per-fragment `row >= m` early return, so the -// loop body keeps only the address math and the three coalesced -// `global_load_dwordx4`. Every other m keeps the guarded instantiation -// byte-for-byte (zero-fill, epilogue row mask, tail-block coverage), and -// the paired M=2/M=16 API shapes with the same (N, K) still take the -// scalar fallback because the `m >= 128` guard is untouched. -// * Why this is the remaining lever and not a new pipeline: rounds 7 -// (deeper register prefetch / stage pairs) and 8 (smaller macro-tile, more -// resident waves) both regressed, so the double-buffered 64-K stage (ONE -// __syncthreads per stage, LDS 26,624 B/block, 2 blocks/CU = 8 waves/CU), -// the 128 x 64 macro-tile, the fragment-interleaved packed B swizzle, the -// load_frag8 / load_frag8_swz consumes, the k0-outer / kk-inner ascending -// m16n16k32 int32 accumulation order and the fused branch-free bf16 -// epilogue are ALL retained unchanged. The round removes instructions that -// are provably dead for this shape without touching any resource, any -// memory access order or any MMA input. -// * Correctness: for m % 128 == 0 no block tile crosses m, so the removed -// guard predicate is never false and the removed zero-fill never reaches an -// accumulator; the surviving assignments, the ascending k-chunk sequence -// and the exact `float(dot) * x_scale * weight_scale` multiply order are -// bit-identical to the guarded path (mismatch 0 / max_abs_error 0.0 -// expected). The guarded instantiation preserves the exact current -// semantics for every m % 128 != 0. -// * Predicted deltas: steady-state loop 107 -> ~93 instructions (the eight -// zero-fills and the two EXEC save/restore + uniform-branch pairs removed), -// valu_instructions strictly below 23,920,384, grid_blocks 1312, -// block_threads 256, LDS/block 26,624 B, arch_vgpr <= 128 with 0 spills, -// vmem_read_instructions 1,574,400, vmem_write_instructions 41,984 and the -// epilogue store count all unchanged. Falsifiable: retained only if -// median_us < 912.7147674560547 (the current shadow base) with no p90 -// regression, mismatch_count == 0, max_abs_error == 0.0, -// graph_capture_passed == true, the exact code object shows 0 spills / -// LDS == 26,624 B / VGPR <= 128 (2 blocks/CU) and the kFullTile symbol's -// loop body no longer contains the A-guard -// `s_and_saveexec`/`s_cbranch_execz`/zero-fill sequence; any median/p90 -// regression, mismatch, spill or occupancy loss restores the round-6 -// source. +// Reported launch geometry for this round (iteration 4, tile shape): +// grid_blocks = 21 x 16 = 336 +// waves_per_block = 512 / 64 = 8 +// estimated_active_cus = 120 (K500SM_AI has 120 CUs; 336 blocks with +// 1 resident block/CU = 2.8 blocks/CU occupy every CU) // // Include order is fixed by the control plane: hip_runtime, hip_bfloat16, // then du_mma (this DTK's du_mma.h is not self-contained otherwise). @@ -470,6 +91,12 @@ constexpr int kTileN = 16; constexpr int kTileK = 32; // INT8 DUMMA unit: m16n16k32, int32 accumulation // 4 wavefronts; must remain a multiple of the gfx928 wavefront size (64). constexpr int kThreadsPerBlock = 256; +// Exact-shape packed-B prefill workgroup: 8 wavefronts = 512 threads, with the +// M-major operand-reuse macro-tile (BM=256 x BN=128) selected by the +// iteration-4 tile-shape round. +constexpr int kPackedThreads = 512; +constexpr int kPackedBM = 256; +constexpr int kPackedBN = 128; // Exact assigned shape (K, N) after TP=8 partitioning. constexpr int kExactN = 2624; @@ -477,53 +104,38 @@ constexpr int kExactK = 6144; using namespace du::dumma; -// Coalesced accumulator epilogue for gfx928 INT8 m16n16k32: the accumulator -// lane mapping is row = lane & 15, col_mod4 = lane >> 4, frag.x[i] maps to -// columns col_mod4 + 4*i. A register 4x4 transpose (two 2x2 steps with -// shfl_xor 16 then 32, one v_cndmask per element per step) re-routes the -// int32 values so lane (r, c4) owns the four CONTIGUOUS columns -// base_col + 4*c4 .. +3; they are scaled (float(dot) * x_scale[row] * -// weight_scale[col], the exact reference multiply order), converted to bf16, -// packed and stored with ONE 8-byte store per lane. The row >= m guard is -// wavefront-uniform (lane & 15 cycles the same 16 rows in every 16-lane -// group), so the shuffles never mix active and inactive lanes. base_col is a -// multiple of 64 for the dispatched tiles, so the float4 weight_scale load -// (col0 % 4 == 0) and the 8-byte store (n even -> offset a multiple of 8) are -// aligned. -// -// Round 6: the fused bf16 conversion itself is made branch-free. The exact -// gfx928 code object of the previous best lowers each of the 32 per-lane -// __float2bfloat16 calls to a ~13-instruction sequence that brackets an -// Inf/NaN special case with EXEC save/restore: -// v_and_b32 (b & 0x7f800000) / v_cmp_ne_u32 / s_and_saveexec_b64 / s_xor_b64 -// / v_bfe_u32 (bit 16) / v_add3_u32 (b + lsb + 0x7fff) / s_andn2_saveexec_b64 -// / v_mov_b32 0 / v_or_b32 (b | 0x10000) / v_cmp_eq_u32_sdwa / v_cndmask_b64 -// / s_or_b64 (restore EXEC) -// The EXEC manipulation cannot be software-pipelined and serialises the -// wavefront. For every FINITE fp32 the library conversion is exactly -// round-to-nearest-even at bit 16, which is the branch-free integer identity -// bf16 = (b + 0x7fff + ((b >> 16) & 1)) >> 16, b = __float_as_uint(v) -// (adding 0x7fff + the retained bit-16 LSB never carries out of the sign, and -// the +((b>>16)&1) term breaks exact ties toward an even LSB, so max-float -// still rounds to +/-Inf and -0.0/0.0 are preserved). Every value converted -// here is finite by construction: |int32 dot| < 2^31 and the dequant scales -// are finite, so no Inf/NaN can reach the conversion. This yields the exact -// same 16-bit payloads as __float2bfloat16 while dropping the EXEC sequence -// and the separate shift/or pack (four bf16 pairs become three VALU ops per -// 8-byte word instead of 4 x 13 + 3). +// Branch-free round-to-nearest-even fp32 -> bf16 high-16 payload: +// bf16 = (b + 0x7fff + ((b >> 16) & 1)) >> 16, b = __float_as_uint(v) +// For every FINITE fp32 this is exactly the normal path of __float2bfloat16, +// while dropping the ~13-instruction EXEC-bracketed Inf/NaN sequence this DTK +// emits for the library call (adding 0x7fff + the retained bit-16 LSB cannot +// carry out of the sign; the +((b>>16)&1) term rounds exact ties to an even +// LSB, max-float still rounds to +/-Inf and +/-0.0/subnormal payloads are +// preserved). Everything converted here is finite by construction: |int32 dot| +// < 2^31 and the dequantization scales are finite. __device__ __forceinline__ unsigned int bf16_rne_high16(float v) { const unsigned int b = __float_as_uint(v); return (b + 0x7fffu + ((b >> 16) & 1u)) >> 16; } -// Round 9 adds the compile-time `kGuarded` flag: kGuarded == false is the -// exact-shape full-tile specialization (m % BM == 0), for which every row of -// every block tile is strictly inside the output, so the `row >= m` early -// return -- which the exact code object lowers to an EXEC save/restore pair -// plus a uniform branch around each of the eight unrolled fragment stores -- -// is removed at compile time. The default kGuarded == true keeps the exact -// current semantics for every other m (including the generic arm and the -// single-buffered siblings, which never pass the flag). +// Fused per-row/per-column dequantization + bf16 epilogue for the m16n16k32 +// int32 accumulator of gfx928 INT8 DUMMA. Accumulator lane mapping: +// row = lane & 15, col_mod4 = lane >> 4, frag.x[i] -> column col_mod4 + 4*i. +// A register 4x4 transpose (two 2x2 steps via shfl_xor 16 then 32) re-routes +// the values so lane (r, c4) owns the four CONTIGUOUS columns base_col + +// 4*c4 .. +3, which are then scaled in the exact reference order +// float(dot) * x_scale[row] * weight_scale[col], rounded to bf16 and stored +// with ONE 8-byte store per lane. base_col is a multiple of 64 for the +// dispatched tiles, so the float4 weight_scale load (col0 % 4 == 0) and the +// 8-byte store are both aligned. The row >= m guard is wavefront-uniform +// (lane & 15 cycles the same 16 rows in each 16-lane group), so the shuffles +// never mix active and inactive lanes. +// +// kGuarded == false is the exact-shape full-tile specialization (m % BM == 0): +// every row of every block tile is strictly inside the output, so the dead +// `row >= m` EXEC bracket around each of the eight unrolled stores is removed +// at compile time. The default kGuarded == true keeps the exact guarded +// semantics for every other m. template __device__ __forceinline__ void store_prefill_fragment_coalesced( const AccFragment& frag, @@ -567,8 +179,8 @@ __device__ __forceinline__ void store_prefill_fragment_coalesced( const int f2 = hi ? t0 : a2; const int f3 = hi ? t1 : a3; - // Lane (r, c4) now owns columns base_col + 4*c4 .. +3 (8 B, 8-byte - // aligned: base_col is a multiple of 64, n*2 is a multiple of 8). + // Lane (r, c4) now owns columns base_col + 4*c4 .. +3 (8 B, 8-byte aligned: + // base_col is a multiple of 64 and n*2 is a multiple of 8). const int col0 = base_col + 4 * c4; const float xs = x_scale[row]; const float4 ws = *reinterpret_cast(weight_scale + col0); @@ -588,15 +200,14 @@ __device__ __forceinline__ void store_prefill_fragment_coalesced( packed; } -// Explicit 8-byte int8 fragment loader (lineage-validated load_frag8): for -// BOTH the row_major matrix_a and the col_major matrix_b m16n16k32 fragments, -// lane l -> row (l & 15), k-quarter (l >> 4) holds the EIGHT CONTIGUOUS bytes -// at p[row * ldm + (l >> 4) * 8 .. +7]. Loading the 8 bytes as one int64 -// forces the ds_read_b64 form (16-lane phases, distinct bank pairs per group -// -> the 4-phase conflict-free floor with an odd 8-byte LDS row stride), -// identical fragment values to the library loader, so the v_mmac inputs and -// the exact int32 accumulation order are unchanged. `lane` is -// threadIdx.x % kWaveSize, so no wave-id builtin is required. +// Explicit 8-byte int8 fragment loader for BOTH the row_major matrix_a and the +// col_major matrix_b m16n16k32 fragments: lane l -> row (l & 15), k-quarter +// (l >> 4) holds the EIGHT CONTIGUOUS bytes at p[row * ldm + (l >> 4) * 8 .. +7]. +// Loading the 8 bytes as one int64 forces the ds_read_b64 form (16-lane phases, +// distinct bank pairs per group -> the 4-phase conflict-free floor with an odd +// 8-byte LDS row stride), identical fragment values to the library loader, so +// the v_mmac inputs and the exact int32 accumulation order are unchanged. +// `lane` is threadIdx.x % kWaveSize, so no wave-id builtin is required. template __device__ __forceinline__ void load_frag8( Frag& f, @@ -609,17 +220,17 @@ __device__ __forceinline__ void load_frag8( reinterpret_cast(f.x)[0] = v; } -// Round-5 fragment-interleaved B swizzle helpers. +// Fragment-interleaved B swizzle helpers. // // Each 32-k x 16-n DUMMA B fragment block is 512 B = [k8 0..3][n 0..15][8 B] -// (n-interleaved k8 groups). The 16-byte chunk (k32, n16, k8, n_pair) holds -// the two 8-byte k-runs of n-rows 2*n_pair and 2*n_pair+1 of one k8 group, -// and sits at byte (k32 * n16g + n16) * 512 + k8 * 128 + n_pair * 16 of the -// packed [n>>4][k>>5][512 B] array (n16g = n>>4). Global elements satisfy +// (n-interleaved k8 groups). The 16-byte chunk (k32, n16, k8, n_pair) holds the +// two 8-byte k-runs of n-rows 2*n_pair and 2*n_pair+1 of one k8 group, and sits +// at byte (k32 * n16g + n16) * 512 + k8 * 128 + n_pair * 16 of the packed +// [n>>4][k>>5][512 B] array (n16g = n>>4). Global elements satisfy // packed element (col, kk) at ((kk>>5)*n16g + (col>>4))*512 + // (((kk>>3)&3)*16 + (col&15))*8 + (kk&7) -- the exact inverse of this chunk -// map, so staging is a flat 16-byte copy and the fragment load reads the -// same bytes the Round-3 layout delivered. +// map, so staging is a flat 16-byte copy and the fragment load reads the same +// bytes the library col_major loader consumes. __device__ __forceinline__ int64_t packed_b_swz_offset( int k32, int n16, @@ -630,12 +241,11 @@ __device__ __forceinline__ int64_t packed_b_swz_offset( static_cast(k8) * 128 + (n_pair << 4); } -// Round-5 col_major B fragment loader on the swizzled tile: lane (n = lane&15, +// col_major B fragment loader on the swizzled tile: lane (n = lane&15, // k8 = lane>>4) reads p + k8*128 + n*8. One ds_read_b64 per fragment and each // 16-lane phase spans exactly one 128-byte k8 block (each bank exactly once), // so the read is conflict-free by construction. Byte placement is identical to -// load_frag8, so the v_mmac operands and the int32 accumulation stay -// bit-exact. +// load_frag8, so the v_mmac operands and the int32 accumulation stay bit-exact. template __device__ __forceinline__ void load_frag8_swz( Frag& f, @@ -648,27 +258,50 @@ __device__ __forceinline__ void load_frag8_swz( reinterpret_cast(f.x)[0] = v; } +// Tile-shape round: balanced per-wave quadrant. Pick kWaveRows (hence +// kWaveCols = kWaves / kWaveRows) so that the wave's m16n16k32 panel +// (kWaveM16 x kWaveN16 fragments) is as square as possible. A square panel +// minimises the LDS fragment reads per v_mmac, (kWaveM16 + kWaveN16) reads for +// kWaveM16 * kWaveN16 MMAs, and keeps the live operand fragment set -- and +// therefore the VGPR bill of the 16-accumulator M-major macro-tile -- small. +// The search is restricted to exact divisor splits of both the tile fragment +// counts and the wave count so the kernel's quadrant arithmetic stays valid; +// for the retained 128 x 128 / 8-wave geometry it reproduces the historical +// 2 wave rows x 4 wave columns split exactly. +__host__ __device__ constexpr int balanced_wave_rows(int bm_frags, + int bn_frags, + int waves) { + int best_rows = 1; + int best_diff = 1 << 30; + for (int rows = 1; rows <= waves; ++rows) { + if (waves % rows != 0) continue; + if (bm_frags % rows != 0) continue; + const int cols = waves / rows; + if (bn_frags % cols != 0) continue; + const int m16 = bm_frags / rows; + const int n16 = bn_frags / cols; + const int diff = (m16 > n16) ? (m16 - n16) : (n16 - m16); + if (diff < best_diff) { + best_diff = diff; + best_rows = rows; + } + } + return best_rows; +} + // Generic native INT8 DUMMA m16n16k32 tiled prefill kernel with the identity // [K, N] weight layout (packed_weight == raw logical weight for every (K, N) -// other than the exact assigned pair). Template parameters: BM x BN output -// tile per block, StageK K rows per single-buffered LDS stage. 256 threads = +// other than the exact assigned pair). Template parameters: BM x BN output tile +// per block, StageK K rows per single-buffered LDS stage. 256 threads = // 4 wavefronts; each wave owns a (BM/2) x (BN/2) quadrant of m16n16k32 int32 // accumulator fragments. Grid = (N/BN) x (ceil(M/BM)). // // K is staged cooperatively in a SINGLE LDS buffer at StageK granularity with -// TWO __syncthreads per stage (one after the staging stores, one after all -// waves consumed the buffer): -// * A[BM, StageK] is staged row-major with flat 16-byte vectors (each thread -// strides over kAVectors = BM*StageK/16 vectors); rows past M are -// zero-filled and masked in the epilogue. -// * B[StageK, BN] is staged row-major from the logical [K, N] weight with -// flat 16-byte vectors. The dispatch guarantees n % BN == 0 and -// k % StageK == 0, so every global vector is aligned and in-bounds. -// * Fragment loads use the library du_load_matrix_sync (row_major for both -// operands) and accumulate with du_mma_sync in ascending k order -- -// k0-outer over stages, kk-inner over kTileK steps -- preserving the exact -// int32 accumulation order of the scalar reference. -// * The epilogue is the coalesced fragment store above. +// TWO __syncthreads per stage. Fragment loads use the library +// du_load_matrix_sync (row_major for both operands) and accumulate with +// du_mma_sync in ascending k order -- k0-outer over stages, kk-inner over +// kTileK steps -- preserving the exact int32 accumulation order of the scalar +// reference. The epilogue is the coalesced fragment store above. template __global__ __launch_bounds__(kThreadsPerBlock) void w8a8_dumma_prefill_tiled_kernel( @@ -787,282 +420,146 @@ w8a8_dumma_prefill_tiled_kernel( } } -// Round-1 packed-weight architecture kernel, kept as the single-buffered -// 128-K sibling of the dispatched <128, 64, 64> double-buffered kernel below -// (Round 2 moved the exact-shape dispatch to that sibling; Round 3 keeps this -// kernel compiled but undispatched as the n-major layout / single-buffer -// control, and only the double-buffered kernel reads the packed B; Round 5 -// replaces that packed B layout with the fragment-interleaved swizzle, so this -// n-major control no longer matches the pack either). It is defined for -// the n-major packed_weight[n * K + kk] = raw[kk * N + n] layout produced by -// launch_pack_w8a8_weight. SAME geometry as the bootstrap -// w8a8_dumma_prefill_tiled_kernel<128, 64, 128>: -// grid (N/64) x (M/128) = 41 x 32 = 1312 blocks, 256 threads = 4 wavefronts, -// one 64 x 32 quadrant of eight m16n16k32 int32 accumulators per wave, -// single-buffered 128-K LDS stage with TWO __syncthreads per stage, same -// k0-outer/kk-inner ascending int32 accumulation order, same coalesced -// fragment epilogue. The two differences, both inside the operand path: -// * B is staged n-major (b_tile[n_row][kk] from the packed [N, K] weight) -// so col_major B fragments are 8 contiguous K bytes per lane and staging -// is one coalesced 16-byte vector per (n_row, k16) pair; -// * A and B LDS row strides are 136 B (StageK + 8): 8-byte aligned with an -// odd 8-byte count (136/8 = 17) so the 16 rows of each 16-lane ds_read_b64 -// phase land on 16 distinct bank pairs (the conflict-free 4-phase floor), -// and 136 % 16 == 8 keeps the two-half int64 staging form. -// Both fragment loaders are the explicit load_frag8 above; the staged bytes -// are the same matrix elements in the same fragment slots as the library -// loaders produced from the identity layout, so every v_mmac input and the -// exact int32 accumulation (k0-outer over 48 stages, kk-inner ascending over -// four m16n16k32 steps) are bit-identical to the bootstrap kernel and to the -// scalar reference. -template -__global__ __launch_bounds__(kThreadsPerBlock) void -w8a8_dumma_prefill_packedb_kernel( - const int8_t* __restrict__ x_q, // [M, K] row-major - const int8_t* __restrict__ packed_w, // [N, K] n-major (exact shape) - const float* __restrict__ x_scale, // [M] - const float* __restrict__ weight_scale, // [N] - hip_bfloat16* __restrict__ out, // [M, N] - int m, - int n, - int k) { - constexpr int kWaveM16 = BM / 32; - constexpr int kWaveN16 = BN / 32; - static_assert(BM % 32 == 0 && BN % 32 == 0, - "block tile must be a multiple of the wave quadrant"); - static_assert(kThreadsPerBlock % kWaveSize == 0, - "blockDim must be a multiple of the gfx928 wavefront size"); - // Bank-safe LDS row stride: StageK + 8 bytes = 34 dwords for StageK = 128. - // 8-aligned with an odd 8-byte count -> every lane's 8-byte fragment is a - // ds_read_b64 at the 4-phase conflict-free floor (see the kernel comment - // above); 8 mod 16 keeps the two-half staging store form. - constexpr int kLdsStride = StageK + 8; - static_assert(kLdsStride % 8 == 0 && (kLdsStride / 8) % 2 == 1, - "LDS row stride must be 8-aligned with an odd 8-byte count"); - static_assert(kLdsStride % 16 == 8, - "LDS row stride must be 8 mod 16 for the two-half staging"); - - const int tid = static_cast(threadIdx.x); - const int wave = tid / kWaveSize; - const int lane = tid % kWaveSize; - const int wave_row = wave >> 1; // 0 or 1 - const int wave_col = wave & 1; // 0 or 1 - const int m0 = static_cast(blockIdx.y) * BM; - const int n0 = static_cast(blockIdx.x) * BN; - - __shared__ __align__(16) int8_t a_tile[BM * kLdsStride]; - __shared__ __align__(16) int8_t b_tile[BN * kLdsStride]; - - DUFragment - a_frag[kWaveM16]; - DUFragment - b_frag[kWaveN16]; - DUFragment - acc[kWaveM16][kWaveN16]; -#pragma unroll - for (int j = 0; j < kWaveM16; ++j) { -#pragma unroll - for (int i = 0; i < kWaveN16; ++i) { - du_fill_fragment(acc[j][i], 0); - } - } - - // Flat 16-byte staging vectors per stage (the dispatch guarantees - // k % StageK == 0 and n % BN == 0). 8 vectors per StageK row for 128. - constexpr int kVecsPerRow = StageK / static_cast(sizeof(int4)); - constexpr int kAVectors = BM * StageK / static_cast(sizeof(int4)); - constexpr int kBVectors = StageK * BN / static_cast(sizeof(int4)); - - for (int k0 = 0; k0 < k; k0 += StageK) { - // Stage A[BM, StageK] row-major with the bank-safe row stride: thread - // `vec` owns the 16-byte chunk at (local_row, k16); rows past M are - // zero-filled and masked in the epilogue. - for (int vec = tid; vec < kAVectors; vec += kThreadsPerBlock) { - const int local_row = vec / kVecsPerRow; - const int k16 = vec - local_row * kVecsPerRow; - const int g_row = m0 + local_row; - int4 v{0, 0, 0, 0}; - if (g_row < m) { - v = *reinterpret_cast( - x_q + static_cast(g_row) * k + k0 + k16 * 16); - } - // Two 8-byte halves: the row stride is 8 mod 16, so odd rows are not - // 16-byte aligned and a single int4 LDS store is illegal. The int64 - // pair lowers to one ds_write2_b64 per thread. - const int64_t* src64 = reinterpret_cast(&v); - int64_t* dst64 = reinterpret_cast( - a_tile + local_row * kLdsStride + k16 * 16); - dst64[0] = src64[0]; - dst64[1] = src64[1]; - } - // Stage B[BN, StageK] n-major from the packed [N, K] weight: thread `vec` - // owns the 16-byte chunk at (n_row, k16); the K axis is always in range. - for (int vec = tid; vec < kBVectors; vec += kThreadsPerBlock) { - const int n_row = vec / kVecsPerRow; - const int k16 = vec - n_row * kVecsPerRow; - const int4 v = *reinterpret_cast( - packed_w + static_cast(n0 + n_row) * k + k0 + k16 * 16); - const int64_t* src64 = reinterpret_cast(&v); - int64_t* dst64 = reinterpret_cast( - b_tile + n_row * kLdsStride + k16 * 16); - dst64[0] = src64[0]; - dst64[1] = src64[1]; - } - // Make the staged tiles visible to every wavefront. - __syncthreads(); - - // Consume the stage: each wave accumulates its (BM/2) x (BN/2) quadrant - // with kk-ascending m16n16k32 MMAs; one ds_read_b64 per fragment. - const int local_row = wave_row * (BM / 2); - const int local_col = wave_col * (BN / 2); -#pragma unroll - for (int kk = 0; kk < StageK; kk += kTileK) { -#pragma unroll - for (int j = 0; j < kWaveM16; ++j) { - load_frag8( - a_frag[j], - a_tile + (local_row + j * kTileM) * kLdsStride + kk, - static_cast(kLdsStride), - lane); - } -#pragma unroll - for (int i = 0; i < kWaveN16; ++i) { - load_frag8( - b_frag[i], - b_tile + (local_col + i * kTileN) * kLdsStride + kk, - static_cast(kLdsStride), - lane); - } -#pragma unroll - for (int j = 0; j < kWaveM16; ++j) { -#pragma unroll - for (int i = 0; i < kWaveN16; ++i) { - du_mma_sync(acc[j][i], a_frag[j], b_frag[i], acc[j][i]); - } - } - } - // Single buffer: every wavefront must finish reading LDS before the next - // stage's stores overwrite it. - __syncthreads(); - } - - const int base_row = m0 + wave_row * (BM / 2); - const int base_col = n0 + wave_col * (BN / 2); -#pragma unroll - for (int j = 0; j < kWaveM16; ++j) { -#pragma unroll - for (int i = 0; i < kWaveN16; ++i) { - store_prefill_fragment_coalesced( - acc[j][i], x_scale, weight_scale, out, m, n, - base_row + j * kTileM, base_col + i * kTileN, lane); - } - } -} - -// Round 9 exact-shape A-staging load. For the dispatched M=4096 every 128-row -// block tile lies fully inside the output, so the `(g_row < m)` row guard is -// dead: the exact code object lowers it to eight unconditional zero-fill -// `v_mov_b32` plus two EXEC save/restore (`s_and_saveexec_b64` / -// `s_cbranch_execz` / `s_or_b64`) uniform-branch pairs per K stage, i.e. ~14 -// of the 107 steady-state loop instructions and an EXEC serialization in -// front of the very `global_load_dwordx4`s that prefetch the next tile. The -// kFullTile instantiation removes the select (one coalesced load per staging -// vector, no zero-fill), while kFullTile == false keeps the zero-fill + row -// mask semantics identical to the current kernel for every m % 128 != 0. +// Exact-shape A-staging load. For the dispatched M=4096 every 256-row block +// tile lies fully inside the output, so the `(g_row < m)` row guard is dead: +// the exact code object would otherwise lower it to unconditional zero-fills +// plus EXEC save/restore uniform-branch pairs per K stage, i.e. dead +// instructions and an EXEC serialization in front of the very +// global_load_dwordx4s that prefetch the next tile. The kFullTile +// instantiation removes the select (one coalesced load per staging vector, no +// zero-fill), while kFullTile == false keeps the zero-fill + row mask +// semantics identical for every m % BM != 0. template __device__ __forceinline__ int4 load_a_stage_vec( - const int8_t* __restrict__ x_q, - int64_t offset, + const int8_t* __restrict__ p, int m, int g_row) { if (kFullTile) { - return *reinterpret_cast(x_q + offset); + return *reinterpret_cast(p); } - return (g_row < m) ? *reinterpret_cast(x_q + offset) - : int4{0, 0, 0, 0}; + return (g_row < m) ? *reinterpret_cast(p) : int4{0, 0, 0, 0}; +} + +// Exact-shape A-staging load through the GLOBAL (non-generic) address form: +// the caller passes the kernel's `x_q` base plus the per-stage byte offset, so +// the load visits exactly the same bytes as the pointer form but through the +// global address space (global_load_dwordx4) instead of the generic +// flat_load_dwordx4 path the pointer form lowers to. Used only by the +// exact-shape full-tile instantiation. +__device__ __forceinline__ int4 load_a_stage_vec_global( + const int8_t* __restrict__ x_q, + int64_t offset) { + return *reinterpret_cast(x_q + offset); } -// Round-2 double-buffered packed-B prefill kernel, dispatched ONLY for -// (m >= 128, n == 2624, k == 6144). SAME macro-tile and SAME operand path as -// the accepted w8a8_dumma_prefill_packedb_kernel<128, 64, 128>: grid -// (N/64) x (M/128) = 41 x 32 = 1312 blocks, 256 threads = 4 wavefronts, one -// 64 x 32 quadrant of eight m16n16k32 int32 accumulators per wave, packed -// n-major B, explicit load_frag8 reads, coalesced epilogue. The ONLY change is -// the K pipeline (see the Round-2 section of the file header): -// * 64-K DOUBLE-buffered stages (2 x 13,824 = 27,648 B LDS/block, still -// 2 blocks/CU = 8 waves/CU): the stage-(s+1) global loads are issued into -// 3 int4 staging registers per thread BEFORE the stage-s consume and -// flushed into the idle LDS buffer after it, so the VMEM latency of stage -// s+1 overlaps the LDS/v_mmac work of stage s (one __syncthreads per -// stage instead of two); -// * k0-outer over 96 ASCENDING 64-K stages with kk-inner ascending over two -// m16n16k32 steps, i.e. the same (k0 + kk) k-chunk sequence -// 0, 32, 64, 96, ... as the accepted 48 x 128-K stages, so every int32 -// accumulator is bit-identical. -// kLdsStride = StageK + 8 = 72 B: 8-aligned with an odd 8-byte count (9) so -// the 16 rows of each 16-lane ds_read_b64 phase land on 16 distinct bank pairs +// Double-buffered packed-B prefill kernel, dispatched ONLY for +// (m >= 128, n == 2624, k == 6144). Geometry: grid ((N+127)/128) x (M/256) = +// 21 x 16 = 336 blocks, 512 threads = 8 wavefronts, and each wave owns a +// kRowsPerWave x kColsPerWave = 64 x 64 quadrant of sixteen m16n16k32 int32 +// accumulator fragments (wave_row = wave / kWaveCols, wave_col = wave % +// kWaveCols, with the balanced 4 x 2 row/column split chosen by +// balanced_wave_rows), so the block macro-tile is BM x BN = 256 x 128. +// +// OPERAND REUSE (the mandated comparison). Per macro-tile every staged A +// element is consumed by the TWO wave columns (A reuse 2) and every staged B +// element by the FOUR wave rows (B reuse 4). Direct per-wave global fragment +// loads cannot replace the LDS stage: a row_major matrix_a fragment phase is +// lane (row = lane & 15, kq = lane >> 4), so 16 lanes touch 16 rows that are +// K = 6,144 B apart -- 16 x 32 B sectors for 128 useful bytes, a 4x fetch +// amplification -- and the swizzled col_major matrix_b fragments would lose +// their on-chip reuse and multiply their fetch. Global REUSE is the lever, so +// the M-major macro-tile is widened to BM = 256: A is read once per 128-column +// tile (21 re-reads) and B once per 256-row tile (16 re-reads), for a total +// global traffic of M*K*21 + N*K*16 = 786 MB against the 128 x 128 tile's +// 1,044 MB (-24.7%), at the same 8 waves/CU. The staging loads stay +// vectorized/coalesced 16-byte-per-thread and the A LDS stride stays the +// bank-safe 72 B with the flat swizzled B tile. The odd N = 20*128 + 64 is +// covered by clamping the column tile origin to n - BN, so the last block +// recomputes 64 overlapping columns (identical int32 accumulation and identical +// bf16 bytes) instead of predicating a partial tile; every staged B chunk +// therefore addresses a valid n16 group of the packed weight. +// +// The K pipeline is a 64-K DOUBLE-buffered stage: the stage-(s+1) global loads +// are issued into 2 int4 staging registers per thread BEFORE the stage-s consume +// and flushed into the idle LDS buffer after it, so the VMEM latency of stage +// s+1 overlaps the LDS/v_mmac work of stage s (one __syncthreads per stage +// instead of two). k0-outer runs over 96 ASCENDING 64-K stages with kk-inner +// ascending over two m16n16k32 steps, i.e. the same (k0 + kk) k-chunk sequence +// 0, 32, 64, ... as any other staging, so every int32 accumulator is +// bit-identical. +// +// kLdsStride = StageK + 8 = 72 B: 8-aligned with an odd 8-byte count (9) so the +// 16 rows of each 16-lane ds_read_b64 phase land on 16 distinct bank pairs // (18r mod 32, r = 0..15) -- the conflict-free 4-phase floor -- and // 72 % 16 == 8 keeps the two-half ds_write2_b64 staging form. // -// Round 5 additionally reads the packed B operand from the fragment- -// interleaved swizzle (see w8a8_pack_bswz_i8_kernel and the Round-5 header -// section): the LDS b_tile is now the SAME [k32][n16][k8][n][8] layout as the -// pack (2 x 256 16-byte chunks per 64-K stage), so staging is one flat -// 16-byte copy per thread and each col_major B fragment is one conflict-free -// ds_read_b64 (load_frag8_swz). The int32 accumulation order is untouched. +// The packed B operand is the fragment-interleaved swizzle (see +// w8a8_pack_bswz_i8_kernel): the LDS b_tile is the SAME [k32][n16][k8][n][8] +// layout as the pack (2 x (BN/16) 512-byte fragment blocks per 64-K stage), so +// staging is one flat 16-byte copy per thread and each col_major B fragment is +// one conflict-free ds_read_b64 (load_frag8_swz). The int32 accumulation order +// is untouched. // -// Round 9 adds the compile-time `kFullTile` flag: it is true exactly when the -// caller's m is a multiple of BM, so every block tile is fully inside the -// output. In that instantiation the A staging loads drop the row guard (see -// load_a_stage_vec) and the epilogue drops the per-fragment row mask (see -// store_prefill_fragment_coalesced); false keeps the guarded form -// byte-for-byte. Geometry, resources, operand path and accumulation order are -// identical in both instantiations. -template -__global__ __launch_bounds__(kThreadsPerBlock) void +// kFullTile is true exactly when the caller's m is a multiple of BM, so every +// block tile is fully inside the output; in that instantiation the A staging +// loads drop the row guard and the epilogue drops the per-fragment row mask. +template +__global__ __launch_bounds__(kBlockThreads) void w8a8_dumma_prefill_packedb_db_kernel( const int8_t* __restrict__ x_q, // [M, K] row-major const int8_t* __restrict__ packed_w, // [n>>4][k>>5][512 B] swizzled B - // fragment blocks (round-5 layout) + // fragment blocks const float* __restrict__ x_scale, // [M] const float* __restrict__ weight_scale, // [N] hip_bfloat16* __restrict__ out, // [M, N] int m, int n, int k) { - constexpr int kWaveM16 = BM / 32; - constexpr int kWaveN16 = BN / 32; - static_assert(BM % 32 == 0 && BN % 32 == 0, + constexpr int kWaves = kBlockThreads / kWaveSize; // 8 + // Balanced, tile-aware wave split: 4 wave rows x 2 wave columns for the + // 256 x 128 macro-tile, i.e. a square 64 x 64 per-wave quadrant. + constexpr int kWaveRows = + balanced_wave_rows(BM / kTileM, BN / kTileN, kWaves); // 4 + constexpr int kWaveCols = kWaves / kWaveRows; // 2 + constexpr int kRowsPerWave = BM / kWaveRows; // 64 + constexpr int kColsPerWave = BN / kWaveCols; // 64 + constexpr int kWaveM16 = kRowsPerWave / kTileM; // 4 + constexpr int kWaveN16 = kColsPerWave / kTileN; // 4 + static_assert(kWaveRows * kWaveCols == kWaves, + "wave split must tile the block exactly"); + static_assert(BM % kWaveRows == 0 && BN % kWaveCols == 0, "block tile must be a multiple of the wave quadrant"); - static_assert(kThreadsPerBlock % kWaveSize == 0, + static_assert(kBlockThreads % kWaveSize == 0, "blockDim must be a multiple of the gfx928 wavefront size"); constexpr int kLdsStride = StageK + 8; static_assert(kLdsStride % 8 == 0 && (kLdsStride / 8) % 2 == 1, "LDS row stride must be 8-aligned with an odd 8-byte count"); static_assert(kLdsStride % 16 == 8, "LDS row stride must be 8 mod 16 for the two-half staging"); - // Round 5: the packed B weight is the fragment-interleaved swizzle (see - // w8a8_pack_bswz_i8_kernel), defined for BN == 64 and StageK == 64 (one - // 32-k x 16-n fragment block per 512 B; 2 k32 blocks x 4 n16 groups x 4 k8 - // groups x 8 n-pairs = 256 16-byte chunks per stage, one per thread). Only - // <128, 64, 64> is ever dispatched by the exact-shape guard, so the chunk - // index math below is exact. - static_assert(BN == 64 && StageK == 64, - "swizzled B pack requires BN == 64 and StageK == 64"); + // The packed B weight is the fragment-interleaved swizzle: one 32-k x 16-n + // fragment block per 512 B; a 64-K stage holds 2 k32 blocks x (BN/16) n16 + // groups x 4 k8 groups x 8 n-pairs = BN*4 16-byte chunks. With 512 threads + // and BN = 128 that is exactly one chunk per thread. + static_assert(StageK == 64, "swizzled B pack requires StageK == 64"); + static_assert(BN % 16 == 0, "swizzled B pack requires BN % 16 == 0"); const int tid = static_cast(threadIdx.x); const int wave = tid / kWaveSize; const int lane = tid % kWaveSize; - const int wave_row = wave >> 1; // 0 or 1 - const int wave_col = wave & 1; // 0 or 1 + const int wave_row = wave / kWaveCols; // 0 .. kWaveRows-1 + const int wave_col = wave % kWaveCols; // 0 .. kWaveCols-1 const int m0 = static_cast(blockIdx.y) * BM; - const int n0 = static_cast(blockIdx.x) * BN; + // Clamp the column origin so the odd (n % BN) columns are covered by an + // overlapping tile: every staged B chunk then addresses a valid n16 group of + // the packed weight, and the duplicated columns recompute byte-identically. + const int n0i = static_cast(blockIdx.x) * BN; + const int n0 = (n0i + BN <= n) ? n0i : (n - BN); const int kStages = k / StageK; // dispatch guarantees k % StageK == 0 const int n16g = n >> 4; // 16-n row groups per 32-k block of the pack __shared__ __align__(16) int8_t a_tile[2][BM * kLdsStride]; - // Round-5 swizzled B tile: flat [k32][n16][k8][n_pair][16 B] = 4,096 B per - // 64-K stage, byte-identical to the packed global layout of that stage. + // Swizzled B tile: flat [k32][n16][k8][n_pair][16 B], byte-identical to the + // packed global layout of that stage: 2 x (BN/16) x 512 = BN x 64 B. __shared__ __align__(16) int8_t b_tile[2][BN * StageK]; DUFragment @@ -1079,56 +576,87 @@ w8a8_dumma_prefill_packedb_db_kernel( } } - // Flat 16-byte staging vectors per stage. For the dispatched <128, 64, 64> - // instantiation the 512 A vectors and 256 B vectors divide evenly over the - // 256 threads (2 + 1 per thread), so the prefetch payload is 3 int4 staging - // registers held live across the consume. + // Flat 16-byte staging vectors per stage. For the dispatched + // <256, 128, 64, 512> instantiation the 1024 A vectors (2 per thread) and the + // 512 B vectors (1 per thread) map exactly onto the 512 threads, so the + // prefetch payload is 3 int4 staging registers held live across the consume. constexpr int kVecsPerRow = StageK / static_cast(sizeof(int4)); constexpr int kAVectors = BM * StageK / static_cast(sizeof(int4)); constexpr int kBVectors = StageK * BN / static_cast(sizeof(int4)); - constexpr int kAVecPerThread = kAVectors / kThreadsPerBlock; - constexpr int kBVecPerThread = kBVectors / kThreadsPerBlock; - static_assert(kAVectors % kThreadsPerBlock == 0 && - kBVectors % kThreadsPerBlock == 0, + constexpr int kAVecPerThread = kAVectors / kBlockThreads; + constexpr int kBVecPerThread = kBVectors / kBlockThreads; + static_assert(kAVectors % kBlockThreads == 0 && + kBVectors % kBlockThreads == 0, "staging vectors must divide evenly across the block"); - // Round 5: the swizzled B stage is exactly one 16-byte chunk per thread, so - // thread `vec` = tid is also the flat [k32][n16][k8][n_pair] chunk index. - static_assert(kBVecPerThread == 1 && kBVectors == kThreadsPerBlock, + // The swizzled B stage is exactly one 16-byte chunk per thread, so thread + // `vec` = tid is also the flat [k32][n16][k8][n_pair] chunk index. + static_assert(kBVecPerThread == 1 && kBVectors == kBlockThreads, "swizzled B staging must be one 16-B chunk per thread"); int4 a_pre[kAVecPerThread]; int4 b_pre[kBVecPerThread]; + // Per-thread GLOBAL staging byte pointers, held as loop-carried induction + // variables: the prologue loads stage 0 from them, they are then advanced to + // stage 1, and every steady-state iteration issues its prefetch from them and + // walks them by the compile-time stage stride -- StageK bytes for A and one + // 64-K swizzle band `(StageK>>5)*n16g*512` for B. + const int8_t* a_src[kAVecPerThread]; + int a_grow[kAVecPerThread]; +#pragma unroll + for (int v = 0; v < kAVecPerThread; ++v) { + const int vec = tid + v * kBlockThreads; + const int local_row = vec / kVecsPerRow; + const int k16 = vec - local_row * kVecsPerRow; + a_grow[v] = m0 + local_row; + a_src[v] = x_q + static_cast(a_grow[v]) * k + k16 * 16; + } + const int8_t* b_src[kBVecPerThread]; +#pragma unroll + for (int v = 0; v < kBVecPerThread; ++v) { + // Chunk (k32, n16, k8, n_pair) of the stage: BN*2 chunks per k32 block + // (4 k8 groups x 8 n-pairs per n16), 32 chunks per n16 group. + const int vec = tid + v * kBlockThreads; + const int k32 = vec / (BN * 2); + const int rem = vec - k32 * (BN * 2); + b_src[v] = packed_w + + packed_b_swz_offset( + k32, (n0 >> 4) + ((rem >> 5) & (BN / 16 - 1)), n16g, + (rem >> 3) & 3, rem & 7); + } + // ---- Prologue: issue the stage-0 global loads, flush them into buffer 0 and // make the tile visible to every wavefront. { - const int k0 = 0; #pragma unroll for (int v = 0; v < kAVecPerThread; ++v) { - const int vec = tid + v * kThreadsPerBlock; + const int vec = tid + v * kBlockThreads; const int local_row = vec / kVecsPerRow; const int k16 = vec - local_row * kVecsPerRow; const int g_row = m0 + local_row; - a_pre[v] = load_a_stage_vec( - x_q, static_cast(g_row) * k + k0 + k16 * 16, m, g_row); + if (kFullTile) { + // Exact-shape full-tile A staging through the global (non-generic) + // address form -- the same bytes as the pointer form, but through the + // load path that emits global_load_dwordx4. + a_pre[v] = load_a_stage_vec_global( + x_q, static_cast(g_row) * k + k16 * 16); + } else { + a_pre[v] = load_a_stage_vec(a_src[v], m, a_grow[v]); + } } #pragma unroll for (int v = 0; v < kBVecPerThread; ++v) { - const int vec = tid + v * kThreadsPerBlock; - // Round 5: the 16-byte chunk (k32 = vec>>7, n16 = (vec>>5)&3, + // The 16-byte chunk (k32 = vec/(BN*2), n16 = (vec>>5)&(BN/16-1), // k8 = (vec>>3)&3, n_pair = vec&7) of the swizzled pack sits at byte - // ((k0>>5)+k32)*n16g*512 + ((n0>>4)+n16)*512 + k8*128 + n_pair*16; - // consecutive lanes read consecutive 16-byte chunks, so a warp's + // (k0>>5 + k32)*n16g*512 + ((n0>>4)+n16)*512 + k8*128 + n_pair*16; the + // per-thread pointer already carries the k32/n16/k8/n_pair part for + // k0 == 0. Consecutive lanes read consecutive 16-byte chunks, so a warp's // staging loads touch 8 fully-used 128-byte lines. - b_pre[v] = *reinterpret_cast( - packed_w + - packed_b_swz_offset((k0 >> 5) + (vec >> 7), - (n0 >> 4) + ((vec >> 5) & 3), n16g, - (vec >> 3) & 3, vec & 7)); + b_pre[v] = *reinterpret_cast(b_src[v]); } #pragma unroll for (int v = 0; v < kAVecPerThread; ++v) { - const int vec = tid + v * kThreadsPerBlock; + const int vec = tid + v * kBlockThreads; const int local_row = vec / kVecsPerRow; const int k16 = vec - local_row * kVecsPerRow; const int64_t* src64 = reinterpret_cast(&a_pre[v]); @@ -1139,112 +667,157 @@ w8a8_dumma_prefill_packedb_db_kernel( } #pragma unroll for (int v = 0; v < kBVecPerThread; ++v) { - const int vec = tid + v * kThreadsPerBlock; - // Round 5: the swizzled tile is a flat copy of the packed stage, so the - // commit is ONE 16-byte-aligned int4 (ds_write_b128) per thread. + const int vec = tid + v * kBlockThreads; + // The swizzled tile is a flat copy of the packed stage, so the commit is + // ONE 16-byte-aligned int4 (ds_write_b128) per thread. *reinterpret_cast(b_tile[0] + vec * 16) = b_pre[v]; } } __syncthreads(); - // ---- Steady state: for stage s, issue the stage-(s+1) global loads into the - // staging registers BEFORE consuming stage s from buffer s&1, then flush the - // prefetched vectors into the idle buffer (s+1)&1 and close the stage with - // ONE __syncthreads (so the vmcnt wait for stage s+1 lands after the MMAC - // burst of stage s, overlapping the VMEM latency of s+1 with the stage-s - // LDS/v_mmac work). The last stage is consumed after the loop without a - // trailing barrier. - for (int s = 0; s < kStages - 1; ++s) { - // Prefetch stage s+1: issue the global loads (held in VGPRs while the - // stage-s consume runs). - const int k0 = (s + 1) * StageK; + // Advance the staging pointers to stage 1. From here on every steady-state + // half prefetches stage s+1 from these pointers and then walks them by the + // compile-time stage stride, so both global staging addresses are pure + // pointer increments instead of a per-stage address expression. + const int64_t b_step = static_cast(StageK >> 5) * n16g * 512; #pragma unroll - for (int v = 0; v < kAVecPerThread; ++v) { - const int vec = tid + v * kThreadsPerBlock; - const int local_row = vec / kVecsPerRow; - const int k16 = vec - local_row * kVecsPerRow; - const int g_row = m0 + local_row; - a_pre[v] = load_a_stage_vec( - x_q, static_cast(g_row) * k + k0 + k16 * 16, m, g_row); - } + for (int v = 0; v < kAVecPerThread; ++v) { + a_src[v] += StageK; + } #pragma unroll - for (int v = 0; v < kBVecPerThread; ++v) { - const int vec = tid + v * kThreadsPerBlock; - // Round 5: the 16-byte chunk (k32 = vec>>7, n16 = (vec>>5)&3, - // k8 = (vec>>3)&3, n_pair = vec&7) of the swizzled pack sits at byte - // ((k0>>5)+k32)*n16g*512 + ((n0>>4)+n16)*512 + k8*128 + n_pair*16; - // consecutive lanes read consecutive 16-byte chunks, so a warp's - // staging loads touch 8 fully-used 128-byte lines. - b_pre[v] = *reinterpret_cast( - packed_w + - packed_b_swz_offset((k0 >> 5) + (vec >> 7), - (n0 >> 4) + ((vec >> 5) & 3), n16g, - (vec >> 3) & 3, vec & 7)); - } + for (int v = 0; v < kBVecPerThread; ++v) { + b_src[v] += b_step; + } - // Consume stage s from buffer s&1: each wave accumulates its (BM/2) x - // (BN/2) quadrant with kk-ascending m16n16k32 MMAs; one ds_read_b64 per - // fragment. - const int local_row = wave_row * (BM / 2); + // ---- Steady state (double-buffer parity specialization). For stage s, issue + // the stage-(s+1) global loads into the staging registers BEFORE consuming + // stage s from buffer s&1, then flush the prefetched vectors into the idle + // buffer (s+1)&1 and close the stage with ONE __syncthreads (so the vmcnt wait + // for stage s+1 lands after the MMAC burst of stage s, overlapping the VMEM + // latency of s+1 with the stage-s LDS/v_mmac work). The last stage is consumed + // after the loop without a trailing barrier. + // + // Walking the stages in compile-time PAIRS makes each half's parity a + // constant (s = 2*sp + h with h unrolled, so s & 1 == h and (s+1) & 1 == h^1), + // which folds the double-buffer parity offsets into the per-half LDS base + // registers / instruction immediates. The odd leftover for the dispatched + // k = 6144 (kStages = 96 -> kSteady = 95, one unpaired stage) is guarded by ONE + // uniform compare in the second half of the last pair, so correctness does not + // depend on the unroll succeeding. + const int kSteady = kStages - 1; + const int kPairIters = (kSteady + 1) >> 1; + for (int sp = 0; sp < kPairIters; ++sp) { #pragma unroll - for (int kk = 0; kk < StageK; kk += kTileK) { + for (int h = 0; h < 2; ++h) { + const int s = 2 * sp + h; + if (h == 0 || s < kSteady) { + // Compile-time buffer parity: cur == s & 1, nxt == (s + 1) & 1. + const int cur = h; + const int nxt = h ^ 1; + // Prefetch stage s+1: issue the global loads (held in VGPRs while the + // stage-s consume runs) from the loop-carried per-thread stage + // pointers, which already sit at the stage-(s+1) addresses. The + // exact-shape full-tile instantiation forms the two A staging addresses + // in the `x_q + ` global form (so the backend + // keeps the non-generic global_load path), while B keeps the + // loop-carried staging pointer. + const int k0 = (s + 1) * StageK; #pragma unroll - for (int j = 0; j < kWaveM16; ++j) { - load_frag8( - a_frag[j], - a_tile[s & 1] + (local_row + j * kTileM) * kLdsStride + kk, - static_cast(kLdsStride), - lane); - } + for (int v = 0; v < kAVecPerThread; ++v) { + const int vec = tid + v * kBlockThreads; + const int local_row = vec / kVecsPerRow; + const int k16 = vec - local_row * kVecsPerRow; + const int g_row = m0 + local_row; + if (kFullTile) { + a_pre[v] = load_a_stage_vec_global( + x_q, static_cast(g_row) * k + k0 + k16 * 16); + } else { + a_pre[v] = load_a_stage_vec(a_src[v], m, a_grow[v]); + } + } #pragma unroll - for (int i = 0; i < kWaveN16; ++i) { - // Round 5: fragment block (k32 = kk>>5, n16 = wave_col*kWaveN16 + i) - // of the swizzled tile -- 512 B per 16-n x 32-k DUMMA B fragment, one - // conflict-free ds_read_b64 per lane. - load_frag8_swz( - b_frag[i], - b_tile[s & 1] + - ((kk >> 5) * (BN / 16) + wave_col * kWaveN16 + i) * 512, - lane); - } + for (int v = 0; v < kBVecPerThread; ++v) { + // One plain 16-byte staging load per thread from the stage-(s+1) + // swizzle chunk; consecutive lanes read consecutive 16-byte chunks, + // so a warp's staging loads touch 8 fully-used 128-byte lines. + b_pre[v] = *reinterpret_cast(b_src[v]); + } + + // Consume stage s from buffer cur: each wave accumulates its + // kRowsPerWave x kColsPerWave quadrant with kk-ascending m16n16k32 MMAs; + // one ds_read_b64 per fragment. + const int local_row = wave_row * kRowsPerWave; #pragma unroll - for (int j = 0; j < kWaveM16; ++j) { + for (int kk = 0; kk < StageK; kk += kTileK) { #pragma unroll - for (int i = 0; i < kWaveN16; ++i) { - du_mma_sync(acc[j][i], a_frag[j], b_frag[i], acc[j][i]); + for (int j = 0; j < kWaveM16; ++j) { + load_frag8( + a_frag[j], + a_tile[cur] + (local_row + j * kTileM) * kLdsStride + kk, + static_cast(kLdsStride), + lane); + } +#pragma unroll + for (int i = 0; i < kWaveN16; ++i) { + // Fragment block (k32 = kk>>5, n16 = wave_col*kWaveN16 + i) of the + // swizzled tile -- 512 B per 16-n x 32-k DUMMA B fragment, one + // conflict-free ds_read_b64 per lane. + load_frag8_swz( + b_frag[i], + b_tile[cur] + + ((kk >> 5) * (BN / 16) + wave_col * kWaveN16 + i) * 512, + lane); + } +#pragma unroll + for (int j = 0; j < kWaveM16; ++j) { +#pragma unroll + for (int i = 0; i < kWaveN16; ++i) { + du_mma_sync(acc[j][i], a_frag[j], b_frag[i], acc[j][i]); + } + } } - } - } - // Flush the prefetched stage-(s+1) vectors into the idle buffer. - { - const int buf = (s + 1) & 1; + // Flush the prefetched stage-(s+1) vectors into the idle buffer nxt. + { #pragma unroll - for (int v = 0; v < kAVecPerThread; ++v) { - const int vec = tid + v * kThreadsPerBlock; - const int local_row = vec / kVecsPerRow; - const int k16 = vec - local_row * kVecsPerRow; - const int64_t* src64 = reinterpret_cast(&a_pre[v]); - int64_t* dst64 = reinterpret_cast( - a_tile[buf] + local_row * kLdsStride + k16 * 16); - dst64[0] = src64[0]; - dst64[1] = src64[1]; - } + for (int v = 0; v < kAVecPerThread; ++v) { + const int vec = tid + v * kBlockThreads; + const int local_row = vec / kVecsPerRow; + const int k16 = vec - local_row * kVecsPerRow; + const int64_t* src64 = reinterpret_cast(&a_pre[v]); + int64_t* dst64 = reinterpret_cast( + a_tile[nxt] + local_row * kLdsStride + k16 * 16); + dst64[0] = src64[0]; + dst64[1] = src64[1]; + } +#pragma unroll + for (int v = 0; v < kBVecPerThread; ++v) { + const int vec = tid + v * kBlockThreads; + // Flat 16-byte swizzled-tile commit (ds_write_b128). + *reinterpret_cast(b_tile[nxt] + vec * 16) = b_pre[v]; + } + } + // Walk the loop-carried staging pointers to the next stage. StageK is a + // compile-time constant, so each A pointer advances with a single 64-bit + // add pair; b_step is the loop-invariant swizzle band. +#pragma unroll + for (int v = 0; v < kAVecPerThread; ++v) { + a_src[v] += StageK; + } #pragma unroll - for (int v = 0; v < kBVecPerThread; ++v) { - const int vec = tid + v * kThreadsPerBlock; - // Round 5: flat 16-byte swizzled-tile commit (ds_write_b128). - *reinterpret_cast(b_tile[buf] + vec * 16) = b_pre[v]; + for (int v = 0; v < kBVecPerThread; ++v) { + b_src[v] += b_step; + } + __syncthreads(); } } - __syncthreads(); } // ---- Tail: consume the last stage (its data was flushed before the last // barrier; no trailing barrier needed). { const int s = kStages - 1; - const int local_row = wave_row * (BM / 2); + const int local_row = wave_row * kRowsPerWave; #pragma unroll for (int kk = 0; kk < StageK; kk += kTileK) { #pragma unroll @@ -1257,9 +830,6 @@ w8a8_dumma_prefill_packedb_db_kernel( } #pragma unroll for (int i = 0; i < kWaveN16; ++i) { - // Round 5: fragment block (k32 = kk>>5, n16 = wave_col*kWaveN16 + i) - // of the swizzled tile -- 512 B per 16-n x 32-k DUMMA B fragment, one - // conflict-free ds_read_b64 per lane. load_frag8_swz( b_frag[i], b_tile[s & 1] + @@ -1276,14 +846,14 @@ w8a8_dumma_prefill_packedb_db_kernel( } } - const int base_row = m0 + wave_row * (BM / 2); - const int base_col = n0 + wave_col * (BN / 2); + const int base_row = m0 + wave_row * kRowsPerWave; + const int base_col = n0 + wave_col * kColsPerWave; #pragma unroll for (int j = 0; j < kWaveM16; ++j) { #pragma unroll for (int i = 0; i < kWaveN16; ++i) { - // Round 9: kGuarded == !kFullTile -- the full-tile instantiation skips - // the dead per-fragment `row >= m` EXEC bracket (see the helper above). + // kGuarded == !kFullTile -- the full-tile instantiation skips the dead + // per-fragment `row >= m` EXEC bracket. store_prefill_fragment_coalesced( acc[j][i], x_scale, weight_scale, out, m, n, base_row + j * kTileM, base_col + i * kTileN, lane); @@ -1292,26 +862,21 @@ w8a8_dumma_prefill_packedb_db_kernel( } // Generic scalar int8/int32 fallback for unmatched shapes and small-M API -// cases (including the paired M=2 and M=16 shapes with the same (N, K) -- -// they never reach the m >= 128 tiled/packed paths). One output element per +// cases (including the paired M=2 and M=16 shapes with the same (N, K) -- they +// never reach the m >= 128 tiled/packed paths). One output element per // grid-stride step; exact int32 accumulation (max assigned K = 6144 keeps the // int8 dot well within int32 range: |dot| <= 6144*127*127 < 2^31), then the // fused float scale (dot * x_scale[m] * weight_scale[n]) and a manual uint16 // bf16 store. // -// Round 1: for the exact (k, n) == (6144, 2624) the packed_weight buffer -// holds a packed layout (produced once, outside the timed region), so the -// fallback decodes that layout there with ascending kk (every other (K, N) -// keeps the identity [K, N] row-major copy). The int32 accumulation stays -// kk-ascending 0..k-1 in both arms; integer addition is exact and commutative, -// so the int32 dot -- and the paired M=2 / M=16 API shapes -- remain -// byte-exact against the identity-layout reference. Round 5 replaces the -// exact-shape pack with the fragment-interleaved swizzle (see -// w8a8_pack_bswz_i8_kernel), so the exact-shape decode below walks +// For the exact (k, n) == (6144, 2624) the packed_weight buffer holds the +// fragment-interleaved swizzle (produced once, outside the timed region), so +// the fallback decodes that layout there with ascending kk (every other (K, N) +// keeps the identity [K, N] row-major copy): // packed[((kk>>5)*n16g + (col>>4))*512 + // (((kk>>3)&3)*16 + (col&15))*8 + (kk&7)], n16g = n>>4, // element-by-element in ascending kk -- identical elements, identical int32 -// accumulators. +// accumulators, so the paired M=2 / M=16 API shapes stay byte-exact. __global__ __launch_bounds__(256) void w8a8_scalar_gemm_kernel( const int8_t* __restrict__ a, // [M, K] row-major const int8_t* __restrict__ b, // packed weight (swizzled/identity) @@ -1333,8 +898,8 @@ __global__ __launch_bounds__(256) void w8a8_scalar_gemm_kernel( const int8_t* a_row = a + static_cast(row) * k; int32_t acc = 0; if (packed_swz) { - // Round-5 swizzle: one 8-byte k-run per (n, k8) inside a 512-byte - // 32-k x 16-n fragment block; walk it in ascending kk. + // Swizzle: one 8-byte k-run per (n, k8) inside a 512-byte 32-k x 16-n + // fragment block; walk it in ascending kk. const int64_t n16g = static_cast(n) >> 4; for (int kk = 0; kk < k; ++kk) { const int64_t off = @@ -1372,46 +937,19 @@ __global__ __launch_bounds__(256) void w8a8_pack_identity_i8_kernel( } } -// Round-1 exact-shape n-major pack: for (k, n) == (6144, 2624) the weight is -// transposed once, outside the timed region and out of Graph capture, into -// packed[n * k + kk] = raw[kk * n + n] so every col_major B fragment is 8 -// contiguous K bytes in the packed buffer and every staging vector is one -// coalesced 16-byte read. Same byte count and buffer as the identity pack, so -// the captured addresses are unchanged. One thread per output byte; runs once -// during weight preparation. -__global__ __launch_bounds__(256) void w8a8_pack_nmajor_i8_kernel( - const int8_t* __restrict__ raw, - int8_t* __restrict__ packed, - int k, - int n) { - const int64_t total = static_cast(k) * n; - const int64_t stride = static_cast(gridDim.x) * blockDim.x; - for (int64_t idx = static_cast(blockIdx.x) * blockDim.x + - threadIdx.x; - idx < total; - idx += stride) { - const int col = static_cast(idx / k); - const int kk = static_cast(idx - static_cast(col) * k); - packed[idx] = raw[static_cast(kk) * n + col]; - } -} - -// Round-5 exact-shape fragment-interleaved B swizzle: for (k, n) == -// (6144, 2624) the weight is packed once, outside the timed region and out of -// Graph capture, into 512-byte 32-k x 16-n DUMMA B fragment blocks laid out -// [k32][n16][k8][n][8]: +// Exact-shape fragment-interleaved B swizzle: for (k, n) == (6144, 2624) the +// weight is packed once, outside the timed region and out of Graph capture, +// into 512-byte 32-k x 16-n DUMMA B fragment blocks laid out [k32][n16][k8][n][8]: // packed[((kk >> 5) * n16g + (n >> 4)) * 512 + // (((kk >> 3) & 3) * 16 + (n & 15)) * 8 + (kk & 7)] = raw[kk * n + n], -// where n16g = n >> 4 (164 for the exact shape). This is a byte-exact -// bijection 192 x 164 x 512 = k*n = 16,121,856: same byte count and buffer as -// the identity/n-major/panel packs, so captured addresses are unchanged. Each -// thread packs one 16-byte chunk = the 8-byte k-runs of two n-rows of one k8 -// group (a strided 2x8 gather on the read side; packing is outside timing, so -// the gather is off the critical path). Consecutive chunks are consecutive -// packed bytes, so the kernel's B staging is a flat 16-byte copy and every -// col_major B fragment read is one conflict-free ds_read_b64 (see -// load_frag8_swz). The round-2 n-major pack kernel above stays compiled as the -// layout control. +// where n16g = n >> 4 (164 for the exact shape). This is a byte-exact bijection +// 192 x 164 x 512 = k*n = 16,121,856: same byte count and buffer as the identity +// pack, so captured addresses are unchanged. Each thread packs one 16-byte chunk +// = the 8-byte k-runs of two n-rows of one k8 group (a strided 2x8 gather on the +// read side; packing is outside timing, so the gather is off the critical +// path). Consecutive chunks are consecutive packed bytes, so the kernel's B +// staging is a flat 16-byte copy and every col_major B fragment read is one +// conflict-free ds_read_b64 (see load_frag8_swz). __global__ __launch_bounds__(256) void w8a8_pack_bswz_i8_kernel( const int8_t* __restrict__ raw, int8_t* __restrict__ packed, @@ -1437,8 +975,8 @@ __global__ __launch_bounds__(256) void w8a8_pack_bswz_i8_kernel( int8_t* dst16 = packed + (c << 4); #pragma unroll for (int i = 0; i < 8; ++i) { - // 16-byte chunk = [n0 row: k0..k0+7][n0+1 row: k0..k0+7] (n-major - // within the k8 group, matching the [k8][n][8] tile layout). + // 16-byte chunk = [n0 row: k0..k0+7][n0+1 row: k0..k0+7] (n-major within + // the k8 group, matching the [k8][n][8] tile layout). dst16[i] = raw[static_cast(k0 + i) * n + n0]; dst16[8 + i] = raw[static_cast(k0 + i) * n + n0 + 1]; } @@ -1462,10 +1000,10 @@ __global__ __launch_bounds__(256) void w8a8_pack_identity_f32_kernel( } // namespace // Stable host launch symbol consumed by csrc/bindings.cpp. Dispatches on the -// caller-provided stream only; never allocates, synchronizes, packs, or -// touches the default stream. The workspace is unused for this shape -// (single-buffered, no split-K): m*n*4 = 42,990,080 B exceeds the 16 MiB -// workspace budget, so workspace_split_k_capacity is zero. +// caller-provided stream only; never allocates, synchronizes, packs, or touches +// the default stream. The workspace is unused for this shape (no split-K): +// m*n*4 = 42,990,080 B exceeds the 16 MiB workspace budget, so +// workspace_split_k_capacity(m, n, k) is zero. extern "C" void launch_w8a8_gemm( const int8_t* a, const int8_t* b, @@ -1486,41 +1024,23 @@ extern "C" void launch_w8a8_gemm( auto* out_ptr = static_cast(out); // Exact assigned shape guard: glm_tp8_fused_qkv_a_proj_m4096 - // (M=4096, N=2624, K=6144). The m >= 128 guard keeps the paired M=2/M=16 - // API shapes with the same (N, K) on the scalar fallback below. Round 2 - // keeps the accepted packed-weight architecture (the exact-fit 2-D macro-tile - // <128, 64, ...> with the packed weight produced once, out of the - // timed region, by launch_pack_w8a8_weight, the bank-safe LDS row strides and - // the explicit 8-byte load_frag8 fragment loads) and changes ONLY the K-stage - // pipeline: the dispatch goes to the 64-K double-buffered prefetch kernel - // <128, 64, 64> instead of the single-buffered <128, 64, 128> one, so the - // stage-(s+1) VMEM latency overlaps the stage-s LDS/v_mmac work. Round 3 - // retains that double-buffered 64-K pipeline unchanged (it is the unique - // 2-resident-block K pipeline inside the 65,536 B LDS budget: 2 x 27,648 = - // 55,296 <= 65,536) and changes only the packed B layout consumed by the two - // B global-load sites -- the weight is now the B-panel layout, so a warp's B - // staging loads touch 4 fully-used 128-byte lines instead of 32 half-used - // ones. Round 5 keeps the kernel, geometry and occupancy identical and - // changes only the packed B layout again: the B-panel pack is replaced by the - // fragment-interleaved swizzle, so the B stage is a flat 16-byte copy and - // every col_major B fragment is one conflict-free ds_read_b64 - // (load_frag8_swz). M = 32*128 and N = 41*64 are both exact, so no tail - // predication is introduced. Round 9 keeps that kernel, geometry and - // occupancy and only selects a compile-time tail-predication specialization: - // when m is a multiple of BM = 128 every block tile is fully inside the - // output, so the instantiation with kFullTile == true drops the dead A-row - // zero-fill/select in both staging sites and the dead epilogue row mask - // (see the Round-9 header section); every other m keeps the guarded - // instantiation byte-for-byte. + // (M=4096, N=2624, K=6144). The m >= 128 guard keeps the paired M=2/M=16 API + // shapes with the same (N, K) on the scalar fallback below. M = 16*256 is + // exact and N = 2624 = 20*128 + 64 is covered by clamping the last column + // tile origin to n - 128, so every block still stages a full 128-column B + // macro-tile and no B chunk ever leaves the packed weight. The compile-time + // kFullTile specialization is selected when m is a multiple of BM = 256, and + // every other m with this (n, k) keeps the guarded instantiation. if (m >= 128 && n == kExactN && k == kExactK) { const dim3 grid( - static_cast(n / 64), - static_cast((m + 127) / 128)); - const dim3 block(static_cast(kThreadsPerBlock)); - if ((m & 127) == 0) { + static_cast((n + kPackedBN - 1) / kPackedBN), + static_cast((m + kPackedBM - 1) / kPackedBM)); + const dim3 block(static_cast(kPackedThreads)); + if ((m % kPackedBM) == 0) { hipLaunchKernelGGL( HIP_KERNEL_NAME( - w8a8_dumma_prefill_packedb_db_kernel<128, 64, 64, true>), + w8a8_dumma_prefill_packedb_db_kernel< + kPackedBM, kPackedBN, 64, kPackedThreads, true>), grid, block, 0, @@ -1536,7 +1056,8 @@ extern "C" void launch_w8a8_gemm( } else { hipLaunchKernelGGL( HIP_KERNEL_NAME( - w8a8_dumma_prefill_packedb_db_kernel<128, 64, 64, false>), + w8a8_dumma_prefill_packedb_db_kernel< + kPackedBM, kPackedBN, 64, kPackedThreads, false>), grid, block, 0, @@ -1555,8 +1076,8 @@ extern "C" void launch_w8a8_gemm( // Generic native INT8 DUMMA prefill path for every other large-M shape with // N % 64 == 0 and K % 128 == 0 (identity [K, N] layout). The A row guard - // zero-fills rows past M and the epilogue masks them, so only the exact - // shape above avoids predication. + // zero-fills rows past M and the epilogue masks them, so only the exact shape + // above avoids predication. if (m >= 128 && (n % 64) == 0 && (k % 128) == 0) { const dim3 grid( static_cast(n / 64), @@ -1608,7 +1129,7 @@ extern "C" void launch_w8a8_gemm( k); } -// Stable host launch symbol consumed by csrc/bindings.cpp. Round 5: the exact +// Stable host launch symbol consumed by csrc/bindings.cpp. The exact // (k, n) == (6144, 2624) int8 weight is packed once (outside the timed region // and out of Graph capture) into the fragment-interleaved swizzle // packed[((kk>>5)*n16g + (n>>4))*512 + (((kk>>3)&3)*16 + (n&15))*8 + (kk&7)] @@ -1628,7 +1149,7 @@ extern "C" void launch_pack_w8a8_weight( constexpr int kPackThreads = 256; const int64_t weight_count = static_cast(k) * n; if (k == kExactK && n == kExactN) { - // Round 5: one thread packs one 16-byte chunk (16 bytes = 2 n-rows x 8 B). + // One thread packs one 16-byte chunk (16 bytes = 2 n-rows x 8 B). const int64_t chunks = weight_count >> 4; int64_t blocks = (chunks + kPackThreads - 1) / kPackThreads; if (blocks > 4096) { diff --git a/metainfer/tasks/dcu_kernel_auto_opt/variant/int8w8a8-gemm/glm52/TP8/M4096/o_proj.hip b/metainfer/tasks/dcu_kernel_auto_opt/variant/int8w8a8-gemm/glm52/TP8/M4096/o_proj.hip index 49710bb2..2a553a70 100644 --- a/metainfer/tasks/dcu_kernel_auto_opt/variant/int8w8a8-gemm/glm52/TP8/M4096/o_proj.hip +++ b/metainfer/tasks/dcu_kernel_auto_opt/variant/int8w8a8-gemm/glm52/TP8/M4096/o_proj.hip @@ -1,76 +1,147 @@ -// @@variant shape=glm_tp8_o_proj_m4096 commit=486f3ec14b6d88c045aa35a5c85d1d44816c8c2d added=2026-08-31 -// median_us=759.9 p90_us=760.8 speedup=72.08 baseline_us=5.478e+04 -// source=glm5-2-dsh-tp8-m4096-1-e6a280a2 -// @@variant shape=glm_tp8_o_proj_m4096 +// @@variant shape=glm_tp8_o_proj_m4096 commit=41d2ef9da654ecb15154a974fa11d84df5e4fc5d added=2026-09-14 +// median_us=651.5 p90_us=654.2 speedup=84.07 baseline_us=5.478e+04 +// source=test-9-11-1/iteration_002/glm_tp8_o_proj_m4096 // INT8 W8A8 GEMM HIP implementation for Hygon K500SM_AI / gfx928. // -// Worker: worker_2 (physical GPU 2), assigned shape: +// Worker: worker_0 (physical GPU 0), assigned shape: // glm_tp8_o_proj_m4096 : M=4096, N=6144, K=2048 // -// Bootstrap strategy (iteration 1, correctness-first but a usable profiling -// baseline - a large-Prefill scalar K loop would not be): -// * Large-prefill path (exact (m, n, k) == (4096, 6144, 2048)): -// native INT8 DUMMA m16n16k32 with int32 accumulation; one 128x64 -// output tile per block; four wavefronts (256 threads); each wave owns -// a 64x32 quadrant built from eight m16n16k32 accumulator fragments; -// the block cooperatively vector-loads A[128,64] and B[64,64] into one -// single-buffered 64-K LDS stage (15,360 B total: 128*80 + 64*80 with -// 16 B padding per row for bank skew); two __syncthreads per stage; -// fused dot * x_scale[m] * weight_scale[n] epilogue stored directly as -// bf16 from the accumulator fragments. Grid dim3(96, 32) = 3072 blocks -// dwarfs the 120 CUs, so no split-K is needed. -// The weight is read in the identity [K, N] int8 layout (bootstrap: -// launch_pack_w8a8_weight is an identity device-to-device copy for -// every (k, n)). A is staged m-major into a_tile[m][k] and its eight -// fragment k-values per lane are contiguous, so A fragments use the -// direct 8-byte LDS fill (load_fragment8, the exact pattern validated -// on the worker-29 TP4 gate_up lineage iteration 13 and the TP8 hy3 -// o_proj lineage iteration 12). B is staged k-major into -// b_tile[k][n] and its fragments use the library du_load_matrix_sync -// row_major loader (8 per-byte LDS reads strided by ldm per lane - -// correct, direct, and simple; the packed n-major layout + 8-byte B -// fragment fill is a later-round optimization once the packed layout -// is introduced). -// * Generic scalar int8/int32 fallback for every unmatched (m, n, k), -// including all small-M API cases (M=2, M=16) and M=3072 with the same -// (K, N) = (2048, 6144); the fallback reads the identity [K, N] layout -// for every (k, n). -// * launch_pack_w8a8_weight: identity device-to-device copy of the raw -// [K, N] weight and the [N] weight_scale for every (k, n) (bootstrap; -// later rounds may change only the pack HIP implementation and the -// matching GEMM interpretation). Packing never happens inside the -// timed GEMM. -// Iteration 5 (packing round): for the exact (k, n) == (2048, 6144) -// launch_pack_w8a8_weight now produces the n-major [N, K] layout -// packed[n*K + kk] = raw[kk*N + n] (same byte count and buffer -> -// graph-stable), and the exact-shape dispatch uses the new -// w8a8_dumma_256x64x64_packedb_kernel, which stages B n-major into -// bank-safe 72-byte LDS rows and fills each col_major B fragment with -// one contiguous 8-byte ds_read_b64 (zero per-byte reads, zero -// reassembly VALU); the scalar fallback decodes the n-major layout -// when (k, n) matches so the paired M=2/M=16 and M=3072 API shapes -// stay byte-exact. Every other (k, n) keeps the identity copy. -// Iteration 6 (epilogue round): the fused scale->bf16->coalesced -// store epilogue of w8a8_dumma_256x64x64_packedb_kernel is -// restructured to register-batch the per-row and per-column scales: -// each lane loads its 8 x_scale rows and its 2 weight_scale float4 -// columns ONCE (issued on the last K stage, before the final -// protective barrier) and the 16 per-fragment coalesced stores reuse -// them; the previous per-call epilogue re-loaded the scales inside -// each store (PMC vmem_read surplus ~153,600 wavefront loads above -// the pure staging floor). Output bits are identical (same transpose, -// same float(dot) * x_scale[row] * weight_scale[col] order, same bf16 -// rounding). No workspace/combine pass exists: the workspace -// argument stays unused and the epilogue is fully fused in-kernel. +// Round 1 (establish_arch) established ONE complete, correct DUMMA +// architecture family and measured median 753.83 us: +// +// * family: packed_weight + staged A/B, 256x64 output macro-tile, native +// INT8 DUMMA m16n16k32 with int32 accumulation, four 64-lane wavefronts +// (256 threads); +// * weight packing (outside the timed region, before Graph capture, via the +// ABI-preserving launch_pack_w8a8_weight): for the exact (k, n) == +// (2048, 6144) the raw logical [K, N] int8 weight is transposed once into +// the n-major [N, K] layout packed[col * K + kk] = raw[kk * N + col]. +// Same byte count and same caller-owned buffer as the identity layout, so +// every captured address is graph-stable. Every other (k, n) keeps the +// identity device-to-device copy. +// * fragment fills: the m16n16k32 matrix_a slot for lane (r = lane & 15, +// kq = lane >> 4) is eight consecutive k bytes at fixed row, and the +// matrix_b (col_major) slot is eight consecutive k bytes at fixed n on the +// n-major tile, so BOTH operands use one contiguous 8-byte ds_read_b64 per +// fragment (load_fragment8) with no per-byte reads and no mask/OR +// reassembly VALU. +// * LDS bank safety: both padded strides are odd multiples of 8 bytes +// (A = 88 = 22 dwords, B = 72 = 18 dwords). 22*r mod 32 and 18*r mod 32 +// over r = 0..15 are permutations of the 16 even dword starts, so each +// 16-lane ds_read_b64 fragment phase lands on 16 distinct bank pairs. +// Both strides are 8 mod 16, so the 16-byte cooperative staging stores are +// emitted as two int64 halves (the validated ds_write2_b64 pattern). +// * fused coalesced epilogue: the 4x4 shfl_xor register transpose keeps the +// exact per-element multiply order float(dot) * x_scale[row] * +// weight_scale[col] and bf16 rounding, then one 8-byte store per lane per +// fragment (100% store-sector efficiency). Each lane's x_scale rows and +// weight_scale float4 columns are loaded ONCE into registers on the last +// K stage, issued before the final protective barrier, so the epilogue +// reads no scale inside the store chains. The epilogue is fully fused: +// no workspace pass, no separate combine kernel. +// +// Round 2 (mandatory decision: operand-reuse round - compare direct loads +// with cooperative A+B LDS staging, quantify A/B reuse per macro-tile, use +// vectorized coalesced global loads with a bank-safe LDS layout): +// +// FALSIFIABLE BOTTLENECK HYPOTHESIS. The round-1 kernel is bound by the +// L2/global operand traffic its macro-tile reuse ratio forces, not by the +// LDS fragment pipe, not by MMAC issue, and not by HBM. With a +// (tileM, tileN) = (256, 64) macro-tile every A byte is re-read N/tileN = +// 96x and every B byte M/tileM = 16x by the 1,536-block grid, i.e. +// A 805.3 MB + B 201.3 MB = 1,006.6 MB of global reads per operator replay. +// The exact-source PMC record (digest acb11003..., grid 1,536 x 256 +// threads, 151 VGPR, 27,136 B LDS, 957,746 grbm_gui_active cycles at +// ~1.29 GHz) shows only 99.3 MB of HBM reads and 50.3 MB of HBM writes +// (198.5 GB/s counter-derived HBM) against 1,044,480 vmem_read_instructions +// of 16-B-per-lane coalesced staging, i.e. ~1.07 GB of L2 read traffic in +// 740.5 us (~1.4 TB/s). HBM is far from its ceiling, so the traffic that +// matters is the L2-resident operand re-read, and the only structural +// lever on it is the macro-tile reuse ratio 1/tileM + 1/tileN. +// +// Direct fragment loads vs cooperative A+B LDS staging. Direct global +// fragment fills were rejected without measuring them: the m16n16k32 +// fragment owns the k dimension and lane index, so lane (r, kq) needs +// 8 consecutive k bytes at one row; in a 64-lane wave that is 16 different +// rows 2,048 B apart per 16-lane group (8 B useful out of a 32-B sector), +// and each wave reads the same A fragment once per n-group. Per block that +// is >= 4x the L2 sectors of the 16-B-per-thread coalesced cooperative +// staging (which touches every operand byte exactly once per block), before +// counting the duplicated A reads. Cooperative A+B staging therefore +// stays, and this round moves the reuse lever: DOUBLE THE N MACRO-TILE. +// +// Change (ONE bounded factor: the macro-tile reuse geometry). The output +// macro-tile becomes 256x128 at the same 256 threads / 4 wavefronts; the +// 2(m) x 2(n) wave split gives every wave a 128x64 quadrant = 8 m-groups x +// 4 n-groups = 32 m16n16k32 int32 accumulators (the A quadrant height and +// the 88-byte A stride are unchanged, so the eight A fragment fills per kk +// keep the exact round-1 addresses; the four B fragment fills per kk are the +// only new ones). Global staging stays per-thread 16-byte coalesced int4 +// loads, 6 per thread per stage (A rows [0,64),[64,128),[128,192),[192,256) +// at k-chunk (tid&3)*16 -- unchanged from round 1 -- plus TWO B bands now: +// n = b_row0 and b_row0 + 64 at the same k-chunk). The single-buffered +// 64-K LDS stage becomes A[256,88] = 22,528 B + B[128,72] = 9,216 B = +// 31,744 B/block, both strides unchanged and still bank-safe, and the two +// int64-half ds_write2_b64 staging stores are unchanged. +// +// Quantified reuse per macro-tile (bytes of global operand read per +// operator replay, M*K*(N/tileN) + N*K*(M/tileM)): +// (256, 64) -> A 805.3 MB + B 201.3 MB = 1,006.6 MB (round 1) +// (256, 128) -> A 402.7 MB + B 201.3 MB = 604.0 MB (this round, -40%) +// (128, 128) -> A 402.7 MB + B 402.7 MB = 805.3 MB (rejected: the +// halved tileM double-charges B, only -20% total) +// (512, 64) -> A 805.3 MB + B 100.7 MB = 906.0 MB (rejected: the +// unchanged tileN leaves the dominant A term untouched) +// LDS fragment reads per MMAC also fall from (8+2)/16 = 0.625 to +// (8+4)/32 = 0.375 per kk, and the block count falls 1,536 -> 768 (still +// 6.4 blocks/CU on the 120-CU part, so split-K remains unnecessary and the +// caller workspace stays unused; the trusted occupancy-probe split +// candidates [2,3,4,5] are irrelevant to a grid this wide). +// +// Expected effect: if the L2 operand-traffic term is the limiter, median +// moves from ~754 us toward ~500-620 us; if the kernel is instead at the +// MMAC/LDS-issue floor, the larger tile stays within a few percent (the +// extra 64 accumulator VGPRs and the 768-block tail are the only costs). +// +// Round 8 (mandatory decision: fix_build - repair the previous round's +// compile/API failure with the smallest source-owned change; no architecture +// change, no redesign). Rounds 6 and 7 produced no built candidate because the +// worker process was terminated by an infrastructure rate limit, and the +// workspace source was restored to the accepted round-2 kernel. Round 6's +// intended epilogue change is re-applied with the compile-safety correction +// demanded by this round. Preserved byte-for-byte: the 256x128 macro-tile, +// 256 threads / 4 wavefronts, 88-byte A and 72-byte B padded strides, the +// single-buffered 64-K LDS stage (A 22,528 B + B 9,216 B = 31,744 B), the +// one-ds_read_b64-per-fragment fills, the 32 int32 m16n16k32 accumulators and +// their exact accumulation order, the two __syncthreads per stage, the exact +// (m,n,k) dispatch guard, the packed n-major weight path and the scalar +// generic fallback. The only edit is the epilogue conversion. hip's +// __float2bfloat16 lowers on this DTK to a NaN-guarded exec-masked sequence +// per element (~12 extra SALU/VALU plus a divergent branch region for each of +// the 128 conversions per lane); it is replaced by the branchless +// finite-domain RNE helper bf16_rne_finite_bits, which stores the identical +// 16-bit payload for every finite value this epilogue can produce and can +// differ only for NaN/Inf. The 4x4 shfl_xor transpose, the per-element +// multiply order float(dot) * x_scale[row] * weight_scale[col], the tail-row +// guard, the 8-byte coalesced store and the dispatch are unchanged. +// +// Generic scalar int8/int32 fallback for every unmatched (m, n, k), including +// all small-M API cases (M=2, M=16) and M=3072 with the same (K, N): it always +// reproduces the exact int32 dot over K in ascending k order, then the fused +// float scale and bf16 store. For (k, n) == (2048, 6144) it decodes the +// n-major packed layout produced above; every other (k, n) reads the identity +// [K, N] layout. // // Rules honored: headers in the known-good order (hip/hip_runtime.h, -// hip/hip_bfloat16.h, du_mma.h); wavefront = 64 and blockDim a multiple of -// 64; int32 accumulation with float conversion only for +// hip/hip_bfloat16.h, du_mma.h); wavefront = 64 and blockDim a multiple of 64; +// int32 accumulation with float conversion only for // dot * x_scale[m] * weight_scale[n]; hipLaunchKernelGGL on the -// caller-provided PyTorch stream; no stream-0 / synchronization / temporary -// allocation in gemm_out; no NVIDIA wmma/mma.sync/PTX/warp-32 masks; all -// barriers on paths reached by every thread in the block; explicit dispatch -// for the assigned shape with a scalar generic fallback for everything else. +// caller-provided PyTorch stream; no allocation / packing / synchronization / +// default-stream launch inside the timed API; no NVIDIA wmma/mma.sync/PTX/ +// FP8/INT4 and no raw inline asm; all __syncthreads() on paths reached by +// every block thread; coalesced lanes follow contiguous N addresses; single +// 31,744 B LDS buffer (no double buffering); explicit exact-shape dispatch for +// the assigned shape with a preserved scalar generic fallback. #include #include @@ -89,62 +160,37 @@ constexpr int kWaveSize = 64; // gfx928 native wavefront constexpr int kTargetM = 4096; constexpr int kTargetN = 6144; constexpr int kTargetK = 2048; -constexpr int kBlockM = 128; -constexpr int kBlockN = 64; -constexpr int kStageK = 64; -constexpr int kBPad = 16; // 64 -> 80-byte LDS row stride (five bank phases) -constexpr int kBStride = kBlockN + kBPad; -constexpr int kAStride = kStageK + kBPad; // 64 -> 80-byte A row stride -constexpr int kBlockThreads = 4 * kWaveSize; - -// Iteration-4 tile-shape constants: 256x64 output tile (M-major doubling of -// the accepted 128x64 tile) at 256 threads = 4 wavefronts, each wave owning a -// 128x32 quadrant (16 m16n16k32 int32 accumulators). -constexpr int kBlockM256 = 2 * kBlockM; - -// Iteration-5 packed-B constants: for the exact (k, n) == (2048, 6144) the -// weight is packed once (outside the timed region) into the n-major [N, K] -// layout packed[n*K + kk] = raw[kk*N + n]; B is then staged n-major into LDS -// rows of 72 B (64 + 8). 72 is an odd 8-byte count, so the 16 lanes of each -// ds_read_b64 fragment-load phase land on 16 distinct bank pairs (18r mod 32, -// r = 0..15 -> all 16 even dword starts: the conflict-free 4-phase floor), -// and 72 % 16 == 8, so staging stores use two int64 halves (ds_write2_b64) -// instead of one misaligned int4. -constexpr int kBPadPacked = 8; // 64 -> 72-byte n-major B LDS row stride -constexpr int kBStridePacked = kStageK + kBPadPacked; -// Iteration-9 (A-stride round) constant: packedb A LDS row stride 64 -> 88 -// bytes, replacing the shared 80-byte kAStride for w8a8_dumma_256x64x64_ -// packedb_kernel only (the identity-layout kernels keep kAStride = 80). -// 88 = 22 dwords: 22r mod 32 is a permutation of the 16 even dword starts -// over r = 0..15, so the 16 lanes of each ds_read_b64 fragment-load phase -// land on 16 distinct bank pairs (the same conflict-free 4-phase floor the -// packed-B stride 72 already uses); the current 80-byte (20-dword) stride -// has 20r mod 32 with period 8, so rows r and r+8 alias onto one bank phase -// and EVERY A fragment read pays a 2-way bank conflict (PMC -// lds_bank_conflicts 20,447,232, deterministic across iterations 5..8). -// 88 % 16 == 8, so the 16-byte staging stores go through two int64 halves -// (ds_write2_b64, the exact accepted packed-B staging pattern) - the same -// instruction count as the current ds_write_b128. A[256,88] = 22,528 B + -// B[64,72] = 4,608 B = 27,136 B/block; 2 blocks/CU = 54,272 <= 65,536, so -// co-residency, VGPR, barrier count and the global-load pattern are all -// unchanged; only the physical LDS addresses move (same bytes stored, same -// bytes read, fragment values bit-identical). -constexpr int kAStridePacked = kStageK + 24; // 64 -> 88-byte A LDS row stride +// Round 2: 256x128 macro-tile, four wavefronts, single-buffered 64-K stage. +// Doubling the N macro-tile halves the A re-read term M*K*(N/tileN) while the +// unchanged tileM keeps the B term N*K*(M/tileM); total global operand reads +// fall 1,006.6 -> 604.0 MB per replay (-40%). +constexpr int kBlockM = 256; +constexpr int kBlockN = 128; +constexpr int kStageK = 64; +constexpr int kBlockThreads = 4 * kWaveSize; // 256 + +// A[256, 88]: m-major, 88 = 22 dwords (odd 8-byte count). 22*r mod 32 over +// r = 0..15 is a permutation of the 16 even dword starts, so every 16-lane +// ds_read_b64 A fragment phase is conflict-free; 88 % 16 == 8 forces the +// two-int64-half (ds_write2_b64) staging store. +constexpr int kAStride = kStageK + 24; // 64 -> 88-byte A LDS row stride +// B[128, 72]: n-major (row index = n), 72 = 18 dwords. 18*r mod 32 over +// r = 0..15 is the same conflict-free permutation; 72 % 16 == 8 gives the same +// two-int64-half staging store. +constexpr int kBStride = kStageK + 8; // 64 -> 72-byte B LDS row stride using bf16_t = hip_bfloat16; -// Direct 8-byte LDS fragment fill for matrix_a on the m-major A tile. -// The m16n16k32 matrix_a row_major loader assigns x[i] = -// p[row*ldm + col_group*8 + i] (row = lane & 15, col_group = lane >> 4), -// i.e. eight consecutive k-values per lane; the library path makes the -// compiler emit per-byte ds_read_u8 loads plus a mask/OR reassembly chain -// per dword that is an arithmetic identity on the v_mmac A operand. Writing -// the same 8 bytes straight into the fragment storage keeps the operand bit -// pattern and the int32 accumulation identical and lets the compiler feed a -// single 8-byte LDS read to the v_mmac. This is the exact pattern validated -// on the worker-29 TP4 gate_up lineage (accepted iteration 13) and the TP8 -// hy3 o_proj lineage (accepted iteration 12). +// Direct 8-byte LDS fragment fill. The m16n16k32 fragment layout assigns +// x[i] = p[(row++) * ldm + col_group*8 + i] (row = lane & 15, +// col_group = lane >> 4), i.e. eight consecutive 8-bit elements per lane on a +// row-major tile. On the m-major A tile these are eight consecutive k bytes of +// one row; on the n-major B tile (row = n) they are eight consecutive k bytes +// of one column. Writing those bytes straight into the fragment storage keeps +// the operand bit pattern, the v_mmac inputs and the int32 accumulation order +// identical to the library loader while emitting one ds_read_b64 instead of +// eight per-byte ds_read_u8 plus a mask/OR reassembly chain per dword. template __device__ __forceinline__ void load_fragment8( Frag& frag, @@ -156,103 +202,46 @@ __device__ __forceinline__ void load_fragment8( *reinterpret_cast(base + off); } -// Coalesced fragment store. The m16n16k32 accumulator lane mapping -// (row = lane & 15, column group c4 = lane >> 4, frag.x[i] -> column -// c4 + 4*i) gives each lane four elements strided by 4 columns, so a direct -// store would be four 2-byte scalar stores per lane whose wavefront -// addresses touch each 32-B sector at 25% utilization. This epilogue -// transposes the 4-element groups within each 4-lane column group (lanes r, -// r+16, r+32, r+48 -- a 4x4 transpose, two 2x2 steps with shfl_xor 16 then -// 32), so lane (r, c4) ends up holding the four CONTIGUOUS columns -// 4*c4 .. 4*c4+3, converts them to bf16, packs 4 bf16 (8 B), and writes ONE -// 8-byte store per lane (100% store sector efficiency). This DTK lowers -// __shfl_xor to ds_bpermute at the block tail where the LDS pipe is idle. -// Only the int32 values are re-routed between lanes; the per-element scale -// multiply order (float(dot) * x_scale[row] * weight_scale[col]) and the -// bf16 rounding are unchanged, so the stored bits are identical to the -// direct per-element store. The row >= m guard is wavefront-uniform (all -// 64 lanes of a wave share the same 16-row window), so the shuffles never -// mix active and inactive lanes; base_col is a multiple of 16 and n*2 a -// multiple of 8, so the float4 weight_scale load and the 8-byte store are -// aligned. -template -__device__ __forceinline__ void store_prefill_fragment_coalesced( - const AccFragment& frag, - const float* __restrict__ x_scale, - const float* __restrict__ weight_scale, - bf16_t* __restrict__ out, - int base_row, - int base_col, - int m, - int n, - int lane) { - const int row = base_row + (lane & 15); - if (row >= m) { - return; // tail-M masking: padded rows never write (wavefront-uniform) - } - const int c4 = lane >> 4; // 0..3 - const int x0 = frag.x[0]; - const int x1 = frag.x[1]; - const int x2 = frag.x[2]; - const int x3 = frag.x[3]; - // Step 1: transpose the 2x2 in-block pairs (c4 bit0 <-> element bit0). - const int s0 = __shfl_xor(x0, 16, kWaveSize); - const int s1 = __shfl_xor(x1, 16, kWaveSize); - const int s2 = __shfl_xor(x2, 16, kWaveSize); - const int s3 = __shfl_xor(x3, 16, kWaveSize); - const bool lo = (c4 & 1) == 0; - const int a0 = lo ? x0 : s1; - const int a1 = lo ? s0 : x1; - const int a2 = lo ? x2 : s3; - const int a3 = lo ? s2 : x3; - // Step 2: transpose the 2x2 blocks (c4 bit1 <-> element bit1). - const int t0 = __shfl_xor(a0, 32, kWaveSize); - const int t1 = __shfl_xor(a1, 32, kWaveSize); - const int t2 = __shfl_xor(a2, 32, kWaveSize); - const int t3 = __shfl_xor(a3, 32, kWaveSize); - const bool hi = (c4 & 2) == 0; - const int f0 = hi ? a0 : t2; - const int f1 = hi ? a1 : t3; - const int f2 = hi ? t0 : a2; - const int f3 = hi ? t1 : a3; - - // Lane (r, c4) now owns columns base_col + 4*c4 .. +3 (8 B, 8-byte - // aligned: base_col is a multiple of 16, n*2 is a multiple of 8). - const int col0 = base_col + 4 * c4; - const float xs = x_scale[row]; - const float4 ws = *reinterpret_cast(weight_scale + col0); - const float v0 = static_cast(f0) * xs * ws.x; - const float v1 = static_cast(f1) * xs * ws.y; - const float v2 = static_cast(f2) * xs * ws.z; - const float v3 = static_cast(f3) * xs * ws.w; - const uint64_t packed = - static_cast( - static_cast(__float2bfloat16(v0))) | - (static_cast( - static_cast(__float2bfloat16(v1))) - << 16) | - (static_cast( - static_cast(__float2bfloat16(v2))) - << 32) | - (static_cast( - static_cast(__float2bfloat16(v3))) - << 48); - *reinterpret_cast(out + static_cast(row) * n + col0) = - packed; +// Round 8 (fix_build / epilogue): plain round-to-nearest-even fp32 -> bf16 +// conversion without hip's __float2bfloat16 NaN/Inf signaling-bit fixup. The +// arithmetic (u + 0x7fff + ((u >> 16) & 1)) >> 16 is exactly the finite-domain +// path of DTK's float_2_bfloatraw (exponent bits != all-ones), so every finite +// fp32 input - including +/-0, denormals, and values whose RNE carries into or +// out of the bf16 exponent field - produces the identical 16-bit pattern as +// __float2bfloat16; the outputs can differ only for NaN/Inf inputs, which this +// kernel's epilogue can never produce (|int32 dot| <= K*127^2 and both scale +// inputs are finite, so the scaled accumulator is always finite). The helper +// is spelled with a union rather than __float_as_uint so it depends on no +// optional device intrinsic; this is the validated formulation from the +// accepted hy3/TP8/M4096 o_proj lineage, not a new architecture. +__device__ __forceinline__ unsigned short bf16_rne_finite_bits(float f) { + union { + float fp32; + unsigned int u32; + } u = {f}; + u.u32 += 0x7fffu + ((u.u32 >> 16) & 1u); // round half to even at bit 16 + return static_cast(u.u32 >> 16); } -// Register-batched variant of the coalesced fragment store (iteration 6, -// epilogue round). Identical to store_prefill_fragment_coalesced in every -// output bit: same 4x4 shuffle transpose, same per-element multiply order -// float(dot) * x_scale[row] * weight_scale[col], same __float2bfloat16 -// rounding, same single 8-byte coalesced store. The only difference is that -// the per-row x_scale and per-column weight_scale values are passed in from -// registers: the caller loads each lane's 8 x_scale rows and 2 weight_scale -// float4 columns exactly once per wave (the per-call epilogue re-loaded the -// scales inside each of the 16 fragment stores), so the stored bits are -// unchanged while the epilogue vmem read count per lane per wave drops from -// up to 8 x_scale + 16 float4 loads to exactly 8 + 2 and the loads are -// independent of (and issue before) the shuffle/convert/store chains. +// Fused coalesced fragment store with register-batched scales. The m16n16k32 +// accumulator lane mapping (row = lane & 15, column group c4 = lane >> 4, +// frag.x[i] -> column c4 + 4*i) gives each lane four elements strided by 4 +// columns, so a direct store would be four 2-byte scalar stores per lane whose +// wavefront addresses touch each 32-B sector at 25% utilization. This +// epilogue transposes the 4-element groups within each 4-lane column group +// (lanes r, r+16, r+32, r+48 - a 4x4 transpose, two 2x2 steps with shfl_xor 16 +// then 32), so lane (r, c4) ends up holding the four CONTIGUOUS columns +// 4*c4 .. 4*c4+3, converts them to bf16, packs 4 bf16 (8 B), and writes ONE +// 8-byte store per lane (100% store sector efficiency). Only the int32 values +// are re-routed between lanes; the per-element multiply order +// (float(dot) * x_scale[row] * weight_scale[col]) and the bf16 rounding are +// unchanged, so the stored bits are identical to a direct per-element store. +// xs is this lane's x_scale[row] and ws the float4 at +// weight_scale + base_col + 4*c4, both preloaded once by the caller. The +// row >= m guard is wavefront-uniform (all 64 lanes share the same 16-row +// window), so the shuffles never mix active and inactive lanes; base_col is a +// multiple of 16 and n*2 a multiple of 8, so the float4 scale load and the +// 8-byte store are aligned. template __device__ __forceinline__ void store_prefill_fragment_coalesced_scaled( const AccFragment& frag, @@ -294,612 +283,42 @@ __device__ __forceinline__ void store_prefill_fragment_coalesced_scaled( const int f2 = hi ? t0 : a2; const int f3 = hi ? t1 : a3; - // Lane (r, c4) owns columns base_col + 4*c4 .. +3 (8 B, 8-byte aligned: - // base_col is a multiple of 16, n*2 is a multiple of 8); ws is the float4 - // at weight_scale + base_col + 4*c4 preloaded by the caller. + // Lane (r, c4) now owns columns base_col + 4*c4 .. +3 (8 B, 8-byte aligned: + // base_col is a multiple of 16 and n*2 is a multiple of 8). const int col0 = base_col + 4 * c4; const float v0 = static_cast(f0) * xs * ws.x; const float v1 = static_cast(f1) * xs * ws.y; const float v2 = static_cast(f2) * xs * ws.z; const float v3 = static_cast(f3) * xs * ws.w; const uint64_t packed = - static_cast( - static_cast(__float2bfloat16(v0))) | - (static_cast( - static_cast(__float2bfloat16(v1))) - << 16) | - (static_cast( - static_cast(__float2bfloat16(v2))) - << 32) | - (static_cast( - static_cast(__float2bfloat16(v3))) - << 48); + static_cast(bf16_rne_finite_bits(v0)) | + (static_cast(bf16_rne_finite_bits(v1)) << 16) | + (static_cast(bf16_rne_finite_bits(v2)) << 32) | + (static_cast(bf16_rne_finite_bits(v3)) << 48); *reinterpret_cast(out + static_cast(row) * n + col0) = packed; } // --------------------------------------------------------------------------- -// Large-M prefill: 128x64 output tile per block, four wavefronts of 64 lanes. -// Each wave owns a 64x32 quadrant (eight m16n16k32 int32 accumulators); the -// block cooperatively stages A[128,64] from x_q (row-major, stride k) and -// B[64,64] from the identity [K, N] weight into a single-buffered LDS -// buffer. Two barriers per stage: one after the cooperative load, one before -// the next stage overwrites LDS. Direct fragment epilogue (coalesced store). +// Exact-shape GEMM: 256x128 output tile per block, four wavefronts. // -// Stage layout (bootstrap, identity [K, N] weight): -// A[128, 80]: m-major, 512 int4s; thread t loads int4 #0 at -// a_tile[(t/4)*80 + (t%4)*16] from x_q[(m0 + t/4)*k + k0 + -// (t%4)*16] and int4 #1 at row +64 (rows 64..127). -// B[64, 80]: k-major, 256 int4s; thread t loads int4 at -// b_tile[(t/4)*80 + (t%4)*16] from -// weight[(k0 + t/4)*N + n0 + (t%4)*16] (16 consecutive n at -// fixed k; one int4 per thread). B fragments are loaded with -// the library row_major loader (ldm = 80): lane (r, c4) holds -// x[i] = b_tile[(kk + r + i)*80 + local_col + c4], i.e. eight -// k-rows strided by ldm - correct for the identity [K, N] -// layout; later rounds can switch to an n-major packed B with -// the 8-byte fragment fill. -// --------------------------------------------------------------------------- -__global__ __launch_bounds__(kBlockThreads) void w8a8_dumma_128x64x64_kernel( - const int8_t* __restrict__ x_q, - const int8_t* __restrict__ weight, - const float* __restrict__ x_scale, - const float* __restrict__ weight_scale, - bf16_t* __restrict__ out, - int m, - int n, - int k) { - const int tid = static_cast(threadIdx.x); - const int wave = tid / kWaveSize; - const int lane = tid % kWaveSize; - const int wave_row = wave >> 1; - const int wave_col = wave & 1; - const int m0 = static_cast(blockIdx.y) * kBlockM; - const int n0 = static_cast(blockIdx.x) * kBlockN; - const int local_row = wave_row * 64; - const int local_col = wave_col * 32; - - // Single-buffered 64-K stage: A[128, 80] + B[64, 80] = 15,360 B/block - // (4 blocks/CU fit the 64 KiB LDS budget; the padded 80-byte strides are - // 16-byte-aligned and break the 64-byte LDS bank periodicity). - __shared__ __align__(16) int8_t a_tile[kBlockM * kAStride]; - __shared__ __align__(16) int8_t b_tile[kBlockN * kBStride]; - - DUFragment - a_frag0, a_frag1; - DUFragment - b_frag0, b_frag1; - DUFragment - acc00, acc01, acc10, acc11, acc20, acc21, acc30, acc31; - du_fill_fragment(acc00, 0); - du_fill_fragment(acc01, 0); - du_fill_fragment(acc10, 0); - du_fill_fragment(acc11, 0); - du_fill_fragment(acc20, 0); - du_fill_fragment(acc21, 0); - du_fill_fragment(acc30, 0); - du_fill_fragment(acc31, 0); - - // Cooperative staging: each thread owns one 16-byte vector in A rows - // [0,64), one in A rows [64,128), and one 16-byte vector of B. - // A[128,64] is 512 int4s; B[64,64] is 256 int4s. - const int vector_byte_offset = tid * static_cast(sizeof(int4)); - const int stage_row = vector_byte_offset / kStageK; // tid / 4, 0..63 - const int stage_col = vector_byte_offset - stage_row * kStageK; // (tid%4)*16 - const int b_row = tid >> 2; // k row of this thread's B int4 - const int b_n16 = (tid & 3) * 16; // n offset (stage-local) of B int4 - - for (int k0 = 0; k0 < k; k0 += kStageK) { - *reinterpret_cast(a_tile + stage_row * kAStride + stage_col) = - *reinterpret_cast( - x_q + static_cast(m0 + stage_row) * k + k0 + stage_col); - *reinterpret_cast(a_tile + - (stage_row + kBlockM / 2) * kAStride + stage_col) = - *reinterpret_cast( - x_q + static_cast(m0 + stage_row + kBlockM / 2) * k + - k0 + stage_col); - *reinterpret_cast(b_tile + b_row * kBStride + b_n16) = - *reinterpret_cast( - weight + (static_cast(k0 + b_row) * n + n0 + b_n16)); - __syncthreads(); - - // Each wave consumes its 64x32 quadrant: eight m16n16k32 MMACs per kk. -#pragma unroll - for (int kk = 0; kk < kStageK; kk += kTileK) { - // A fragments: direct 8-byte LDS fill (m-major A tile; each lane's - // eight fragment k-values are contiguous). - load_fragment8(a_frag0, a_tile + local_row * kAStride + kk, kAStride, - lane); - load_fragment8(a_frag1, a_tile + (local_row + kTileM) * kAStride + kk, - kAStride, lane); - // B fragments: library row_major loader on the k-major [K, N] LDS - // tile (bootstrap, identity layout; ldm = kBStride). - du_load_matrix_sync(b_frag0, b_tile + kk * kBStride + local_col, - kBStride); - du_load_matrix_sync(b_frag1, - b_tile + kk * kBStride + local_col + kTileN, - kBStride); - du_mma_sync(acc00, a_frag0, b_frag0, acc00); - du_mma_sync(acc01, a_frag0, b_frag1, acc01); - du_mma_sync(acc10, a_frag1, b_frag0, acc10); - du_mma_sync(acc11, a_frag1, b_frag1, acc11); - load_fragment8( - a_frag0, a_tile + (local_row + 2 * kTileM) * kAStride + kk, - kAStride, lane); - load_fragment8( - a_frag1, a_tile + (local_row + 3 * kTileM) * kAStride + kk, - kAStride, lane); - du_mma_sync(acc20, a_frag0, b_frag0, acc20); - du_mma_sync(acc21, a_frag0, b_frag1, acc21); - du_mma_sync(acc30, a_frag1, b_frag0, acc30); - du_mma_sync(acc31, a_frag1, b_frag1, acc31); - } - - // Protect the LDS buffer from the next stage's cooperative overwrite. - __syncthreads(); - } - - const int base_row = m0 + local_row; - const int base_col = n0 + local_col; - // Coalesced epilogue store (one 8-byte store per lane per fragment; scales - // + bf16 conversion fused in-kernel, no workspace pass). - store_prefill_fragment_coalesced(acc00, x_scale, weight_scale, out, - base_row, base_col, m, n, lane); - store_prefill_fragment_coalesced(acc01, x_scale, weight_scale, out, - base_row, base_col + kTileN, m, n, lane); - store_prefill_fragment_coalesced(acc10, x_scale, weight_scale, out, - base_row + kTileM, base_col, m, n, lane); - store_prefill_fragment_coalesced(acc11, x_scale, weight_scale, out, - base_row + kTileM, base_col + kTileN, m, n, - lane); - store_prefill_fragment_coalesced(acc20, x_scale, weight_scale, out, - base_row + 2 * kTileM, base_col, m, n, - lane); - store_prefill_fragment_coalesced(acc21, x_scale, weight_scale, out, - base_row + 2 * kTileM, base_col + kTileN, - m, n, lane); - store_prefill_fragment_coalesced(acc30, x_scale, weight_scale, out, - base_row + 3 * kTileM, base_col, m, n, - lane); - store_prefill_fragment_coalesced(acc31, x_scale, weight_scale, out, - base_row + 3 * kTileM, base_col + kTileN, - m, n, lane); -} - -// --------------------------------------------------------------------------- -// Iteration-3 pipeline-control variant: double-buffered K-stage of the same -// 128x64 tile / 64-K stage / fragment loads / epilogue as the accepted -// single-buffered kernel. Stage s+1's three global loads (2 A int4s + 1 B -// int4 per thread) are issued BEFORE the stage s MMAC burst and the loaded -// vectors stay in VGPR; the publish (ds_write into the idle buffer) happens -// AFTER the burst, so the compiler's vmcnt(0) wait sits behind 16 v_mmac + -// ~70 LDS reads instead of directly behind the load issue (the accepted -// single-buffered kernel waits immediately after 3 loads per wave per -// stage). One __syncthreads per stage (32/block vs 64/block: the barrier -// doubles as read-done for stage s+1 and write-done for stage s; the last -// stage needs no fence before the register epilogue). Costs: LDS -// 15,360 -> 30,720 B/block (A[2][128,80] + B[2][64,80]), residency -// 4 -> 2 blocks/CU = 8 waves/CU; VGPR cap at 2 blocks/CU is 128, so the -// ~12-VGPR loop-carried prefetch payload cannot change residency. Retained -// only if ISA/PMC evidence shows the vmcnt wait moved off the load->ds_write -// path (covered by the MMAC burst) and the measured median/p90 beat the -// 1162.15/1164.03 guard. -// --------------------------------------------------------------------------- -__global__ __launch_bounds__(kBlockThreads) void w8a8_dumma_128x64x64_db_kernel( - const int8_t* __restrict__ x_q, - const int8_t* __restrict__ weight, - const float* __restrict__ x_scale, - const float* __restrict__ weight_scale, - bf16_t* __restrict__ out, - int m, - int n, - int k) { - const int tid = static_cast(threadIdx.x); - const int wave = tid / kWaveSize; - const int lane = tid % kWaveSize; - const int wave_row = wave >> 1; - const int wave_col = wave & 1; - const int m0 = static_cast(blockIdx.y) * kBlockM; - const int n0 = static_cast(blockIdx.x) * kBlockN; - const int local_row = wave_row * 64; - const int local_col = wave_col * 32; - - // Double-buffered 64-K stage: A[2][128,80] + B[2][64,80] = 30,720 B/block - // (2 blocks/CU fit the 64 KiB LDS budget; padded 80-byte strides and bank - // phases unchanged from the accepted kernel). - __shared__ __align__(16) int8_t a_tile[2][kBlockM * kAStride]; - __shared__ __align__(16) int8_t b_tile[2][kBlockN * kBStride]; - - DUFragment - a_frag0, a_frag1; - DUFragment - b_frag0, b_frag1; - DUFragment - acc00, acc01, acc10, acc11, acc20, acc21, acc30, acc31; - du_fill_fragment(acc00, 0); - du_fill_fragment(acc01, 0); - du_fill_fragment(acc10, 0); - du_fill_fragment(acc11, 0); - du_fill_fragment(acc20, 0); - du_fill_fragment(acc21, 0); - du_fill_fragment(acc30, 0); - du_fill_fragment(acc31, 0); - - // Cooperative staging indexing is identical to the accepted kernel: each - // thread owns two 16-byte A vectors (rows [0,64) and [64,128)) and one - // 16-byte B vector. - const int vector_byte_offset = tid * static_cast(sizeof(int4)); - const int stage_row = vector_byte_offset / kStageK; // tid / 4, 0..63 - const int stage_col = vector_byte_offset - stage_row * kStageK; // (tid%4)*16 - const int b_row = tid >> 2; // k row of this thread's B int4 - const int b_n16 = (tid & 3) * 16; // n offset (stage-local) of B int4 - - const int k_stages = k / kStageK; - - // Loop-carried prefetch payload (stage s+1 global vectors in VGPR). - int4 pa0, pa1, pb0; - - // Prologue: fetch stage 0 into registers, publish into buffer 0, fence. - { - const int k0 = 0; - pa0 = *reinterpret_cast( - x_q + static_cast(m0 + stage_row) * k + k0 + stage_col); - pa1 = *reinterpret_cast( - x_q + static_cast(m0 + stage_row + kBlockM / 2) * k + k0 + - stage_col); - pb0 = *reinterpret_cast( - weight + (static_cast(k0 + b_row) * n + n0 + b_n16)); - *reinterpret_cast(a_tile[0] + stage_row * kAStride + stage_col) = - pa0; - *reinterpret_cast(a_tile[0] + - (stage_row + kBlockM / 2) * kAStride + - stage_col) = pa1; - *reinterpret_cast(b_tile[0] + b_row * kBStride + b_n16) = pb0; - __syncthreads(); - } - - for (int s = 0; s < k_stages; ++s) { - const int cur = s & 1; - const int nxt = cur ^ 1; - - // Prefetch stage s+1: issue the global loads BEFORE the stage s MMAC - // burst; the loaded vectors stay in VGPR and the compiler's vmcnt(0) wait - // is inserted only at the publish below (covered by the burst). - if (s + 1 < k_stages) { - const int k0 = (s + 1) * kStageK; - pa0 = *reinterpret_cast( - x_q + static_cast(m0 + stage_row) * k + k0 + stage_col); - pa1 = *reinterpret_cast( - x_q + static_cast(m0 + stage_row + kBlockM / 2) * k + k0 + - stage_col); - pb0 = *reinterpret_cast( - weight + (static_cast(k0 + b_row) * n + n0 + b_n16)); - } - - // Consume stage s from the current buffer: bit-identical fragment values, - // MMAC sequence, int32 accumulation order and lane mapping to the - // accepted kernel (only the buffer base changes). -#pragma unroll - for (int kk = 0; kk < kStageK; kk += kTileK) { - load_fragment8(a_frag0, a_tile[cur] + local_row * kAStride + kk, - kAStride, lane); - load_fragment8(a_frag1, a_tile[cur] + (local_row + kTileM) * kAStride + - kk, - kAStride, lane); - du_load_matrix_sync(b_frag0, - b_tile[cur] + kk * kBStride + local_col, kBStride); - du_load_matrix_sync(b_frag1, - b_tile[cur] + kk * kBStride + local_col + kTileN, - kBStride); - du_mma_sync(acc00, a_frag0, b_frag0, acc00); - du_mma_sync(acc01, a_frag0, b_frag1, acc01); - du_mma_sync(acc10, a_frag1, b_frag0, acc10); - du_mma_sync(acc11, a_frag1, b_frag1, acc11); - load_fragment8( - a_frag0, a_tile[cur] + (local_row + 2 * kTileM) * kAStride + kk, - kAStride, lane); - load_fragment8( - a_frag1, a_tile[cur] + (local_row + 3 * kTileM) * kAStride + kk, - kAStride, lane); - du_mma_sync(acc20, a_frag0, b_frag0, acc20); - du_mma_sync(acc21, a_frag0, b_frag1, acc21); - du_mma_sync(acc30, a_frag1, b_frag0, acc30); - du_mma_sync(acc31, a_frag1, b_frag1, acc31); - } - - // Publish stage s+1 into the idle buffer after the burst (vmcnt wait - // covered by the burst), then one fence: read-done for s+1 and - // write-done for s (buffer cur is free for the s+2 publish). - if (s + 1 < k_stages) { - *reinterpret_cast(a_tile[nxt] + stage_row * kAStride + - stage_col) = pa0; - *reinterpret_cast(a_tile[nxt] + - (stage_row + kBlockM / 2) * kAStride + - stage_col) = pa1; - *reinterpret_cast(b_tile[nxt] + b_row * kBStride + b_n16) = pb0; - __syncthreads(); - } - } - - const int base_row = m0 + local_row; - const int base_col = n0 + local_col; - store_prefill_fragment_coalesced(acc00, x_scale, weight_scale, out, - base_row, base_col, m, n, lane); - store_prefill_fragment_coalesced(acc01, x_scale, weight_scale, out, - base_row, base_col + kTileN, m, n, lane); - store_prefill_fragment_coalesced(acc10, x_scale, weight_scale, out, - base_row + kTileM, base_col, m, n, lane); - store_prefill_fragment_coalesced(acc11, x_scale, weight_scale, out, - base_row + kTileM, base_col + kTileN, m, n, - lane); - store_prefill_fragment_coalesced(acc20, x_scale, weight_scale, out, - base_row + 2 * kTileM, base_col, m, n, - lane); - store_prefill_fragment_coalesced(acc21, x_scale, weight_scale, out, - base_row + 2 * kTileM, base_col + kTileN, - m, n, lane); - store_prefill_fragment_coalesced(acc30, x_scale, weight_scale, out, - base_row + 3 * kTileM, base_col, m, n, - lane); - store_prefill_fragment_coalesced(acc31, x_scale, weight_scale, out, - base_row + 3 * kTileM, base_col + kTileN, - m, n, lane); -} - -// --------------------------------------------------------------------------- -// Iteration-4 tile-shape variant: 256x64 output tile (M-major doubling of the -// accepted 128x64 tile) at 256 threads = 4 wavefronts, each wave owning a -// 128x32 quadrant (sixteen m16n16k32 int32 accumulators). This is the -// measured tile axis pushed one step further: per-FLOP B-fragment LDS reads -// scale as 1/(quadrant m-height) (the 64x128 flip's quadrants were 32x64 and -// lost +8% with a doubled per-FLOP B cost), so doubling the quadrant height -// to 128 HALVES the per-FLOP B ds_read_u8 + reassembly VALU - the measured -// LDS-read/wait bottleneck (PMC lds_wait ~= lds_instructions) - while keeping -// the same 64-column quadrant width (B library row_major loader ldm = 80). -// B reuse doubles (each staged B element is consumed by all 256 block rows), -// so the grid-level B re-read drops from M/128 = 32x to M/256 = 16x (staged B -// traffic 402.7 -> 201.3 MB/replay; A re-read stays N/64 = 96x = 805.3 MB; -// total 1.208 -> 1.007 GB, -17%). Single-buffered 64-K stage: A[256,80] + -// B[64,80] = 25,600 B/block -> 2 blocks/CU = 8 waves/CU, the SAME -// co-residency structure as the accepted double-buffered 128x64 kernel -// (2 blocks x 4 waves), so the comparison is not residency-confounded. -// Two __syncthreads per stage, direct 8-byte A fragment fills, fused -// coalesced scale->bf16 epilogue, row>=m guard. Grid dim3(96,16) = 1536 -// blocks = 12.8 blocks/CU, so the MxN output-tile grid still dwarfs the -// 120 CUs. Fragment values, MMAC sequence, int32 accumulation order -// (k0-outer 64, kk-inner 32, hardware k-order in m16n16k32), -// element-to-lane mapping, barrier count and the fused epilogue are -// bit-identical to the accepted kernels (only the tile height, the quadrant -// split and the staging vectorization change), so exact bf16 equality -// (mismatch 0 / max_abs_error 0.0) is expected without tolerance debate. -// --------------------------------------------------------------------------- -__global__ __launch_bounds__(kBlockThreads) - void w8a8_dumma_256x64x64_kernel( - const int8_t* __restrict__ x_q, - const int8_t* __restrict__ weight, - const float* __restrict__ x_scale, - const float* __restrict__ weight_scale, - bf16_t* __restrict__ out, - int m, - int n, - int k) { - const int tid = static_cast(threadIdx.x); - const int wave = tid / kWaveSize; - const int lane = tid % kWaveSize; - // 4 waves: wave_row = (wave>>1)*128 covers rows 0..255 in two 128-row bands - // (waves 2w, 2w+1 share the band), wave_col picks the 32-col half. - const int wave_row = (wave >> 1) * (kBlockM256 / 2); - const int wave_col = wave & 1; - const int m0 = static_cast(blockIdx.y) * kBlockM256; - const int n0 = static_cast(blockIdx.x) * kBlockN; - const int local_row = wave_row; - const int local_col = wave_col * 32; - - // Single-buffered 64-K stage: A[256, 80] + B[64, 80] = 25,600 B/block - // (2 blocks/CU fit the 64 KiB LDS budget; padded 80-byte strides and bank - // phases unchanged from the accepted kernels). - __shared__ __align__(16) int8_t a_tile[kBlockM256 * kAStride]; - __shared__ __align__(16) int8_t b_tile[kBlockN * kBStride]; - - DUFragment - a_frag0, a_frag1, a_frag2, a_frag3, a_frag4, a_frag5, a_frag6, a_frag7; - DUFragment - b_frag0, b_frag1; - // Sixteen m16n16k32 int32 accumulators (8 m-groups x 2 n-groups per wave). - DUFragment - acc00, acc01, acc10, acc11, acc20, acc21, acc30, acc31, - acc40, acc41, acc50, acc51, acc60, acc61, acc70, acc71; - du_fill_fragment(acc00, 0); - du_fill_fragment(acc01, 0); - du_fill_fragment(acc10, 0); - du_fill_fragment(acc11, 0); - du_fill_fragment(acc20, 0); - du_fill_fragment(acc21, 0); - du_fill_fragment(acc30, 0); - du_fill_fragment(acc31, 0); - du_fill_fragment(acc40, 0); - du_fill_fragment(acc41, 0); - du_fill_fragment(acc50, 0); - du_fill_fragment(acc51, 0); - du_fill_fragment(acc60, 0); - du_fill_fragment(acc61, 0); - du_fill_fragment(acc70, 0); - du_fill_fragment(acc71, 0); - - // Cooperative staging: A[256,64] is 1,024 int4s (4 per thread: rows - // [0,64), [64,128), [128,192), [192,256)) and B[64,64] is 256 int4s (one - // per thread), 5 loads per thread per stage. - const int a_row0 = tid >> 2; // 0..63 - const int a_col = (tid & 3) * 16; // 0,16,32,48 - - for (int k0 = 0; k0 < k; k0 += kStageK) { - *reinterpret_cast(a_tile + a_row0 * kAStride + a_col) = - *reinterpret_cast( - x_q + static_cast(m0 + a_row0) * k + k0 + a_col); - *reinterpret_cast(a_tile + - (a_row0 + kBlockM256 / 4) * kAStride + a_col) = - *reinterpret_cast( - x_q + static_cast(m0 + a_row0 + kBlockM256 / 4) * k + - k0 + a_col); - *reinterpret_cast(a_tile + - (a_row0 + kBlockM256 / 2) * kAStride + a_col) = - *reinterpret_cast( - x_q + static_cast(m0 + a_row0 + kBlockM256 / 2) * k + - k0 + a_col); - *reinterpret_cast(a_tile + - (a_row0 + 3 * kBlockM256 / 4) * kAStride + - a_col) = - *reinterpret_cast( - x_q + static_cast(m0 + a_row0 + 3 * kBlockM256 / 4) * k + - k0 + a_col); - const int b_row = tid >> 2; // k row of this thread's B int4, 0..63 - const int b_n16 = (tid & 3) * 16; - *reinterpret_cast(b_tile + b_row * kBStride + b_n16) = - *reinterpret_cast( - weight + (static_cast(k0 + b_row) * n + n0 + b_n16)); - __syncthreads(); - - // Each wave consumes its 128x32 quadrant: eight m-groups x two n-groups, - // 16 m16n16k32 MMACs per kk. The A fragment loads and the MMAC sequence - // keep the exact int32 accumulation order (k0-outer 64, kk-inner 32, - // hardware k-order in m16n16k32) and the element-to-lane mapping of the - // accepted kernels; only the number of m-groups per wave changes. -#pragma unroll - for (int kk = 0; kk < kStageK; kk += kTileK) { - load_fragment8(a_frag0, a_tile + local_row * kAStride + kk, kAStride, - lane); - load_fragment8(a_frag1, a_tile + (local_row + kTileM) * kAStride + kk, - kAStride, lane); - load_fragment8(a_frag2, a_tile + (local_row + 2 * kTileM) * kAStride + - kk, - kAStride, lane); - load_fragment8(a_frag3, a_tile + (local_row + 3 * kTileM) * kAStride + - kk, - kAStride, lane); - du_load_matrix_sync(b_frag0, b_tile + kk * kBStride + local_col, - kBStride); - du_load_matrix_sync(b_frag1, - b_tile + kk * kBStride + local_col + kTileN, - kBStride); - du_mma_sync(acc00, a_frag0, b_frag0, acc00); - du_mma_sync(acc01, a_frag0, b_frag1, acc01); - du_mma_sync(acc10, a_frag1, b_frag0, acc10); - du_mma_sync(acc11, a_frag1, b_frag1, acc11); - du_mma_sync(acc20, a_frag2, b_frag0, acc20); - du_mma_sync(acc21, a_frag2, b_frag1, acc21); - du_mma_sync(acc30, a_frag3, b_frag0, acc30); - du_mma_sync(acc31, a_frag3, b_frag1, acc31); - load_fragment8(a_frag4, a_tile + (local_row + 4 * kTileM) * kAStride + - kk, - kAStride, lane); - load_fragment8(a_frag5, a_tile + (local_row + 5 * kTileM) * kAStride + - kk, - kAStride, lane); - load_fragment8(a_frag6, a_tile + (local_row + 6 * kTileM) * kAStride + - kk, - kAStride, lane); - load_fragment8(a_frag7, a_tile + (local_row + 7 * kTileM) * kAStride + - kk, - kAStride, lane); - du_mma_sync(acc40, a_frag4, b_frag0, acc40); - du_mma_sync(acc41, a_frag4, b_frag1, acc41); - du_mma_sync(acc50, a_frag5, b_frag0, acc50); - du_mma_sync(acc51, a_frag5, b_frag1, acc51); - du_mma_sync(acc60, a_frag6, b_frag0, acc60); - du_mma_sync(acc61, a_frag6, b_frag1, acc61); - du_mma_sync(acc70, a_frag7, b_frag0, acc70); - du_mma_sync(acc71, a_frag7, b_frag1, acc71); - } - - // Protect the LDS buffer from the next stage's cooperative overwrite. - __syncthreads(); - } - - const int base_row = m0 + local_row; - const int base_col = n0 + local_col; - store_prefill_fragment_coalesced(acc00, x_scale, weight_scale, out, - base_row, base_col, m, n, lane); - store_prefill_fragment_coalesced(acc01, x_scale, weight_scale, out, - base_row, base_col + kTileN, m, n, lane); - store_prefill_fragment_coalesced(acc10, x_scale, weight_scale, out, - base_row + kTileM, base_col, m, n, lane); - store_prefill_fragment_coalesced(acc11, x_scale, weight_scale, out, - base_row + kTileM, base_col + kTileN, m, n, - lane); - store_prefill_fragment_coalesced(acc20, x_scale, weight_scale, out, - base_row + 2 * kTileM, base_col, m, n, - lane); - store_prefill_fragment_coalesced(acc21, x_scale, weight_scale, out, - base_row + 2 * kTileM, base_col + kTileN, - m, n, lane); - store_prefill_fragment_coalesced(acc30, x_scale, weight_scale, out, - base_row + 3 * kTileM, base_col, m, n, - lane); - store_prefill_fragment_coalesced(acc31, x_scale, weight_scale, out, - base_row + 3 * kTileM, base_col + kTileN, - m, n, lane); - store_prefill_fragment_coalesced(acc40, x_scale, weight_scale, out, - base_row + 4 * kTileM, base_col, m, n, - lane); - store_prefill_fragment_coalesced(acc41, x_scale, weight_scale, out, - base_row + 4 * kTileM, base_col + kTileN, - m, n, lane); - store_prefill_fragment_coalesced(acc50, x_scale, weight_scale, out, - base_row + 5 * kTileM, base_col, m, n, - lane); - store_prefill_fragment_coalesced(acc51, x_scale, weight_scale, out, - base_row + 5 * kTileM, base_col + kTileN, - m, n, lane); - store_prefill_fragment_coalesced(acc60, x_scale, weight_scale, out, - base_row + 6 * kTileM, base_col, m, n, - lane); - store_prefill_fragment_coalesced(acc61, x_scale, weight_scale, out, - base_row + 6 * kTileM, base_col + kTileN, - m, n, lane); - store_prefill_fragment_coalesced(acc70, x_scale, weight_scale, out, - base_row + 7 * kTileM, base_col, m, n, - lane); - store_prefill_fragment_coalesced(acc71, x_scale, weight_scale, out, - base_row + 7 * kTileM, base_col + kTileN, - m, n, lane); -} - -// --------------------------------------------------------------------------- -// Iteration-5 packed-B variant of the accepted 256x64 kernel: the B operand -// switches to the one-time n-major [N, K] pack (packed[n*K + kk] = -// raw[kk*N + n], produced by launch_pack_w8a8_weight outside the timed -// region and Graph capture; same byte count and same buffer, so captured -// addresses are unchanged) and the B fragment path switches from the -// library row_major loader (8 per-byte ds_read_u8 strided by ldm per lane -// plus mask/OR reassembly VALU per dword - the measured LDS-read axis) to -// the lineage-validated 8-byte fragment fill (load_fragment8, one -// ds_read_b64 per col_major fragment, zero reassembly VALU) on the n-major -// staged tile. The m16n16k32 matrix_b hardware slot for lane -// (r = lane & 15, kq = lane >> 4) is B[k = kk + kq*8 + i][n = local_col + -// i*16 + r] - eight consecutive k bytes at fixed n - so staging B n-major -// (b_tile[n][k]) makes each lane's eight fragment bytes CONTIGUOUS in LDS: -// the fill reads the same elements into the same fragment slots with the -// same v_mmac inputs and the exact int32 accumulation order (k0-outer 64, -// kk-inner 32, hardware k-order), so exact bf16 equality (mismatch 0 / -// max_abs_error 0.0) is expected without tolerance debate. LDS bank -// safety: the n-major B tile uses the 72-byte row stride (odd 8-byte -// count) so the 16 lanes of each ds_read_b64 phase read 18r mod 32 -// (r = 0..15) distinct bank pairs - conflict-free; 72 % 16 == 8 forces the -// two-int64-half staging stores (ds_write2_b64). Iteration 9 gives the A -// tile the same conflict-free floor: the A row stride is 88 bytes (22 -// dwords; 22r mod 32 over r = 0..15 is a permutation of the 16 even dword -// starts), replacing the 80-byte stride's 2-way class (20r mod 32, period -// 8, rows r and r+8 alias), and A staging uses the same two-int64-half -// ds_write2_b64 pattern (88 % 16 == 8). Everything else is bit-identical -// to the accepted kernel: same bytes staged/read (only the physical LDS -// addresses move), same 128x32 quadrant per wave, same MMAC sequence, two -// __syncthreads per stage, fused coalesced scale->bf16 epilogue, row>=m -// guard, grid dim3(96,16) = 1536 blocks. LDS 22,528 (A) + 4,608 (B) = -// 27,136 B/block -> 2 blocks/CU = 8 waves/CU, the same co-residency -// structure as the accepted kernel. +// 4 waves: wave_row = (wave >> 1) * 128 covers rows 0..255 in two 128-row +// bands (waves 2w and 2w+1 share the band), wave_col = (wave & 1) * 64 selects +// the 64-column half. Each wave consumes a 128x64 quadrant: eight m-groups x +// four n-groups = thirty-two m16n16k32 int32 accumulators. The eight A +// fragment addresses per kk and the 88-byte A stride are byte-identical to +// round 1; only the n-major B tile and its four fragment fills are new. +// +// Staging (6 int4 global loads per thread per stage): +// A[256, 88]: thread t owns 16-byte A chunks at (row = t >> 2, +// col = (t & 3) * 16) in each of the four 64-row bands; each +// store is two int64 halves because 88 % 16 == 8. +// B[128, 72]: thread t owns the 16-byte k-chunk at (n_row = t >> 2, +// k16 = (t & 3) * 16) and at n_row + 64 of the packed n-major +// weight; each store is two int64 halves because 72 % 16 == 8. // --------------------------------------------------------------------------- __global__ __launch_bounds__(kBlockThreads) - void w8a8_dumma_256x64x64_packedb_kernel( + void w8a8_dumma_256x128x64_packedb_kernel( const int8_t* __restrict__ x_q, const int8_t* __restrict__ packed_w, // [N, K] n-major (exact shape) const float* __restrict__ x_scale, @@ -911,100 +330,105 @@ __global__ __launch_bounds__(kBlockThreads) const int tid = static_cast(threadIdx.x); const int wave = tid / kWaveSize; const int lane = tid % kWaveSize; - // 4 waves: wave_row = (wave>>1)*128 covers rows 0..255 in two 128-row bands - // (waves 2w, 2w+1 share the band), wave_col picks the 32-col half. - const int wave_row = (wave >> 1) * (kBlockM256 / 2); - const int wave_col = wave & 1; - const int m0 = static_cast(blockIdx.y) * kBlockM256; + const int wave_row = (wave >> 1) * (kBlockM / 2); + const int wave_col = (wave & 1) * (kBlockN / 2); + const int m0 = static_cast(blockIdx.y) * kBlockM; const int n0 = static_cast(blockIdx.x) * kBlockN; const int local_row = wave_row; - const int local_col = wave_col * 32; + const int local_col = wave_col; - // Single-buffered 64-K stage: A[256, 88] (iteration-9 stride) + B[64, 72] - // n-major = 27,136 B/block (2 blocks/CU fit the 64 KiB LDS budget). - __shared__ __align__(16) int8_t a_tile[kBlockM256 * kAStridePacked]; - __shared__ __align__(16) int8_t b_tile[kBlockN * kBStridePacked]; + // Single-buffered 64-K stage: A[256, 88] + B[128, 72] = 31,744 B/block. + __shared__ __align__(16) int8_t a_tile[kBlockM * kAStride]; + __shared__ __align__(16) int8_t b_tile[kBlockN * kBStride]; DUFragment a_frag0, a_frag1, a_frag2, a_frag3, a_frag4, a_frag5, a_frag6, a_frag7; DUFragment - b_frag0, b_frag1; - // Sixteen m16n16k32 int32 accumulators (8 m-groups x 2 n-groups per wave). + b_frag0, b_frag1, b_frag2, b_frag3; + // Thirty-two m16n16k32 int32 accumulators (8 m-groups x 4 n-groups per wave). DUFragment - acc00, acc01, acc10, acc11, acc20, acc21, acc30, acc31, - acc40, acc41, acc50, acc51, acc60, acc61, acc70, acc71; + acc00, acc01, acc02, acc03, acc10, acc11, acc12, acc13, + acc20, acc21, acc22, acc23, acc30, acc31, acc32, acc33, + acc40, acc41, acc42, acc43, acc50, acc51, acc52, acc53, + acc60, acc61, acc62, acc63, acc70, acc71, acc72, acc73; du_fill_fragment(acc00, 0); du_fill_fragment(acc01, 0); + du_fill_fragment(acc02, 0); + du_fill_fragment(acc03, 0); du_fill_fragment(acc10, 0); du_fill_fragment(acc11, 0); + du_fill_fragment(acc12, 0); + du_fill_fragment(acc13, 0); du_fill_fragment(acc20, 0); du_fill_fragment(acc21, 0); + du_fill_fragment(acc22, 0); + du_fill_fragment(acc23, 0); du_fill_fragment(acc30, 0); du_fill_fragment(acc31, 0); + du_fill_fragment(acc32, 0); + du_fill_fragment(acc33, 0); du_fill_fragment(acc40, 0); du_fill_fragment(acc41, 0); + du_fill_fragment(acc42, 0); + du_fill_fragment(acc43, 0); du_fill_fragment(acc50, 0); du_fill_fragment(acc51, 0); + du_fill_fragment(acc52, 0); + du_fill_fragment(acc53, 0); du_fill_fragment(acc60, 0); du_fill_fragment(acc61, 0); + du_fill_fragment(acc62, 0); + du_fill_fragment(acc63, 0); du_fill_fragment(acc70, 0); du_fill_fragment(acc71, 0); + du_fill_fragment(acc72, 0); + du_fill_fragment(acc73, 0); - // Cooperative staging: A[256,64] is 1,024 int4s (4 per thread: rows - // [0,64), [64,128), [128,192), [192,256)) and B[64,64] is 256 int4s (one - // per thread), 5 loads per thread per stage. const int a_row0 = tid >> 2; // 0..63 - const int a_col = (tid & 3) * 16; // 0,16,32,48 - // B staging is n-major: thread owns the 16-byte k-chunk at - // (n_row = tid >> 2, k16 = (tid & 3) * 16). + const int a_col = (tid & 3) * 16; // 0, 16, 32, 48 const int b_n_row = tid >> 2; // 0..63 - const int b_k16 = (tid & 3) * 16; // 0,16,32,48 + const int b_k16 = (tid & 3) * 16; // 0, 16, 32, 48 const int base_row = m0 + local_row; const int base_col = n0 + local_col; - // Iteration-6 (epilogue round) register-batched scales: each lane's 16 - // fragment stores cover the 8 m-groups (rows base_row + r + 16*i, - // r = lane & 15) x 2 n-groups (cols base_col + 4*c4 and - // base_col + 16 + 4*c4, c4 = lane >> 4), so each lane needs exactly 8 - // x_scale values and 2 weight_scale float4s. They are loaded ONCE on the - // last K stage, issued BEFORE the final protective barrier (uniform - // branch: k0 is block-uniform, so the barrier below is reached by every - // thread), so the vmem latency overlaps the barrier and the 16 - // shuffle/convert/store chains; the previous per-call epilogue re-loaded - // the scales inside each of the 16 fragment stores (PMC vmem_read - // surplus ~24-25 wavefront loads/wave above the 5/thread/stage staging - // floor matches ~8 x_scale + ~16 float4 re-loads per lane per wave). + + // Epilogue scale registers: each lane's 32 fragment stores cover the 8 + // m-groups (rows base_row + 16*i + r, r = lane & 15) x 4 n-groups + // (cols base_col + 16*j + 4*c4, c4 = lane >> 4), so each lane needs exactly + // 8 x_scale values and 4 weight_scale float4s. They are loaded once on the + // last K stage (uniform branch: k0 is block-uniform, so the following + // barrier is reached by every thread) and reused by all thirty-two coalesced + // stores; xs_m is zeroed for out-of-range tail rows. float xs_m[8]; - float4 ws0, ws1; + float4 ws0, ws1, ws2, ws3; for (int k0 = 0; k0 < k; k0 += kStageK) { const int4 av0 = *reinterpret_cast( x_q + static_cast(m0 + a_row0) * k + k0 + a_col); const int4 av1 = *reinterpret_cast( - x_q + static_cast(m0 + a_row0 + kBlockM256 / 4) * k + k0 + + x_q + static_cast(m0 + a_row0 + kBlockM / 4) * k + k0 + a_col); const int4 av2 = *reinterpret_cast( - x_q + static_cast(m0 + a_row0 + kBlockM256 / 2) * k + k0 + + x_q + static_cast(m0 + a_row0 + kBlockM / 2) * k + k0 + a_col); const int4 av3 = *reinterpret_cast( - x_q + static_cast(m0 + a_row0 + 3 * kBlockM256 / 4) * k + - k0 + a_col); + x_q + static_cast(m0 + a_row0 + 3 * kBlockM / 4) * k + k0 + + a_col); // A rows are 88 B apart (8 mod 16), so each 16-byte staging store is two - // int64 halves (ds_write2_b64, one instruction) - the exact accepted - // packed-B staging pattern below; a single int4 store would be - // misaligned on odd rows. + // int64 halves (ds_write2_b64); a single int4 store would be misaligned on + // odd rows. const int64_t* asrc0 = reinterpret_cast(&av0); const int64_t* asrc1 = reinterpret_cast(&av1); const int64_t* asrc2 = reinterpret_cast(&av2); const int64_t* asrc3 = reinterpret_cast(&av3); int64_t* adst0 = reinterpret_cast( - a_tile + a_row0 * kAStridePacked + a_col); + a_tile + a_row0 * kAStride + a_col); int64_t* adst1 = reinterpret_cast( - a_tile + (a_row0 + kBlockM256 / 4) * kAStridePacked + a_col); + a_tile + (a_row0 + kBlockM / 4) * kAStride + a_col); int64_t* adst2 = reinterpret_cast( - a_tile + (a_row0 + kBlockM256 / 2) * kAStridePacked + a_col); + a_tile + (a_row0 + kBlockM / 2) * kAStride + a_col); int64_t* adst3 = reinterpret_cast( - a_tile + (a_row0 + 3 * kBlockM256 / 4) * kAStridePacked + a_col); + a_tile + (a_row0 + 3 * kBlockM / 4) * kAStride + a_col); adst0[0] = asrc0[0]; adst0[1] = asrc0[1]; adst1[0] = asrc1[0]; @@ -1013,78 +437,95 @@ __global__ __launch_bounds__(kBlockThreads) adst2[1] = asrc2[1]; adst3[0] = asrc3[0]; adst3[1] = asrc3[1]; - // Stage B[64, 64] n-major from the packed [N, K] weight: 16 consecutive - // k bytes per thread at fixed n (one coalesced int4 global load). Row - // stride 72 is 8 mod 16, so the LDS store is two int64 halves - // (ds_write2_b64) - a single int4 store would be misaligned on odd rows. - const int4 bv = *reinterpret_cast( - packed_w + - (static_cast(n0 + b_n_row) * k + k0 + b_k16)); - const int64_t* bsrc64 = reinterpret_cast(&bv); - int64_t* bdst64 = reinterpret_cast( - b_tile + b_n_row * kBStridePacked + b_k16); - bdst64[0] = bsrc64[0]; - bdst64[1] = bsrc64[1]; + // B staging is n-major: 16 consecutive k bytes at fixed n (one coalesced + // int4 global load from the packed [N, K] weight) in two 64-row bands; + // row stride 72 is 8 mod 16, so each LDS store is two int64 halves. + const int4 bv0 = *reinterpret_cast( + packed_w + static_cast(n0 + b_n_row) * k + k0 + b_k16); + const int4 bv1 = *reinterpret_cast( + packed_w + static_cast(n0 + b_n_row + kBlockN / 2) * k + k0 + + b_k16); + const int64_t* bsrc0 = reinterpret_cast(&bv0); + const int64_t* bsrc1 = reinterpret_cast(&bv1); + int64_t* bdst0 = reinterpret_cast( + b_tile + b_n_row * kBStride + b_k16); + int64_t* bdst1 = reinterpret_cast( + b_tile + (b_n_row + kBlockN / 2) * kBStride + b_k16); + bdst0[0] = bsrc0[0]; + bdst0[1] = bsrc0[1]; + bdst1[0] = bsrc1[0]; + bdst1[1] = bsrc1[1]; __syncthreads(); - // Each wave consumes its 128x32 quadrant: eight m-groups x two n-groups, - // 16 m16n16k32 MMACs per kk. The A fragment loads and the MMAC sequence - // keep the exact int32 accumulation order (k0-outer 64, kk-inner 32, - // hardware k-order in m16n16k32) and the element-to-lane mapping of the - // accepted kernels; only the B fragment fill (one 8-byte contiguous LDS - // read per col_major fragment on the n-major tile) changes. + // Each wave consumes its 128x64 quadrant: eight m-groups x four n-groups, + // 32 m16n16k32 MMACs per kk, in the exact int32 accumulation order + // (k0-outer 64, kk-inner 32, hardware k-order inside m16n16k32). The B + // fragments are loaded once per kk and reused by all eight m-groups; the A + // fragments are consumed in two batches of four to bound their live range + // to eight VGPRs at a time. #pragma unroll for (int kk = 0; kk < kStageK; kk += kTileK) { - load_fragment8(a_frag0, a_tile + local_row * kAStridePacked + kk, - kAStridePacked, lane); - load_fragment8(a_frag1, - a_tile + (local_row + kTileM) * kAStridePacked + kk, - kAStridePacked, lane); - load_fragment8(a_frag2, - a_tile + (local_row + 2 * kTileM) * kAStridePacked + kk, - kAStridePacked, lane); - load_fragment8(a_frag3, - a_tile + (local_row + 3 * kTileM) * kAStridePacked + kk, - kAStridePacked, lane); - load_fragment8(b_frag0, b_tile + local_col * kBStridePacked + kk, - kBStridePacked, lane); - load_fragment8(b_frag1, - b_tile + (local_col + kTileN) * kBStridePacked + kk, - kBStridePacked, lane); + load_fragment8(b_frag0, b_tile + local_col * kBStride + kk, kBStride, + lane); + load_fragment8(b_frag1, b_tile + (local_col + kTileN) * kBStride + kk, + kBStride, lane); + load_fragment8(b_frag2, b_tile + (local_col + 2 * kTileN) * kBStride + kk, + kBStride, lane); + load_fragment8(b_frag3, b_tile + (local_col + 3 * kTileN) * kBStride + kk, + kBStride, lane); + load_fragment8(a_frag0, a_tile + local_row * kAStride + kk, kAStride, + lane); + load_fragment8(a_frag1, a_tile + (local_row + kTileM) * kAStride + kk, + kAStride, lane); + load_fragment8(a_frag2, a_tile + (local_row + 2 * kTileM) * kAStride + kk, + kAStride, lane); + load_fragment8(a_frag3, a_tile + (local_row + 3 * kTileM) * kAStride + kk, + kAStride, lane); du_mma_sync(acc00, a_frag0, b_frag0, acc00); du_mma_sync(acc01, a_frag0, b_frag1, acc01); + du_mma_sync(acc02, a_frag0, b_frag2, acc02); + du_mma_sync(acc03, a_frag0, b_frag3, acc03); du_mma_sync(acc10, a_frag1, b_frag0, acc10); du_mma_sync(acc11, a_frag1, b_frag1, acc11); + du_mma_sync(acc12, a_frag1, b_frag2, acc12); + du_mma_sync(acc13, a_frag1, b_frag3, acc13); du_mma_sync(acc20, a_frag2, b_frag0, acc20); du_mma_sync(acc21, a_frag2, b_frag1, acc21); + du_mma_sync(acc22, a_frag2, b_frag2, acc22); + du_mma_sync(acc23, a_frag2, b_frag3, acc23); du_mma_sync(acc30, a_frag3, b_frag0, acc30); du_mma_sync(acc31, a_frag3, b_frag1, acc31); - load_fragment8(a_frag4, - a_tile + (local_row + 4 * kTileM) * kAStridePacked + kk, - kAStridePacked, lane); - load_fragment8(a_frag5, - a_tile + (local_row + 5 * kTileM) * kAStridePacked + kk, - kAStridePacked, lane); - load_fragment8(a_frag6, - a_tile + (local_row + 6 * kTileM) * kAStridePacked + kk, - kAStridePacked, lane); - load_fragment8(a_frag7, - a_tile + (local_row + 7 * kTileM) * kAStridePacked + kk, - kAStridePacked, lane); + du_mma_sync(acc32, a_frag3, b_frag2, acc32); + du_mma_sync(acc33, a_frag3, b_frag3, acc33); + load_fragment8(a_frag4, a_tile + (local_row + 4 * kTileM) * kAStride + kk, + kAStride, lane); + load_fragment8(a_frag5, a_tile + (local_row + 5 * kTileM) * kAStride + kk, + kAStride, lane); + load_fragment8(a_frag6, a_tile + (local_row + 6 * kTileM) * kAStride + kk, + kAStride, lane); + load_fragment8(a_frag7, a_tile + (local_row + 7 * kTileM) * kAStride + kk, + kAStride, lane); du_mma_sync(acc40, a_frag4, b_frag0, acc40); du_mma_sync(acc41, a_frag4, b_frag1, acc41); + du_mma_sync(acc42, a_frag4, b_frag2, acc42); + du_mma_sync(acc43, a_frag4, b_frag3, acc43); du_mma_sync(acc50, a_frag5, b_frag0, acc50); du_mma_sync(acc51, a_frag5, b_frag1, acc51); + du_mma_sync(acc52, a_frag5, b_frag2, acc52); + du_mma_sync(acc53, a_frag5, b_frag3, acc53); du_mma_sync(acc60, a_frag6, b_frag0, acc60); du_mma_sync(acc61, a_frag6, b_frag1, acc61); + du_mma_sync(acc62, a_frag6, b_frag2, acc62); + du_mma_sync(acc63, a_frag6, b_frag3, acc63); du_mma_sync(acc70, a_frag7, b_frag0, acc70); du_mma_sync(acc71, a_frag7, b_frag1, acc71); + du_mma_sync(acc72, a_frag7, b_frag2, acc72); + du_mma_sync(acc73, a_frag7, b_frag3, acc73); } - // Last stage only: prefetch the epilogue's per-row x_scale and - // per-column weight_scale values into registers (uniform branch: k0 is - // block-uniform, so the barrier below is reached by every thread; the - // row < m guard keeps the x_scale reads in bounds for any m tail). + // Last stage only: prefetch the epilogue's per-row x_scale and per-column + // weight_scale values into registers (uniform branch; the row < m guard + // keeps the x_scale reads in bounds for any m tail). if (k0 + kStageK >= k) { const int r = lane & 15; const int c4 = lane >> 4; @@ -1097,7 +538,12 @@ __global__ __launch_bounds__(kBlockThreads) 4 * c4); ws1 = *reinterpret_cast(weight_scale + base_col + kTileN + 4 * c4); + ws2 = *reinterpret_cast(weight_scale + base_col + + 2 * kTileN + 4 * c4); + ws3 = *reinterpret_cast(weight_scale + base_col + + 3 * kTileN + 4 * c4); } + // Protect the LDS buffer from the next stage's cooperative overwrite. __syncthreads(); } @@ -1106,58 +552,105 @@ __global__ __launch_bounds__(kBlockThreads) base_col, m, n, lane); store_prefill_fragment_coalesced_scaled(acc01, xs_m[0], ws1, out, base_row, base_col + kTileN, m, n, lane); + store_prefill_fragment_coalesced_scaled(acc02, xs_m[0], ws2, out, base_row, + base_col + 2 * kTileN, m, n, lane); + store_prefill_fragment_coalesced_scaled(acc03, xs_m[0], ws3, out, base_row, + base_col + 3 * kTileN, m, n, lane); store_prefill_fragment_coalesced_scaled(acc10, xs_m[1], ws0, out, base_row + kTileM, base_col, m, n, lane); store_prefill_fragment_coalesced_scaled(acc11, xs_m[1], ws1, out, base_row + kTileM, base_col + kTileN, m, n, lane); + store_prefill_fragment_coalesced_scaled(acc12, xs_m[1], ws2, out, + base_row + kTileM, + base_col + 2 * kTileN, m, n, lane); + store_prefill_fragment_coalesced_scaled(acc13, xs_m[1], ws3, out, + base_row + kTileM, + base_col + 3 * kTileN, m, n, lane); store_prefill_fragment_coalesced_scaled(acc20, xs_m[2], ws0, out, base_row + 2 * kTileM, base_col, m, n, lane); store_prefill_fragment_coalesced_scaled(acc21, xs_m[2], ws1, out, base_row + 2 * kTileM, base_col + kTileN, m, n, lane); + store_prefill_fragment_coalesced_scaled(acc22, xs_m[2], ws2, out, + base_row + 2 * kTileM, + base_col + 2 * kTileN, m, n, lane); + store_prefill_fragment_coalesced_scaled(acc23, xs_m[2], ws3, out, + base_row + 2 * kTileM, + base_col + 3 * kTileN, m, n, lane); store_prefill_fragment_coalesced_scaled(acc30, xs_m[3], ws0, out, base_row + 3 * kTileM, base_col, m, n, lane); store_prefill_fragment_coalesced_scaled(acc31, xs_m[3], ws1, out, base_row + 3 * kTileM, base_col + kTileN, m, n, lane); + store_prefill_fragment_coalesced_scaled(acc32, xs_m[3], ws2, out, + base_row + 3 * kTileM, + base_col + 2 * kTileN, m, n, lane); + store_prefill_fragment_coalesced_scaled(acc33, xs_m[3], ws3, out, + base_row + 3 * kTileM, + base_col + 3 * kTileN, m, n, lane); store_prefill_fragment_coalesced_scaled(acc40, xs_m[4], ws0, out, base_row + 4 * kTileM, base_col, m, n, lane); store_prefill_fragment_coalesced_scaled(acc41, xs_m[4], ws1, out, base_row + 4 * kTileM, base_col + kTileN, m, n, lane); + store_prefill_fragment_coalesced_scaled(acc42, xs_m[4], ws2, out, + base_row + 4 * kTileM, + base_col + 2 * kTileN, m, n, lane); + store_prefill_fragment_coalesced_scaled(acc43, xs_m[4], ws3, out, + base_row + 4 * kTileM, + base_col + 3 * kTileN, m, n, lane); store_prefill_fragment_coalesced_scaled(acc50, xs_m[5], ws0, out, base_row + 5 * kTileM, base_col, m, n, lane); store_prefill_fragment_coalesced_scaled(acc51, xs_m[5], ws1, out, base_row + 5 * kTileM, base_col + kTileN, m, n, lane); + store_prefill_fragment_coalesced_scaled(acc52, xs_m[5], ws2, out, + base_row + 5 * kTileM, + base_col + 2 * kTileN, m, n, lane); + store_prefill_fragment_coalesced_scaled(acc53, xs_m[5], ws3, out, + base_row + 5 * kTileM, + base_col + 3 * kTileN, m, n, lane); store_prefill_fragment_coalesced_scaled(acc60, xs_m[6], ws0, out, base_row + 6 * kTileM, base_col, m, n, lane); store_prefill_fragment_coalesced_scaled(acc61, xs_m[6], ws1, out, base_row + 6 * kTileM, base_col + kTileN, m, n, lane); + store_prefill_fragment_coalesced_scaled(acc62, xs_m[6], ws2, out, + base_row + 6 * kTileM, + base_col + 2 * kTileN, m, n, lane); + store_prefill_fragment_coalesced_scaled(acc63, xs_m[6], ws3, out, + base_row + 6 * kTileM, + base_col + 3 * kTileN, m, n, lane); store_prefill_fragment_coalesced_scaled(acc70, xs_m[7], ws0, out, base_row + 7 * kTileM, base_col, m, n, lane); store_prefill_fragment_coalesced_scaled(acc71, xs_m[7], ws1, out, base_row + 7 * kTileM, base_col + kTileN, m, n, lane); + store_prefill_fragment_coalesced_scaled(acc72, xs_m[7], ws2, out, + base_row + 7 * kTileM, + base_col + 2 * kTileN, m, n, lane); + store_prefill_fragment_coalesced_scaled(acc73, xs_m[7], ws3, out, + base_row + 7 * kTileM, + base_col + 3 * kTileN, m, n, lane); } // --------------------------------------------------------------------------- -// Generic scalar fallback: one thread per output element. Exact int32 dot -// over K, then fused x_scale * weight_scale, then bf16 store. Handles every -// unmatched (m, n, k), including all small-M API cases (M=2, M=16) and any -// M with the same (K, N). For the exact (k, n) == (2048, 6144) the weight is -// the n-major packed [N, K] layout (produced once out of the timed region), -// which the fallback decodes; every other (k, n) keeps the identity [K, N] -// layout. +// Generic scalar fallback: one thread per output element. Exact int32 dot +// over K in ascending k order, then fused x_scale * weight_scale, then bf16 +// store. Handles every unmatched (m, n, k), including all small-M API cases +// (M=2, M=16) and any M with the same (K, N). For the exact +// (k, n) == (2048, 6144) the packed_weight buffer holds the n-major transpose +// packed[col * K + kk] = raw[kk * N + col] (produced once outside the timed +// region), which this fallback decodes; every other (k, n) keeps the identity +// [K, N] row-major layout. // --------------------------------------------------------------------------- __global__ __launch_bounds__(256) void w8a8_gemm_scalar_fallback_kernel( const int8_t* __restrict__ x_q, @@ -1178,12 +671,6 @@ __global__ __launch_bounds__(256) void w8a8_gemm_scalar_fallback_kernel( const int col = static_cast(linear - static_cast(row) * n); int32_t acc = 0; const int8_t* a_row = x_q + static_cast(row) * k; - // Iteration 5: for the exact (k, n) == (2048, 6144) the packed_weight - // buffer holds the n-major transpose packed[col*k + kk] = raw[kk*n + col] - // (produced once out of the timed region), so the fallback decodes that - // layout there (keeps the paired M=2/M=16 and M=3072 API shapes with the - // same (K, N) byte-exact); every other (k, n) keeps the identity [K, N] - // row-major copy. const bool packed_nmajor = (k == kTargetK && n == kTargetN); const int8_t* b_ptr = packed_nmajor ? weight + static_cast(col) * k : weight + col; @@ -1198,16 +685,13 @@ __global__ __launch_bounds__(256) void w8a8_gemm_scalar_fallback_kernel( } // --------------------------------------------------------------------------- -// Identity packing (bootstrap; outside the timed region and outside Graph -// capture; the packed buffer keeps the same byte count K*N and the same -// allocated address, so the layout is graph-stable). launch_pack_w8a8_weight -// is an identity device-to-device copy for every (k, n): the packed weight -// is byte-identical to the raw [K, N] weight and the packed scale is -// byte-identical to the raw [N] scale. Later Parallel explore rounds may -// replace these kernels with a real layout transform and must then update -// the matching GEMM interpretation; the identity D2D copy is the generic -// fallback for unmatched (k, n). +// Weight packing, executed once by the trusted binding before Graph capture +// and never inside launch_w8a8_gemm. The packed buffer keeps the same byte +// count as the raw [K, N] weight, so captured addresses are graph-stable. // --------------------------------------------------------------------------- + +// Generic identity device-to-device copy for every (k, n) that has no +// specialized packing. __global__ __launch_bounds__(256) void w8a8_pack_identity_kernel( const int8_t* __restrict__ raw, int8_t* __restrict__ packed, @@ -1219,14 +703,8 @@ __global__ __launch_bounds__(256) void w8a8_pack_identity_kernel( } } -// Exact-shape n-major pack (iteration 5): for (k, n) == (2048, 6144) the -// weight is transposed once, outside the timed region and out of Graph -// capture, into packed[col * k + kk] = raw[kk * n + col] so every col_major -// B fragment is 8 contiguous K bytes in the packed buffer (and in the -// staged n-major LDS tile). Same byte count and same buffer as the -// identity pack, so captured addresses are unchanged; correctness is -// checked against the raw logical [K, N] weight. One thread per output -// byte; runs once during weight prep. +// Exact-shape n-major transpose for (k, n) == (2048, 6144): +// packed[col * k + kk] = raw[kk * n + col]. One thread per output byte. __global__ __launch_bounds__(256) void w8a8_pack_nmajor_i8_kernel( const int8_t* __restrict__ raw, int8_t* __restrict__ packed, @@ -1244,6 +722,7 @@ __global__ __launch_bounds__(256) void w8a8_pack_nmajor_i8_kernel( } } +// The [N] weight scale needs no layout change. __global__ __launch_bounds__(256) void w8a8_pack_scale_identity_kernel( const float* __restrict__ raw, float* __restrict__ packed, @@ -1278,19 +757,17 @@ extern "C" void launch_w8a8_gemm( (void)workspace_bytes; auto* out_bf16 = reinterpret_cast(out); - // Explicit dispatch. The assigned shape (M=4096, N=6144, K=2048) takes the - // iteration-5 packed-B DUMMA 256x64 path (w8a8_dumma_256x64x64_packedb_kernel - // consumes the n-major packed [N, K] weight produced by - // launch_pack_w8a8_weight; the identity-layout kernels stay compiled for - // re-dispatch only together with an identity pack revert); every other + // Explicit exact-shape dispatch. The assigned shape (M=4096, N=6144, + // K=2048) takes the 256x128 packed-B DUMMA path, which consumes the n-major + // packed [N, K] weight produced by launch_pack_w8a8_weight. Every other // (m, n, k) - including small-M API cases (M=2, M=16) and M=3072 with the - // same (K, N) - takes the scalar fallback, which decodes the n-major - // packed layout when (k, n) == (2048, 6144) and the identity [K, N] - // layout otherwise. + // same (K, N) - takes the scalar fallback, which decodes the n-major packed + // layout when (k, n) == (2048, 6144) and the identity [K, N] layout + // otherwise. if (m == kTargetM && n == kTargetN && k == kTargetK) { - const dim3 grid(kTargetN / kBlockN, kTargetM / kBlockM256); + const dim3 grid(kTargetN / kBlockN, kTargetM / kBlockM); const dim3 block(kBlockThreads); - hipLaunchKernelGGL(w8a8_dumma_256x64x64_packedb_kernel, + hipLaunchKernelGGL(w8a8_dumma_256x128x64_packedb_kernel, grid, block, 0, stream, a, b, x_scale, weight_scale, out_bf16, m, n, k); } else { @@ -1313,13 +790,12 @@ extern "C" void launch_pack_w8a8_weight( int k, int n, hipStream_t stream) { - // Iteration 5: the exact (k, n) == (2048, 6144) weight is packed once, - // outside the timed region and out of Graph capture, into the n-major - // [N, K] layout packed[col*k + kk] = raw[kk*n + col] (same byte count and - // same buffer -> graph-stable addresses) so every col_major B fragment is - // 8 contiguous K bytes; every other (k, n) keeps the identity - // device-to-device copy (the scalar fallback decodes the n-major layout - // when (k, n) matches and the identity layout otherwise). + // For the exact (k, n) == (2048, 6144) the logical [K, N] weight is + // transposed once, outside the timed region and outside Graph capture, into + // the n-major [N, K] layout packed[col * k + kk] = raw[kk * n + col] (same + // byte count and same caller-owned buffer -> graph-stable addresses), so + // every col_major B fragment is 8 contiguous K bytes; every other (k, n) + // keeps the identity device-to-device copy. constexpr int kBlock = 256; const dim3 block(kBlock); diff --git a/metainfer/tasks/dcu_kernel_auto_opt/variant/int8w8a8-gemm/glm52/TP8/M4096/shared_down_proj.hip b/metainfer/tasks/dcu_kernel_auto_opt/variant/int8w8a8-gemm/glm52/TP8/M4096/shared_down_proj.hip index a1b49be9..e07786c9 100644 --- a/metainfer/tasks/dcu_kernel_auto_opt/variant/int8w8a8-gemm/glm52/TP8/M4096/shared_down_proj.hip +++ b/metainfer/tasks/dcu_kernel_auto_opt/variant/int8w8a8-gemm/glm52/TP8/M4096/shared_down_proj.hip @@ -1,313 +1,266 @@ -// @@variant shape=glm_tp8_shared_down_proj_m4096 commit=9b56135e1fec933006c477066840f9c84f497cef added=2026-08-31 -// median_us=278.4 p90_us=279.2 speedup=63.57 baseline_us=1.77e+04 -// source=glm5-2-dsh-tp8-m4096-1-e6a280a2 +// @@variant shape=glm_tp8_shared_down_proj_m4096 commit=44018b50c8c0557effd9aa58807ccc6a69dead1f added=2026-09-14 +// median_us=146.7 p90_us=147.2 speedup=120.7 baseline_us=1.77e+04 +// source=test-9-11-1/iteration_002/glm_tp8_shared_down_proj_m4096 (retested A/B) // MetaInfer W8A8 INT8 GEMM backend for Hygon gfx928 (K500SM_AI). -// worker_3 (iteration 3), assigned shapes: -// glm_tp8_shared_gate_up_proj_m4096 : (M, N, K) = (4096, 512, 6144) -// glm_tp8_shared_down_proj_m4096 : (M, N, K) = (4096, 6144, 256) // -// This file provides the two stable host launch symbols consumed by -// csrc/bindings.cpp: -// * launch_w8a8_gemm(...) - timed, graph-safe GEMM on the caller's -// HIP stream; no allocation, packing, -// autotuning, or synchronization. Only the -// caller-provided out and workspace are -// touched (workspace is unused this round). -// * launch_pack_w8a8_weight(...) - out-of-timed-region packing. Identity -// device-to-device copy for every (k, n) -// pair EXCEPT the assigned gate_up weight -// (k, n) == (6144, 512), which is packed -// once as a [K, N] -> [N, K] transpose -// (n-major rows, k contiguous) so the timed -// GEMM consumes B fragments as -// k-contiguous bytes. The pack stays -// out-of-timed, out-of-Graph, same byte -// count, same graph-stable buffer. -// -// Bootstrap strategy (correctness-first, usable profiling baseline): -// * Native INT8 DUMMA m16n16k32 tiled kernel with int32 accumulation for -// every assigned shape with M >= 128 (both assigned shapes qualify). -// * 2-D macro-tile family (64x64, 64x128, 128x64) with one 32x32 quadrant -// per wavefront (four m16n16k32 int32 accumulators kept resident over the -// whole K loop). Single-buffered K loop (K stage 128, two barriers per -// stage): no split-K, no raw asm, no speculative double-buffer pipeline. +// worker_0 (iteration 2, operand-reuse round). Assigned shape: +// glm_tp8_shared_down_proj_m4096 : (M, N, K) = (4096, 6144, 256) // -// Round 1 (baseline): the 64x128 tile measured 517.4 us median / 49.81 TOPS; -// routing the exact gate_up shape to the 64x64 tile measured 432.5 us -// (accepted; 19.6% improvement). +// This file provides the two stable host launch symbols consumed by the +// trusted csrc/bindings.cpp: +// * launch_w8a8_gemm(...) - timed, graph-safe GEMM on the caller's +// HIP stream. No allocation, no packing, no +// autotuning, no host/device synchronization +// and no default-stream launch. Only the +// caller-provided out is written; the +// caller-provided workspace is unused +// because there is no split-K. +// * launch_pack_w8a8_weight(...) - out-of-timed-region packing. The exact +// down_proj (k, n) == (256, 6144) pair is +// packed once into the round-5 panel layout +// [N/128][K/64][128][64] (n-major rows of +// 64 k-bytes per panel) so the timed GEMM +// consumes each 128x64 B stage tile as ONE +// contiguous 8 KiB region and each lane's +// fragment stays k-contiguous; every other +// (k, n) stays the identity device-to-device +// copy of the logical [K, N] weight. // -// Round 2 (operand-reuse, mandated decision): cooperative A+B LDS staging is -// kept and direct per-wave global loads are rejected (A is reused 64x per -// macro-tile via kBlockN and B 64x via kBlockM; direct loads would refetch -// both operands once per wavefront, 4x the 28.3 MB staged global traffic -> -// ~1.73M vmem_read instructions vs 432K, and the TP4 gate_up direct-A arm -// measured 7.9x slower). The focused change makes the staged B layout -// bank-safe for the fragment consumers: the gate_up weight is packed once -// [K,N] -> [N,K] (n-major, k contiguous) and B fragments are loaded -// col_major from a 144-B (128+16) row-strided LDS tile, replacing the -// byte-scattered row_major B reads on the identity [K,N] layout that are the -// 17.3M PMC bank-conflict source. Global loads stay 16-byte vectorized and -// coalesced on both operands. LDS stays 19,456 B/block -> 3 blocks/CU -// (58,368 B, unchanged; round-1's "4 blocks/CU" estimate was impossible on -// both LDS (4x19,456 = 77,824 B > 64 KiB) and VGPR (4x256x72 = 73,728 > -// 65,536)). down_proj keeps the identity 64x128 arm; every unmatched shape -// keeps the generic arms and the scalar fallback (which decodes the packed -// [N,K] layout when (k, n) == (6144, 512) so the paired M=2/M=16 gate_up -// tails stay correct). -// * A[kBlockM, kStageK] and B staged in bank-skewed LDS (row strides -// 144/80/144 = 16-byte-aligned non-power-of-two), consumed with the -// library du_load_matrix_sync loaders: A row_major (unchanged, load- -// bearing on every shape), B row_major for the identity [K, N] layout -// (down_proj + unmatched shapes) and B col_major for the gate_up -// [N, K] packed layout (round 2). +// Round 1 (bootstrap, accepted at 313.66 us median): native INT8 DUMMA +// m16n16k32 tiled kernel with exact int32 accumulation for every M >= 128. +// * 64x128 macro-tile: 8 wavefronts, one 32x32 quadrant per wavefront, four +// m16n16k32 int32 accumulators kept resident across the whole K loop. A and +// B are staged cooperatively into bank-skewed LDS and consumed with the +// library du_load_matrix_sync fragment loaders. +// * SINGLE-buffered K loop, K stage 128 (A[64,128] + B[128,128] = 27,648 +// B/block -> 2 blocks/CU): two barriers per stage, no split-K, no raw asm, +// no speculative double-buffer pipeline. K=256 is two stages. // * Fused direct fragment -> x_scale -> weight_scale -> bf16 epilogue using // the verified gfx928 accumulator ownership (row = lane & 15, -// col_mod4 = lane >> 4, x[i] -> columns col_mod4 + 4*i). The int32 dot is -// exact for every assigned shape (max |dot| = 6144*127*127 << 2^31), the -// int32 -> float conversion is exact, and the single fp32 multiply -// followed by one bfloat16 rounding matches the reference bit-for-bit. -// * Tail M rows are zero-filled on load and masked on store, so any -// M >= 128 is supported by the tiled path. -// * All other (m, n, k) - including every M < 128 API case and the paired -// M=2/M=16 shapes with the same (N, K) - go to a scalar int8/int32 -// fallback that decodes the [N, K] packed layout when (k, n) == -// (6144, 512) and the identity [K, N] layout otherwise. -// -// Round 3 (pipeline, mandated decision): compare single and double buffering -// across K tiles. The exact (4096, 512, 6144) guard now routes to -// w8a8_dumma_prefill_tiled_kernel<64, 64, 64, true, true>: a software- -// pipelined 64-K double-buffered K loop (prologue + one barrier per stage, -// 97 barriers for 96 stages) that issues stage s+1's global loads into the -// idle LDS buffer before the stage-s MMAC burst, overlapping the -// global-load latency that the round-2 single buffer serializes in front of -// every stage (ISA-verified order: global_load_dwordx4 -> ds_write_b128 -> -// s_waitcnt -> s_barrier -> ds_read2 x16 -> v_mmac x16 -> s_barrier, with -// the next stage's loads gated behind the previous stage's barrier). The -// 128-K double-buffered variant is arithmetically impossible at 3 blocks/CU -// (2 x 19,456 = 38,912 B/block x 3 = 116,736 > 64 KiB -> 1 block/CU, a -// harmful occupancy loss), so the pipelined comparison uses the 64-K -// double buffer: LDS 20,480 B/block x 3 = 61,440 <= 64 KiB and -// VGPR 64-72 x 256 x 3 <= 65,536, i.e. 3 blocks/CU preserved (no occupancy -// confound). Everything else is byte-identical: same int4 staging pattern -// (same 28.3 MB global traffic and ~432K vmem_read instructions), same -// k0-outer/kk-inner int32 order (stage size changes only regroup exact -// integer sums, so results stay bit-identical), same col_major B fragments -// (stride 80 = 64+16 bank skew), same fused epilogue, same pack, same -// generic single-buffered arms and scalar fallback. Decision rule (mandate): -// retain double buffering only if PMC shows reduced VMEM/LDS stall evidence -// (lds_wait_instructions drops below ~1.2M, lds_instructions stays ~1.97M, -// occupancy stays 3 blocks/CU) AND the median improves >2%; otherwise the -// round's evidence retains the single-buffered round-2 structure. -// -// Round 10 (fragment-load-form round, restarted from the accepted round-3 -// source after the round-9 infra kill): replace the library -// du_load_matrix_sync fragment loads of the exact-shape gate_up kernel with -// the lineage-validated explicit load_frag8 8-byte ds_read_b64 loader. The -// exact (4096, 512, 6144) guard now routes to -// w8a8_dumma_prefill_tiled_kernel<64, 64, 64, true, true, true> (new -// kDirectFrag template arm). The round-3 ISA shows every fragment load as -// ds_read2_b32 + per-byte v_and/v_or_b32_sdwa/v_or3 mask-OR "identity" -// reassembly (~9.4M of the 15.43M VALU instructions per replay); loading the -// same 8 bytes per lane as one int64 (ds_read_b64) removes that VALU and -// halves the fragment bank-conflict factor (32-lane dword phases at stride -// 80 -> 16-lane 8-byte phases). Element-to-slot fragment mapping, v_mmac -// operands, exact k0-outer/kk-inner int32 accumulation, staging/global -// traffic, barriers, tile, grid, LDS bytes, occupancy (3 blocks/CU) and the -// fused epilogue are unchanged; results stay bit-identical. The generic arms -// (<64,128,128>, <128,64,128>, <64,64,128>, all kBNMajor=false) keep the -// library loaders and byte-identical codegen/resources (if constexpr). -// -// Round 11 (LDS bank-skew round, HIP-only; ISA policy keeps raw asm -// disallowed because the HIP plateau rule - 8 valid HIP rounds within +/-2% -// of the then-current best - is unmet). The accepted round-10 kernel still -// shows PMC lds_bank_conflicts = 9,437,184, EXACTLY unchanged from the -// round-3 code object (the -9.07% round-10 win came from VALU removal, not -// from conflicts). 9,437,184 = 192 x 512 blocks x 96 stages: every -// block-stage pays 192 conflict cycles = 128 from the 16 ds_read2_b64 -// fragment reads + 64 from the 8 ds_write_b128 staging stores (the 16-B/lane -// store floor). The read half is a stride defect: at stride 80 = 20 dwords, -// 20*r mod 32 has period 8, so every 16-lane read phase groups rows r and -// r+8 onto the same bank pair - a 2-way conflict on ALL 4 sub-phases of -// every fragment read. The exact (4096, 512, 6144) guard now routes to -// w8a8_dumma_prefill_tiled_kernel<64, 64, 64, true, true, true, true> (new -// kSkew8 template arm) which stages both operands at the down_proj-validated -// 8-mod-16 row strides A = 88 (64+24) and B = 72 (64+8) instead of 80: -// 22*r and 18*r mod 32 are distinct over r = 0..15 and gcd(22,32) = -// gcd(18,32) = 2 does not divide 1, so every 16-lane read phase lands on 16 -// DISTINCT bank pairs - the fragment reads become conflict-free (128 of the -// 192 cycles removed; only the 2-way 16-B/lane staging-store floor of 64 -// cycles remains -> lds_bank_conflicts expected ~3.15M). The 8-mod-16 row -// starts forbid 16-byte staging stores, so each 16-byte int4 is staged as -// two 8-byte halves (ds_write2_b64), the validated down_proj staging form -// (its B tile is exactly stride 72; its A tile exactly stride 88). Global -// loads (int4, same 432,128 vmem_read), pack layout, barrier structure -// (97/block), tile/grid (512 blocks), LDS bytes (A[64,88] + B[64,72] x 2 = -// 20,480 B/block -> 3 blocks/CU unchanged), VGPR, the k0-outer/kk-inner -// int32 order, the element-to-slot fragment mapping and the fused epilogue -// are untouched; only the LDS row strides and the staging store granularity -// change, so results stay bit-identical. Generic arms (<64,128,128>, -// <128,64,128>, <64,64,128>) keep kSkew8 = false and byte-identical -// codegen/resources (if constexpr). +// col_mod4 = lane >> 4, frag.x[i] -> columns col_mod4 + 4*i). The int32 dot +// is exact for every allowed K and the fp32 left-associated multiply chain +// matches the mathematical reference. +// * A second 64x64 arm covers large-M shapes whose N is a multiple of 64 but +// not of 128. Every other (m, n, k) - including every M < 128 API case and +// the paired M=2/M=16 shapes with the same (N, K) - goes to the scalar +// int8/int32 fallback. // -// Round 16 (lead-2 loop-carried payload round, HIP-only; raw asm stays -// DISALLOWED - plateau=false, the recent-valid-improvement window -// [-34.44%, +2.82%, +9.97%] does not meet three valid HIP rounds within -// [-2%, +2%) of the then-current best, and no prior ISA-guided round -// recorded a compiler limitation with target instructions). The accepted -// round-11 exact code object (digest 9889820e, median 293.30 us / p90 -// 293.545) shows the software-pipeline intent is DEFEATED by the compiler's -// wait placement: per stage the steady-state loop is -// global_load_dwordx4 (A prefetch s+1) -> global_load_dwordx4 (B prefetch -// s+1) -> s_waitcnt vmcnt(1) -> s_waitcnt vmcnt(0) <- waits IMMEDIATELY -// -> 2 x ds_write2_b64 (staging stores) -> 4 x ds_read2_b64 (fragment -// reads, stage s) -> 8 x v_mmac CONTIGUOUS (stage s) -> s_barrier -> loop. -// i.e. the stage-(s+1) global loads are waited ~12-16 instructions after -// issue, BEFORE the stage-s MMAC burst, so the full global-load latency -// (~600-1000+ cycles under 120-CU L2 contention) sits on the per-stage -// critical path with zero overlap (the round-12 analysis' -// "prefetch global load -> ds_write2_b64 -> s_barrier -> ds_read2_b64 -> -// v_mmac" chain; PMC: 6.0M VALU + 1.18M LDS + 465K VMEM wave-instructions -// over 293 us x 120 CUs, i.e. ~95-99% issue-stalled). Iteration history -// rules out the other levers on this exact shape: tile aspect (iter 4 -// 32x64: 749.9 us; iter 8 64x128 w8: 395.4 us), 2-wave quadrants (iter 6: -// 629.0 us - wave count per CU is load-bearing), LDS-side fragment -// prefetch (iter 7: 337.97 us, flat/worse), stage 32 / 6 blocks per CU -// (iter 12: 447.4 us - per-stage fixed cost, not occupancy, is the lever). -// The one untested mechanism that directly removes the exposed vmcnt wait: -// a LEAD-2 LOOP-CARRIED REGISTER PAYLOAD (the TP4 o_proj lineage's -// validated "stage-top publish" form). The exact (4096, 512, 6144) guard -// now routes to w8a8_dumma_prefill_tiled_kernel<64,64,64,true,true,true, -// true,true> (new kPayload template arm): each thread keeps the one A int4 -// + one B int4 of stage s+2 in VGPR across the stage-(s+1) barrier, issues -// those global loads at the TOP of iteration s (no wait), and PUBLISHES -// the payload carried from iteration s-1 into the idle LDS buffer at the -// TOP of iteration s (before the stage-s MMAC burst). The first use of the -// payload registers is the next iteration's publish, and the s_barrier -// fences outstanding vmem, so the compiler can no longer place -// s_waitcnt vmcnt(0) immediately after the loads: the wait lands after the -// stage-s fragment reads + MMAC burst (immediately before the barrier), and -// the global-load latency overlaps the whole compute segment instead of -// serializing in front of it. One barrier per 64-K stage is preserved -// (prologue + 96 = 97/block); the publish targets the idle buffer (WAR -// retired by the previous barrier, RAW retired by the stage barrier), so no -// second barrier is introduced. Everything else is byte-identical: 64x64 -// tile / 256 threads / 4 waves / 32x32 quadrant per wave, grid (8,64) = -// 512 blocks, kSkew8 strides 88/72 with ds_write2_b64 staging (8 per -// block-stage), same 16 ds_read2_b64 fragment reads per block-stage (same -// lds_instructions ~1,179,648), same 20,480 B LDS/block -> 3 blocks/CU -// (payload costs 8 VGPRs: ~54 total, 54 x 256 x 3 = 41,472 <= 65,536; no -// spill risk at 3 blocks/CU), same int4 global loads (same ~432,128 -// vmem_read / 32,768 vmem_write), same k0-outer/kk-inner int32 order and -// the same element-to-slot fragment mapping - the payload changes only WHEN -// global bytes land in LDS, never which bytes or the accumulation order, so -// results stay bit-identical (mismatch 0 / max_abs_error 0.0 expected). -// Generic arms (<64,128,128>, <128,64,128>, <64,64,128>, all kBNMajor= -// false) keep kPayload = false and byte-identical codegen/resources -// (if constexpr). +// Round 2 (this session, mandated operand-reuse decision): compare direct +// per-wave global loads with cooperative A+B LDS staging on the exact +// (4096, 6144, 256) shape. +// * Reuse accounting for the 64x128 macro-tile with one 32x32 quadrant per +// wave: every A element participates in kBlockN = 128 output MACs and every +// B element in kBlockM = 64 MACs, so cooperative staging keeps ONE global +// read and ONE LDS write per element while direct per-wave fragment loads +// would re-read the A tile kBlockN/32 = 4 times and the B tile +// kBlockM/32 = 2 times from global (per 64-K stage: 12,288 B staged vs +// 32,768 B of fragment reads, a 2.67x global-traffic amplification on an +// already L2-resident operand stream). Direct loads are rejected on that +// amplification and on the sibling lineage's measured 7.9x direct-arm +// regressions. +// * The bootstrap exact-shape arm reads the identity [K, N] B layout with +// byte-scattered row_major library fragment loads: its exact code object +// and PMC show 3,686,400 LDS instructions, 7,864,320 LDS bank conflicts, +// 5,131,888 LDS waits and 17.4M VALU per operator replay at 313.66 us +// median (11% issue utilization). +// * Focused change: the exact (m, n, k) == (4096, 6144, 256) guard now routes +// to w8a8_dumma_proj_operand_reuse_kernel<64, 128, 64>, which keeps +// cooperative A+B LDS staging, keeps 16-byte vectorized coalesced global +// staging loads on both operands, and fixes the LDS consumer side: +// - the one-time pack transposes B to the n-major [N, K] layout, so the +// staged B tile is [kBlockN, kStageK] with k contiguous and each +// matrix_b col_major fragment lane owns 8 contiguous bytes (the +// identity-layout row_major B reads are the bank-conflict source); +// round 5 keeps the identical staged LDS bytes but re-orders the +// packed buffer into the [N/128][K/64][128][64] panel layout so each +// block-stage B tile is one contiguous global region (see below); +// - A and B fragments are read with the explicit 8-byte load_frag8 +// loader (ONE 8-byte LDS read per fragment) instead of the library +// per-byte loaders plus reassembly VALU; the element-to-slot mapping +// is identical, so the MMAC operands and accumulation are unchanged; +// - LDS row strides are the bank-safe 8-mod-16 values A = 88 (64+24) and +// B = 72 (64+8): 22*r and 18*r mod 32 are 16 distinct values over +// r = 0..15, so every 16-lane ds_read_b64 fragment phase lands on 16 +// distinct bank pairs. The 8-mod-16 row starts forbid 16-byte stores, +// so each staged int4 is written as two 8-byte halves; +// - 64-K STAGE single buffer: 2 barriers/stage (8 per block for K=256), +// LDS A[64,88] + B[128,72] = 14,848 B/block -> 2 blocks/CU (occupancy +// unchanged from the bootstrap's 27,648 B / 2 blocks/CU), 64x128 tile, +// 512 threads / 8 waves, grid (48, 64) = 3072 blocks. +// The k0-outer / kk-inner int32 accumulation order, the element-to-slot +// fragment mapping and the fused epilogue are unchanged, so results stay +// bit-identical (mismatch 0 / max_abs_error 0.0 expected). +// * Expected measured effect: lds_instructions 3,686,400 -> ~540,672, +// lds_bank_conflicts 7,864,320 -> ~1,179,648, lds_wait 5,131,888 -> +// ~162,664, VALU 17.4M -> ~6M, median 313.66 us -> ~275-290 us. +// * Every other shape keeps the bootstrap arms. The scalar fallback decodes +// the packed [N, K] layout for (k, n) == (256, 6144) so the paired M=2/M=16 +// API shapes stay correct, and the packed pair never reaches an +// identity-layout tiled arm. // -// Round 2 (this session, glm_tp8_shared_down_proj_m4096 exact-shape arm): -// the generic <64,128,128> identity-layout arm that served down_proj -// (4096, 6144, 256) measured 314.72 us median with PMC -// lds_bank_conflicts = 7,864,320, ~600 LDS instructions and heavy per-byte -// fragment-reassembly VALU per block-stage (library row_major loaders on -// the identity [K, N] B layout). The exact (4096, 6144, 256) guard now -// routes to w8a8_dumma_prefill_tiled_kernel<64, 128, 64, true, true, true, -// true> - the file's own lineage-validated gate_up machinery transplanted -// to down_proj: B packed once [K,N] -> [N,K] n-major (kBNMajor, same -// transpose kernel as gate_up, same byte count / graph-stable buffer, -// out-of-timed/out-of-Graph), 64-K double-buffered K loop (kDoubleBuffer, -// prologue + 4 stages for K=256, one barrier per stage), explicit 8-byte -// load_frag8 fragment reads (kDirectFrag, one ds_read_b64 per fragment, -// zero reassembly VALU) and the 8-mod-16 bank-skewed LDS strides A=88 / -// B=72 (kSkew8, down_proj-validated, conflict-free 16-lane fragment -// phases). LDS A[64,88] + B[128,72] x 2 = 29,696 B/block -> 2 blocks/CU -// (occupancy unchanged vs the 27,648-B single buffer); grid (48, 64) = -// 3072 blocks; the stage size 128->64 only regroups exact integer sums -// (k0-outer / kk-inner int32 order over k = 0,32,64,96,128,160,192,224 is -// unchanged) and load_frag8 fills the same x[0..7] slots with the same -// little-endian bytes, so results stay bit-identical (mismatch 0 / -// max_abs_error 0.0 expected). +// Round 5 (this session, mandated PACKING round): test one weight +// packing/swizzle that makes each DUMMA B tile vector-loadable and +// LDS-bank-safe, with the pack outside timing and the graph-stable packed +// layout validated. +// * The accepted round-2 arm stages its 128x64 B tile from the plain +// n-major [N, K] packed weight: per block-stage the 512 int4 loads are 16 +// n-rows x 4 int4 per 64-lane warp at a 256-B row stride, i.e. every warp +// instruction touches 16 DISTINCT 128-B lines with only 64 B used in each +// (the same line is completed by another stage of the same block). Still +// 16-byte vector loads, but scattered at line granularity. +// * Focused change: the one-time pack for (k, n) == (256, 6144) ONLY becomes +// the panel layout [N/128][K/64][128][64] (n-major 64-k-byte rows per +// panel, panels ordered n-tile outer / k-tile inner). Byte-exact index +// relation: packed[((nt*(k/64)+kt)*128+nn)*64+kk64] == +// raw[(nt*128+nn)*k + kt*64+kk64]. Each block's per-stage B tile +// (n0 == 128*blockIdx.x, k0 == 64*stage) is now ONE contiguous 8 KiB +// region, so a 64-lane warp instruction reads 1 KiB contiguous (full +// 128-B line utilization) instead of 16 half-used lines; the per-thread +// address stays one base + one local_col*kStageK + v*16 offset. +// * Everything else is byte-identical: same 64-K single-buffered stage with +// two barriers, same 64x128 tile / 512 threads / 8 waves / 32x32 quadrant +// per wave, same grid (48, 64) = 3072 blocks, same LDS A[64,88] + +// B[128,72] = 14,848 B/block -> 2 blocks/CU, same ds_write2_b64 8-byte-half +// staging stores and same conflict-free load_frag8 ds_read_b64 fragment +// reads (the LDS-bank-safe strides 88/72 and the staged LDS bytes are +// untouched - only the GLOBAL byte order of the packed buffer changes), +// same k0-outer/kk-inner int32 order, same fused epilogue -> mismatch 0 / +// max_abs_error 0.0 expected. The scalar fallback decodes the panel layout +// for (k, n) == (256, 6144) so the paired M=2/M=16 tails stay correct; +// every other (k, n) keeps the identity copy. Packing runs +// out-of-timed/out-of-Graph in launch_pack_w8a8_weight, same byte count +// (256*6144), same grid (n*k/16 threads), same graph-stable buffer. +// * Falsifiable gate: median_us < 273.2886219024658 AND p90_us < +// 275.1925849914551 (and mismatch 0 / graph capture true) -> the +// line-scattered B staging was binding and the panel pack wins; otherwise +// the plain n-major pack is retained and this round records that the B +// stream is L2-resident (90.4% hit, same 32 sectors/warp either way) and +// NOT binding on the latency-stalled critical path. // -// Round 3 (this session, glm_tp8_shared_down_proj_m4096 pipeline round): -// mandated single vs double buffering comparison across K tiles. The -// accepted round-2 arm (<64,128,64,true,true,true,true>, 286.53 us median / -// 286.64 us p90, digest cd9791d0) is the 64-K DOUBLE-buffered pipeline; its -// exact code object shows the software pipeline already works on this shape -// (steady-state loop: ds_write2_b64 publish s+1 -> global_load_dwordx4 x2 -// for s+2 -> ds_read2_b64 x4 (fragment reads s+1) -> v_mmac x8 -> s_waitcnt -// vmcnt(0) lgkmcnt(0) -> s_barrier: the stage-(s+2) loads stay in flight -// across the fragment reads + MMAC burst and are waited only after it, i.e. -// reduced VMEM stalls), and PMC shows no harmful LDS or occupancy growth vs -// the pre-round-2 single buffer (29,696 vs 27,648 B/block, 2 blocks/CU -// pinned; lds_wait 162,664 / lds_instructions 540,672 / conflicts -// 1,179,648). The (4096, 6144, 256) guard now routes to the SINGLE-buffered -// form <64,128,64,true,false,true,true>: identical stage size 64, identical -// kBNMajor/kDirectFrag/kSkew8/tile/grid/epilogue, kDoubleBuffer = false, so -// each stage serializes its global-load round trip (load -> wait -> -// ds_write2_b64) in front of the MMAC burst with two barriers per stage -// (8/block vs 5/block) and LDS halves to 14,848 B/block. The single-buffer -// branch now threads kSkew8 and kDirectFrag through its staging/MMAC calls -// (previously hardcoded to the library-loader defaults; generic arms keep -// byte-identical codegen via if constexpr). Falsifiable gate: if the -// single-buffered median is NOT strictly below 286.53368949890137 (p90 -// guard < 286.6385078430176), double buffering is retained with the ISA/PMC -// evidence above; if it IS below, the single buffer wins on this shape -// (LDS halved; VGPR may drop <= 42 and open 3 blocks/CU, the trusted -// occupancy-probe set [2, 3, 4]). Results stay bit-identical either way -// (same stage size, same k0-outer/kk-inner int32 order, same bytes). +// Round 6 (this session, mandated EPILOGUE round): fuse per-row x per-column +// scaling, bf16 conversion and a fully coalesced final store into the compute +// kernel and remove any unnecessary workspace/combine pass. +// * The accepted arm already fuses the scaling, conversion and store into +// w8a8_dumma_proj_operand_reuse_kernel, and the operator never allocates or +// reads the caller workspace (there is no split-K partial and no combine +// kernel), so there is no pass to remove. What the round-2..5 epilogue did +// NOT do is store coalesced: the gfx928 fragment ownership (row = lane & 15, +// col = (lane >> 4) + 4*i) makes each 64-lane *bf16* store instruction +// carry 16 rows x 4 columns, i.e. 16 distinct 128-B global lines with only +// 8 useful bytes in each. The exact code object confirms it: 16 +// global_store_short_d16_hi per thread, interleaved with ~20 scalar +// x_scale/weight_scale loads (the 21 static global loads) and s_waitcnt +// vmcnt chains, and the counter-derived write traffic is 57,966,336 B for a +// 50,331,648 B bf16 output (1.152x sector overhead) out of 61,997,440 B +// total per operator replay. +// * Focused change: the exact-arm epilogue now (a) cooperatively stages the +// 64 x_scale and 128 weight_scale values into the retired A LDS tile with +// one coalesced load per thread, (b) writes each thread's 16 scaled bf16 +// results into a 64-row x 64-column half of the retired B LDS tile with the +// bank-safe 8-mod-16 element stride 68 (136 B), and (c) after one barrier +// writes the half back with 16-byte stores - 8 threads cover one full +// 128-B output line, so a 64-lane store instruction moves 8 complete lines +// instead of 16 partial ones. Two column halves plus one scale-publish +// barrier (5 extra barriers per block); the staged bytes are the same +// values in the same logical [M, N] order, and the scale multiply order +// ((float)acc * x_scale * weight_scale) and the bf16 conversion are +// byte-identical, so results stay bit-exact (mismatch 0 / max_abs_error 0.0 +// expected). +// * The A/B staging buffers are dead after the K loop, so the epilogue reuses +// them: LDS stays 14,848 B/block -> 2 blocks/CU, the grid stays (48, 64) = +// 3072 blocks and the tile stays 64x128 / 512 threads / 8 waves. Every +// other shape keeps the bootstrap arms and the scalar fallback (including +// the shared pair M=2/M=16 down_proj tails) untouched. +// * Falsifiable gate: median_us < 270.556 us (>= 1% under the 273.2886219 us +// official best) AND p90_us <= 275.1925849914551, with the coalesced store +// cutting write_bytes_per_operator_replay from 57,966,336 toward the +// 50,331,648 B theoretical minimum and replacing 393,216 vmem_write +// instructions with ~98,304 (16 bytes each). If the median does not clear +// the 1% bar, the direct 2-byte-scatter epilogue is restored. // -// Round 4 (this session, glm_tp8_shared_down_proj_m4096 tile-shape round): -// the balanced 1:1 aspect probe <64,64,64,true,false,true,true> (256 thr = -// 4 waves, 6144 blocks, LDS 10,240 B/block -> 4 blocks/CU, doubled A -// staging) measured 297.15 us median / 297.97 us p90 and was REJECTED -// (guard restored to the round-3 single-buffered <64,128,64,true,false, -// true,true> winner, 281.65 us / 282.52 us): co-residency was already -// saturated at 2 blocks/CU and the +33% A-side staging cost was not repaid. +// Round 15 (final consolidate / HIP-only polish on the frozen winning arm): +// move the exact arm's scale delivery from the retired-A LDS slab to +// per-thread registers. +// * Frozen winning architecture (accepted best 158.0229902267456 us median / +// 158.49021911621094 us p90, source digest c548257a): 64x128 macro-tile, +// 512 threads / 8 waves with a 32x32 quadrant per wave, 64-K +// single-buffered two-barrier stages, round-5 panel pack +// [N/128][K/64][128][64], grid (48,64) = 3072 blocks, A[64,88] + B[128,72] +// = 14,848 B LDS -> 2 blocks/CU (arch_vgpr 48), round-6 LDS-staged +// coalesced fused epilogue. +// * Evidence this round acts on: the exact-source PMC (120 CU, 3072 x 512, +// 147.532 us profiled) reports lds_instructions 1,631,232 with +// lds_wait_instructions 1,486,046 at ~11% SIMD issue utilization, i.e. ~66 +// LDS instructions per wave of which ~60 stall; the epilogue's scale +// read-back alone is ~10 of those LDS reads per wave (x_scale[row + r] and +// the four weight_scale columns of each of the two column groups, once per +// accumulator fragment), all of them on the post-K-loop critical path. The +// per-block tail is otherwise serialized behind 5 barriers (scale publish + +// publish/WAR per 64-column half). +// * Focused change (one bounded mechanism, exact `(m,n,k) == (4096,6144,256)` +// arm only): each thread loads its OWN two row scales (rows +// local_row + (lane & 15) and local_row + kTileM + (lane & 15)) and eight +// column scales (columns local_col + (lane >> 4) + 4*i for the two kTileN +// column groups) into registers ONCE before the K loop, and +// stage_epilogue_fragment consumes those registers instead of reading +// xs_tile/ws_tile from the retired A tile. Consequences: (a) no +// x_scale/weight_scale is staged into or read back from LDS anywhere in the +// exact arm, (b) the per-block scale-publish barrier disappears (block +// barriers 13 -> 12: 8 K-loop + 4 epilogue), (c) the 10 global scale loads +// per thread are issued before the K loop, so their latency hides behind the +// four 64-K stages instead of sitting in front of the staging multiplies. +// * Byte-identical data path: the values are the same x_scale[m0 + row] / +// weight_scale[n0 + col], the multiply order +// ((float)acc * x_scale * weight_scale) and the bf16 conversion are +// unchanged, and every staged LDS address (stride 68) and every coalesced +// 16-byte global store address is unchanged, so mismatch 0 / max_abs_error +// 0.0 are expected. The guard launches exactly grid(n/128, m/64) with +// m == 4096 and n == 6144, so the registered rows/columns are always in +// range (m0 + local_row + kTileM + 15 <= 4095, n0 + local_col + kTileN + +// 3 + 12 <= 6139). +// * Everything else stays byte-identical: tile, grid, waves, K stage, +// single buffering, panel pack, A/B strides and vectorized staging, LDS +// 14,848 B, kernel name / launch ABI, all shape guards, the generic +// prefill arms and the scalar fallback. +// * Falsifiable prediction: median_us <= 158.0229902267456 (expected +// ~155.8-158.0 us) AND p90_us <= 158.49021911621094, with lds_instructions +// per wave down by ~10 and lds_wait down by a similar amount while +// arch_vgpr stays <= 64 (no spills) and the LDS footprint stays 14,848 B. +// If the median does not improve, the register scales were not on the +// critical path (the LDS broadcast reads were already cheap) and the +// round-6 LDS slab is restored. // -// Round 5 (this session, glm_tp8_shared_down_proj_m4096 PACKING round): test -// one weight packing/swizzle that makes each DUMMA B tile vector-loadable -// and LDS-bank-safe, with the packing outside timing and the graph-stable -// packed layout validated. The accepted round-3 arm stages its 128x64 B -// tile from the plain n-major [N,K] packed weight: per block-stage the 512 -// int4 loads are 16 rows x 4 int4 per warp at 256-B row stride, i.e. every -// warp instruction touches 16 DISTINCT 64-B segments (50% of each 128-B L2 -// line) - vector loads, but scattered at L2 granularity. The round changes -// the one-time pack for (k,n) == (256,6144) ONLY to the panel layout -// [N/128][K/64][128][64] (n-major 64-k-byte rows per panel; byte-exact index -// relation packed[((nt*(k/64)+kt)*128+nn)*64+kk64] == raw[(nt*128+nn)*k + -// kt*64+kk64], verified by simulation): each block's per-stage B tile is now -// ONE contiguous 8 KiB region, so every warp instruction reads 1 KiB -// contiguous (full sector/line utilization; per block-stage the B staging -// collapses from 128 half-used 128-B lines to 64 fully-used lines). The new -// exact-shape arm w8a8_dumma_prefill_tiled_kernel<64,128,64,true,false, -// true,true,false,true> (new kBPanel template flag, default false) is -// byte-identical in EVERYTHING else: same 64-K single-buffered stage, same -// kBNMajor/kDirectFrag/kSkew8, same 64x128 tile / 512 thr / 8 waves / 32x32 -// quadrant, same grid (48,64) = 3072 blocks, same LDS A[64,88]+B[128,72] = -// 14,848 B/block @ 2 blocks/CU, same ds_write2_b64 staging stores and same -// conflict-free load_frag8 ds_read2_b64 fragment reads (LDS-bank-safe -// preserved - only the GLOBAL byte order of the packed buffer changes, -// never the staged LDS bytes), same k0-outer/kk-inner int32 order, same -// fused epilogue -> mismatch 0 / max_abs_error 0.0 expected. The scalar -// fallback decodes the panel layout for (k,n) == (256,6144) so M<128 tails -// stay correct; gate_up (6144,512) keeps its plain [N,K] transpose pack and -// its own arm; generic arms are untouched (kBPanel=false). Packing runs -// out-of-timed/out-of-Graph in launch_pack_w8a8_weight, same byte count -// (256*6144), same graph-stable buffer. Expected effect: B global staging -// becomes perfectly coalesced 1 KiB-per-warp streams with half the L2 line -// count; PMC vmem_read_instructions unchanged (~614,400 - same 512 int4 -// loads/block-stage), lds_* identical (LDS layout untouched), l2_hits/l2_ -// misses shift slightly in the direction of fewer miss sectors, valu ~flat -// (address math per thread unchanged: one base + offset). FALSIFIABLE GATE: -// median_us < 281.6507148742676 AND p90_us < 282.5235176086426 -> B-global -// coalescing was binding on this shape and the panel pack wins; otherwise -// the plain n-major pack is retained and this round records that B reads -// are L2-resident (90.4% hit) and NOT binding on the LDS-bound critical -// path (consistent with the lineage rule that pack layout stops paying once -// fragment loads/strides are fixed). +// Round 17 (fix_build, final round): restore a compiling candidate on the +// frozen round-15 winning arm after round 16 produced no valid proposal (the +// worker process exited on a rate-limit crash before any build ran). The only +// landed correction is the round-16 dead-code cleanup of the exact arm's A +// staging helper, re-applied with a consistent API so the definition and its +// single call site agree: +// * stage_proj_tiles is instantiated ONLY by the exact arm, whose host guard +// requires m == kExactM and launches exactly grid(n/kBlockN, m/kBlockM), so +// every staged row m0 + local_row is in range and the per-int4 +// `global_row < m` compare/select/zero-fill was dead. The helper drops its +// now-unused `m` parameter (the call site drops the argument) and gains a +// static_assert that the exact arm covers whole kBlockM row tiles; every +// staged A byte is unchanged, so results stay bit-exact. +// * Deliberately NOT included: the round-13/16 final-half post-store barrier +// skip. That is a runtime change whose measured p90 regressed at the 158-us +// baseline; this round is a build fix, not a latency change. +// * Everything else is byte-identical to the accepted round-15 best: 64x128 +// macro-tile, 512 threads / 8 waves with a 32x32 quadrant per wave, 64-K +// single-buffered two-barrier stages, round-5 panel pack +// [N/128][K/64][128][64], round-15 register scales, round-6 LDS-staged +// coalesced epilogue (both epilogue barriers kept), A[64,88] + B[128,72] = +// 14,848 B LDS, grid (48,64) = 3072 blocks, 2 blocks/CU, kernel name / +// launch ABI, every shape guard and the generic scalar fallback. +// * Falsifiable prediction: build_success true, correctness exact (mismatch 0 +// / max_abs_error 0.0), graph capture true, median_us flat at the accepted +// best 148.41579914093018 us (the removed compare/select was not on the +// critical path), p90_us <= 148.8046169281006, arch_vgpr <= 64, +// scratch_bytes 0 and LDS 14,848 B. // -// Header order is fixed by the control plane: hip_runtime first (du_mma.h -// is not self-contained before it), hip_bfloat16 second, du_mma.h last. +// Header order is fixed by the control plane: hip_runtime first (du_mma.h is +// not self-contained before it), hip_bfloat16 second, du_mma.h last. #include #include @@ -325,12 +278,32 @@ constexpr int kTileK = 32; // gfx928 wavefront is 64 lanes; every blockDim must be a multiple of 64. constexpr int kWaveSize = 64; -// Large-prefill dispatch geometry. Each macro-tile config uses a 32x32 -// quadrant per wave, so waves per block = (BlockM/32) * (BlockN/32). +// Large-prefill dispatch geometry. Each macro-tile config owns a 32x32 +// quadrant per wavefront, so waves per block = (BlockM/32) * (BlockN/32). constexpr int kPrefillMinM = 128; -constexpr int kPrefillStageK = 128; constexpr int kDefaultBlockM = 64; -constexpr int kDefaultBlockN = 64; +constexpr int kDefaultBlockN = 128; +constexpr int kDefaultStageK = 128; + +// Round 2 exact-shape arm: the assigned (4096, 6144, 256) shape only. +constexpr int kExactM = 4096; +constexpr int kExactN = 6144; +constexpr int kExactK = 256; +constexpr int kProjBlockM = 64; +constexpr int kProjBlockN = 128; +constexpr int kProjStageK = 64; +// The exact-shape arm uses the bank-safe 8-mod-16 LDS row strides A = 88 +// (kProjStageK + 24) and B = 72 (kProjStageK + 8); see the round-2 header +// comment. The stride scheme assumes kStageK == 64. + +// Round 6 (epilogue round) LDS staging geometry for the fused epilogue: the +// 64x128 bf16 output tile is staged one 64-column half at a time (kEpiCols) +// into the retired B staging buffer with the bank-safe 8-mod-16 element row +// stride kEpiStride = 68 bf16 = 136 B (16 row starts land on 16 distinct LDS +// banks and the stride stays 8-byte aligned for the int64 read-back). The +// retired A staging buffer holds the 64 x_scale and 128 weight_scale values. +constexpr int kEpiCols = 64; +constexpr int kEpiStride = 68; constexpr int kScalarThreads = 256; @@ -338,8 +311,9 @@ using namespace du::dumma; // Verified gfx928 INT8 DUMMA accumulator ownership: // row = lane & 15, col_mod4 = lane >> 4, frag.x[i] -> columns -// col_mod4 + 4*i. Scale by x_scale[row] and weight_scale[col] and store -// bf16 directly from the fragment (no accumulator LDS round trip). +// col_mod4 + 4*i. +// Scale by x_scale[row] and weight_scale[col] and store bf16 directly from the +// fragment (no accumulator LDS round trip). Tail rows (row >= m) are masked. template __device__ __forceinline__ void store_prefill_fragment( const AccFragment& frag, @@ -360,61 +334,37 @@ __device__ __forceinline__ void store_prefill_fragment( #pragma unroll for (int i = 0; i < 4; ++i) { const int col = base_col + col_mod4 + 4 * i; - const float scaled = static_cast(frag.x[i]) * xs * weight_scale[col]; - out[row * n + col] = __float2bfloat16(scaled); + const float scaled = + static_cast(frag.x[i]) * xs * weight_scale[col]; + out[static_cast(row) * n + col] = __float2bfloat16(scaled); } } -// Round 10 (fragment-load-form round): explicit 8-byte fragment loader -// replacing the library du_load_matrix_sync for the exact-shape gate_up -// kernel. Validated on the sibling TP8 down_proj accepted kernel (iter 8, -// -25.9%: 230.91 -> 171.19 us) and the TP4 gate_up lineage (iter 13 -// load_fragment8, -21%: 280.05 -> 220.71 us). For BOTH the library row_major -// matrix_a and col_major matrix_b m16n16k32 int8 loaders, lane l -> row -// (l & 15), k-quarter (l >> 4), and the loader fetches the same eight -// contiguous bytes at p[row*ldm + (l>>4)*8 .. +7] and stores them into the -// same x[0..7] slots in the same little-endian order (the round-3 ISA shows -// the compiler lowering this to ds_read2_b32 followed by a per-byte -// v_and/v_or_b32_sdwa/v_or3 mask-OR "identity" reassembly - ~9.4M dead-ish -// VALU instructions per replay, ~61% of the 15.43M VALU total). Loading the -// same 8 bytes as one int64 forces the 8-byte ds_read_b64 form instead: -// (a) the mask/OR reassembly VALU disappears, and (b) the LDS access -// changes from ds_read2_b32 (32-lane dword phases - at the stride-80 row -// pattern every phase groups rows 0-15 of k-quarter q with rows 0-15 of -// k-quarter q+1 onto the same 16 bank phases, a 4-way conflict per phase) -// to ds_read_b64 (16-lane phases: rows 0-15 land on 16 bank-pairs -// {20*r mod 32} = 8 distinct pairs hit twice - a 2-way conflict per phase, -// half of the current fragment-conflict factor). The element-to-slot -// mapping is unchanged (f.x[0..7] = the same 8 bytes in the same -// little-endian order), the v_mmac operands and the exact k0-outer/kk-inner -// int32 accumulation sequence are untouched, and the staging stores, LDS -// strides, barriers, tile, grid, occupancy and epilogue are byte-identical. +// Round 2 exact-shape fragment loader. The library row_major matrix_a and +// col_major matrix_b m16n16k32 int8 loaders both map lane l -> row (l & 15), +// k-quarter (l >> 4), and fetch the eight contiguous bytes at +// p[row*ldm + (l >> 4)*8 .. +7] into the fragment's x[0..7] slots in the same +// little-endian order. Loading those 8 bytes as one int64 emits a single +// 8-byte LDS read instead of the library's byte-wise loads plus per-byte +// reassembly VALU; the element-to-slot mapping is identical, so the MMAC +// operands and the exact int32 accumulation are unchanged. template -__device__ __forceinline__ void load_frag8(Frag& f, const int8_t* p, - unsigned ldm) { - const unsigned row = __lane_id() & 0xf; - const unsigned kq = __lane_id() >> 4; +__device__ __forceinline__ void load_frag8(Frag& f, + const int8_t* p, + unsigned ldm, + int lane) { + const unsigned row = static_cast(lane) & 0xf; + const unsigned kq = static_cast(lane) >> 4; const int64_t v = *reinterpret_cast(p + row * ldm + (kq << 3)); reinterpret_cast(f.x)[0] = v; } -// Stage one K tile (k0) of A[kBlockM, kStageK] and B into the caller-offset -// LDS buffers (bank-skewed row strides; int4 vectorized coalesced global -// loads, zero-filled tail M rows). kBNMajor == true stages B from the packed -// [N, K] n-major gate_up layout (n-major rows, k contiguous); otherwise from -// the identity [K, N] layout. Shared by the single-buffered and -// double-buffered K loops; the buffer offset (0 or kABytes/kBMaxBytes) is -// applied by the caller so one body serves both pipeline forms. -// -// kSkew8 (round 11, exact-shape arm only): A and B row strides become the -// down_proj-validated 8-mod-16 values 88 (kStageK + 24) and 72 (kStageK + 8) -// so every 16-lane fragment-read phase lands on 16 distinct bank pairs (see -// the round-11 header comment). The 8-mod-16 row starts forbid 16-byte -// stores, so each 16-byte int4 chunk is staged as two 8-byte halves -// (ds_write2_b64); the global int4 loads are byte-identical. -template +// Stage one K tile (k0) of A[kBlockM, kStageK] (row-major, zero-filled tail M +// rows) and B[kStageK, kBlockN] (identity [K, N] layout) into LDS with +// 16-byte-vectorized coalesced global loads. The B row stride kBlockN + 16 is +// the non-power-of-two bank skew that spreads the row_major fragment reads. +template __device__ __forceinline__ void stage_prefill_tiles( const int8_t* __restrict__ x_q, const int8_t* __restrict__ weight, @@ -428,20 +378,18 @@ __device__ __forceinline__ void stage_prefill_tiles( int k, int k0) { constexpr int kThreads = kBlockM * kBlockN / 1024 * kWaveSize; - // 16-byte bank skew: keeps int4 staging stores aligned while spreading the - // fragment rows across distinct LDS banks. - constexpr int kAStride = kSkew8 ? kStageK + 24 : kStageK + sizeof(int4); - constexpr int kBStride = kBlockN + sizeof(int4); // identity [K,N] rows - constexpr int kBNStride = kSkew8 ? kStageK + 8 : kStageK + sizeof(int4); - static_assert(!kSkew8 || kStageK == 64, - "kSkew8 strides 88/72 assume kStageK == 64"); + constexpr int kAStride = kStageK + sizeof(int4); + constexpr int kBStride = kBlockN + sizeof(int4); constexpr int kAInt4PerRow = kStageK / sizeof(int4); constexpr int kAInt4 = kBlockM * kAInt4PerRow; constexpr int kBInt4PerRow = kBlockN / sizeof(int4); constexpr int kBInt4 = kStageK * kBInt4PerRow; - constexpr int kBNInt4PerRow = kStageK / sizeof(int4); - constexpr int kBNInt4 = kBlockN * kBNInt4PerRow; - // Stage A[kBlockM, kStageK] into LDS (zero-filled tail M rows). + static_assert(kAStride % sizeof(int4) == 0, + "A LDS row stride must stay int4 aligned"); + static_assert(kBStride % sizeof(int4) == 0, + "B LDS row stride must stay int4 aligned"); + + // Stage A[kBlockM, kStageK]; rows beyond m contribute zeros. for (int vec = tid; vec < kAInt4; vec += kThreads) { const int local_row = vec / kAInt4PerRow; const int v = vec - local_row * kAInt4PerRow; @@ -452,186 +400,163 @@ __device__ __forceinline__ void stage_prefill_tiles( x_q + static_cast(global_row) * k + k0 + v * static_cast(sizeof(int4))) : int4{0, 0, 0, 0}; - if constexpr (kSkew8) { - // 8-mod-16 row starts: write the 16 bytes as two 8-byte halves so every - // store stays 8-byte aligned (ds_write2_b64 staging; same LDS bytes). - int8_t* dst = - a_tile + local_row * kAStride + v * static_cast(sizeof(int4)); - reinterpret_cast(dst)[0] = - reinterpret_cast(&val)[0]; - reinterpret_cast(dst)[1] = - reinterpret_cast(&val)[1]; - } else { - reinterpret_cast(a_tile + local_row * kAStride)[v] = val; - } + reinterpret_cast(a_tile + local_row * kAStride)[v] = val; } - if constexpr (kBNMajor) { - // Stage B[kBlockN, kStageK] from the packed [N, K] weight (n-major: - // row = n_local, k contiguous). Same vectorized coalesced int4 pattern - // as the identity arm, one per 16 consecutive k bytes. - // - // kBPanel (round 5, down_proj exact-shape arm only): the one-time pack - // for (k,n) == (256,6144) is the panel layout [N/128][K/64][128][64] - - // each block's per-stage B tile is one contiguous 8 KiB region, so a - // 64-lane warp instruction reads 1 KiB contiguous (full L2 line/sector - // utilization) instead of 16 rows x 64 B scattered at 256-B stride - // (50% of each 128-B line). The per-thread address is still one base + - // offset: packed byte offset = panel_base + local_col*64 + v*16 where - // panel_base = (n0/128)*(k/64)*8192 + (k0/64)*8192. The LDS stores are - // untouched (same stride-72 ds_write2_b64 halves -> LDS-bank-safe - // staging and conflict-free fragment reads preserved); only the global - // byte order of the loaded int4 changes, so the staged LDS bytes are - // identical to the plain n-major arm (bit-identical results). - for (int vec = tid; vec < kBNInt4; vec += kThreads) { - const int local_col = vec / kBNInt4PerRow; - const int v = vec - local_col * kBNInt4PerRow; - int4 val; - if constexpr (kBPanel) { - const int64_t panel_base = - (static_cast(n0) / kBlockN) * (k / kStageK) * - (kBlockN * kStageK) + - (k0 / kStageK) * (kBlockN * kStageK); - val = *reinterpret_cast( - weight + panel_base + - static_cast(local_col) * kStageK + - v * static_cast(sizeof(int4))); - } else { - val = *reinterpret_cast( - weight + static_cast(n0 + local_col) * k + k0 + + + // Stage B[kStageK, kBlockN] from the identity [K, N] weight (n%kBlockN == 0 + // is guaranteed by dispatch, so there is no N tail). + for (int vec = tid; vec < kBInt4; vec += kThreads) { + const int kk = vec / kBInt4PerRow; + const int v = vec - kk * kBInt4PerRow; + reinterpret_cast(b_tile + kk * kBStride)[v] = + *reinterpret_cast( + weight + static_cast(k0 + kk) * n + n0 + v * static_cast(sizeof(int4))); - } - if constexpr (kSkew8) { - int8_t* dst = - b_tile + local_col * kBNStride + - v * static_cast(sizeof(int4)); - reinterpret_cast(dst)[0] = - reinterpret_cast(&val)[0]; - reinterpret_cast(dst)[1] = - reinterpret_cast(&val)[1]; - } else { - reinterpret_cast(b_tile + local_col * kBNStride)[v] = val; - } - } - } else { - // Stage B[kStageK, kBlockN] into LDS from the identity [K, N] weight. - for (int vec = tid; vec < kBInt4; vec += kThreads) { - const int kk = vec / kBInt4PerRow; - const int v = vec - kk * kBInt4PerRow; - reinterpret_cast(b_tile + kk * kBStride)[v] = - *reinterpret_cast( - weight + static_cast(k0 + kk) * n + n0 + - v * static_cast(sizeof(int4))); - } } } -// Round 16 (lead-2 loop-carried payload, exact-shape arm only): the register -// half of the lead-2 software pipeline. Each thread prefetches exactly one A -// int4 and one B int4 of a later K stage into thread-local registers and -// returns WITHOUT waiting (the vmcnt wait is emitted by the compiler at the -// first use, one iteration later). The <64,64,64> arm stages kStageK * 64 -// bytes per operand per stage, i.e. kAInt4 == kBNInt4 == kThreads == 256: -// one int4 per thread per operand, so the whole payload costs 8 VGPRs. The -// (row, v) mapping is the same as stage_prefill_tiles, so -// publish_stage_payload writes the exact same LDS addresses the staging -// loop would (bit-identical staging bytes, just loaded one stage earlier). -template -__device__ __forceinline__ void prefetch_stage_payload( +// One wavefront's 32x32-quadrant MMAC burst for the K tile staged at +// a_tile/b_tile. k0-outer / kk-inner int32 accumulation order; the int8 dot is +// exact in int32 for every allowed K. +template +__device__ __forceinline__ void mmac_prefill_stage( + DUFragment& + a_frag0, + DUFragment& + a_frag1, + DUFragment& acc00, + DUFragment& acc01, + DUFragment& acc10, + DUFragment& acc11, + int8_t* a_tile, + int8_t* b_tile, + int local_row, + int local_col) { + constexpr int kAStride = kStageK + sizeof(int4); + constexpr int kBStride = kBlockN + sizeof(int4); + DUFragment + b_frag0, b_frag1; +#pragma unroll + for (int kk = 0; kk < kStageK; kk += kTileK) { + du_load_matrix_sync( + a_frag0, a_tile + local_row * kAStride + kk, kAStride); + du_load_matrix_sync( + a_frag1, a_tile + (local_row + kTileM) * kAStride + kk, kAStride); + du_load_matrix_sync( + b_frag0, b_tile + kk * kBStride + local_col, kBStride); + du_load_matrix_sync( + b_frag1, b_tile + kk * kBStride + local_col + kTileN, kBStride); + du_mma_sync(acc00, a_frag0, b_frag0, acc00); + du_mma_sync(acc01, a_frag0, b_frag1, acc01); + du_mma_sync(acc10, a_frag1, b_frag0, acc10); + du_mma_sync(acc11, a_frag1, b_frag1, acc11); + } +} + +// Round 2 exact-shape staging: A[kBlockM, kStageK] from the row-major [M, K] +// activation (zero-filled tail rows) and B[kBlockN, kStageK] from the packed +// weight, both with 16-byte vectorized coalesced global int4 loads. The LDS +// row strides are the bank-safe 8-mod-16 values A = 88 / B = 72; 8-mod-16 row +// starts forbid 16-byte stores, so each int4 is written as two 8-byte halves +// (ds_write_b64 pair, same LDS bytes as a single 16-byte store would produce). +// +// kBPanel (round 5, exact-shape arm only, default true): B is read from the +// panel layout [N/128][K/64][128][64] instead of the plain n-major [N, K] +// transpose. Within a panel (nt = n0/128, kt = k0/64) the B rows are +// kStageK = 64 bytes apart with k contiguous, so local_col*kStageK + v*16 == +// vec*16 and the whole 64-lane warp instruction reads kBlockN*kStageK/8 = 1 KiB +// of consecutive bytes. The staged LDS bytes are byte-identical to the plain +// form (the same int4 of the same logical [K, N] element block lands at the +// same LDS address), so the LDS-bank-safe fragment side is untouched. +template +__device__ __forceinline__ void stage_proj_tiles( const int8_t* __restrict__ x_q, const int8_t* __restrict__ weight, + int8_t* a_tile, + int8_t* b_tile, int tid, int m0, int n0, - int m, int k, - int k0, - int4& a_payload, - int4& b_payload) { + int k0) { constexpr int kThreads = kBlockM * kBlockN / 1024 * kWaveSize; - constexpr int kAInt4PerRow = kStageK / static_cast(sizeof(int4)); + constexpr int kAStride = kStageK + 24; + constexpr int kBStride = kStageK + 8; + constexpr int kAInt4PerRow = kStageK / sizeof(int4); constexpr int kAInt4 = kBlockM * kAInt4PerRow; - constexpr int kBNInt4PerRow = kStageK / static_cast(sizeof(int4)); - constexpr int kBNInt4 = kBlockN * kBNInt4PerRow; - static_assert(kAInt4 == kThreads && kBNInt4 == kThreads, - "payload arm requires exactly one int4 per thread per " - "operand (exact-shape <64,64,64> arm)"); - static_assert(kBNMajor, "payload arm stages the packed n-major B layout"); - // Same vec -> (local_row, v) mapping as stage_prefill_tiles, so the - // publish half stores to the same LDS addresses. - const int a_local_row = tid / kAInt4PerRow; - const int a_v = tid - a_local_row * kAInt4PerRow; - const int global_row = m0 + a_local_row; - // Tail M rows are zero-filled exactly as in stage_prefill_tiles. - a_payload = global_row < m - ? *reinterpret_cast( - x_q + static_cast(global_row) * k + k0 + - a_v * static_cast(sizeof(int4))) - : int4{0, 0, 0, 0}; - const int b_local_col = tid / kBNInt4PerRow; - const int b_v = tid - b_local_col * kBNInt4PerRow; - b_payload = *reinterpret_cast( - weight + static_cast(n0 + b_local_col) * k + k0 + - b_v * static_cast(sizeof(int4))); -} + constexpr int kBInt4PerRow = kStageK / sizeof(int4); + constexpr int kBInt4 = kBlockN * kBInt4PerRow; + static_assert(kAStride % 8 == 0 && kBStride % 8 == 0, + "8-mod-16 strides must stay 8-byte aligned"); + static_assert(kAInt4 <= kThreads && kBInt4 <= kThreads, + "one staging pass per thread is enough for this arm"); + static_assert(!kBPanel || (kBlockN == kProjBlockN && kStageK == kProjStageK), + "panel pack is specialized to the 128x64 B stage tile"); + static_assert(kExactM % kBlockM == 0, + "exact-arm guard launches whole kBlockM row tiles, so no M " + "tail can reach this staging helper"); -// Round 16: the LDS half of the lead-2 payload pipeline. Writes the payload -// carried in registers (loaded by prefetch_stage_payload one stage earlier) -// into the caller-offset LDS tile with the exact same 8-byte-half -// ds_write2_b64 stores (kSkew8 strides 88/72) that stage_prefill_tiles -// emits, so each thread republishes exactly the bytes it loaded. -template -__device__ __forceinline__ void publish_stage_payload( - int4 a_payload, - int4 b_payload, - int8_t* a_tile, - int8_t* b_tile, - int tid) { - constexpr int kThreads = kBlockM * kBlockN / 1024 * kWaveSize; - constexpr int kAStride = kSkew8 ? kStageK + 24 : kStageK + sizeof(int4); - constexpr int kBNStride = kSkew8 ? kStageK + 8 : kStageK + sizeof(int4); - constexpr int kAInt4PerRow = kStageK / static_cast(sizeof(int4)); - constexpr int kAInt4 = kBlockM * kAInt4PerRow; - constexpr int kBNInt4PerRow = kStageK / static_cast(sizeof(int4)); - constexpr int kBNInt4 = kBlockN * kBNInt4PerRow; - static_assert(kAInt4 == kThreads && kBNInt4 == kThreads, - "payload arm requires exactly one int4 per thread per " - "operand (exact-shape <64,64,64> arm)"); - static_assert(kBNMajor, "payload arm stages the packed n-major B layout"); - const int a_local_row = tid / kAInt4PerRow; - const int a_v = tid - a_local_row * kAInt4PerRow; - int8_t* a_dst = - a_tile + a_local_row * kAStride + a_v * static_cast(sizeof(int4)); - if constexpr (kSkew8) { - // 8-mod-16 row starts: write the 16 bytes as two 8-byte halves - // (ds_write2_b64 staging, identical to stage_prefill_tiles). - reinterpret_cast(a_dst)[0] = - reinterpret_cast(&a_payload)[0]; - reinterpret_cast(a_dst)[1] = - reinterpret_cast(&a_payload)[1]; - } else { - reinterpret_cast(a_dst)[0] = a_payload; + // Stage A[kBlockM, kStageK]. The exact-arm host guard requires m == kExactM + // and launches exactly grid(n/kBlockN, m/kBlockM), so every staged row + // m0 + local_row is in range and the former `global_row < m` + // compare/select/zero-fill was dead code (round 17 fix_build). Every staged + // A byte is identical to the zero-filled form. + for (int vec = tid; vec < kAInt4; vec += kThreads) { + const int local_row = vec / kAInt4PerRow; + const int v = vec - local_row * kAInt4PerRow; + const int4 val = *reinterpret_cast( + x_q + static_cast(m0 + local_row) * k + k0 + + v * static_cast(sizeof(int4))); + int8_t* dst = + a_tile + local_row * kAStride + v * static_cast(sizeof(int4)); + reinterpret_cast(dst)[0] = + reinterpret_cast(&val)[0]; + reinterpret_cast(dst)[1] = + reinterpret_cast(&val)[1]; } - const int b_local_col = tid / kBNInt4PerRow; - const int b_v = tid - b_local_col * kBNInt4PerRow; - int8_t* b_dst = - b_tile + b_local_col * kBNStride + b_v * static_cast(sizeof(int4)); - if constexpr (kSkew8) { - reinterpret_cast(b_dst)[0] = - reinterpret_cast(&b_payload)[0]; - reinterpret_cast(b_dst)[1] = - reinterpret_cast(&b_payload)[1]; - } else { - reinterpret_cast(b_dst)[0] = b_payload; + + // Stage B[kBlockN, kStageK] from the packed weight: the round-5 panel + // layout [N/128][K/64][128][64] (one contiguous kBlockN*kStageK = 8 KiB + // region per timed block-stage, fully coalesced 1 KiB per 64-lane warp) or + // the round-2 plain n-major [N, K] transpose. Both forms place the same + // bytes at the same LDS address, so only the global read side changes. + for (int vec = tid; vec < kBInt4; vec += kThreads) { + const int local_col = vec / kBInt4PerRow; + const int v = vec - local_col * kBInt4PerRow; + int4 val; + if constexpr (kBPanel) { + // Panel (nt = n0/kBlockN, kt = k0/kStageK) base in bytes. Within a + // panel the local B rows are kStageK bytes apart and k is contiguous, + // so local_col*kStageK + v*16 == vec*16. + const int64_t panel_base = + (static_cast(n0) / kBlockN) * (k / kStageK) * + (static_cast(kBlockN) * kStageK) + + (static_cast(k0) / kStageK) * + (static_cast(kBlockN) * kStageK); + val = *reinterpret_cast( + weight + panel_base + static_cast(local_col) * kStageK + + v * static_cast(sizeof(int4))); + } else { + // Plain n-major [N, K]: packed[col * k + kk] == raw[kk * n + col]. + val = *reinterpret_cast( + weight + static_cast(n0 + local_col) * k + k0 + + v * static_cast(sizeof(int4))); + } + int8_t* dst = + b_tile + local_col * kBStride + v * static_cast(sizeof(int4)); + reinterpret_cast(dst)[0] = + reinterpret_cast(&val)[0]; + reinterpret_cast(dst)[1] = + reinterpret_cast(&val)[1]; } } -// One wave's 32x32-quadrant MMAC burst for the K tile staged at a_tile/b_tile -// (caller-offset to the target buffer). k0-outer / kk-inner int32 accumulation -// order; stage-size changes only regroup exact integer sums (max |dot| = -// 6144*127*127 << 2^31), so results stay bit-identical. -template -__device__ __forceinline__ void mmac_prefill_stage( +// Round 2 exact-shape MMAC burst: eight explicit 8-byte load_frag8 fragment +// reads (A row_major / B col_major, bank-safe 88/72 strides) and the same +// eight m16n16k32 MMACs in the same k0-outer / kk-inner int32 order the +// library-loader bootstrap used, so the accumulation is bit-identical. +template +__device__ __forceinline__ void mmac_proj_stage( DUFragment& a_frag0, DUFragment& @@ -643,127 +568,257 @@ __device__ __forceinline__ void mmac_prefill_stage( int8_t* a_tile, int8_t* b_tile, int local_row, - int local_col) { - // kSkew8 (round 11, exact-shape arm only): A 88 / B 72 row strides; the - // load_frag8 ldm arguments below pick them up unchanged (same 8-byte - // aligned fragment reads, conflict-free 16-lane bank phases). - constexpr int kAStride = kSkew8 ? kStageK + 24 : kStageK + sizeof(int4); - constexpr int kBStride = kBlockN + sizeof(int4); // identity [K,N] rows - constexpr int kBNStride = kSkew8 ? kStageK + 8 : kStageK + sizeof(int4); - if constexpr (kBNMajor) { - if constexpr (kDirectFrag) { - // Round 10 exact-shape path: explicit load_frag8 8-byte ds_read_b64 - // fragment reads replacing the library loaders (see load_frag8 - // comment). Both steps of the 64-K stage are unrolled into - // independent fragment register sets so all eight reads of a stage - // issue before the MMACs; the kk = 0 MMAC group runs first, then the - // kk = kTileK group - the exact k0-outer/kk-inner int32 order and the - // element-to-slot fragment mapping of the library path are preserved - // (f.x[0..7] = the same 8 bytes in the same little-endian order), so - // results stay bit-identical. - static_assert(kStageK == 2 * kTileK, - "kDirectFrag two-step unroll assumes kStageK == 2*kTileK"); - DUFragment - a_frag0_1, a_frag1_1; - DUFragment - b_frag0, b_frag1, b_frag0_1, b_frag1_1; - load_frag8(a_frag0, a_tile + local_row * kAStride, kAStride); - load_frag8(a_frag1, a_tile + (local_row + kTileM) * kAStride, kAStride); - load_frag8(b_frag0, b_tile + local_col * kBNStride, kBNStride); - load_frag8(b_frag1, b_tile + (local_col + kTileN) * kBNStride, - kBNStride); - load_frag8(a_frag0_1, a_tile + local_row * kAStride + kTileK, - kAStride); - load_frag8(a_frag1_1, - a_tile + (local_row + kTileM) * kAStride + kTileK, kAStride); - load_frag8(b_frag0_1, b_tile + local_col * kBNStride + kTileK, - kBNStride); - load_frag8(b_frag1_1, - b_tile + (local_col + kTileN) * kBNStride + kTileK, - kBNStride); - du_mma_sync(acc00, a_frag0, b_frag0, acc00); - du_mma_sync(acc01, a_frag0, b_frag1, acc01); - du_mma_sync(acc10, a_frag1, b_frag0, acc10); - du_mma_sync(acc11, a_frag1, b_frag1, acc11); - du_mma_sync(acc00, a_frag0_1, b_frag0_1, acc00); - du_mma_sync(acc01, a_frag0_1, b_frag1_1, acc01); - du_mma_sync(acc10, a_frag1_1, b_frag0_1, acc10); - du_mma_sync(acc11, a_frag1_1, b_frag1_1, acc11); - } else { - DUFragment - b_frag0, b_frag1; + int local_col, + int lane) { + constexpr int kAStride = kStageK + 24; + constexpr int kBStride = kStageK + 8; + static_assert(kStageK == 2 * kTileK, + "exact-shape arm unrolls the two 32-K steps of a 64-K stage"); + DUFragment + a_frag0_1, a_frag1_1; + DUFragment + b_frag0, b_frag1, b_frag0_1, b_frag1_1; + load_frag8(a_frag0, a_tile + local_row * kAStride, kAStride, lane); + load_frag8(a_frag1, a_tile + (local_row + kTileM) * kAStride, kAStride, lane); + load_frag8(b_frag0, b_tile + local_col * kBStride, kBStride, lane); + load_frag8(b_frag1, b_tile + (local_col + kTileN) * kBStride, kBStride, lane); + load_frag8(a_frag0_1, a_tile + local_row * kAStride + kTileK, kAStride, lane); + load_frag8(a_frag1_1, a_tile + (local_row + kTileM) * kAStride + kTileK, + kAStride, lane); + load_frag8(b_frag0_1, b_tile + local_col * kBStride + kTileK, kBStride, lane); + load_frag8(b_frag1_1, b_tile + (local_col + kTileN) * kBStride + kTileK, + kBStride, lane); + du_mma_sync(acc00, a_frag0, b_frag0, acc00); + du_mma_sync(acc01, a_frag0, b_frag1, acc01); + du_mma_sync(acc10, a_frag1, b_frag0, acc10); + du_mma_sync(acc11, a_frag1, b_frag1, acc11); + du_mma_sync(acc00, a_frag0_1, b_frag0_1, acc00); + du_mma_sync(acc01, a_frag0_1, b_frag1_1, acc01); + du_mma_sync(acc10, a_frag1_1, b_frag0_1, acc10); + du_mma_sync(acc11, a_frag1_1, b_frag1_1, acc11); +} + +// Round 6 (EPILOGUE round) fused-epilogue helpers. +// +// Rounds 2-5 converted each accumulator fragment straight to bf16 and issued 16 +// scattered 2-byte global stores per thread. One 64-lane store instruction +// carried 16 rows x 4 columns, i.e. 16 distinct 128-B lines with only 8 useful +// bytes in each, and the exact gfx928 code object shows the epilogue +// serialized behind 16 global_store_short plus ~20 scalar x_scale / +// weight_scale loads per thread. Round 6 keeps the per-row x per-column scale +// multiply and the bf16 conversion inside the compute kernel but routes the +// result through a shared-memory staging tile and writes the tile back with +// fully coalesced 16-byte stores: 8 threads cover one full 128-B row segment, +// so a 64-lane store instruction moves 8 complete lines instead of 16 +// fragments of lines. The per-row / per-column scales are staged cooperatively +// into LDS once per block (one coalesced global load per thread), which removes +// the scalar global scale loads entirely; round 15 later replaces that LDS slab +// with per-thread scale registers (see below). +// +// The staged half tile is written as plain bf16 elements; the 8-mod-16 row +// stride (136 B) keeps the 16 row starts on distinct LDS banks and stays +// 8-byte aligned for the int64 read-back. +// +// Round 15 (final consolidate polish): the two row scales and the four +// per-column scales this wave needs for one kTileN column group are passed in +// as registers instead of being read back from an LDS scale slab. The wave's +// rows are local_row + (lane & 15) and local_row + kTileM + (lane & 15), and +// its columns are col + (lane >> 4) + 4*i, so every thread owns exactly two row +// scales and eight column scales (four per column group). They are loaded once +// per thread before the K loop, their global-load latency hides behind the four +// 64-K stages, and the epilogue therefore issues no scale LDS reads at all. +// The multiply order ((float)acc * x_scale * weight_scale), the bf16 conversion +// and every staged LDS / stored global address are unchanged, so results stay +// bit-exact. +template +__device__ __forceinline__ void stage_epilogue_fragment( + const AccFragment& frag, + float xs, + const float (&ws)[4], + hip_bfloat16* __restrict__ epi, + int row, + int col, + int col_base, + int lane) { + const int r = lane & 15; + const int q = lane >> 4; #pragma unroll - for (int kk = 0; kk < kStageK; kk += kTileK) { - du_load_matrix_sync( - a_frag0, a_tile + local_row * kAStride + kk, kAStride); - du_load_matrix_sync( - a_frag1, a_tile + (local_row + kTileM) * kAStride + kk, kAStride); - du_load_matrix_sync( - b_frag0, b_tile + local_col * kBNStride + kk, kBNStride); - du_load_matrix_sync( - b_frag1, b_tile + (local_col + kTileN) * kBNStride + kk, - kBNStride); - du_mma_sync(acc00, a_frag0, b_frag0, acc00); - du_mma_sync(acc01, a_frag0, b_frag1, acc01); - du_mma_sync(acc10, a_frag1, b_frag0, acc10); - du_mma_sync(acc11, a_frag1, b_frag1, acc11); - } - } - } else { - DUFragment - b_frag0, b_frag1; + for (int i = 0; i < 4; ++i) { + const float scaled = static_cast(frag.x[i]) * xs * ws[i]; + epi[(row + r) * kStride + (col - col_base) + q + 4 * i] = + __float2bfloat16(scaled); + } +} + +// Coalesced write-back of one staged 64-column bf16 half: thread `tid` owns a +// 16-byte segment of one output row (8 threads per 128-B line), so the whole +// block writes each half with one 16-byte store per thread and fully-used +// 128-B lines, in the same logical [M, N] bf16 order the scalar fallback +// produces. The staged LDS row stride (136 B) is only 8-byte aligned, so the +// LDS side reads two int64 halves and the global side re-forms the aligned +// 16-byte vector. +struct alignas(16) EpiQuad { + int64_t lo; + int64_t hi; +}; + +__device__ __forceinline__ void store_epilogue_half( + const hip_bfloat16* __restrict__ epi, + hip_bfloat16* __restrict__ out, + int m0, + int n0, + int col_base, + int n, + int tid) { + const int row = tid >> 3; + const int seg = tid & 7; + const int64_t* src = + reinterpret_cast(epi + row * kEpiStride + seg * 8); + EpiQuad q; + q.lo = src[0]; + q.hi = src[1]; + hip_bfloat16* dst = out + static_cast(m0 + row) * n + n0 + + col_base + seg * 8; + *reinterpret_cast(dst) = q; +} + +// Round 2 exact-shape kernel: one block computes a kBlockM x kBlockN output +// tile of the assigned (4096, 6144, 256) shape. (kBlockM/32) * (kBlockN/32) +// wavefronts each own a 32x32 quadrant (four m16n16k32 DUMMA accumulators), +// while the block cooperatively stages A[kBlockM, kStageK] from the +// row-major [M, K] activation and B[kBlockN, kStageK] from the round-5 panel +// packed weight. Single-buffered 64-K stages (two barriers per stage). The +// round-6 fused epilogue stages the scaled bf16 output through LDS and writes +// it back coalesced; there is no workspace use and no separate convert or +// combine pass anywhere in this operator. +template +__global__ __launch_bounds__(kBlockM * kBlockN / 1024 * kWaveSize) void +w8a8_dumma_proj_operand_reuse_kernel( + const int8_t* __restrict__ x_q, + const int8_t* __restrict__ weight, + const float* __restrict__ x_scale, + const float* __restrict__ weight_scale, + hip_bfloat16* __restrict__ out, + int m, + int n, + int k) { + constexpr int kWavesN = kBlockN / 32; + constexpr int kAStride = kStageK + 24; + constexpr int kBStride = kStageK + 8; + constexpr int kABytes = kBlockM * kAStride; + constexpr int kBBytes = kBlockN * kBStride; + static_assert(kStageK == 2 * kTileK, + "exact-shape arm unrolls the two 32-K steps of a 64-K stage"); + static_assert(kAStride % 8 == 0 && kBStride % 8 == 0, + "8-mod-16 strides must stay 8-byte aligned"); + static_assert(kBlockM % 32 == 0 && kBlockN % 32 == 0, + "macro-tile must be a whole number of 32x32 quadrants"); + static_assert(kBlockM % kTileM == 0 && kBlockN % kTileN == 0, + "macro-tile must be a whole number of DUMMA tiles"); + + const int tid = static_cast(threadIdx.x); + const int wave = tid / kWaveSize; + const int lane = tid - wave * kWaveSize; + const int wave_row = wave / kWavesN; + const int wave_col = wave - wave_row * kWavesN; + const int m0 = static_cast(blockIdx.y) * kBlockM; + const int n0 = static_cast(blockIdx.x) * kBlockN; + const int local_row = wave_row * 32; + const int local_col = wave_col * 32; + + __shared__ __align__(16) int8_t a_tile[kABytes]; + __shared__ __align__(16) int8_t b_tile[kBBytes]; + + DUFragment + a_frag0, a_frag1; + DUFragment + acc00, acc01, acc10, acc11; + du_fill_fragment(acc00, 0); + du_fill_fragment(acc01, 0); + du_fill_fragment(acc10, 0); + du_fill_fragment(acc11, 0); + + // Round 15 register scales: the two row scales and the eight column scales + // this wave's epilogue needs are loaded ONCE per thread here, before the K + // loop, so their global-load latency is hidden behind the four 64-K stages + // and the epilogue neither stages nor reads any scale through LDS. local_row + // / local_col are wave_row*32 / wave_col*32; the exact arm always has + // m0 == 64*blockIdx.y so m0 + local_row + kTileM + 15 <= 4095 < m, and + // n0 + local_col + kTileN + 3 + 12 <= 6139 < n, so no tail masking is needed. + const int epi_r = lane & 15; + const int epi_q = lane >> 4; + const float xs0 = x_scale[m0 + local_row + epi_r]; + const float xs1 = x_scale[m0 + local_row + kTileM + epi_r]; + float ws_a[kTileN / 4]; + float ws_b[kTileN / 4]; +#pragma unroll + for (int i = 0; i < kTileN / 4; ++i) { + ws_a[i] = weight_scale[n0 + local_col + epi_q + 4 * i]; + ws_b[i] = weight_scale[n0 + local_col + kTileN + epi_q + 4 * i]; + } + + for (int k0 = 0; k0 < k; k0 += kStageK) { + stage_proj_tiles( + x_q, weight, a_tile, b_tile, tid, m0, n0, k, k0); + __syncthreads(); + + mmac_proj_stage( + a_frag0, a_frag1, acc00, acc01, acc10, acc11, a_tile, b_tile, + local_row, local_col, lane); + // Retire this stage's fragment reads before the next stage's staging + // writes overwrite the single LDS buffer (WAR hazard across waves). + __syncthreads(); + } + + // ---- Round 6 fused coalesced epilogue (round-15 register scales) ------ + // The A and B staging buffers are dead after the K loop (the final + // __syncthreads retires the last fragment reads), so the B tile becomes the + // bf16 output staging area: same 14,848 B footprint, same 2 blocks/CU + // occupancy. The scale factors live in the per-thread registers loaded before + // the K loop, so the retired A tile is not touched here and no scale-publish + // barrier is needed. Rows are always in range for this arm because the host + // guard launches exactly grid(n / kBlockN, m / kBlockM). + constexpr int kEpiHalfBytes = kBlockM * kEpiStride * sizeof(hip_bfloat16); + static_assert(kEpiHalfBytes <= kBBytes, + "one 64-column bf16 half must fit the retired B tile"); + static_assert(kBlockN % kEpiCols == 0, "epilogue halves must tile kBlockN"); + auto* epi = reinterpret_cast(b_tile); + #pragma unroll - for (int kk = 0; kk < kStageK; kk += kTileK) { - du_load_matrix_sync( - a_frag0, a_tile + local_row * kAStride + kk, kAStride); - du_load_matrix_sync( - a_frag1, a_tile + (local_row + kTileM) * kAStride + kk, kAStride); - du_load_matrix_sync( - b_frag0, b_tile + kk * kBStride + local_col, kBStride); - du_load_matrix_sync( - b_frag1, b_tile + kk * kBStride + local_col + kTileN, kBStride); - du_mma_sync(acc00, a_frag0, b_frag0, acc00); - du_mma_sync(acc01, a_frag0, b_frag1, acc01); - du_mma_sync(acc10, a_frag1, b_frag0, acc10); - du_mma_sync(acc11, a_frag1, b_frag1, acc11); + for (int half = 0; half < kBlockN / kEpiCols; ++half) { + const int col_base = half * kEpiCols; + if (local_col >= col_base && local_col < col_base + kEpiCols) { + stage_epilogue_fragment( + acc00, xs0, ws_a, epi, local_row, local_col, col_base, lane); + stage_epilogue_fragment( + acc01, xs0, ws_b, epi, local_row, local_col + kTileN, col_base, + lane); + stage_epilogue_fragment( + acc10, xs1, ws_a, epi, local_row + kTileM, local_col, col_base, + lane); + stage_epilogue_fragment( + acc11, xs1, ws_b, epi, local_row + kTileM, local_col + kTileN, + col_base, lane); } + __syncthreads(); + store_epilogue_half(epi, out, m0, n0, col_base, n, tid); + // Retire this half's staging reads before the next half overwrites the + // single staging buffer (WAR hazard across waves). + __syncthreads(); } } // Large-prefill path: one block computes a kBlockM x kBlockN output tile. -// (kBlockM/32) x (kBlockN/32) wavefronts each own a 32x32 quadrant (four +// (kBlockM/32) * (kBlockN/32) wavefronts each own a 32x32 quadrant (four // m16n16k32 DUMMA accumulators), while the block cooperatively stages -// A[kBlockM, kStageK] and B in bank-skewed LDS. +// A[kBlockM, kStageK] and B[kStageK, kBlockN] in bank-skewed LDS. // -// kDoubleBuffer == false: single-buffered K loop (two barriers per stage); -// stage s+1's global loads cannot start until the stage-s barrier, so each -// stage serializes one global-load round trip in front of the compute -// (bootstrap structure; co-resident block streams hide it at the CU level). -// -// kDoubleBuffer == true: software-pipelined K loop (round 3). Stage s+1's -// global loads are issued into the idle LDS buffer before the stage-s MMAC -// burst, overlapping the serialized global-load latency with compute. One -// barrier per stage (plus one prologue): it both retires stage-s fragment -// reads (WAR on the buffer just read) and makes stage s+1's ds_writes -// visible (RAW on the prefetched buffer). The gate_up shape uses -// <64, 64, 64, true, true>: two 64-K tiles = 20,480 B/block x 3 blocks/CU = -// 61,440 <= 64 KiB (occupancy preserved; a 128-K double buffer would need -// 38,912 B/block -> 1 block/CU, a harmful occupancy loss). -// -// kBNMajor selects the B operand layout: -// * false (identity): B staged [K, N] row-major, library row_major B -// fragments (byte-scattered LDS reads; correct for the raw [K, N] weight -// and every non-gate_up shape). -// * true (gate_up only): B staged [N, K] n-major with k contiguous (row -// stride 144 = 128+16, or 80 = 64+16 for the 64-K double buffer), -// library col_major B fragments -> each lane's elements are k-contiguous -// bytes instead of byte-scattered columns. This is the bank-safe consumer -// layout for the B side (the 17.3M PMC bank-conflict source in round 1). -// The one-time pack in launch_pack_w8a8_weight provides the [N, K] buffer -// for (k, n) == (6144, 512) only. -template +// Single-buffered K loop (two barriers per stage): stage s+1's global loads +// cannot start until the stage-s barrier, so each stage serializes one +// global-load round trip in front of its MMAC burst. Co-resident blocks hide +// that latency at the CU level, and the bootstrap deliberately avoids the +// copy/pipeline complexity that later rounds may add. +template __global__ __launch_bounds__(kBlockM * kBlockN / 1024 * kWaveSize) void w8a8_dumma_prefill_tiled_kernel( const int8_t* __restrict__ x_q, @@ -775,64 +830,22 @@ w8a8_dumma_prefill_tiled_kernel( int n, int k) { constexpr int kWavesN = kBlockN / 32; - constexpr int kThreads = kBlockM * kBlockN / 1024 * kWaveSize; - // 16-byte bank skew: keeps int4 staging stores aligned while spreading the - // fragment rows across distinct LDS banks. The staging loops live in - // stage_prefill_tiles below; the strides remain here as the static-asserted - // alignment contract of every instantiation. kSkew8 (round 11, exact-shape - // arm only) switches A/B to the down_proj-validated 8-mod-16 strides - // 88/72; LDS bytes stay A[64,88] + B[64,72] x 2 = 20,480 B/block so the - // 3-blocks/CU occupancy of the accepted kernel is preserved exactly. - constexpr int kAStride = kSkew8 ? kStageK + 24 : kStageK + sizeof(int4); - constexpr int kBStride = kBlockN + sizeof(int4); // identity [K,N] rows - constexpr int kBNStride = kSkew8 ? kStageK + 8 : kStageK + sizeof(int4); - constexpr int kBNInt4 = kBlockN * (kStageK / sizeof(int4)); - // Double buffering uses kBuffers = 2 LDS tile copies; the gate_up shape - // pairs it with kStageK = 64 so 3 blocks/CU is preserved (see kernel - // comment above). - constexpr int kBuffers = kDoubleBuffer ? 2 : 1; + constexpr int kAStride = kStageK + sizeof(int4); + constexpr int kBStride = kBlockN + sizeof(int4); constexpr int kABytes = kBlockM * kAStride; - // One shared B tile sized for the larger of the two layouts so an inactive - // arm never allocates dead LDS (keeps <64,64,128> at 19,456 B total and - // 3 blocks/CU). kSkew8 implies kBNMajor, so only the n-major arm is live. - constexpr int kBMaxBytes = - kSkew8 ? kBlockN * kBNStride - : (kStageK * kBStride > kBlockN * kBNStride - ? kStageK * kBStride - : kBlockN * kBNStride); - static_assert(kSkew8 ? (kAStride % 8 == 0) : (kAStride % sizeof(int4) == 0), - "A LDS row stride must stay int4 aligned (or 8-byte aligned " - "under kSkew8)"); - static_assert(kBStride % static_cast(sizeof(int4)) == 0, - "B LDS row stride must stay int4 aligned"); - static_assert( - kSkew8 ? (kBNStride % 8 == 0) : (kBNStride % sizeof(int4) == 0), - "n-major B LDS row stride must stay int4 aligned (or 8-byte aligned " - "under kSkew8)"); - static_assert(!kSkew8 || kBNMajor, - "kSkew8 is only defined for the packed n-major B layout"); - static_assert(!kSkew8 || kStageK == 64, - "kSkew8 strides 88/72 assume kStageK == 64"); + constexpr int kBBytes = kStageK * kBStride; static_assert(kStageK % kTileK == 0, "K stage must be a multiple of the DUMMA K unit"); static_assert(kStageK % static_cast(sizeof(int4)) == 0, "K stage must keep int4 staging aligned"); - static_assert(kBNInt4 % kThreads == 0, - "n-major B staging must divide evenly across threads"); - static_assert(!kPayload || (kBNMajor && kDoubleBuffer && kDirectFrag && - kSkew8 && kStageK == 64 && kBlockM == 64 && - kBlockN == 64), - "payload arm is specialized to the exact-shape <64,64,64> " - "double-buffered load_frag8/kSkew8 arm (one int4 per thread " - "per operand)"); - static_assert( - !kBPanel || (kBNMajor && kStageK == 64 && kBlockN == 128 && !kPayload), - "kBPanel is specialized to the down_proj panel pack (n-major 128x64 " - "per-stage panels, 64-K stages, non-payload pipeline)"); + static_assert(kBlockM % 32 == 0 && kBlockN % 32 == 0, + "macro-tile must be a whole number of 32x32 quadrants"); + static_assert(kBlockM % kTileM == 0 && kBlockN % kTileN == 0, + "macro-tile must be a whole number of DUMMA tiles"); const int tid = static_cast(threadIdx.x); const int wave = tid / kWaveSize; - const int lane = tid % kWaveSize; + const int lane = tid - wave * kWaveSize; const int wave_row = wave / kWavesN; const int wave_col = wave - wave_row * kWavesN; const int m0 = static_cast(blockIdx.y) * kBlockM; @@ -840,8 +853,8 @@ w8a8_dumma_prefill_tiled_kernel( const int local_row = wave_row * 32; const int local_col = wave_col * 32; - __shared__ __align__(16) int8_t a_tile[kBuffers * kABytes]; - __shared__ __align__(16) int8_t b_tile[kBuffers * kBMaxBytes]; + __shared__ __align__(16) int8_t a_tile[kABytes]; + __shared__ __align__(16) int8_t b_tile[kBBytes]; DUFragment a_frag0, a_frag1; @@ -852,144 +865,44 @@ w8a8_dumma_prefill_tiled_kernel( du_fill_fragment(acc10, 0); du_fill_fragment(acc11, 0); - if constexpr (kDoubleBuffer) { - if constexpr (kPayload) { - // Round 16 lead-2 loop-carried payload pipeline (exact-shape arm - // only): stage s+1's global loads are issued a FULL iteration early - // (top of iteration s-1) into a two-int4-per-thread register payload - // and stay outstanding across the stage-(s-1) fragment reads + MMAC - // burst and the stage barrier; the vmcnt wait lands at the top of - // iteration s (first use of the payload registers), immediately - // before the ds_write2 publish into the idle LDS buffer. The - // s_barrier fences outstanding vmem, so the wait can no longer sit - // right after the load issue: the global-load latency overlaps the - // whole compute segment instead of serializing in front of every - // stage (the round-11 ISA defect). Same 2 buffers, same one barrier - // per stage (prologue + 96 = 97/block), same 16 ds_read2_b64 + - // 8 ds_write2_b64 per block-stage, same staging bytes/addresses - // (bit-identical int32 order). - int4 a_payload, b_payload; - // Prologue: issue the stage-1 global loads first (so they ride behind - // the stage-0 inline staging), stage k0 = 0 into buffer 0 inline - // (same load+wait+write path as the accepted kernel), then the first - // barrier. - prefetch_stage_payload( - x_q, weight, tid, m0, n0, m, k, kStageK, a_payload, b_payload); - stage_prefill_tiles( - x_q, weight, a_tile, b_tile, tid, m0, n0, m, n, k, 0); - __syncthreads(); - for (int k0 = 0; k0 < k; k0 += kStageK) { - const int buf = (k0 / kStageK) & 1; - if (k0 + kStageK < k) { - // Publish the payload carried from the previous iteration (stage - // k0+kStageK) into the idle buffer: the vmcnt wait for the loads - // issued one iteration earlier is emitted here at the first use, - // after the previous MMAC burst + barrier. - publish_stage_payload( - a_payload, b_payload, a_tile + (buf ^ 1) * kABytes, - b_tile + (buf ^ 1) * kBMaxBytes, tid); - } - if (k0 + 2 * kStageK < k) { - // Issue stage k0+2*kStageK's global loads into the payload - // registers (no wait); they stay in flight across the MMAC burst - // and the barrier below and are waited at the next iteration's - // publish (RAW). - prefetch_stage_payload( - x_q, weight, tid, m0, n0, m, k, k0 + 2 * kStageK, a_payload, - b_payload); - } - // Each wave computes its 32x32 quadrant: four m16n16k32 DUMMA tiles. - // k0-outer / kk-inner int32 accumulation order. - mmac_prefill_stage( - a_frag0, a_frag1, acc00, acc01, acc10, acc11, - a_tile + buf * kABytes, b_tile + buf * kBMaxBytes, - local_row, local_col); - // One barrier per stage: (a) WAR - all stage-s fragment reads of the - // buffer just read are retired before it becomes the publish target - // two stages later; (b) RAW - every thread's stage-(s+1) ds_writes - // are visible before the next iteration reads them. - __syncthreads(); - } - } else { - // Prologue: stage k0 = 0 into buffer 0, then the pipelined loop. - stage_prefill_tiles( - x_q, weight, a_tile, b_tile, tid, m0, n0, m, n, k, 0); - __syncthreads(); - for (int k0 = 0; k0 < k; k0 += kStageK) { - const int buf = (k0 / kStageK) & 1; - if (k0 + kStageK < k) { - // Prefetch stage s+1 into the idle buffer: global loads issue here, - // before the stage-s MMAC burst, and complete (RAW) behind the - // single barrier below. - stage_prefill_tiles( - x_q, weight, a_tile + (buf ^ 1) * kABytes, - b_tile + (buf ^ 1) * kBMaxBytes, tid, m0, n0, m, n, k, - k0 + kStageK); - } - // Each wave computes its 32x32 quadrant: four m16n16k32 DUMMA tiles. - // k0-outer / kk-inner int32 accumulation order. - mmac_prefill_stage( - a_frag0, a_frag1, acc00, acc01, acc10, acc11, - a_tile + buf * kABytes, b_tile + buf * kBMaxBytes, - local_row, local_col); - // One barrier per stage: (a) WAR - all stage-s fragment reads of the - // buffer just read are retired before it becomes the prefetch target - // two stages later; (b) RAW - every thread's stage-(s+1) ds_writes are - // visible before the next iteration reads them. - __syncthreads(); - } - } - } else { - for (int k0 = 0; k0 < k; k0 += kStageK) { - // Stage A[kBlockM, kStageK] and B into the single LDS buffer - // (kSkew8 8-mod-16 row strides 88/72 with ds_write2_b64 staging). - stage_prefill_tiles( - x_q, weight, a_tile, b_tile, tid, m0, n0, m, n, k, k0); - __syncthreads(); + for (int k0 = 0; k0 < k; k0 += kStageK) { + stage_prefill_tiles( + x_q, weight, a_tile, b_tile, tid, m0, n0, m, n, k, k0); + __syncthreads(); - // Each wave computes its 32x32 quadrant: four m16n16k32 DUMMA tiles. - // k0-outer / kk-inner int32 accumulation order (kDirectFrag: eight - // load_frag8 ds_read2_b64 fragment reads, zero reassembly VALU). - mmac_prefill_stage( - a_frag0, a_frag1, acc00, acc01, acc10, acc11, - a_tile, b_tile, local_row, local_col); - // Retire this stage's reads before the next stage's staging writes - // overwrite the single LDS buffer (WAR hazard across waves). - __syncthreads(); - } + mmac_prefill_stage( + a_frag0, a_frag1, acc00, acc01, acc10, acc11, a_tile, b_tile, + local_row, local_col); + // Retire this stage's fragment reads before the next stage's staging + // writes overwrite the single LDS buffer (WAR hazard across waves). + __syncthreads(); } const int base_row = m0 + wave_row * 32; const int base_col = n0 + wave_col * 32; store_prefill_fragment( - acc00, x_scale, weight_scale, out, m, n, - base_row, base_col, lane); + acc00, x_scale, weight_scale, out, m, n, base_row, base_col, lane); store_prefill_fragment( - acc01, x_scale, weight_scale, out, m, n, - base_row, base_col + kTileN, lane); + acc01, x_scale, weight_scale, out, m, n, base_row, base_col + kTileN, + lane); store_prefill_fragment( - acc10, x_scale, weight_scale, out, m, n, - base_row + kTileM, base_col, lane); + acc10, x_scale, weight_scale, out, m, n, base_row + kTileM, base_col, + lane); store_prefill_fragment( - acc11, x_scale, weight_scale, out, m, n, - base_row + kTileM, base_col + kTileN, lane); + acc11, x_scale, weight_scale, out, m, n, base_row + kTileM, + base_col + kTileN, lane); } // Generic scalar fallback: one output element per grid-stride iteration with // exact int32 accumulation. Correct for any (m, n, k), including the small-M -// API shapes (M=2/16) and any unmatched geometry. The weight is the identity -// [K, N] row-major layout for every (k, n) EXCEPT the gate_up weight -// (k, n) == (6144, 512), whose buffer is packed [N, K] n-major; this kernel -// decodes that layout so the paired gate_up tails stay correct. -__global__ __launch_bounds__(kScalarThreads) void -w8a8_gemm_scalar_kernel( +// API shapes (M=2/16) and any unmatched geometry. The weight buffer is the +// identity [K, N] row-major layout for every (k, n) EXCEPT the packed +// down_proj pair (k, n) == (256, 6144), whose buffer is the round-5 panel +// layout [N/128][K/64][128][64] +// (packed[((nt*4+kt)*128+nn)*64+kk64] == raw[(nt*128+nn)*k + kt*64+kk64] +// with nt = col/128, nn = col%128, kt = kk/64, kk64 = kk%64); this kernel +// decodes that layout so the paired M=2/M=16 down_proj tails stay correct. +__global__ __launch_bounds__(kScalarThreads) void w8a8_gemm_scalar_kernel( const int8_t* __restrict__ x_q, const int8_t* __restrict__ weight, const float* __restrict__ x_scale, @@ -998,15 +911,7 @@ w8a8_gemm_scalar_kernel( int m, int n, int k) { - // Decode the packed layout when this weight was packed that way: - // gate_up (k, n) == (6144, 512) and, since round 2 of this session, - // down_proj (k, n) == (256, 6144) - since round 5 in the PANEL layout - // [N/128][K/64][128][64] (n-major 64-k-byte rows per panel; byte-exact - // relation packed[((nt*4+kt)*128+nn)*64+kk64] == raw[(nt*128+nn)*k + - // kt*64+kk64] with nt = col/128, nn = col%128, kt = kk/64, kk64 = kk%64); - // identity [K, N] otherwise. - const bool b_panel = (k == 256 && n == 6144); - const bool b_nmajor = (k == 6144 && n == 512) || b_panel; + const bool b_panel = (k == kExactK && n == kExactN); const int64_t total = static_cast(m) * n; const int64_t stride = static_cast(gridDim.x) * blockDim.x; for (int64_t idx = static_cast(blockIdx.x) * blockDim.x + @@ -1014,85 +919,56 @@ w8a8_gemm_scalar_kernel( idx < total; idx += stride) { const int row = static_cast(idx / n); const int col = static_cast(idx - static_cast(row) * n); - int32_t acc = 0; const int8_t* a_row = x_q + static_cast(row) * k; - for (int kk = 0; kk < k; ++kk) { - int64_t b_off; - if (b_panel) { - const int nt = col / 128; - const int nn = col - nt * 128; - const int kt = kk / 64; - const int kk64 = kk - kt * 64; - b_off = (static_cast(nt * 4 + kt) * 128 + nn) * 64 + kk64; - } else { - b_off = b_nmajor ? static_cast(col) * k + kk - : static_cast(kk) * n + col; + int32_t acc = 0; + if (b_panel) { + // Round-5 panel decode: the logical [K, N] element (row kk, column + // col) lives at ((nt*(k/64)+kt)*128 + nn)*64 + kk64. + const int nt = col / kProjBlockN; + const int nn = col - nt * kProjBlockN; + const int64_t panel_base = + static_cast(nt) * (k / kProjStageK) * + (kProjBlockN * kProjStageK) + + static_cast(nn) * kProjStageK; + for (int kk = 0; kk < k; ++kk) { + const int kt = kk / kProjStageK; + const int kk64 = kk - kt * kProjStageK; + acc += static_cast(a_row[kk]) * + static_cast( + weight[panel_base + + static_cast(kt) * + (kProjBlockN * kProjStageK) + + kk64]); + } + } else { + for (int kk = 0; kk < k; ++kk) { + acc += static_cast(a_row[kk]) * + static_cast( + weight[static_cast(kk) * n + col]); } - acc += static_cast(a_row[kk]) * - static_cast(weight[b_off]); } - const float scaled = static_cast(acc) * x_scale[row] * - weight_scale[col]; + const float scaled = + static_cast(acc) * x_scale[row] * weight_scale[col]; out[idx] = __float2bfloat16(scaled); } } -// Bootstrap pack: identity device-to-device copy for every (k, n) pair -// (raw [K, N] layout preserved). Runs outside the timed/CUDA-Graph region. -__global__ __launch_bounds__(kScalarThreads) void -w8a8_pack_identity_kernel( +// Bootstrap pack: identity device-to-device copy of the logical [K, N] weight +// and the [N] weight scale. Runs outside the timed/CUDA-Graph region; the +// destination buffers are the graph-stable ones returned by pack_weight. +__global__ __launch_bounds__(kScalarThreads) void w8a8_pack_identity_kernel( const int8_t* __restrict__ raw_weight, const float* __restrict__ weight_scale, int8_t* __restrict__ packed_weight, float* __restrict__ packed_weight_scale, int64_t weight_elems, int n) { - const int64_t idx = - static_cast(blockIdx.x) * blockDim.x + threadIdx.x; - if (idx < weight_elems) { - packed_weight[idx] = raw_weight[idx]; - } - if (idx < n) { - packed_weight_scale[idx] = weight_scale[idx]; - } -} - -// Shape-specific gate_up pack: transpose raw [K, N] -> packed [N, K] -// (n-major rows, k contiguous) for (k, n) == (6144, 512) only. Byte-exact -// index relation: packed[cc * k + kk] == raw[kk * n + cc]. Each thread -// gathers the 16 bytes of one aligned int4 k-block from 16 raw rows (stride -// n) and stores one aligned int4, keeping the packed write side fully -// coalesced (same gather + aligned int4-store pattern as the validated -// o_proj transpose repair). Runs outside the timed/CUDA-Graph region; same -// byte count, same graph-stable buffer. -__global__ __launch_bounds__(kScalarThreads) void -w8a8_pack_gateup_transpose_kernel( - const int8_t* __restrict__ raw_weight, - const float* __restrict__ weight_scale, - int8_t* __restrict__ packed_weight, - float* __restrict__ packed_weight_scale, - int k, - int n) { - const int k4 = k / static_cast(sizeof(int4)); - const int64_t total4 = static_cast(n) * k4; - const int64_t stride4 = static_cast(gridDim.x) * blockDim.x; - for (int64_t idx4 = static_cast(blockIdx.x) * blockDim.x + + const int64_t stride = static_cast(gridDim.x) * blockDim.x; + for (int64_t idx = static_cast(blockIdx.x) * blockDim.x + threadIdx.x; - idx4 < total4; idx4 += stride4) { - const int cc = static_cast(idx4 / k4); - const int kblk = static_cast(idx4 - static_cast(cc) * k4); - const int kk = kblk * static_cast(sizeof(int4)); - const int8_t* src = raw_weight + static_cast(kk) * n + cc; - int4 v; - char* vp = reinterpret_cast(&v); -#pragma unroll - for (int i = 0; i < static_cast(sizeof(int4)); ++i) { - vp[i] = src[static_cast(i) * n]; - } - reinterpret_cast(packed_weight + static_cast(cc) * k + - kk)[0] = v; + idx < weight_elems; idx += stride) { + packed_weight[idx] = raw_weight[idx]; } - const int64_t stride = static_cast(gridDim.x) * blockDim.x; for (int64_t idx = static_cast(blockIdx.x) * blockDim.x + threadIdx.x; idx < n; idx += stride) { @@ -1100,25 +976,23 @@ w8a8_pack_gateup_transpose_kernel( } } -// Round 5 (this session): shape-specific down_proj panel pack for -// (k, n) == (256, 6144) only. Raw [K, N] -> panel layout -// [N/128][K/64][128][64]: n-major rows of 64 k-bytes per panel, panels -// ordered (n-tile outer, k-tile inner). Byte-exact index relation (verified -// by simulation): +// Round 5 (PACKING round) down_proj pack: raw [K, N] -> panel layout +// [N/128][K/64][128][64] for (k, n) == (256, 6144) only. Byte-exact index +// relation (verified by the int4 decode below): // packed[((nt * (k/64) + kt) * 128 + nn) * 64 + kk64] == // raw[(nt * 128 + nn) * k + (kt * 64 + kk64)] // with nt in [0, n/128), kt in [0, k/64), nn in [0, 128), kk64 in [0, 64). // Each packed int4 (idx4) decodes as v = idx4 % 4, row_p = idx4 / 4, // nn = row_p % 128, kt = (row_p / 128) % (k/64), nt = (row_p / 128) / (k/64), -// kk = kt * 64 + v * 16, cc = nt * 128 + nn, and -// packed[idx4*16 .. +16] == raw[(kk + i) * n + cc] for i in 0..15 (the same -// 16-row gather at column cc as the gate_up transpose, stored into a -// different packed offset; each thread stores one aligned int4, so the -// packed write side stays fully coalesced). The panel layout makes every -// per-block B stage tile (128 n x 64 k) ONE contiguous 8 KiB region, so the -// timed kernel's B staging reads 1 KiB per warp instruction contiguously. -// Runs outside the timed/CUDA-Graph region; same byte count, same -// graph-stable buffer, byte-exact vs the raw logical [K, N] weight. +// kk = kt * 64 + v * 16, cc = nt * 128 + nn; the 16 stored bytes are the same +// 16-row gather at column cc as the round-2 transpose, so every thread still +// stores one aligned int4 and the packed write side stays fully coalesced. +// The panel layout makes each timed block's per-stage B tile (128 n x 64 k = +// 8 KiB) ONE contiguous region, so a 64-lane warp's B staging load reads 1 KiB +// contiguous instead of 16 rows x 64 B at a 256-B row stride (16 distinct +// 128-B lines per warp, half of each line used). Runs outside the +// timed/CUDA-Graph region; same byte count, same grid, same graph-stable +// buffer, byte-exact vs the raw logical [K, N] weight. __global__ __launch_bounds__(kScalarThreads) void w8a8_pack_downproj_panel_kernel( const int8_t* __restrict__ raw_weight, @@ -1127,21 +1001,20 @@ w8a8_pack_downproj_panel_kernel( float* __restrict__ packed_weight_scale, int k, int n) { - constexpr int kBlockN = 128; - constexpr int kStageK = 64; - const int64_t total4 = (static_cast(n) * k) / 16; + const int64_t total4 = + (static_cast(n) * k) / static_cast(sizeof(int4)); const int64_t stride4 = static_cast(gridDim.x) * blockDim.x; for (int64_t idx4 = static_cast(blockIdx.x) * blockDim.x + - threadIdx.x; + threadIdx.x; idx4 < total4; idx4 += stride4) { const int v = static_cast(idx4 & 3); const int64_t row_p = idx4 >> 2; - const int nn = static_cast(row_p % kBlockN); - const int64_t kt_tmp = row_p / kBlockN; // nt * (k/kStageK) + kt - const int kt = static_cast(kt_tmp % (k / kStageK)); - const int nt = static_cast(kt_tmp / (k / kStageK)); - const int kk = kt * kStageK + v * 16; - const int cc = nt * kBlockN + nn; + const int nn = static_cast(row_p % kProjBlockN); + const int64_t kt_tmp = row_p / kProjBlockN; // nt * (k/64) + kt + const int kt = static_cast(kt_tmp % (k / kProjStageK)); + const int nt = static_cast(kt_tmp / (k / kProjStageK)); + const int kk = kt * kProjStageK + v * static_cast(sizeof(int4)); + const int cc = nt * kProjBlockN + nn; const int8_t* src = raw_weight + static_cast(kk) * n + cc; int4 val; char* vp = reinterpret_cast(&val); @@ -1149,6 +1022,8 @@ w8a8_pack_downproj_panel_kernel( for (int i = 0; i < static_cast(sizeof(int4)); ++i) { vp[i] = src[static_cast(i) * n]; } + // The panel offset of idx4 is exactly idx4 * 16 bytes: + // ((nt*(k/64)+kt)*128 + nn)*64 + v*16. reinterpret_cast(packed_weight + idx4 * 16)[0] = val; } const int64_t stride = static_cast(gridDim.x) * blockDim.x; @@ -1173,141 +1048,73 @@ extern "C" void launch_w8a8_gemm( int n, int k, hipStream_t stream) { - // No split-K: the provided workspace is not touched. The timed operator - // performs no allocation, synchronization, packing, or default-stream - // launch; only the caller-provided out is written. + // No split-K: the caller-provided workspace is intentionally unused, and the + // timed operator performs no allocation, synchronization, packing, or + // default-stream launch. (void)workspace; (void)workspace_bytes; auto* out_bf16 = static_cast(out); - // Native INT8 DUMMA m16n16k32 tiled path for every large-M shape. Both - // assigned shapes qualify: gate_up (4096, 512, 6144) and down_proj - // (4096, 6144, 256) both have M >= 128, N % 128 == 0 and K % 128 == 0. - // Explicit dispatch by launch geometry; unmatched shapes fall through to - // the scalar path (no shape is specialized away). - if (m >= kPrefillMinM && (n % kDefaultBlockN == 0) && - (k % kPrefillStageK == 0)) { - // Exact-shape guard for the assigned gate_up shape (round-3 pipeline - // decision, round-10 load_frag8, round-11 kSkew8, round-16 kPayload): - // cooperative A+B staging with the n-major [N, K] packed B operand - // (kBNMajor) consumed as col_major fragments, software-pipelined across - // K tiles (kDoubleBuffer) with a 64-K double buffer: 64x64 tile, 512 - // blocks, 20,480 B LDS/block, 3 blocks/CU (61,440 B) - occupancy - // preserved vs the accepted round-2 single buffer (19,456 B x 3 = - // 58,368 B). One barrier per 64-K stage + prologue (97 for 96 stages). - // Round 11 stages A/B at the 8-mod-16 strides 88/72 (kSkew8) so the - // fragment reads are bank-conflict-free (LDS bytes and occupancy - // unchanged). Round 16 (kPayload) issues the stage-(s+1) global loads a - // full iteration early into a two-int4-per-thread register payload and - // publishes them into the idle LDS buffer at the top of iteration s, so - // the vmcnt wait lands after the stage-s MMAC burst instead of in front - // of it (LDS bytes, barriers, traffic, occupancy and results unchanged). - if (m == 4096 && n == 512 && k == 6144) { - const dim3 grid(n / 64, (m + 63) / 64); - const dim3 block(4 * kWaveSize); - hipLaunchKernelGGL( - HIP_KERNEL_NAME(w8a8_dumma_prefill_tiled_kernel< - 64, 64, 64, true, true, true, true, true>), - grid, block, 0, stream, - a, b, x_scale, weight_scale, out_bf16, m, n, k); - return; - } - // Exact-shape guard for the assigned down_proj shape (round 2 of this - // session): the generic <64,128,128> identity-layout arm measured - // 314.72 us median / 315.18 us p90 with PMC lds_bank_conflicts = - // 7,864,320, ~600 LDS instructions and heavy per-byte fragment - // reassembly VALU per block-stage (library row_major loaders on the - // identity [K,N] B layout). The round-2 guard transplanted the file's - // lineage-validated gate_up machinery: n-major packed B (kBNMajor, pack - // in launch_pack_w8a8_weight for (k,n) == (256,6144)), 64-K - // double-buffered K loop (kDoubleBuffer), explicit 8-byte load_frag8 - // fragment reads (kDirectFrag), 8-mod-16 bank-skewed LDS strides - // A=88/B=72 (kSkew8); accepted at 286.53 us median / 286.64 us p90 with - // PMC lds_instructions 540,672, conflicts 1,179,648, lds_wait 162,664. - // - // Round 3 (this session, pipeline round): compare single and double - // buffering across K tiles. The (4096, 6144, 256) guard now routes to - // w8a8_dumma_prefill_tiled_kernel<64, 128, 64, true, false, true, true> - // - the SINGLE-buffered form of the accepted round-2 arm: same 64-K - // stage, same kBNMajor/kDirectFrag/kSkew8, same tile/grid/epilogue, but - // kDoubleBuffer = false, so each stage serializes one global-load round - // trip (load -> wait -> ds_write2_b64) in front of its MMAC burst with - // two barriers per stage (8/block vs the accepted 5/block), and the LDS - // halves from 29,696 to 14,848 B/block (A[64,88] + B[128,72] x 1). The - // accepted kernel's exact code object shows the double-buffered loop - // already overlaps stage-(s+1) loads with the stage-s MMAC burst - // (global_load x2 -> ds_read2_b64 x4 -> v_mmac x8 -> s_waitcnt - // vmcnt(0) lgkmcnt(0) -> s_barrier), so this round measures whether that - // overlap is worth the extra 14,848 B of LDS: if the single-buffered - // median is not strictly below the accepted 286.53 us (with the p90 - // noise guard), double buffering is retained per the pipeline mandate. - // Results stay bit-identical: stage size, k0-outer/kk-inner int32 order - // and the element-to-slot fragment mapping are unchanged (only the - // buffer count and barrier count differ), so mismatch 0 is expected. - // - // Round 5 (this session, PACKING round): the (4096, 6144, 256) guard - // now routes to w8a8_dumma_prefill_tiled_kernel<64, 128, 64, true, - // false, true, true, false, true> (new kBPanel template flag): the - // one-time pack for (k, n) == (256, 6144) becomes the panel layout - // [N/128][K/64][128][64] (n-major 64-k-byte rows per panel, byte-exact - // vs the raw [K,N] weight, same byte count / graph-stable buffer, - // out-of-timed/out-of-Graph) so each block's per-stage B tile is one - // contiguous 8 KiB region and every warp's int4 loads read 1 KiB - // contiguous (full L2 line utilization) instead of 16 rows x 64 B at - // 256-B stride (half-used lines). Everything else is byte-identical to - // the round-3 winner: same single-buffered 64-K stage, same - // kBNMajor/kDirectFrag/kSkew8, same 64x128 tile / 512 threads / 8 waves, - // same grid (48,64) = 3072 blocks, same LDS A[64,88] + B[128,72] = - // 14,848 B/block @ 2 blocks/CU, same ds_write2_b64 staging and - // load_frag8 fragment reads (LDS bytes and bank behavior untouched), - // same k0-outer/kk-inner int32 order and element-to-slot fragment - // mapping -> mismatch 0 / max_abs_error 0.0 expected. - if (m == 4096 && n == 6144 && k == 256) { - const dim3 grid(n / 128, (m + 63) / 64); + // Native INT8 DUMMA m16n16k32 tiled path for M >= 128. Explicit dispatch by + // launch geometry; every unmatched shape falls through to the scalar path. + if (m >= kPrefillMinM) { + if (m == kExactM && n == kExactN && k == kExactK) { + // Round 2 operand-reuse arm + round 5 panel pack + round 6 coalesced + // fused epilogue + round 15 register scales: cooperative A+B staging with + // the panel-packed B operand consumed as col_major fragments, explicit + // 8-byte load_frag8 reads and bank-safe 8-mod-16 LDS strides (A=88, + // B=72), 64-K single-buffered stages, then an LDS-staged bf16 epilogue + // that applies the per-thread register x_scale/weight_scale, converts and + // writes 16-byte coalesced stores (no workspace, no separate + // convert/combine pass, no LDS scale slab). 64x128 tile, 8 waves + // (32x32 quadrant per wave), grid (6144/128, 4096/64) = (48, 64) = 3072 + // blocks, LDS A[64,88] + B[128,72] = 14,848 B/block (B reused by the + // epilogue) -> 2 blocks/CU, K=256 = four 64-K stages. Weight layout: the + // round-5 panel [N/128][K/64][128][64] (see launch_pack_w8a8_weight). + const dim3 grid(n / kProjBlockN, m / kProjBlockM); const dim3 block(8 * kWaveSize); hipLaunchKernelGGL( - HIP_KERNEL_NAME(w8a8_dumma_prefill_tiled_kernel< - 64, 128, 64, true, false, true, true, false, true>), + HIP_KERNEL_NAME(w8a8_dumma_proj_operand_reuse_kernel< + kProjBlockM, kProjBlockN, kProjStageK>), grid, block, 0, stream, a, b, x_scale, weight_scale, out_bf16, m, n, k); return; } - if (n % 128 == 0) { - // 64x128 tile, 8 waves (32x32 quadrant per wave). - // down_proj: grid (6144/128, 4096/64) = (48, 64) = 3072 blocks. - const dim3 grid(n / 128, (m + 63) / 64); - const dim3 block(8 * kWaveSize); - hipLaunchKernelGGL( - HIP_KERNEL_NAME(w8a8_dumma_prefill_tiled_kernel<64, 128, 128>), - grid, block, 0, stream, - a, b, x_scale, weight_scale, out_bf16, m, n, k); - return; - } - if (m % 128 == 0) { - // 128x64 tile, 8 waves. - const dim3 grid(n / 64, m / 128); - const dim3 block(8 * kWaveSize); - hipLaunchKernelGGL( - HIP_KERNEL_NAME(w8a8_dumma_prefill_tiled_kernel<128, 64, 128>), - grid, block, 0, stream, - a, b, x_scale, weight_scale, out_bf16, m, n, k); - return; - } - { - // 64x64 tile, 4 waves (generic default). - const dim3 grid(n / 64, (m + 63) / 64); - const dim3 block(4 * kWaveSize); - hipLaunchKernelGGL( - HIP_KERNEL_NAME(w8a8_dumma_prefill_tiled_kernel<64, 64, 128>), - grid, block, 0, stream, - a, b, x_scale, weight_scale, out_bf16, m, n, k); - return; + if (!(k == kExactK && n == kExactN)) { + if (n % kDefaultBlockN == 0 && k % kDefaultStageK == 0) { + // 64x128 tile, 8 waves (32x32 quadrant per wave): grid (48, 64) = + // 3072 blocks for the assigned shape, 27,648 B LDS/block -> 2 + // blocks/CU, K=256 = two 128-K stages. + const dim3 grid(n / kDefaultBlockN, (m + kDefaultBlockM - 1) / + kDefaultBlockM); + const dim3 block(8 * kWaveSize); + hipLaunchKernelGGL( + HIP_KERNEL_NAME(w8a8_dumma_prefill_tiled_kernel< + kDefaultBlockM, kDefaultBlockN, kDefaultStageK>), + grid, block, 0, stream, + a, b, x_scale, weight_scale, out_bf16, m, n, k); + return; + } + if (n % 64 == 0 && k % 64 == 0) { + // 64x64 tile, 4 waves: covers large-M shapes whose N is a multiple of + // 64 but not of 128, grid (n/64, ceil(m/64)), 10,240 B LDS/block. + const dim3 grid(n / 64, (m + 63) / 64); + const dim3 block(4 * kWaveSize); + hipLaunchKernelGGL( + HIP_KERNEL_NAME(w8a8_dumma_prefill_tiled_kernel<64, 64, 64>), + grid, block, 0, stream, + a, b, x_scale, weight_scale, out_bf16, m, n, k); + return; + } } + // The packed (k, n) == (256, 6144) buffer must never reach an + // identity-layout tiled arm: any non-exact large-M shape with that pair + // drops to the layout-decoding scalar fallback below. } - // Generic scalar fallback for every unmatched (m, n, k) and M < 128 - // (including the paired M=2/M=16 API shapes with the same (N, K), which - // decode the packed [N, K] gate_up layout when (k, n) == (6144, 512)). + // Generic scalar fallback for every unmatched (m, n, k), every M < 128, and + // the paired M=2/M=16 API shapes with the same (N, K). It decodes the packed + // panel [N/128][K/64][128][64] layout when (k, n) == (256, 6144). const int64_t total = static_cast(m) * n; const unsigned blocks = static_cast( (total + kScalarThreads - 1) / kScalarThreads); @@ -1325,29 +1132,15 @@ extern "C" void launch_pack_w8a8_weight( int k, int n, hipStream_t stream) { - // Assigned gate_up weight (K=6144, N=512): one-time [K,N] -> [N,K] - // transpose pack (n-major, k contiguous) so the timed GEMM reads B - // fragments as k-contiguous bytes from the staged LDS tile. Runs outside - // the timed/CUDA-Graph region; same byte count, same graph-stable buffer, - // byte-exact vs the raw logical [K,N] weight. - if (k == 6144 && n == 512) { + // Round 5: the assigned down_proj weight (k, n) == (256, 6144) is packed + // once into the panel layout [N/128][K/64][128][64] (n-major 64-k-byte rows + // per panel) so each timed block-stage reads its 128x64 B tile as ONE + // contiguous 8 KiB region while the staged LDS bytes stay identical to the + // round-2 n-major [N, K] transpose. One-time, out-of-timed/out-of-Graph, + // same byte count, same graph-stable buffer, byte-exact vs the raw logical + // [K, N] weight. + if (k == kExactK && n == kExactN) { const int64_t total4 = static_cast(n) * (k / 16); - const unsigned blocks = static_cast( - (total4 + kScalarThreads - 1) / kScalarThreads); - hipLaunchKernelGGL( - w8a8_pack_gateup_transpose_kernel, - dim3(blocks), dim3(kScalarThreads), 0, stream, - raw_weight, weight_scale, packed_weight, packed_weight_scale, k, n); - return; - } - // Round 5 (this session): the assigned down_proj weight (K=256, N=6144) - // gets the PANEL pack [N/128][K/64][128][64] (n-major 64-k-byte rows per - // panel; each per-block B stage tile is one contiguous 8 KiB region so - // the timed kernel's B staging reads 1 KiB per warp instruction - // contiguously). One-time, out-of-timed/out-of-Graph, same byte count, - // same graph-stable buffer, byte-exact vs the raw [K,N] weight. - if (k == 256 && n == 6144) { - const int64_t total4 = (static_cast(n) * k) / 16; const unsigned blocks = static_cast( (total4 + kScalarThreads - 1) / kScalarThreads); hipLaunchKernelGGL( @@ -1357,7 +1150,8 @@ extern "C" void launch_pack_w8a8_weight( return; } // Identity device-to-device copy for every other (k, n) pair, including - // unmatched (K, N). Runs outside the timed/CUDA-Graph region. + // unmatched (K, N). Runs outside the timed/CUDA-Graph region; same byte + // count and same graph-stable destination buffers. const int64_t weight_elems = static_cast(k) * n; const int64_t total = weight_elems > n ? weight_elems : n; const unsigned blocks = static_cast( diff --git a/metainfer/tasks/dcu_kernel_auto_opt/variant/int8w8a8-gemm/glm52/TP8/M4096/shared_gate_up_proj.hip b/metainfer/tasks/dcu_kernel_auto_opt/variant/int8w8a8-gemm/glm52/TP8/M4096/shared_gate_up_proj.hip index fec33f67..8a57f4bf 100644 --- a/metainfer/tasks/dcu_kernel_auto_opt/variant/int8w8a8-gemm/glm52/TP8/M4096/shared_gate_up_proj.hip +++ b/metainfer/tasks/dcu_kernel_auto_opt/variant/int8w8a8-gemm/glm52/TP8/M4096/shared_gate_up_proj.hip @@ -1,212 +1,103 @@ -// @@variant shape=glm_tp8_shared_gate_up_proj_m4096 commit=df2c88db09d3836df8ff7a4e99e19584fe6d4163 added=2026-08-31 -// median_us=273.8 p90_us=275.3 speedup=77.62 baseline_us=2.125e+04 -// source=glm5-2-dsh-tp8-m4096-1-e6a280a2 +// @@variant shape=glm_tp8_shared_gate_up_proj_m4096 commit=1aa11051ede5b81487b51f5949af8c5177ffd4a4 added=2026-09-14 +// median_us=249.9 p90_us=251.9 speedup=85.06 baseline_us=2.125e+04 +// source=test-9-11-1/iteration_002/glm_tp8_shared_gate_up_proj_m4096 // MetaInfer W8A8 INT8 GEMM backend for Hygon gfx928 (K500SM_AI). -// worker_3 (iteration 3), assigned shapes: +// worker_1 (iteration 1, establish_arch round), assigned shape: // glm_tp8_shared_gate_up_proj_m4096 : (M, N, K) = (4096, 512, 6144) -// glm_tp8_shared_down_proj_m4096 : (M, N, K) = (4096, 6144, 256) // // This file provides the two stable host launch symbols consumed by // csrc/bindings.cpp: // * launch_w8a8_gemm(...) - timed, graph-safe GEMM on the caller's // HIP stream; no allocation, packing, // autotuning, or synchronization. Only the -// caller-provided out and workspace are -// touched (workspace is unused this round). -// * launch_pack_w8a8_weight(...) - out-of-timed-region packing. Identity -// device-to-device copy for every (k, n) -// pair EXCEPT the assigned gate_up weight -// (k, n) == (6144, 512), which is packed -// once as a [K, N] -> [N, K] transpose -// (n-major rows, k contiguous) so the timed -// GEMM consumes B fragments as -// k-contiguous bytes. The pack stays -// out-of-timed, out-of-Graph, same byte -// count, same graph-stable buffer. +// caller-provided out is touched (no +// split-K, so the workspace is untouched). +// * launch_pack_w8a8_weight(...) - out-of-timed-region packing. The assigned +// gate_up weight (k, n) == (6144, 512) is +// packed once as a [K, N] -> [N, K] +// transpose (n-major rows, k contiguous); +// every other (k, n) pair keeps the +// byte-identical identity copy. // -// Bootstrap strategy (correctness-first, usable profiling baseline): -// * Native INT8 DUMMA m16n16k32 tiled kernel with int32 accumulation for -// every assigned shape with M >= 128 (both assigned shapes qualify). -// * 2-D macro-tile family (64x64, 64x128, 128x64) with one 32x32 quadrant -// per wavefront (four m16n16k32 int32 accumulators kept resident over the -// whole K loop). Single-buffered K loop (K stage 128, two barriers per -// stage): no split-K, no raw asm, no speculative double-buffer pipeline. +// Round 1 (establish_arch; architecture experiment = packed_weight family): +// the accepted bootstrap is a correct native INT8 DUMMA m16n16k32 64x64x128 +// tiled kernel (431.615 us median / 432.025 us p90, 59.71 TOPS, 75.35 GB/s, +// passed, graph_capture_passed, mismatch_count 0). Its fresh PMC bundle +// exposes the dominant steady-state defect: +// * lds_bank_conflicts = 17,301,504 over 512 blocks x 48 K-stages = 704 +// conflict cycles per block-stage, sourced by the identity [K, N] B tile: +// the matrix_b row_major loader must gather its 8 elements per lane from +// 8 different k-rows at the kBlockN+16 = 80 B row stride, so the LDS +// reads are byte-scattered and heavily aliased across the wavefront; +// * lds_wait_instructions = 11,954,696 (~486 stall cycles per block-stage); +// * lds_instructions = 7,471,104. +// This round establishes the packed-weight architecture geometry for the +// exact gate_up (K, N) pair: +// * launch_pack_w8a8_weight transposes raw [K, N] into packed [N, K] +// (n-major rows, k contiguous) for (k, n) == (6144, 512) ONLY. Packing +// runs before graph capture, uses the same byte count / graph-stable +// buffer, and is byte-exact: packed[cc * k + kk] == raw[kk * n + cc]. +// * The prefill kernel stages B[kBlockN, kStageK] from that packed layout +// with 16-byte vectorized coalesced int4 global loads into a bank-skewed +// LDS tile (row stride kStageK + 16 B; 80 B at the round-11 64-K packed +// stage) and consumes it with the +// matrix_b col_major fragment loader: each lane's 8 elements are now 8 +// CONSECUTIVE k-bytes of one packed row instead of 8 byte-scattered +// columns, which removes the byte aliasing of the row_major path. +// * A stays row_major from the identity [M, K] activation with the same +// bank-skewed kStageK + 16 B LDS row stride; A staging / global traffic / +// epilogue are unchanged. // -// Round 1 (baseline): the 64x128 tile measured 517.4 us median / 49.81 TOPS; -// routing the exact gate_up shape to the 64x64 tile measured 432.5 us -// (accepted; 19.6% improvement). -// -// Round 2 (operand-reuse, mandated decision): cooperative A+B LDS staging is -// kept and direct per-wave global loads are rejected (A is reused 64x per -// macro-tile via kBlockN and B 64x via kBlockM; direct loads would refetch -// both operands once per wavefront, 4x the 28.3 MB staged global traffic -> -// ~1.73M vmem_read instructions vs 432K, and the TP4 gate_up direct-A arm -// measured 7.9x slower). The focused change makes the staged B layout -// bank-safe for the fragment consumers: the gate_up weight is packed once -// [K,N] -> [N,K] (n-major, k contiguous) and B fragments are loaded -// col_major from a 144-B (128+16) row-strided LDS tile, replacing the -// byte-scattered row_major B reads on the identity [K,N] layout that are the -// 17.3M PMC bank-conflict source. Global loads stay 16-byte vectorized and -// coalesced on both operands. LDS stays 19,456 B/block -> 3 blocks/CU -// (58,368 B, unchanged; round-1's "4 blocks/CU" estimate was impossible on -// both LDS (4x19,456 = 77,824 B > 64 KiB) and VGPR (4x256x72 = 73,728 > -// 65,536)). down_proj keeps the identity 64x128 arm; every unmatched shape -// keeps the generic arms and the scalar fallback (which decodes the packed -// [N,K] layout when (k, n) == (6144, 512) so the paired M=2/M=16 gate_up -// tails stay correct). -// * A[kBlockM, kStageK] and B staged in bank-skewed LDS (row strides -// 144/80/144 = 16-byte-aligned non-power-of-two), consumed with the -// library du_load_matrix_sync loaders: A row_major (unchanged, load- -// bearing on every shape), B row_major for the identity [K, N] layout -// (down_proj + unmatched shapes) and B col_major for the gate_up -// [N, K] packed layout (round 2). +// Geometry (unchanged from the accepted bootstrap, so this round isolates the +// operand-layout factor): +// * 64x64 macro-tile, 4 wavefronts per block (256 threads), each wavefront +// owns one 32x32 quadrant = four resident m16n16k32 int32 accumulators +// held across the whole K loop. +// * SINGLE-buffered K loop: cooperative vectorized (int4) coalesced A/B +// global loads into bank-skewed LDS, then the MMAC burst, then two +// barriers per stage. No split-K, no raw asm. Round 11 (final consolidate +// polish) halves the PACKED arm's K stage from 128 to 64 (kPackedStageK); +// the identity [K, N] arm keeps the 128-K stage byte-identically. +// * Identity [K, N] arm: LDS = A[64, 144] (9,216 B) + B[128, 80] +// (10,240 B) = 19,456 B/block -> 3 blocks/CU. Packed [N, K] arm (round 11 +// geometry): 64-K stage -> LDS = A[64, 80] (5,120 B) + B[64, 80] +// (5,120 B) = 10,240 B/block, so it is no longer LDS-limited and reaches +// the 4-block/CU VGPR ceiling (4 x 256 threads x 64 VGPR = 65,536, the +// whole per-CU VGPR file; 4 x 10,240 = 40,960 B <= 64 KiB LDS), i.e. 16 +// resident waves/CU instead of 12. +// * Grid = (N/64, ceil(M/64)) = (8, 64) = 512 blocks. // * Fused direct fragment -> x_scale -> weight_scale -> bf16 epilogue using -// the verified gfx928 accumulator ownership (row = lane & 15, -// col_mod4 = lane >> 4, x[i] -> columns col_mod4 + 4*i). The int32 dot is -// exact for every assigned shape (max |dot| = 6144*127*127 << 2^31), the -// int32 -> float conversion is exact, and the single fp32 multiply -// followed by one bfloat16 rounding matches the reference bit-for-bit. -// * Tail M rows are zero-filled on load and masked on store, so any -// M >= 128 is supported by the tiled path. -// * All other (m, n, k) - including every M < 128 API case and the paired -// M=2/M=16 shapes with the same (N, K) - go to a scalar int8/int32 +// the gfx928 int8 DUMMA accumulator ownership (row = lane & 15, +// col_mod4 = lane >> 4, frag.x[i] -> columns col_mod4 + 4*i). The int32 +// dot is exact for every assigned K (max |dot| = 6144*127*127 << 2^31), +// and the exact fp32 order (dot * x_scale[m]) * weight_scale[n] then one +// bfloat16 rounding matches the trusted int64 reference bit-for-bit. +// * Tail M rows are zero-filled on load and masked on store, so any M >= 128 +// works with the tiled path. +// * Every other (m, n, k) - including every M < 128 API case and the paired +// M=2/M=16 shapes with the same (N, K) - goes to a scalar int8/int32 // fallback that decodes the [N, K] packed layout when (k, n) == // (6144, 512) and the identity [K, N] layout otherwise. // -// Round 3 (pipeline, mandated decision): compare single and double buffering -// across K tiles. The exact (4096, 512, 6144) guard now routes to -// w8a8_dumma_prefill_tiled_kernel<64, 64, 64, true, true>: a software- -// pipelined 64-K double-buffered K loop (prologue + one barrier per stage, -// 97 barriers for 96 stages) that issues stage s+1's global loads into the -// idle LDS buffer before the stage-s MMAC burst, overlapping the -// global-load latency that the round-2 single buffer serializes in front of -// every stage (ISA-verified order: global_load_dwordx4 -> ds_write_b128 -> -// s_waitcnt -> s_barrier -> ds_read2 x16 -> v_mmac x16 -> s_barrier, with -// the next stage's loads gated behind the previous stage's barrier). The -// 128-K double-buffered variant is arithmetically impossible at 3 blocks/CU -// (2 x 19,456 = 38,912 B/block x 3 = 116,736 > 64 KiB -> 1 block/CU, a -// harmful occupancy loss), so the pipelined comparison uses the 64-K -// double buffer: LDS 20,480 B/block x 3 = 61,440 <= 64 KiB and -// VGPR 64-72 x 256 x 3 <= 65,536, i.e. 3 blocks/CU preserved (no occupancy -// confound). Everything else is byte-identical: same int4 staging pattern -// (same 28.3 MB global traffic and ~432K vmem_read instructions), same -// k0-outer/kk-inner int32 order (stage size changes only regroup exact -// integer sums, so results stay bit-identical), same col_major B fragments -// (stride 80 = 64+16 bank skew), same fused epilogue, same pack, same -// generic single-buffered arms and scalar fallback. Decision rule (mandate): -// retain double buffering only if PMC shows reduced VMEM/LDS stall evidence -// (lds_wait_instructions drops below ~1.2M, lds_instructions stays ~1.97M, -// occupancy stays 3 blocks/CU) AND the median improves >2%; otherwise the -// round's evidence retains the single-buffered round-2 structure. -// -// Round 10 (fragment-load-form round, restarted from the accepted round-3 -// source after the round-9 infra kill): replace the library -// du_load_matrix_sync fragment loads of the exact-shape gate_up kernel with -// the lineage-validated explicit load_frag8 8-byte ds_read_b64 loader. The -// exact (4096, 512, 6144) guard now routes to -// w8a8_dumma_prefill_tiled_kernel<64, 64, 64, true, true, true> (new -// kDirectFrag template arm). The round-3 ISA shows every fragment load as -// ds_read2_b32 + per-byte v_and/v_or_b32_sdwa/v_or3 mask-OR "identity" -// reassembly (~9.4M of the 15.43M VALU instructions per replay); loading the -// same 8 bytes per lane as one int64 (ds_read_b64) removes that VALU and -// halves the fragment bank-conflict factor (32-lane dword phases at stride -// 80 -> 16-lane 8-byte phases). Element-to-slot fragment mapping, v_mmac -// operands, exact k0-outer/kk-inner int32 accumulation, staging/global -// traffic, barriers, tile, grid, LDS bytes, occupancy (3 blocks/CU) and the -// fused epilogue are unchanged; results stay bit-identical. The generic arms -// (<64,128,128>, <128,64,128>, <64,64,128>, all kBNMajor=false) keep the -// library loaders and byte-identical codegen/resources (if constexpr). -// -// Round 11 (LDS bank-skew round, HIP-only; ISA policy keeps raw asm -// disallowed because the HIP plateau rule - 8 valid HIP rounds within +/-2% -// of the then-current best - is unmet). The accepted round-10 kernel still -// shows PMC lds_bank_conflicts = 9,437,184, EXACTLY unchanged from the -// round-3 code object (the -9.07% round-10 win came from VALU removal, not -// from conflicts). 9,437,184 = 192 x 512 blocks x 96 stages: every -// block-stage pays 192 conflict cycles = 128 from the 16 ds_read2_b64 -// fragment reads + 64 from the 8 ds_write_b128 staging stores (the 16-B/lane -// store floor). The read half is a stride defect: at stride 80 = 20 dwords, -// 20*r mod 32 has period 8, so every 16-lane read phase groups rows r and -// r+8 onto the same bank pair - a 2-way conflict on ALL 4 sub-phases of -// every fragment read. The exact (4096, 512, 6144) guard now routes to -// w8a8_dumma_prefill_tiled_kernel<64, 64, 64, true, true, true, true> (new -// kSkew8 template arm) which stages both operands at the down_proj-validated -// 8-mod-16 row strides A = 88 (64+24) and B = 72 (64+8) instead of 80: -// 22*r and 18*r mod 32 are distinct over r = 0..15 and gcd(22,32) = -// gcd(18,32) = 2 does not divide 1, so every 16-lane read phase lands on 16 -// DISTINCT bank pairs - the fragment reads become conflict-free (128 of the -// 192 cycles removed; only the 2-way 16-B/lane staging-store floor of 64 -// cycles remains -> lds_bank_conflicts expected ~3.15M). The 8-mod-16 row -// starts forbid 16-byte staging stores, so each 16-byte int4 is staged as -// two 8-byte halves (ds_write2_b64), the validated down_proj staging form -// (its B tile is exactly stride 72; its A tile exactly stride 88). Global -// loads (int4, same 432,128 vmem_read), pack layout, barrier structure -// (97/block), tile/grid (512 blocks), LDS bytes (A[64,88] + B[64,72] x 2 = -// 20,480 B/block -> 3 blocks/CU unchanged), VGPR, the k0-outer/kk-inner -// int32 order, the element-to-slot fragment mapping and the fused epilogue -// are untouched; only the LDS row strides and the staging store granularity -// change, so results stay bit-identical. Generic arms (<64,128,128>, -// <128,64,128>, <64,64,128>) keep kSkew8 = false and byte-identical -// codegen/resources (if constexpr). +// Iteration 14 (consolidate; final HIP-only polish of the fixed packed_weight +// architecture): the accepted packed arm's four fragment loads per MMAC step +// are switched from the library du_load_matrix_sync lowering (ds_read2_b32 +// plus per-byte v_and / v_or_b32_sdwa / v_or3 mask-OR "identity" reassembly) +// to an explicit 8-byte load_frag8 int64 (ds_read_b64) loader, for the packed +// n-major arm ONLY (new kDirectFrag template flag, set true for the exact +// (K, N) = (6144, 512) dispatch; the identity [K, N] arm keeps the library +// loaders byte-identically). Lane -> (row = lane & 15, k-quarter = lane >> 4) +// and the eight contiguous bytes at p[row * ldm + (lane >> 4) * 8] land in +// the same x[0..7] slots in the same little-endian order, so the v_mmac +// operands, the exact k0-outer / kk-inner int32 accumulation order and every +// result are unchanged. LDS strides (A = kStageK + 16, packed B = kStageK + +// 16), int4 staging, global traffic, single buffer, two barriers per stage, +// tile, grid (512 blocks), 4 waves / 256 threads, 10,240 B LDS, occupancy and +// the fused epilogue are byte-identical; both pack kernels, the host shape +// guards and the scalar fallback are untouched. // -// Round 16 (lead-2 loop-carried payload round, HIP-only; raw asm stays -// DISALLOWED - plateau=false, the recent-valid-improvement window -// [-34.44%, +2.82%, +9.97%] does not meet three valid HIP rounds within -// [-2%, +2%) of the then-current best, and no prior ISA-guided round -// recorded a compiler limitation with target instructions). The accepted -// round-11 exact code object (digest 9889820e, median 293.30 us / p90 -// 293.545) shows the software-pipeline intent is DEFEATED by the compiler's -// wait placement: per stage the steady-state loop is -// global_load_dwordx4 (A prefetch s+1) -> global_load_dwordx4 (B prefetch -// s+1) -> s_waitcnt vmcnt(1) -> s_waitcnt vmcnt(0) <- waits IMMEDIATELY -// -> 2 x ds_write2_b64 (staging stores) -> 4 x ds_read2_b64 (fragment -// reads, stage s) -> 8 x v_mmac CONTIGUOUS (stage s) -> s_barrier -> loop. -// i.e. the stage-(s+1) global loads are waited ~12-16 instructions after -// issue, BEFORE the stage-s MMAC burst, so the full global-load latency -// (~600-1000+ cycles under 120-CU L2 contention) sits on the per-stage -// critical path with zero overlap (the round-12 analysis' -// "prefetch global load -> ds_write2_b64 -> s_barrier -> ds_read2_b64 -> -// v_mmac" chain; PMC: 6.0M VALU + 1.18M LDS + 465K VMEM wave-instructions -// over 293 us x 120 CUs, i.e. ~95-99% issue-stalled). Iteration history -// rules out the other levers on this exact shape: tile aspect (iter 4 -// 32x64: 749.9 us; iter 8 64x128 w8: 395.4 us), 2-wave quadrants (iter 6: -// 629.0 us - wave count per CU is load-bearing), LDS-side fragment -// prefetch (iter 7: 337.97 us, flat/worse), stage 32 / 6 blocks per CU -// (iter 12: 447.4 us - per-stage fixed cost, not occupancy, is the lever). -// The one untested mechanism that directly removes the exposed vmcnt wait: -// a LEAD-2 LOOP-CARRIED REGISTER PAYLOAD (the TP4 o_proj lineage's -// validated "stage-top publish" form). The exact (4096, 512, 6144) guard -// now routes to w8a8_dumma_prefill_tiled_kernel<64,64,64,true,true,true, -// true,true> (new kPayload template arm): each thread keeps the one A int4 -// + one B int4 of stage s+2 in VGPR across the stage-(s+1) barrier, issues -// those global loads at the TOP of iteration s (no wait), and PUBLISHES -// the payload carried from iteration s-1 into the idle LDS buffer at the -// TOP of iteration s (before the stage-s MMAC burst). The first use of the -// payload registers is the next iteration's publish, and the s_barrier -// fences outstanding vmem, so the compiler can no longer place -// s_waitcnt vmcnt(0) immediately after the loads: the wait lands after the -// stage-s fragment reads + MMAC burst (immediately before the barrier), and -// the global-load latency overlaps the whole compute segment instead of -// serializing in front of it. One barrier per 64-K stage is preserved -// (prologue + 96 = 97/block); the publish targets the idle buffer (WAR -// retired by the previous barrier, RAW retired by the stage barrier), so no -// second barrier is introduced. Everything else is byte-identical: 64x64 -// tile / 256 threads / 4 waves / 32x32 quadrant per wave, grid (8,64) = -// 512 blocks, kSkew8 strides 88/72 with ds_write2_b64 staging (8 per -// block-stage), same 16 ds_read2_b64 fragment reads per block-stage (same -// lds_instructions ~1,179,648), same 20,480 B LDS/block -> 3 blocks/CU -// (payload costs 8 VGPRs: ~54 total, 54 x 256 x 3 = 41,472 <= 65,536; no -// spill risk at 3 blocks/CU), same int4 global loads (same ~432,128 -// vmem_read / 32,768 vmem_write), same k0-outer/kk-inner int32 order and -// the same element-to-slot fragment mapping - the payload changes only WHEN -// global bytes land in LDS, never which bytes or the accumulation order, so -// results stay bit-identical (mismatch 0 / max_abs_error 0.0 expected). -// Generic arms (<64,128,128>, <128,64,128>, <64,64,128>, all kBNMajor= -// false) keep kPayload = false and byte-identical codegen/resources -// (if constexpr). -// -// Header order is fixed by the control plane: hip_runtime first (du_mma.h -// is not self-contained before it), hip_bfloat16 second, du_mma.h last. +// Header order is fixed by the control plane: hip_runtime first (du_mma.h is +// not self-contained before it), hip_bfloat16 second, du_mma.h last. #include #include @@ -224,21 +115,51 @@ constexpr int kTileK = 32; // gfx928 wavefront is 64 lanes; every blockDim must be a multiple of 64. constexpr int kWaveSize = 64; -// Large-prefill dispatch geometry. Each macro-tile config uses a 32x32 -// quadrant per wave, so waves per block = (BlockM/32) * (BlockN/32). +// Large-prefill dispatch geometry: one 32x32 DUMMA quadrant per wavefront. constexpr int kPrefillMinM = 128; constexpr int kPrefillStageK = 128; constexpr int kDefaultBlockM = 64; constexpr int kDefaultBlockN = 64; +// Exact assigned shape: the only (K, N) pair whose packed weight is stored +// n-major (transposed). Every other pair keeps the identity [K, N] layout. +constexpr int kGateUpK = 6144; +constexpr int kGateUpN = 512; + +// Round 11 (consolidate): the fixed winning packed_weight architecture keeps +// its 64x64 macro-tile, 4-wave/256-thread block, single-buffered stage +// structure and 16-byte bank skew; only the PACKED arm's K stage depth is +// polished from the generic 128 down to 64. The packed arm's LDS footprint +// drops from A[64,144] + B[64,144] = 18,432 B to A[64,80] + B[64,80] = +// 10,240 B/block, so the 64 KiB per-CU LDS part no longer caps it at 3 +// blocks/CU and it reaches the 4-blocks/CU VGPR ceiling (4 x 256 x 64 = the +// whole 65,536-dword per-CU VGPR file; 4 x 10,240 = 40,960 B). That is 16 +// resident waves/CU instead of 12, giving +33% concurrent coverage of the +// single-buffered global-load -> barrier -> MMAC -> barrier dependency that +// dominates each stage. The current PMC confirms the stage is latency- rather +// than bandwidth-limited: 97.6 GB/s counter-derived HBM bandwidth against +// 88.8 GB/s algorithmic bandwidth at a 91.8% L2 hit rate, so the staged +// operand traffic is far from saturating the memory system. +// +// k = 6144 = 96 x 64, kPackedStageK % kTileK == 0 and the kk inner order +// still advances in 32-K DUMMA steps, so the k0-outer / kk-inner int32 +// accumulation visits the exact same K sequence and every result stays +// bit-identical to the accepted 128-K stage. The identity [K, N] arm, the +// host shape guards, both pack kernels and the scalar fallback are untouched. +constexpr int kPackedStageK = 64; +static_assert(kPackedStageK % kTileK == 0, + "packed K stage must be a multiple of the DUMMA K unit"); +static_assert(kGateUpK % kPackedStageK == 0, + "packed K stage must tile the assigned gate_up K exactly"); + constexpr int kScalarThreads = 256; using namespace du::dumma; // Verified gfx928 INT8 DUMMA accumulator ownership: // row = lane & 15, col_mod4 = lane >> 4, frag.x[i] -> columns -// col_mod4 + 4*i. Scale by x_scale[row] and weight_scale[col] and store -// bf16 directly from the fragment (no accumulator LDS round trip). +// col_mod4 + 4*i. Scale by x_scale[row] and weight_scale[col] and store bf16 +// directly from the fragment (no accumulator LDS round trip). template __device__ __forceinline__ void store_prefill_fragment( const AccFragment& frag, @@ -259,38 +180,30 @@ __device__ __forceinline__ void store_prefill_fragment( #pragma unroll for (int i = 0; i < 4; ++i) { const int col = base_col + col_mod4 + 4 * i; - const float scaled = static_cast(frag.x[i]) * xs * weight_scale[col]; + const float scaled = + static_cast(frag.x[i]) * xs * weight_scale[col]; out[row * n + col] = __float2bfloat16(scaled); } } -// Round 10 (fragment-load-form round): explicit 8-byte fragment loader -// replacing the library du_load_matrix_sync for the exact-shape gate_up -// kernel. Validated on the sibling TP8 down_proj accepted kernel (iter 8, -// -25.9%: 230.91 -> 171.19 us) and the TP4 gate_up lineage (iter 13 -// load_fragment8, -21%: 280.05 -> 220.71 us). For BOTH the library row_major -// matrix_a and col_major matrix_b m16n16k32 int8 loaders, lane l -> row -// (l & 15), k-quarter (l >> 4), and the loader fetches the same eight -// contiguous bytes at p[row*ldm + (l>>4)*8 .. +7] and stores them into the -// same x[0..7] slots in the same little-endian order (the round-3 ISA shows -// the compiler lowering this to ds_read2_b32 followed by a per-byte -// v_and/v_or_b32_sdwa/v_or3 mask-OR "identity" reassembly - ~9.4M dead-ish -// VALU instructions per replay, ~61% of the 15.43M VALU total). Loading the -// same 8 bytes as one int64 forces the 8-byte ds_read_b64 form instead: -// (a) the mask/OR reassembly VALU disappears, and (b) the LDS access -// changes from ds_read2_b32 (32-lane dword phases - at the stride-80 row -// pattern every phase groups rows 0-15 of k-quarter q with rows 0-15 of -// k-quarter q+1 onto the same 16 bank phases, a 4-way conflict per phase) -// to ds_read_b64 (16-lane phases: rows 0-15 land on 16 bank-pairs -// {20*r mod 32} = 8 distinct pairs hit twice - a 2-way conflict per phase, -// half of the current fragment-conflict factor). The element-to-slot -// mapping is unchanged (f.x[0..7] = the same 8 bytes in the same -// little-endian order), the v_mmac operands and the exact k0-outer/kk-inner -// int32 accumulation sequence are untouched, and the staging stores, LDS -// strides, barriers, tile, grid, occupancy and epilogue are byte-identical. +// Iteration 14 (consolidate polish, packed arm only): explicit 8-byte fragment +// loader replacing the library du_load_matrix_sync calls in the packed +// n-major arm. For BOTH the library row_major matrix_a and col_major +// matrix_b m16n16k32 int8 loaders, lane l -> row (l & 15), k-quarter +// (l >> 4), and the loader fetches the same eight contiguous bytes at +// p[row * ldm + (l >> 4) * 8 .. +7] and stores them into the same x[0..7] +// slots in the same little-endian order (the accepted code object lowers the +// library calls to ds_read2_b32 plus a per-byte v_and / v_or_b32_sdwa / +// v_or3 mask-OR "identity" reassembly). Loading the same 8 bytes as one +// int64 forces the 8-byte ds_read_b64 form: the reassembly VALU disappears +// and the LDS access moves from 32-lane dword phases to 16-lane 8-byte +// phases. The element-to-slot mapping, the v_mmac operands, the +// k0-outer/kk-inner int32 accumulation order, the LDS strides (A = kStageK + +// 16, packed B = kStageK + 16), staging, barriers, tile/grid/occupancy and +// the fused epilogue are unchanged, so results stay bit-identical. template -__device__ __forceinline__ void load_frag8(Frag& f, const int8_t* p, - unsigned ldm) { +__device__ __forceinline__ void load_frag8( + Frag& f, const int8_t* p, unsigned ldm) { const unsigned row = __lane_id() & 0xf; const unsigned kq = __lane_id() >> 4; const int64_t v = @@ -298,22 +211,21 @@ __device__ __forceinline__ void load_frag8(Frag& f, const int8_t* p, reinterpret_cast(f.x)[0] = v; } -// Stage one K tile (k0) of A[kBlockM, kStageK] and B into the caller-offset -// LDS buffers (bank-skewed row strides; int4 vectorized coalesced global -// loads, zero-filled tail M rows). kBNMajor == true stages B from the packed -// [N, K] n-major gate_up layout (n-major rows, k contiguous); otherwise from -// the identity [K, N] layout. Shared by the single-buffered and -// double-buffered K loops; the buffer offset (0 or kABytes/kBMaxBytes) is -// applied by the caller so one body serves both pipeline forms. +// Stage one K tile (k0) of A[kBlockM, kStageK] and B into the caller LDS +// buffers with int4-vectorized coalesced global loads. Tail M rows are +// zero-filled so the MACs contribute nothing and the guarded store never +// writes them. // -// kSkew8 (round 11, exact-shape arm only): A and B row strides become the -// down_proj-validated 8-mod-16 values 88 (kStageK + 24) and 72 (kStageK + 8) -// so every 16-lane fragment-read phase lands on 16 distinct bank pairs (see -// the round-11 header comment). The 8-mod-16 row starts forbid 16-byte -// stores, so each 16-byte int4 chunk is staged as two 8-byte halves -// (ds_write2_b64); the global int4 loads are byte-identical. -template +// kBNMajor == true stages B[kBlockN, kStageK] from the packed [N, K] layout +// (n-major rows, k contiguous) that launch_pack_w8a8_weight produces for the +// exact gate_up (k, n) pair: row = local N, columns = k, row stride +// kStageK + 16 B (80 B at the round-11 64-K packed stage). The B fragment +// consumer then reads 8 consecutive k bytes per lane (col_major), replacing +// the byte-scattered identity-layout row_major reads that source the measured +// 17.3M bank conflicts. +// kBNMajor == false stages B[kStageK, kBlockN] from the identity [K, N] +// weight with row stride kBlockN + 16 = 80 B (unchanged bootstrap path). +template __device__ __forceinline__ void stage_prefill_tiles( const int8_t* __restrict__ x_q, const int8_t* __restrict__ weight, @@ -327,18 +239,17 @@ __device__ __forceinline__ void stage_prefill_tiles( int k, int k0) { constexpr int kThreads = kBlockM * kBlockN / 1024 * kWaveSize; + constexpr int kInt4 = static_cast(sizeof(int4)); // 16-byte bank skew: keeps int4 staging stores aligned while spreading the // fragment rows across distinct LDS banks. - constexpr int kAStride = kSkew8 ? kStageK + 24 : kStageK + sizeof(int4); - constexpr int kBStride = kBlockN + sizeof(int4); // identity [K,N] rows - constexpr int kBNStride = kSkew8 ? kStageK + 8 : kStageK + sizeof(int4); - static_assert(!kSkew8 || kStageK == 64, - "kSkew8 strides 88/72 assume kStageK == 64"); - constexpr int kAInt4PerRow = kStageK / sizeof(int4); + constexpr int kAStride = kStageK + kInt4; + constexpr int kBStride = kBlockN + kInt4; // identity [K, N] rows + constexpr int kBNStride = kStageK + kInt4; // packed [N, K] rows + constexpr int kAInt4PerRow = kStageK / kInt4; constexpr int kAInt4 = kBlockM * kAInt4PerRow; - constexpr int kBInt4PerRow = kBlockN / sizeof(int4); + constexpr int kBInt4PerRow = kBlockN / kInt4; constexpr int kBInt4 = kStageK * kBInt4PerRow; - constexpr int kBNInt4PerRow = kStageK / sizeof(int4); + constexpr int kBNInt4PerRow = kStageK / kInt4; constexpr int kBNInt4 = kBlockN * kBNInt4PerRow; // Stage A[kBlockM, kStageK] into LDS (zero-filled tail M rows). for (int vec = tid; vec < kAInt4; vec += kThreads) { @@ -349,162 +260,52 @@ __device__ __forceinline__ void stage_prefill_tiles( global_row < m ? *reinterpret_cast( x_q + static_cast(global_row) * k + k0 + - v * static_cast(sizeof(int4))) + v * kInt4) : int4{0, 0, 0, 0}; - if constexpr (kSkew8) { - // 8-mod-16 row starts: write the 16 bytes as two 8-byte halves so every - // store stays 8-byte aligned (ds_write2_b64 staging; same LDS bytes). - int8_t* dst = - a_tile + local_row * kAStride + v * static_cast(sizeof(int4)); - reinterpret_cast(dst)[0] = - reinterpret_cast(&val)[0]; - reinterpret_cast(dst)[1] = - reinterpret_cast(&val)[1]; - } else { - reinterpret_cast(a_tile + local_row * kAStride)[v] = val; - } + reinterpret_cast(a_tile + local_row * kAStride)[v] = val; } if constexpr (kBNMajor) { - // Stage B[kBlockN, kStageK] from the packed [N, K] weight (n-major: - // row = n_local, k contiguous). Same vectorized coalesced int4 pattern - // as the identity arm, one per 16 consecutive k bytes. + // Stage B[kBlockN, kStageK] from the packed [N, K] weight (n-major rows, + // k contiguous): row = local N, 16 consecutive k bytes per int4. + static_assert(kBNInt4 % kThreads == 0, + "packed n-major B staging must divide evenly"); for (int vec = tid; vec < kBNInt4; vec += kThreads) { const int local_col = vec / kBNInt4PerRow; const int v = vec - local_col * kBNInt4PerRow; - const int4 val = *reinterpret_cast( - weight + static_cast(n0 + local_col) * k + k0 + - v * static_cast(sizeof(int4))); - if constexpr (kSkew8) { - int8_t* dst = - b_tile + local_col * kBNStride + - v * static_cast(sizeof(int4)); - reinterpret_cast(dst)[0] = - reinterpret_cast(&val)[0]; - reinterpret_cast(dst)[1] = - reinterpret_cast(&val)[1]; - } else { - reinterpret_cast(b_tile + local_col * kBNStride)[v] = val; - } + reinterpret_cast(b_tile + local_col * kBNStride)[v] = + *reinterpret_cast( + weight + static_cast(n0 + local_col) * k + k0 + + v * kInt4); } } else { - // Stage B[kStageK, kBlockN] into LDS from the identity [K, N] weight. + // Stage B[kStageK, kBlockN] from the identity [K, N] weight. for (int vec = tid; vec < kBInt4; vec += kThreads) { const int kk = vec / kBInt4PerRow; const int v = vec - kk * kBInt4PerRow; reinterpret_cast(b_tile + kk * kBStride)[v] = *reinterpret_cast( - weight + static_cast(k0 + kk) * n + n0 + - v * static_cast(sizeof(int4))); + weight + static_cast(k0 + kk) * n + n0 + v * kInt4); } } } -// Round 16 (lead-2 loop-carried payload, exact-shape arm only): the register -// half of the lead-2 software pipeline. Each thread prefetches exactly one A -// int4 and one B int4 of a later K stage into thread-local registers and -// returns WITHOUT waiting (the vmcnt wait is emitted by the compiler at the -// first use, one iteration later). The <64,64,64> arm stages kStageK * 64 -// bytes per operand per stage, i.e. kAInt4 == kBNInt4 == kThreads == 256: -// one int4 per thread per operand, so the whole payload costs 8 VGPRs. The -// (row, v) mapping is the same as stage_prefill_tiles, so -// publish_stage_payload writes the exact same LDS addresses the staging -// loop would (bit-identical staging bytes, just loaded one stage earlier). -template -__device__ __forceinline__ void prefetch_stage_payload( - const int8_t* __restrict__ x_q, - const int8_t* __restrict__ weight, - int tid, - int m0, - int n0, - int m, - int k, - int k0, - int4& a_payload, - int4& b_payload) { - constexpr int kThreads = kBlockM * kBlockN / 1024 * kWaveSize; - constexpr int kAInt4PerRow = kStageK / static_cast(sizeof(int4)); - constexpr int kAInt4 = kBlockM * kAInt4PerRow; - constexpr int kBNInt4PerRow = kStageK / static_cast(sizeof(int4)); - constexpr int kBNInt4 = kBlockN * kBNInt4PerRow; - static_assert(kAInt4 == kThreads && kBNInt4 == kThreads, - "payload arm requires exactly one int4 per thread per " - "operand (exact-shape <64,64,64> arm)"); - static_assert(kBNMajor, "payload arm stages the packed n-major B layout"); - // Same vec -> (local_row, v) mapping as stage_prefill_tiles, so the - // publish half stores to the same LDS addresses. - const int a_local_row = tid / kAInt4PerRow; - const int a_v = tid - a_local_row * kAInt4PerRow; - const int global_row = m0 + a_local_row; - // Tail M rows are zero-filled exactly as in stage_prefill_tiles. - a_payload = global_row < m - ? *reinterpret_cast( - x_q + static_cast(global_row) * k + k0 + - a_v * static_cast(sizeof(int4))) - : int4{0, 0, 0, 0}; - const int b_local_col = tid / kBNInt4PerRow; - const int b_v = tid - b_local_col * kBNInt4PerRow; - b_payload = *reinterpret_cast( - weight + static_cast(n0 + b_local_col) * k + k0 + - b_v * static_cast(sizeof(int4))); -} - -// Round 16: the LDS half of the lead-2 payload pipeline. Writes the payload -// carried in registers (loaded by prefetch_stage_payload one stage earlier) -// into the caller-offset LDS tile with the exact same 8-byte-half -// ds_write2_b64 stores (kSkew8 strides 88/72) that stage_prefill_tiles -// emits, so each thread republishes exactly the bytes it loaded. -template -__device__ __forceinline__ void publish_stage_payload( - int4 a_payload, - int4 b_payload, - int8_t* a_tile, - int8_t* b_tile, - int tid) { - constexpr int kThreads = kBlockM * kBlockN / 1024 * kWaveSize; - constexpr int kAStride = kSkew8 ? kStageK + 24 : kStageK + sizeof(int4); - constexpr int kBNStride = kSkew8 ? kStageK + 8 : kStageK + sizeof(int4); - constexpr int kAInt4PerRow = kStageK / static_cast(sizeof(int4)); - constexpr int kAInt4 = kBlockM * kAInt4PerRow; - constexpr int kBNInt4PerRow = kStageK / static_cast(sizeof(int4)); - constexpr int kBNInt4 = kBlockN * kBNInt4PerRow; - static_assert(kAInt4 == kThreads && kBNInt4 == kThreads, - "payload arm requires exactly one int4 per thread per " - "operand (exact-shape <64,64,64> arm)"); - static_assert(kBNMajor, "payload arm stages the packed n-major B layout"); - const int a_local_row = tid / kAInt4PerRow; - const int a_v = tid - a_local_row * kAInt4PerRow; - int8_t* a_dst = - a_tile + a_local_row * kAStride + a_v * static_cast(sizeof(int4)); - if constexpr (kSkew8) { - // 8-mod-16 row starts: write the 16 bytes as two 8-byte halves - // (ds_write2_b64 staging, identical to stage_prefill_tiles). - reinterpret_cast(a_dst)[0] = - reinterpret_cast(&a_payload)[0]; - reinterpret_cast(a_dst)[1] = - reinterpret_cast(&a_payload)[1]; - } else { - reinterpret_cast(a_dst)[0] = a_payload; - } - const int b_local_col = tid / kBNInt4PerRow; - const int b_v = tid - b_local_col * kBNInt4PerRow; - int8_t* b_dst = - b_tile + b_local_col * kBNStride + b_v * static_cast(sizeof(int4)); - if constexpr (kSkew8) { - reinterpret_cast(b_dst)[0] = - reinterpret_cast(&b_payload)[0]; - reinterpret_cast(b_dst)[1] = - reinterpret_cast(&b_payload)[1]; - } else { - reinterpret_cast(b_dst)[0] = b_payload; - } -} - -// One wave's 32x32-quadrant MMAC burst for the K tile staged at a_tile/b_tile -// (caller-offset to the target buffer). k0-outer / kk-inner int32 accumulation -// order; stage-size changes only regroup exact integer sums (max |dot| = -// 6144*127*127 << 2^31), so results stay bit-identical. +// One wave's 32x32-quadrant MMAC burst for the K tile staged at a_tile/b_tile. +// k0-outer / kk-inner int32 accumulation order; stage-size changes only +// regroup exact integer sums, so results stay bit-identical to the scalar +// fallback and to the int64 reference. +// +// kBNMajor == true consumes B as matrix_b col_major fragments (8 consecutive +// k bytes per lane, row stride kStageK + 16 B) from the packed [N, K] tile; the +// element-to-slot mapping and the v_mmac operand bit patterns are the same as +// the identity row_major path, so the int32 accumulation stays bit-identical. +// +// kDirectFrag == true (iteration 14, packed arm only) replaces the four +// library du_load_matrix_sync fragment loads with the explicit 8-byte +// load_frag8 int64 loader, which halves the LDS read phase count and removes +// the per-byte mask-OR reassembly VALU; the loaded bytes and fragment slots +// are identical. template + bool kDirectFrag = false> __device__ __forceinline__ void mmac_prefill_stage( DUFragment& a_frag0, @@ -518,56 +319,26 @@ __device__ __forceinline__ void mmac_prefill_stage( int8_t* b_tile, int local_row, int local_col) { - // kSkew8 (round 11, exact-shape arm only): A 88 / B 72 row strides; the - // load_frag8 ldm arguments below pick them up unchanged (same 8-byte - // aligned fragment reads, conflict-free 16-lane bank phases). - constexpr int kAStride = kSkew8 ? kStageK + 24 : kStageK + sizeof(int4); - constexpr int kBStride = kBlockN + sizeof(int4); // identity [K,N] rows - constexpr int kBNStride = kSkew8 ? kStageK + 8 : kStageK + sizeof(int4); + constexpr int kInt4 = static_cast(sizeof(int4)); + constexpr int kAStride = kStageK + kInt4; + constexpr int kBStride = kBlockN + kInt4; // identity [K, N] rows + constexpr int kBNStride = kStageK + kInt4; // packed [N, K] rows if constexpr (kBNMajor) { - if constexpr (kDirectFrag) { - // Round 10 exact-shape path: explicit load_frag8 8-byte ds_read_b64 - // fragment reads replacing the library loaders (see load_frag8 - // comment). Both steps of the 64-K stage are unrolled into - // independent fragment register sets so all eight reads of a stage - // issue before the MMACs; the kk = 0 MMAC group runs first, then the - // kk = kTileK group - the exact k0-outer/kk-inner int32 order and the - // element-to-slot fragment mapping of the library path are preserved - // (f.x[0..7] = the same 8 bytes in the same little-endian order), so - // results stay bit-identical. - static_assert(kStageK == 2 * kTileK, - "kDirectFrag two-step unroll assumes kStageK == 2*kTileK"); - DUFragment - a_frag0_1, a_frag1_1; - DUFragment - b_frag0, b_frag1, b_frag0_1, b_frag1_1; - load_frag8(a_frag0, a_tile + local_row * kAStride, kAStride); - load_frag8(a_frag1, a_tile + (local_row + kTileM) * kAStride, kAStride); - load_frag8(b_frag0, b_tile + local_col * kBNStride, kBNStride); - load_frag8(b_frag1, b_tile + (local_col + kTileN) * kBNStride, - kBNStride); - load_frag8(a_frag0_1, a_tile + local_row * kAStride + kTileK, - kAStride); - load_frag8(a_frag1_1, - a_tile + (local_row + kTileM) * kAStride + kTileK, kAStride); - load_frag8(b_frag0_1, b_tile + local_col * kBNStride + kTileK, - kBNStride); - load_frag8(b_frag1_1, - b_tile + (local_col + kTileN) * kBNStride + kTileK, - kBNStride); - du_mma_sync(acc00, a_frag0, b_frag0, acc00); - du_mma_sync(acc01, a_frag0, b_frag1, acc01); - du_mma_sync(acc10, a_frag1, b_frag0, acc10); - du_mma_sync(acc11, a_frag1, b_frag1, acc11); - du_mma_sync(acc00, a_frag0_1, b_frag0_1, acc00); - du_mma_sync(acc01, a_frag0_1, b_frag1_1, acc01); - du_mma_sync(acc10, a_frag1_1, b_frag0_1, acc10); - du_mma_sync(acc11, a_frag1_1, b_frag1_1, acc11); - } else { - DUFragment - b_frag0, b_frag1; + DUFragment + b_frag0, b_frag1; #pragma unroll - for (int kk = 0; kk < kStageK; kk += kTileK) { + for (int kk = 0; kk < kStageK; kk += kTileK) { + if constexpr (kDirectFrag) { + // Iteration 14: one int64 ds_read_b64 per fragment (same 8 bytes per + // lane into the same x[0..7] slots) instead of the library + // ds_read2_b32 + mask/OR reassembly form. + load_frag8(a_frag0, a_tile + local_row * kAStride + kk, kAStride); + load_frag8(a_frag1, + a_tile + (local_row + kTileM) * kAStride + kk, kAStride); + load_frag8(b_frag0, b_tile + local_col * kBNStride + kk, kBNStride); + load_frag8(b_frag1, + b_tile + (local_col + kTileN) * kBNStride + kk, kBNStride); + } else { du_load_matrix_sync( a_frag0, a_tile + local_row * kAStride + kk, kAStride); du_load_matrix_sync( @@ -577,11 +348,11 @@ __device__ __forceinline__ void mmac_prefill_stage( du_load_matrix_sync( b_frag1, b_tile + (local_col + kTileN) * kBNStride + kk, kBNStride); - du_mma_sync(acc00, a_frag0, b_frag0, acc00); - du_mma_sync(acc01, a_frag0, b_frag1, acc01); - du_mma_sync(acc10, a_frag1, b_frag0, acc10); - du_mma_sync(acc11, a_frag1, b_frag1, acc11); } + du_mma_sync(acc00, a_frag0, b_frag0, acc00); + du_mma_sync(acc01, a_frag0, b_frag1, acc01); + du_mma_sync(acc10, a_frag1, b_frag0, acc10); + du_mma_sync(acc11, a_frag1, b_frag1, acc11); } } else { DUFragment @@ -592,8 +363,7 @@ __device__ __forceinline__ void mmac_prefill_stage( a_frag0, a_tile + local_row * kAStride + kk, kAStride); du_load_matrix_sync( a_frag1, a_tile + (local_row + kTileM) * kAStride + kk, kAStride); - du_load_matrix_sync( - b_frag0, b_tile + kk * kBStride + local_col, kBStride); + du_load_matrix_sync(b_frag0, b_tile + kk * kBStride + local_col, kBStride); du_load_matrix_sync( b_frag1, b_tile + kk * kBStride + local_col + kTileN, kBStride); du_mma_sync(acc00, a_frag0, b_frag0, acc00); @@ -604,40 +374,23 @@ __device__ __forceinline__ void mmac_prefill_stage( } } -// Large-prefill path: one block computes a kBlockM x kBlockN output tile. -// (kBlockM/32) x (kBlockN/32) wavefronts each own a 32x32 quadrant (four -// m16n16k32 DUMMA accumulators), while the block cooperatively stages -// A[kBlockM, kStageK] and B in bank-skewed LDS. +// Single-buffered large-prefill path: one block computes a kBlockM x kBlockN +// output tile. (kBlockM/32) x (kBlockN/32) wavefronts each own a 32x32 +// quadrant (four m16n16k32 DUMMA accumulators) while the block cooperatively +// stages A[kBlockM, kStageK] and B in bank-skewed LDS. // -// kDoubleBuffer == false: single-buffered K loop (two barriers per stage); -// stage s+1's global loads cannot start until the stage-s barrier, so each -// stage serializes one global-load round trip in front of the compute -// (bootstrap structure; co-resident block streams hide it at the CU level). +// kBNMajor selects the B operand layout: false is the identity [K, N] staging +// with row_major fragments (any (k, n)); true is the packed [N, K] staging +// with col_major fragments (exact gate_up (k, n) only). // -// kDoubleBuffer == true: software-pipelined K loop (round 3). Stage s+1's -// global loads are issued into the idle LDS buffer before the stage-s MMAC -// burst, overlapping the serialized global-load latency with compute. One -// barrier per stage (plus one prologue): it both retires stage-s fragment -// reads (WAR on the buffer just read) and makes stage s+1's ds_writes -// visible (RAW on the prefetched buffer). The gate_up shape uses -// <64, 64, 64, true, true>: two 64-K tiles = 20,480 B/block x 3 blocks/CU = -// 61,440 <= 64 KiB (occupancy preserved; a 128-K double buffer would need -// 38,912 B/block -> 1 block/CU, a harmful occupancy loss). +// Grid covers N in whole tiles (dispatch guarantees N % kBlockN == 0) and M +// in ceil-divided tiles (row tail handled by the zero-fill + masked store). // -// kBNMajor selects the B operand layout: -// * false (identity): B staged [K, N] row-major, library row_major B -// fragments (byte-scattered LDS reads; correct for the raw [K, N] weight -// and every non-gate_up shape). -// * true (gate_up only): B staged [N, K] n-major with k contiguous (row -// stride 144 = 128+16, or 80 = 64+16 for the 64-K double buffer), -// library col_major B fragments -> each lane's elements are k-contiguous -// bytes instead of byte-scattered columns. This is the bank-safe consumer -// layout for the B side (the 17.3M PMC bank-conflict source in round 1). -// The one-time pack in launch_pack_w8a8_weight provides the [N, K] buffer -// for (k, n) == (6144, 512) only. +// kDirectFrag (iteration 14) selects the explicit 8-byte load_frag8 fragment +// loader for the packed n-major arm; the identity arm and every generic +// geometry keep the library du_load_matrix_sync loaders. template + bool kDirectFrag = false> __global__ __launch_bounds__(kBlockM * kBlockN / 1024 * kWaveSize) void w8a8_dumma_prefill_tiled_kernel( const int8_t* __restrict__ x_q, @@ -649,56 +402,45 @@ w8a8_dumma_prefill_tiled_kernel( int n, int k) { constexpr int kWavesN = kBlockN / 32; - constexpr int kThreads = kBlockM * kBlockN / 1024 * kWaveSize; - // 16-byte bank skew: keeps int4 staging stores aligned while spreading the - // fragment rows across distinct LDS banks. The staging loops live in - // stage_prefill_tiles below; the strides remain here as the static-asserted - // alignment contract of every instantiation. kSkew8 (round 11, exact-shape - // arm only) switches A/B to the down_proj-validated 8-mod-16 strides - // 88/72; LDS bytes stay A[64,88] + B[64,72] x 2 = 20,480 B/block so the - // 3-blocks/CU occupancy of the accepted kernel is preserved exactly. - constexpr int kAStride = kSkew8 ? kStageK + 24 : kStageK + sizeof(int4); - constexpr int kBStride = kBlockN + sizeof(int4); // identity [K,N] rows - constexpr int kBNStride = kSkew8 ? kStageK + 8 : kStageK + sizeof(int4); - constexpr int kBNInt4 = kBlockN * (kStageK / sizeof(int4)); - // Double buffering uses kBuffers = 2 LDS tile copies; the gate_up shape - // pairs it with kStageK = 64 so 3 blocks/CU is preserved (see kernel - // comment above). - constexpr int kBuffers = kDoubleBuffer ? 2 : 1; + constexpr int kInt4 = static_cast(sizeof(int4)); + constexpr int kAStride = kStageK + kInt4; + constexpr int kBStride = kBlockN + kInt4; // identity [K, N] rows + constexpr int kBNStride = kStageK + kInt4; // packed [N, K] rows constexpr int kABytes = kBlockM * kAStride; - // One shared B tile sized for the larger of the two layouts so an inactive - // arm never allocates dead LDS (keeps <64,64,128> at 19,456 B total and - // 3 blocks/CU). kSkew8 implies kBNMajor, so only the n-major arm is live. - constexpr int kBMaxBytes = - kSkew8 ? kBlockN * kBNStride - : (kStageK * kBStride > kBlockN * kBNStride - ? kStageK * kBStride - : kBlockN * kBNStride); - static_assert(kSkew8 ? (kAStride % 8 == 0) : (kAStride % sizeof(int4) == 0), - "A LDS row stride must stay int4 aligned (or 8-byte aligned " - "under kSkew8)"); - static_assert(kBStride % static_cast(sizeof(int4)) == 0, - "B LDS row stride must stay int4 aligned"); - static_assert( - kSkew8 ? (kBNStride % 8 == 0) : (kBNStride % sizeof(int4) == 0), - "n-major B LDS row stride must stay int4 aligned (or 8-byte aligned " - "under kSkew8)"); - static_assert(!kSkew8 || kBNMajor, - "kSkew8 is only defined for the packed n-major B layout"); - static_assert(!kSkew8 || kStageK == 64, - "kSkew8 strides 88/72 assume kStageK == 64"); + constexpr int kBBytes = kStageK * kBStride; + constexpr int kBNBytes = kBlockN * kBNStride; + // The B tile is sized exactly for the layout the instantiation uses, so an + // inactive arm never allocates dead LDS: identity (kStageK=128) 128 x 80 = + // 10,240 B, packed (kStageK=64) 64 x 80 = 5,120 B. With A[64, 80] = + // 5,120 B the packed arm is 10,240 B/block, so the packed instantiation now + // reaches the 4-blocks/CU VGPR ceiling (4 x 256 x 64 = 65,536 VGPR; + // 4 x 10,240 = 40,960 <= 64 KiB LDS) instead of the 3 blocks/CU the 18,432 B + // packed arm allowed. The identity arm keeps its 19,456 B / 3 blocks/CU. + constexpr int kBAlloc = kBNMajor ? kBNBytes : kBBytes; + // Consolidation guard: the packed arm must keep the 4-blocks/CU LDS budget + // that its 64-K stage buys; this fires the build if a future stage/tile edit + // would silently drop the packed arm back to 3 blocks/CU. + static_assert(!kBNMajor || 4 * (kABytes + kBAlloc) <= 64 * 1024, + "packed arm must keep 4 blocks/CU on the 64 KiB LDS part"); static_assert(kStageK % kTileK == 0, "K stage must be a multiple of the DUMMA K unit"); - static_assert(kStageK % static_cast(sizeof(int4)) == 0, + static_assert(kStageK % kInt4 == 0, "K stage must keep int4 staging aligned"); - static_assert(kBNInt4 % kThreads == 0, - "n-major B staging must divide evenly across threads"); - static_assert(!kPayload || (kBNMajor && kDoubleBuffer && kDirectFrag && - kSkew8 && kStageK == 64 && kBlockM == 64 && - kBlockN == 64), - "payload arm is specialized to the exact-shape <64,64,64> " - "double-buffered load_frag8/kSkew8 arm (one int4 per thread " - "per operand)"); + static_assert(kAStride % kInt4 == 0, + "A LDS row stride must stay int4 aligned"); + static_assert(kBStride % kInt4 == 0, + "B LDS row stride must stay int4 aligned"); + static_assert(kBNStride % kInt4 == 0, + "packed B LDS row stride must stay int4 aligned"); + static_assert(kBlockM % 32 == 0 && kBlockN % 32 == 0, + "each wavefront owns a 32x32 DUMMA quadrant"); + static_assert(!kDirectFrag || kBNMajor, + "direct 8-byte fragment loader is defined for the packed " + "n-major arm only"); + static_assert(!kBNMajor || (kBlockN * (kStageK / kInt4)) % + (kBlockM * kBlockN / 1024 * kWaveSize) == + 0, + "packed n-major B staging must divide evenly across threads"); const int tid = static_cast(threadIdx.x); const int wave = tid / kWaveSize; @@ -710,8 +452,8 @@ w8a8_dumma_prefill_tiled_kernel( const int local_row = wave_row * 32; const int local_col = wave_col * 32; - __shared__ __align__(16) int8_t a_tile[kBuffers * kABytes]; - __shared__ __align__(16) int8_t b_tile[kBuffers * kBMaxBytes]; + __shared__ __align__(16) int8_t a_tile[kABytes]; + __shared__ __align__(16) int8_t b_tile[kBAlloc]; DUFragment a_frag0, a_frag1; @@ -722,111 +464,20 @@ w8a8_dumma_prefill_tiled_kernel( du_fill_fragment(acc10, 0); du_fill_fragment(acc11, 0); - if constexpr (kDoubleBuffer) { - if constexpr (kPayload) { - // Round 16 lead-2 loop-carried payload pipeline (exact-shape arm - // only): stage s+1's global loads are issued a FULL iteration early - // (top of iteration s-1) into a two-int4-per-thread register payload - // and stay outstanding across the stage-(s-1) fragment reads + MMAC - // burst and the stage barrier; the vmcnt wait lands at the top of - // iteration s (first use of the payload registers), immediately - // before the ds_write2 publish into the idle LDS buffer. The - // s_barrier fences outstanding vmem, so the wait can no longer sit - // right after the load issue: the global-load latency overlaps the - // whole compute segment instead of serializing in front of every - // stage (the round-11 ISA defect). Same 2 buffers, same one barrier - // per stage (prologue + 96 = 97/block), same 16 ds_read2_b64 + - // 8 ds_write2_b64 per block-stage, same staging bytes/addresses - // (bit-identical int32 order). - int4 a_payload, b_payload; - // Prologue: issue the stage-1 global loads first (so they ride behind - // the stage-0 inline staging), stage k0 = 0 into buffer 0 inline - // (same load+wait+write path as the accepted kernel), then the first - // barrier. - prefetch_stage_payload( - x_q, weight, tid, m0, n0, m, k, kStageK, a_payload, b_payload); - stage_prefill_tiles( - x_q, weight, a_tile, b_tile, tid, m0, n0, m, n, k, 0); - __syncthreads(); - for (int k0 = 0; k0 < k; k0 += kStageK) { - const int buf = (k0 / kStageK) & 1; - if (k0 + kStageK < k) { - // Publish the payload carried from the previous iteration (stage - // k0+kStageK) into the idle buffer: the vmcnt wait for the loads - // issued one iteration earlier is emitted here at the first use, - // after the previous MMAC burst + barrier. - publish_stage_payload( - a_payload, b_payload, a_tile + (buf ^ 1) * kABytes, - b_tile + (buf ^ 1) * kBMaxBytes, tid); - } - if (k0 + 2 * kStageK < k) { - // Issue stage k0+2*kStageK's global loads into the payload - // registers (no wait); they stay in flight across the MMAC burst - // and the barrier below and are waited at the next iteration's - // publish (RAW). - prefetch_stage_payload( - x_q, weight, tid, m0, n0, m, k, k0 + 2 * kStageK, a_payload, - b_payload); - } - // Each wave computes its 32x32 quadrant: four m16n16k32 DUMMA tiles. - // k0-outer / kk-inner int32 accumulation order. - mmac_prefill_stage( - a_frag0, a_frag1, acc00, acc01, acc10, acc11, - a_tile + buf * kABytes, b_tile + buf * kBMaxBytes, - local_row, local_col); - // One barrier per stage: (a) WAR - all stage-s fragment reads of the - // buffer just read are retired before it becomes the publish target - // two stages later; (b) RAW - every thread's stage-(s+1) ds_writes - // are visible before the next iteration reads them. - __syncthreads(); - } - } else { - // Prologue: stage k0 = 0 into buffer 0, then the pipelined loop. - stage_prefill_tiles( - x_q, weight, a_tile, b_tile, tid, m0, n0, m, n, k, 0); - __syncthreads(); - for (int k0 = 0; k0 < k; k0 += kStageK) { - const int buf = (k0 / kStageK) & 1; - if (k0 + kStageK < k) { - // Prefetch stage s+1 into the idle buffer: global loads issue here, - // before the stage-s MMAC burst, and complete (RAW) behind the - // single barrier below. - stage_prefill_tiles( - x_q, weight, a_tile + (buf ^ 1) * kABytes, - b_tile + (buf ^ 1) * kBMaxBytes, tid, m0, n0, m, n, k, - k0 + kStageK); - } - // Each wave computes its 32x32 quadrant: four m16n16k32 DUMMA tiles. - // k0-outer / kk-inner int32 accumulation order. - mmac_prefill_stage( - a_frag0, a_frag1, acc00, acc01, acc10, acc11, - a_tile + buf * kABytes, b_tile + buf * kBMaxBytes, - local_row, local_col); - // One barrier per stage: (a) WAR - all stage-s fragment reads of the - // buffer just read are retired before it becomes the prefetch target - // two stages later; (b) RAW - every thread's stage-(s+1) ds_writes are - // visible before the next iteration reads them. - __syncthreads(); - } - } - } else { - for (int k0 = 0; k0 < k; k0 += kStageK) { - // Stage A[kBlockM, kStageK] and B into the single LDS buffer. - stage_prefill_tiles( - x_q, weight, a_tile, b_tile, tid, m0, n0, m, n, k, k0); - __syncthreads(); - - // Each wave computes its 32x32 quadrant: four m16n16k32 DUMMA tiles. - // k0-outer / kk-inner int32 accumulation order. - mmac_prefill_stage( - a_frag0, a_frag1, acc00, acc01, acc10, acc11, - a_tile, b_tile, local_row, local_col); - // Retire this stage's reads before the next stage's staging writes - // overwrite the single LDS buffer (WAR hazard across waves). - __syncthreads(); - } + for (int k0 = 0; k0 < k; k0 += kStageK) { + // Stage A[kBlockM, kStageK] and B into the single LDS buffer, then + // publish them to every wavefront. + stage_prefill_tiles( + x_q, weight, a_tile, b_tile, tid, m0, n0, m, n, k, k0); + __syncthreads(); + // Each wave computes its 32x32 quadrant: four m16n16k32 DUMMA tiles. + // k0-outer / kk-inner int32 accumulation order. + mmac_prefill_stage( + a_frag0, a_frag1, acc00, acc01, acc10, acc11, + a_tile, b_tile, local_row, local_col); + // Retire this stage's reads before the next stage's staging writes + // overwrite the single LDS buffer (WAR hazard across waves). + __syncthreads(); } const int base_row = m0 + wave_row * 32; @@ -847,10 +498,10 @@ w8a8_dumma_prefill_tiled_kernel( // Generic scalar fallback: one output element per grid-stride iteration with // exact int32 accumulation. Correct for any (m, n, k), including the small-M -// API shapes (M=2/16) and any unmatched geometry. The weight is the identity -// [K, N] row-major layout for every (k, n) EXCEPT the gate_up weight -// (k, n) == (6144, 512), whose buffer is packed [N, K] n-major; this kernel -// decodes that layout so the paired gate_up tails stay correct. +// API shapes (M=2/16). The weight is the identity [K, N] row-major layout for +// every (k, n) EXCEPT the assigned gate_up pair (k, n) == (6144, 512), whose +// buffer is packed [N, K] n-major; this kernel decodes that layout so the +// paired gate_up M=2/M=16 tails stay correct. __global__ __launch_bounds__(kScalarThreads) void w8a8_gemm_scalar_kernel( const int8_t* __restrict__ x_q, @@ -861,7 +512,7 @@ w8a8_gemm_scalar_kernel( int m, int n, int k) { - const bool b_nmajor = (k == 6144 && n == 512); + const bool b_nmajor = (k == kGateUpK && n == kGateUpN); const int64_t total = static_cast(m) * n; const int64_t stride = static_cast(gridDim.x) * blockDim.x; for (int64_t idx = static_cast(blockIdx.x) * blockDim.x + @@ -878,14 +529,15 @@ w8a8_gemm_scalar_kernel( acc += static_cast(a_row[kk]) * static_cast(b_base[kk * b_stride]); } - const float scaled = static_cast(acc) * x_scale[row] * - weight_scale[col]; + const float scaled = + static_cast(acc) * x_scale[row] * weight_scale[col]; out[idx] = __float2bfloat16(scaled); } } -// Bootstrap pack: identity device-to-device copy for every (k, n) pair -// (raw [K, N] layout preserved). Runs outside the timed/CUDA-Graph region. +// Bootstrap pack: identity device-to-device copy of the raw [K, N] weight and +// the [N] fp32 scale for every (k, n) pair that is not the assigned gate_up +// pair. Runs outside the timed/CUDA-Graph region. __global__ __launch_bounds__(kScalarThreads) void w8a8_pack_identity_kernel( const int8_t* __restrict__ raw_weight, @@ -909,9 +561,8 @@ w8a8_pack_identity_kernel( // index relation: packed[cc * k + kk] == raw[kk * n + cc]. Each thread // gathers the 16 bytes of one aligned int4 k-block from 16 raw rows (stride // n) and stores one aligned int4, keeping the packed write side fully -// coalesced (same gather + aligned int4-store pattern as the validated -// o_proj transpose repair). Runs outside the timed/CUDA-Graph region; same -// byte count, same graph-stable buffer. +// coalesced. Runs outside the timed/CUDA-Graph region; same byte count, same +// graph-stable buffer. __global__ __launch_bounds__(kScalarThreads) void w8a8_pack_gateup_transpose_kernel( const int8_t* __restrict__ raw_weight, @@ -924,7 +575,7 @@ w8a8_pack_gateup_transpose_kernel( const int64_t total4 = static_cast(n) * k4; const int64_t stride4 = static_cast(gridDim.x) * blockDim.x; for (int64_t idx4 = static_cast(blockIdx.x) * blockDim.x + - threadIdx.x; + threadIdx.x; idx4 < total4; idx4 += stride4) { const int cc = static_cast(idx4 / k4); const int kblk = static_cast(idx4 - static_cast(cc) * k4); @@ -936,8 +587,8 @@ w8a8_pack_gateup_transpose_kernel( for (int i = 0; i < static_cast(sizeof(int4)); ++i) { vp[i] = src[static_cast(i) * n]; } - reinterpret_cast(packed_weight + static_cast(cc) * k + - kk)[0] = v; + reinterpret_cast( + packed_weight + static_cast(cc) * k + kk)[0] = v; } const int64_t stride = static_cast(gridDim.x) * blockDim.x; for (int64_t idx = static_cast(blockIdx.x) * blockDim.x + @@ -962,80 +613,43 @@ extern "C" void launch_w8a8_gemm( int k, hipStream_t stream) { // No split-K: the provided workspace is not touched. The timed operator - // performs no allocation, synchronization, packing, or default-stream - // launch; only the caller-provided out is written. + // performs no allocation, synchronization, packing, autotuning, or + // default-stream launch; only the caller-provided out is written. (void)workspace; (void)workspace_bytes; auto* out_bf16 = static_cast(out); - // Native INT8 DUMMA m16n16k32 tiled path for every large-M shape. Both - // assigned shapes qualify: gate_up (4096, 512, 6144) and down_proj - // (4096, 6144, 256) both have M >= 128, N % 128 == 0 and K % 128 == 0. - // Explicit dispatch by launch geometry; unmatched shapes fall through to - // the scalar path (no shape is specialized away). + // Native INT8 DUMMA m16n16k32 tiled path for every shape whose geometry + // matches the staged 64x64 tiles. The assigned shape (4096, 512, 6144) + // takes the packed-weight (n-major B) arm with the round-11 64-K stage + // (kPackedStageK, 10,240 B LDS -> 4 blocks/CU) and the iteration-14 direct + // 8-byte load_frag8 fragment loader (kDirectFrag); any other matching + // geometry keeps the identity [K, N] row_major B arm on the 128-K stage + // with the library loaders. Unmatched geometry and M < 128 fall through to + // the scalar fallback (no shape is specialized away). if (m >= kPrefillMinM && (n % kDefaultBlockN == 0) && (k % kPrefillStageK == 0)) { - // Exact-shape guard for the assigned gate_up shape (round-3 pipeline - // decision, round-10 load_frag8, round-11 kSkew8, round-16 kPayload): - // cooperative A+B staging with the n-major [N, K] packed B operand - // (kBNMajor) consumed as col_major fragments, software-pipelined across - // K tiles (kDoubleBuffer) with a 64-K double buffer: 64x64 tile, 512 - // blocks, 20,480 B LDS/block, 3 blocks/CU (61,440 B) - occupancy - // preserved vs the accepted round-2 single buffer (19,456 B x 3 = - // 58,368 B). One barrier per 64-K stage + prologue (97 for 96 stages). - // Round 11 stages A/B at the 8-mod-16 strides 88/72 (kSkew8) so the - // fragment reads are bank-conflict-free (LDS bytes and occupancy - // unchanged). Round 16 (kPayload) issues the stage-(s+1) global loads a - // full iteration early into a two-int4-per-thread register payload and - // publishes them into the idle LDS buffer at the top of iteration s, so - // the vmcnt wait lands after the stage-s MMAC burst instead of in front - // of it (LDS bytes, barriers, traffic, occupancy and results unchanged). - if (m == 4096 && n == 512 && k == 6144) { - const dim3 grid(n / 64, (m + 63) / 64); - const dim3 block(4 * kWaveSize); + const dim3 grid(n / kDefaultBlockN, + (m + kDefaultBlockM - 1) / kDefaultBlockM); + const dim3 block(kDefaultBlockM * kDefaultBlockN / 1024 * kWaveSize); + if (k == kGateUpK && n == kGateUpN) { hipLaunchKernelGGL( HIP_KERNEL_NAME(w8a8_dumma_prefill_tiled_kernel< - 64, 64, 64, true, true, true, true, true>), - grid, block, 0, stream, - a, b, x_scale, weight_scale, out_bf16, m, n, k); - return; - } - if (n % 128 == 0) { - // 64x128 tile, 8 waves (32x32 quadrant per wave). - // down_proj: grid (6144/128, 4096/64) = (48, 64) = 3072 blocks. - const dim3 grid(n / 128, (m + 63) / 64); - const dim3 block(8 * kWaveSize); - hipLaunchKernelGGL( - HIP_KERNEL_NAME(w8a8_dumma_prefill_tiled_kernel<64, 128, 128>), - grid, block, 0, stream, - a, b, x_scale, weight_scale, out_bf16, m, n, k); - return; - } - if (m % 128 == 0) { - // 128x64 tile, 8 waves. - const dim3 grid(n / 64, m / 128); - const dim3 block(8 * kWaveSize); - hipLaunchKernelGGL( - HIP_KERNEL_NAME(w8a8_dumma_prefill_tiled_kernel<128, 64, 128>), + kDefaultBlockM, kDefaultBlockN, kPackedStageK, + true, true>), grid, block, 0, stream, a, b, x_scale, weight_scale, out_bf16, m, n, k); - return; - } - { - // 64x64 tile, 4 waves (generic default). - const dim3 grid(n / 64, (m + 63) / 64); - const dim3 block(4 * kWaveSize); + } else { hipLaunchKernelGGL( - HIP_KERNEL_NAME(w8a8_dumma_prefill_tiled_kernel<64, 64, 128>), + HIP_KERNEL_NAME(w8a8_dumma_prefill_tiled_kernel< + kDefaultBlockM, kDefaultBlockN, kPrefillStageK, + false>), grid, block, 0, stream, a, b, x_scale, weight_scale, out_bf16, m, n, k); - return; } + return; } - // Generic scalar fallback for every unmatched (m, n, k) and M < 128 - // (including the paired M=2/M=16 API shapes with the same (N, K), which - // decode the packed [N, K] gate_up layout when (k, n) == (6144, 512)). const int64_t total = static_cast(m) * n; const unsigned blocks = static_cast( (total + kScalarThreads - 1) / kScalarThreads); @@ -1053,13 +667,14 @@ extern "C" void launch_pack_w8a8_weight( int k, int n, hipStream_t stream) { - // Assigned gate_up weight (K=6144, N=512): one-time [K,N] -> [N,K] + // Assigned gate_up weight (K=6144, N=512): one-time [K, N] -> [N, K] // transpose pack (n-major, k contiguous) so the timed GEMM reads B // fragments as k-contiguous bytes from the staged LDS tile. Runs outside // the timed/CUDA-Graph region; same byte count, same graph-stable buffer, - // byte-exact vs the raw logical [K,N] weight. - if (k == 6144 && n == 512) { - const int64_t total4 = static_cast(n) * (k / 16); + // byte-exact vs the raw logical [K, N] weight. + if (k == kGateUpK && n == kGateUpN) { + const int64_t total4 = + static_cast(n) * (k / static_cast(sizeof(int4))); const unsigned blocks = static_cast( (total4 + kScalarThreads - 1) / kScalarThreads); hipLaunchKernelGGL( @@ -1069,7 +684,8 @@ extern "C" void launch_pack_w8a8_weight( return; } // Identity device-to-device copy for every other (k, n) pair, including - // unmatched (K, N). Runs outside the timed/CUDA-Graph region. + // unmatched (K, N) and the paired M=2/M=16 API tails with the assigned + // (K, N). Runs outside the timed/CUDA-Graph region. const int64_t weight_elems = static_cast(k) * n; const int64_t total = weight_elems > n ? weight_elems : n; const unsigned blocks = static_cast( diff --git a/metainfer/tasks/dcu_kernel_auto_opt/variant/int8w8a8-gemm/hy3/TP4/M4096/o_proj.hip b/metainfer/tasks/dcu_kernel_auto_opt/variant/int8w8a8-gemm/hy3/TP4/M4096/o_proj.hip index 94a1fee0..83102b88 100644 --- a/metainfer/tasks/dcu_kernel_auto_opt/variant/int8w8a8-gemm/hy3/TP4/M4096/o_proj.hip +++ b/metainfer/tasks/dcu_kernel_auto_opt/variant/int8w8a8-gemm/hy3/TP4/M4096/o_proj.hip @@ -1,71 +1,212 @@ -// @@variant shape=hy3_tp4_o_proj_m4096 commit=7c36244e756baf1e8268807cb6481eb118a30133 added=2026-08-23 -// median_us=803.9 p90_us=805.1 tops=85.48 bandwidth_gb_s=62.65 speedup=24.73 baseline_us=1.988e+04 -// source=hy3-tp4-m4096-dsh-test1-bfdf3002 +// @@variant shape=hy3_tp4_o_proj_m4096 commit=a030cfe61386a23a42e172497185fff364c58a85 added=2026-09-14 +// median_us=453.2 p90_us=454.2 speedup=43.87 baseline_us=1.988e+04 +// source=test-9-11-1/iteration_002/hy3_tp4_o_proj_m4096 (retested A/B) // INT8 W8A8 GEMM HIP implementation for Hygon K500SM_AI / gfx928. // -// Worker: worker_1 (physical GPU 1), assigned shape: -// hy3_tp4_o_proj_m4096 : M=4096, N=4096, K=2048 +// Worker: worker_0 (physical GPU 0). Assigned shape: +// hy3_tp4_o_proj_m4096 : M=4096, N=4096, K=2048 (logical (K, N) = (2048, 4096)) +// +// Iteration 1 - architecture establishment (one bounded DUMMA geometry round). +// +// * Exact assigned-shape path (m, n, k) == (4096, 4096, 2048): native gfx928 +// INT8 DUMMA m16n16k32 with int32 accumulation and a 128x64 output macro- +// tile per block. +// - Four wavefronts (256 threads); each wave owns a 64x32 quadrant built +// from eight m16n16k32 accumulator fragments (acc00..acc31). +// - Grid is (N/64) x (M/128) = 64 x 32 = 2048 blocks. With /64 KiB LDS +// and this kernel's 30,720 B/block the resource budget allows two +// resident blocks per CU, so the 2048-block grid covers all 120 CUs +// (~8.5 waves of 240 resident blocks). +// - 64-deep K stages, double buffered: the ping-pong LDS pair holds +// A[128,64] and packed-B[64,64] panels (2 x 20 KiB + 2 x 5 KiB +// buffers, 30,720 B total). The loop-carried int4 payload for stage +// k0 + 64 is published into the idle buffer at the top of stage k0 +// (before the MMAC burst) and the global prefetch for stage k0 + 128 +// has a full stage of lead, so global load latency retires under the +// burst and only one __syncthreads per K stage is needed. +// - Both LDS tiles use an 80-byte row stride (five 16-byte bank phases) +// instead of the power-of-two 64-byte stride, which breaks the +// periodicity that made every DUMMA fragment load collide on a handful +// of LDS banks; all cooperative int4 staging stores stay 16-byte +// aligned. +// - The epilogue fuses float(dot) * x_scale[m] * weight_scale[n] and +// stores bf16 directly from the accumulator fragments (no extra LDS +// round trip, no separate epilogue kernel): verified gfx928 int8 +// m16n16k32 ownership is lane&15 -> row, lane>>4 -> col%4, and x[i] -> +// columns col%4 + 4*i. +// - No split-K: the 2048-block MxN grid already exposes ample +// parallelism, so the workspace is never touched and no combine pass +// is paid inside the timed region. +// +// Iteration 2 - B-operand LDS transpose for vectorized fragment loads: +// PMC/ISA audit of the iteration-1 source (11,272,192 LDS instructions, +// 31,457,280 bank conflicts) showed the operand split exactly. The A +// operand's row-major loader emits one 8-byte ds_read2_b32 per m16n16k32 +// fragment (its per-lane bytes are contiguous in K), but the B operand's +// row-major loader walks the 80-byte LDS row stride, so each B fragment +// costs eight scalar ds_read_u8: 32 of the 40 loop LDS reads per 64-K +// stage. This round quantifies operand reuse per 128x64 macro-tile and +// keeps cooperative A+B LDS staging (direct global operand loads would +// re-request ~1 GB of A plus ~0.5 GB of B per replay and lean on L2). The +// focused change is the B staging layout: the tile is now n-major +// ([64 n][64 k] with the same 80-byte bank-phase stride) and is consumed +// with the col_major loader, whose per-lane bytes are eight contiguous K +// bytes. Each B fragment therefore collapses from eight ds_read_u8 to a +// single ds_read2_b32, cutting the loop LDS reads from ~40 to ~12 per +// stage. The global B panel is repacked to [N/64, 64, K] (same byte count) +// so the cooperative int4 stage load stays one coalesced 64-byte segment +// per n row and no transpose is paid inside the timed GEMM. A/B reuse per +// macro-tile is unchanged (each staged A byte feeds 64 output columns, each +// staged B byte 128 output rows), the LDS footprint stays 30,720 B, and two +// blocks/CU remain resident. +// +// Iteration 3 - single-buffered K pipeline (round's mandated single-vs-double +// buffering comparison): +// PMC/ISA of the accepted iteration-2 source (3,932,160 LDS instructions, +// 18,874,368 bank conflicts = 4.80 conflict cycles/LDS, +// lds_wait_instructions 3,680,076 = 93.6% of LDS instructions, 1,089,536 VMEM +// read instructions, 30,720 B LDS, 80 VGPR) shows the kernel is latency +// bound, not LDS-instruction bound: after the iteration-2 vectorization the +// LDS stream is only 12 ds_read2_b32 per 64-K stage, yet almost every one +// still registers a wait, and the only resource that keeps residency at 2 +// blocks/CU is LDS (30,720 * 3 > 65,536) while the 80-VGPR budget already +// allows 3 (61,440 <= 65,536). The round therefore drops the ping-pong pair +// for a single 15,360 B A+B tile and re-publishes the next stage after the +// burst instead of at the top of the stage. This trades one extra barrier per +// K stage and the publish-under-burst overlap for a third resident block +// (12 waves/CU instead of 8). Falsifiable: if the kernel is truly stall-bound, +// the extra block hides enough LDS/VMEM latency to move median below +// 736.46 us; if the exposed LDS store latency and doubled barrier count +// dominate, median regresses and double buffering is retained per the round's +// decision rule. Fragment loads, MMAC schedule, staging mapping, global +// access pattern, packed layout, and the generic fallback are unchanged. +// +// Iteration 4 - square macro-tile (M-tile : N-tile aspect-ratio round): +// PMC/ISA of the accepted iteration-3 source (3,932,160 LDS instructions, +// 18,874,368 bank conflicts = 4.80 conflict cycles/LDS, 8,923,656 +// lds_wait_instructions, 1,089,536 VMEM reads, 15,360 B LDS at 88 arch VGPR, +// 2048 blocks, 594 us of the 655 us median) shows the residual cost is +// per-MAC issue overhead, not LDS capacity: with the 2:1 128x64 macro-tile +// the block still issues 60 LDS wave-instructions (48 fragment reads + 12 +// cooperative stage stores), 12 VMEM loads and 2 barriers for every 524,288 +// MACs, and each 64x32 wave quadrant pays six 8-byte fragment loads for only +// eight m16n16k32 MMACs (0.75 LDS loads/MMAC). This round retunes the +// M-tile : N-tile aspect ratio from 2:1 to 1:1 with the same K depth and the +// same single-buffered pipeline: the macro-tile becomes 128x128 (grid +// 32x32 = 1024 blocks, still 8.5 blocks per each of the 120 CUs) and each of +// the four wavefronts owns a 64x64 quadrant built from sixteen m16n16k32 +// accumulators. A square quadrant is the fragment-shape optimum: eight +// 8-byte fragment loads feed sixteen MMACs (0.5 LDS loads/MMAC, -33%), a +// stage store grows to four int4 per thread so the block pays 4 VMEM loads +// instead of 3 for twice the MACs (-33% VMEM loads/MAC), and the two +// barriers are amortized over 2x the work (-50% barriers/MAC). A 1:1 tile +// also balances the reconvergent operand traffic exactly -- each staged A +// byte feeds 128 output columns and each staged B byte feeds 128 output +// rows, so per-replay L2 reads fall from ~768 MB (A 512 MB + B 256 MB) to +// ~512 MB (A 256 MB + B 256 MB). LDS is 20,480 B (A[128,80] + B[128,80]), +// so two blocks stay resident (40,960 B); VGPR grows by the eight extra int32 +// accumulator fragments (~115 expected, under the 128-VGPR two-block cap). +// Falsifiable: if the residual time is per-MAC instruction/latency overhead +// that a square quadrant and half the barriers can hide, median_us drops +// below the current 654.9467849731445; if the kernel instead needs the 12 +// resident waves of the 128x64 tile (or 16 accumulators push VGPR past 128), +// median rises and the 2:1 tile is retained. K depth, the single-buffer +// publish schedule, the 80-byte five-bank-phase strides, the exact int32 +// accumulation order, the packed layout, the shape guards and the scalar +// fallback are unchanged. +// +// Iteration 5 - packed-B fragment-interleaved swizzle (round's mandated +// packing/swizzle round): +// PMC/ISA of the accepted iteration-4 source (2,621,440 LDS instructions, +// 12,582,912 LDS bank conflicts = 4.80 conflict cycles per LDS instruction, +// 4,869,824 lds_wait_instructions, 827,392 VMEM reads, 20,480 B LDS at 128 +// arch VGPR, 1024 blocks, 500.3 us profiled against the 544.8 us median) +// shows the B operand is already one 8-byte vector LDS read per fragment but +// still not bank-safe: the n-major B tile's 80-byte (20-word) row stride +// makes the 16-row sweep of every col_major B fragment alias the same bank +// pairs, so the conflicts survive the iteration-2 vectorization. This round +// replaces the [N/64, 64, K] panel layout with ONE fragment-interleaved +// swizzle, still a byte-count-preserving permutation of the logical [K, N] +// weight. For each 64-K stage and each 128-column block the packed data is +// sixteen 512-byte fragment blocks ordered [k32][n16], each block stored as +// [k8 0..3][n 0..15][8 B]: +// packed[((k>>6)*(N/128) + (n>>7))*16 + ((k>>5)&1)*8 + ((n>>4)&7)]*512 +// + (((k>>3)&3)*16 + (n&15))*8 + (k&7) +// An explicit col_major 8-byte loader (lane n = lane&15, k8 = lane>>4 reads +// p + k8*128 + n*8) then puts each 16-lane phase on exactly one 128-byte k8 +// block -- all 32 banks exactly once, conflict-free by construction. The +// block's 64-K B stage is one contiguous 8 KiB region, so the cooperative +// stage store becomes two flat 16-byte copies per thread (chunk tid and +// chunk tid+256) instead of two strided n-row vectors, and the global read +// is a fully coalesced 8 KiB stream. The explicit loader's byte placement is +// identical to du_load_matrix_sync, so every v_mmac operand, the +// k0-outer/kk-inner accumulation order and every int32 accumulator are +// bit-identical. A keeps its row-major 80-byte stride and the single- +// buffered 64-K pipeline with its publish schedule is unchanged; LDS/block +// falls from 20,480 B to 18,432 B (10,240 A + 8,192 B), so two blocks still +// stay resident at the 128-VGPR launch bound. Falsifiable: if the residual +// LDS cost is bank-conflict serialization, lds_bank_conflicts fall sharply +// and median_us drops below the current 544.7953414916992; if the conflicts +// were already covered by the two resident blocks and the explicit loader +// only adds address ALU, median rises and the panel layout is retained. +// +// Iteration 6 - fused coalesced epilogue (round's mandated epilogue round): +// PMC/ISA of the accepted iteration-5 source (2,097,152 LDS instructions, +// 6,291,456 LDS bank conflicts, 4,884,093 lds_wait_instructions, 827,392 +// VMEM reads, 262,144 VMEM writes, 18,432 B LDS at 120 arch VGPR, 1024 +// blocks, 435.5 us profiled against the 479.2 us median) shows the residual +// non-GEMM cost is the epilogue store, not the compute loop: the direct +// accumulator store issues four 2-byte scalar global stores per lane per +// fragment, and because the m16n16k32 ownership gives each lane four columns +// strided by 4 (row = lane&15, c4 = lane>>4, x[i] -> c4 + 4*i) a store +// instruction scatters over 16 rows x 8 bytes and touches each 32-byte +// sector at 25% utilization, so the epilogue pays 262,144 wave-level store +// instructions hauling 4x the output sectors it needs. This round fuses the +// per-row/per-column scales and the bf16 conversion into ONE register-only +// transpose store: a 4x4 in-wave transpose (two 2x2 steps, __shfl_xor 16 +// then 32; this DTK lowers it to ds_bpermute at the block tail where the LDS +// pipe is idle) re-routes the four int32 values so lane (r, c4) ends up +// holding the four CONTIGUOUS columns 4*c4..4*c4+3, which are then scaled, +// converted to bf16 and written as ONE 8-byte store per lane. Every 32-byte +// sector is 100% utilized and the epilogue issues 4x fewer store +// instructions (16 per wave instead of 64); the per-column weight scale also +// collapses from four scalar loads to one float4 load per fragment. Only the +// int32 values are re-routed between lanes, and the per-element multiply +// order float(dot) * x_scale[row] * weight_scale[col] plus +// __float2bfloat16 are unchanged, so every stored bf16 bit is identical. The +// kernel already performs no split-K and never touches the workspace (the +// timed operator is a single compute dispatch), so no workspace/combine pass +// exists to remove; LDS stays 18,432 B, and the single-buffered 64-K +// pipeline, the fragment loaders, the exact int32 accumulation order, the +// shape guards and the scalar fallback are unchanged. Falsifiable: if the +// residual time is store-sector over-fetch and the shuffle ALU is hidden, +// vmem_write instructions fall ~4x and median_us drops below the current +// 479.2323112487793; if the transpose ds_bpermute/v_cndmask chain contends +// with the LDS pipe or the extra live scale registers spill, median rises +// and the direct store is retained. +// +// * launch_pack_w8a8_weight packs exactly (k, n) == (2048, 4096) into the +// fragment-interleaved swizzle above (byte-count preserving, same +// allocation and same graph-stable captured addresses). Every other +// (k, n) keeps the identity device-to-device copy, and +// packed_weight_scale is always the identity copy of weight_scale. Packing +// happens before Graph capture and never inside the timed GEMM. // -// Iteration 1 - DUMMA throughput baseline with a 2-D macro-tile (128x64). -// * Large-prefill path (exact (m, n, k) == (4096, 4096, 2048)): -// native INT8 DUMMA m16n16k32 with int32 accumulation; one 128x64 -// output tile per block; four wavefronts (256 threads); each wave owns -// a 64x32 quadrant built from eight m16n16k32 fragments; block -// cooperatively vector-loads A[128,64] and packed B panels into 26 KiB -// of LDS with a padded 80-byte B row stride; 64-deep K double buffering -// with a register prefetch of the next stage (one loop-carried barrier -// per 64-K stage); fused dot * x_scale[m] * weight_scale[n] epilogue -// stored directly as bf16 from the accumulator fragments. -// Rationale vs the bootstrap 64x64x128 single-buffer tile (verified on -// the exact shape in the trusted ledger): 128x64 doubles B reuse across -// rows, halves the grid to 2048 blocks, stages each B stage as one -// coalesced 4 KiB stream from pre-packed [N/64, K, 64] panels, breaks -// the 64-byte LDS bank periodicity with an 80-byte stride, and hides -// global->LDS latency with double buffering. -// * launch_pack_w8a8_weight packs exactly (k, n) == (2048, 4096) into -// contiguous [N/64, K, 64] int8 panels; every other (k, n) keeps the -// identity copy. Packing never happens inside the timed GEMM. // * Generic scalar int8/int32 fallback for every unmatched (m, n, k), -// including the paired M=2 decode shape with the same (K, N); the -// fallback reads the packed panel layout whenever (k, n) == (2048, 4096) -// and the identity layout otherwise. +// including all M < 128 API cases and the paired small-M shape with the +// same (N, K) == (4096, 2048). The fallback decodes the fragment- +// interleaved swizzle for that exact (k, n) pair and the identity layout +// otherwise, so it is correct for every packed/unpacked weight. // -// Iteration 3 - A-tile LDS bank-conflict fix: -// PMC on the accepted iteration-1 source: 77,594,624 LDS bank conflicts -// (~6.88 per LDS instruction). Root cause in the fragment loads: the A -// tile still uses an unpadded 64-byte row stride (16 banks/row), so the -// bank pattern repeats every two rows and an m16n16k32 A-fragment load -// (64 lanes x 8 B over 16 rows) puts eight lanes on the same bank pair -// (8-way conflicts), while the already padded 80-byte B rows (20 -// banks/row, period 8) only reach 4-way over their 32-row span - matching -// the PMC mix (8 A-loads x 8-way + 4 B-loads x 4-way per 64-K stage). -// Focused change: pad the A LDS rows 64 -> 80 bytes (five bank phases, -// same as B) so each A-fragment load drops from 8-way to 2-way bank -// conflicts. LDS grows 26,624 -> 30,720 B and stays at 2 blocks/CU -// (61,440 B of the 64 KiB LDS budget); 80 is a multiple of 16 so every -// cooperative int4 stage store stays aligned; the packed [N/64, K, 64] -// global layout, exact int32 accumulation, grid, and fallback are -// untouched. +// The timed operator performs no allocation, compilation, autotuning, weight +// packing, host/device synchronization, or default-stream launch; it uses only +// the caller-provided out and workspace and launches on the caller-provided +// PyTorch HIP stream, so it is CUDA-Graph safe. This round does not use the +// workspace at all. // -// Iteration 10 - stage-boundary restructure (early publish, one full stage of -// write->read distance): -// Rounds 7 (in-loop A-fragment prefetch, 845.0 us) and 8 (occupancy probe, -// 866.6 us) falsified the in-kk LDS latency and wave-count theories and -// indicted the structural stage-start chain: [last MMACs] -> [s_waitcnt -// vmcnt for the prefetch loads] -> [3x ds_write_b128] -> [s_waitcnt -// lgkmcnt(0)] -> [s_barrier] -> [stage-start fragment loads] (ISA 0x3BC0- -// 0x3BF4 on the accepted source). The next-stage LDS publish currently -// happens AFTER the MMAC burst, so the store issue+retire latency and the -// fresh-write visibility sit directly between the barrier and the next -// stage's first reads. Focused change: the loop-carried int4 payload for -// stage k0+kStageK is published into the idle ping-pong buffer at the TOP -// of stage k0 (before the burst) and the global prefetch for stage -// k0+2*kStageK now has a full extra stage of lead, so the store latency -// retires under the burst and the stage-end barrier publishes data that has -// been resident in LDS for a full stage before the next stage reads it. -// One barrier per stage, 30,720 B LDS (2 blocks/CU), grid, packed layout, -// exact int32 accumulation order, shape guards, and the scalar fallback are -// unchanged. +// Include order is known-good for this DTK: hip_runtime.h first, then +// hip_bfloat16.h, then du_mma.h (du_mma.h is not self-contained before the HIP +// runtime headers; du_mma_common.h is not an installed public header). #include #include @@ -79,29 +220,65 @@ using namespace du::dumma; constexpr int kTileM = 16; constexpr int kTileN = 16; -constexpr int kTileK = 32; // gfx928 INT8 DUMMA primitive: m16n16k32 +constexpr int kTileK = 32; // gfx928 INT8 DUMMA primitive: m16n16k32 constexpr int kWaveSize = 64; // gfx928 native wavefront + constexpr int kTargetM = 4096; constexpr int kTargetN = 4096; constexpr int kTargetK = 2048; -constexpr int kBlockM = 128; -constexpr int kBlockN = 64; -constexpr int kStageK = 64; -constexpr int kBPad = 16; // 64 -> 80-byte LDS row stride (five bank phases) -constexpr int kBStride = kBlockN + kBPad; -constexpr int kAStride = kStageK + kBPad; // 64 -> 80-byte A row stride + +constexpr int kBlockM = 128; // output tile rows per block +constexpr int kBlockN = 128; // output tile columns per block +constexpr int kStageK = 64; // K depth staged per barrier +constexpr int kBPad = 16; // 64 -> 80-byte A LDS row stride (five bank phases) +constexpr int kAStride = kStageK + kBPad; constexpr int kBlockThreads = 4 * kWaveSize; +// Iteration 5 - fragment-interleaved packed-B swizzle. Each 32-k x 16-n DUMMA +// B fragment block is 512 B stored as [k8 0..3][n 0..15][8 B]; a 64-K stage of +// a 128-column block is (kStageK/kTileK) * (kBlockN/kTileN) = 16 such blocks, +// i.e. 8,192 B held in exactly the packed global order. The cooperative stage +// store is therefore two flat 16-byte copies per thread (chunk tid and chunk +// tid + kBlockThreads) and the col_major fragment load is one 8-byte read per +// lane at p + k8*128 + n*8. +constexpr int kSwzBlock = 512; // bytes per fragment block +constexpr int kKGroup = 8; // int8 k bytes owned by one col_major lane +constexpr int kBSwzBlocks = (kStageK / kTileK) * (kBlockN / kTileN); +constexpr int kBSwzBytes = kBSwzBlocks * kSwzBlock; +constexpr int kBChunks = kBSwzBytes / static_cast(sizeof(int4)); +static_assert(kBChunks == 2 * kBlockThreads, + "each thread stages exactly two int4 chunks of packed B"); +static_assert(kBSwzBytes == kBlockN * kStageK, + "the swizzle is byte-count preserving for the B stage"); + using bf16_t = hip_bfloat16; // --------------------------------------------------------------------------- -// Large-M prefill: 128x64 output tile per block, four wavefronts of 64 lanes. -// Each wave owns a 64x32 quadrant (eight m16n16k32 accumulators); the block -// cooperatively stages A[128,64] and packed-B [64,64] panels in two ping-pong -// buffers of 64-K stages (26 KiB total LDS). One barrier per stage. +// Iteration 6 - fused, fully coalesced epilogue. The per-row x_scale, the +// per-column weight_scale, the bf16 conversion and the store all happen here +// inside the compute kernel (no separate epilogue/combine pass and no +// workspace traffic). +// +// Verified gfx928 int8 m16n16k32 accumulator ownership (cross-checked against +// du_store_matrix_sync): lane & 15 selects the fragment row, lane >> 4 selects +// the column group c4, and x[i] maps to columns c4 + 4*i. A direct store is +// therefore four 2-byte scalar stores per lane whose wavefront addresses touch +// each 32-byte sector at 25% utilization. This helper transposes the 4-element +// groups within each 4-lane column group (lanes r, r+16, r+32, r+48 -- a 4x4 +// transpose done as two 2x2 steps with __shfl_xor 16 then 32; this DTK lowers +// __shfl_xor to ds_bpermute at the block tail where the LDS pipe is idle), so +// lane (r, c4) ends up holding the four CONTIGUOUS columns 4*c4 .. 4*c4+3, and +// writes them as ONE 8-byte store (100% sector efficiency). The per-column +// weight scale collapses from four scalar loads to one float4 load. Only the +// int32 values are re-routed between lanes; the per-element multiply order +// (float(dot) * x_scale[row] * weight_scale[col]) and the __float2bfloat16 +// rounding are unchanged, so the stored bits are identical to the direct +// per-element store. The row >= m guard is wavefront-uniform (base_row is a +// multiple of 16 and m is a multiple of 16 on every launched path), so the +// shuffles never mix active and inactive lanes. // --------------------------------------------------------------------------- template -__device__ __forceinline__ void store_prefill_fragment( +__device__ __forceinline__ void store_prefill_fragment_coalesced( const AccFragment& frag, const float* __restrict__ x_scale, const float* __restrict__ weight_scale, @@ -111,25 +288,103 @@ __device__ __forceinline__ void store_prefill_fragment( int m, int n, int lane) { - // Verified gfx928 int8 m16n16k32 accumulator ownership (matches - // du_store_matrix_sync): lane & 15 selects the row, lane >> 4 selects - // col % 4, and x[i] maps to columns col%4 + 4*i. const int row = base_row + (lane & 15); if (row >= m) { - return; // tail-M masking: padded rows never write - } - const int col_mod4 = lane >> 4; -#pragma unroll - for (int i = 0; i < 4; ++i) { - const int col = base_col + col_mod4 + 4 * i; - const float scaled = static_cast(frag.x[i]) * - x_scale[row] * weight_scale[col]; - out[static_cast(row) * n + col] = __float2bfloat16(scaled); + return; // tail-M masking: padded rows never write (wavefront-uniform) } + const int c4 = lane >> 4; // 0..3 + const int x0 = frag.x[0]; + const int x1 = frag.x[1]; + const int x2 = frag.x[2]; + const int x3 = frag.x[3]; + // Step 1: transpose the 2x2 in-block pairs (c4 bit0 <-> element bit0). + const int s0 = __shfl_xor(x0, 16, kWaveSize); + const int s1 = __shfl_xor(x1, 16, kWaveSize); + const int s2 = __shfl_xor(x2, 16, kWaveSize); + const int s3 = __shfl_xor(x3, 16, kWaveSize); + const bool lo = (c4 & 1) == 0; + const int a0 = lo ? x0 : s1; + const int a1 = lo ? s0 : x1; + const int a2 = lo ? x2 : s3; + const int a3 = lo ? s2 : x3; + // Step 2: transpose the 2x2 blocks (c4 bit1 <-> element bit1). + const int t0 = __shfl_xor(a0, 32, kWaveSize); + const int t1 = __shfl_xor(a1, 32, kWaveSize); + const int t2 = __shfl_xor(a2, 32, kWaveSize); + const int t3 = __shfl_xor(a3, 32, kWaveSize); + const bool hi = (c4 & 2) == 0; + const int f0 = hi ? a0 : t2; + const int f1 = hi ? a1 : t3; + const int f2 = hi ? t0 : a2; + const int f3 = hi ? t1 : a3; + + // Lane (r, c4) now owns columns base_col + 4*c4 .. +3 (8 B, 8-byte aligned: + // base_col is a multiple of 16, n*2 is a multiple of 8). + const int col0 = base_col + 4 * c4; + const float xs = x_scale[row]; + const float4 ws = *reinterpret_cast(weight_scale + col0); + const float v0 = static_cast(f0) * xs * ws.x; + const float v1 = static_cast(f1) * xs * ws.y; + const float v2 = static_cast(f2) * xs * ws.z; + const float v3 = static_cast(f3) * xs * ws.w; + const uint64_t packed = + static_cast( + static_cast(__float2bfloat16(v0))) | + (static_cast( + static_cast(__float2bfloat16(v1))) + << 16) | + (static_cast( + static_cast(__float2bfloat16(v2))) + << 32) | + (static_cast( + static_cast(__float2bfloat16(v3))) + << 48); + *reinterpret_cast(out + static_cast(row) * n + col0) = + packed; } -__global__ __launch_bounds__(kBlockThreads) void -w8a8_dumma_128x64x64_packed_kernel( +// --------------------------------------------------------------------------- +// Iteration 5 - explicit col_major B fragment loader on the fragment- +// interleaved swizzled tile. The gfx928 int8 m16n16k32 col_major matrix_b +// mapping is n = lane & 15, k8 = lane >> 4 (each lane owns eight contiguous k +// bytes), so on a [k8 0..3][n 0..15][8 B] block the lane's data sits at +// p + k8*128 + n*8. One 8-byte vector LDS read per lane, and each 16-lane +// phase spans exactly one 128-byte k8 block -- all 32 banks exactly once, so +// the fragment load is conflict-free by construction. The byte placement is +// identical to du_load_matrix_sync, so the +// v_mmac_i32_16x16x32_i8 operands and the int32 accumulation are bit-identical +// to the previous n-major panel layout. +// --------------------------------------------------------------------------- +__device__ __forceinline__ void load_b_frag8_swizzled( + DUFragment& frag, + const int8_t* __restrict__ p, + int lane) { + const unsigned n = static_cast(lane) & 0xfu; + const unsigned k8 = static_cast(lane) >> 4; + const int64_t v = *reinterpret_cast(p + (k8 << 7) + (n << 3)); + *reinterpret_cast(&frag.x[0]) = v; +} + +// --------------------------------------------------------------------------- +// Exact-shape prefill: 128x128 output tile per block, four wavefronts of 64 +// lanes. Each wave owns a 64x64 quadrant (sixteen m16n16k32 accumulators); the +// block cooperatively stages A[128,64] (row-major, k-major per lane) and the +// fragment-interleaved packed-B 64-K stage (16 x 512 B = 8,192 B, [k32][n16] +// block order with each block stored [k8][n][8 B]) in ONE 64-K buffer (18,432 B +// total) with two barriers per stage (iteration-3 pipeline). The swizzled B +// tile plus the explicit col_major 8-byte loader makes every B fragment one +// conflict-free ds_read_b64 (each 16-lane phase spans exactly one 128-byte k8 +// block), and the square 64x64 quadrant pays eight fragment loads per sixteen +// MMACs instead of six per eight. +// --------------------------------------------------------------------------- +// The 2 in the launch bound asks the compiler to keep two blocks (eight waves) +// resident per CU, which caps the VGPR budget at 128 per thread: with sixteen +// int32 accumulator fragments (64 VGPRs) plus the four-int4 pipeline payload +// the natural allocation lands under that cap, and the bound prevents a +// compiler choice that would silently drop residency to one block (four +// waves) per CU. +__global__ __launch_bounds__(kBlockThreads, 2) void +w8a8_dumma_128x128x64_packed_kernel( const int8_t* __restrict__ x_q, const int8_t* __restrict__ weight, const float* __restrict__ x_scale, @@ -145,164 +400,237 @@ w8a8_dumma_128x64x64_packed_kernel( const int wave_col = wave & 1; const int m0 = static_cast(blockIdx.y) * kBlockM; const int n0 = static_cast(blockIdx.x) * kBlockN; - // Packed layout: panel p = B[:, p*64:(p+1)*64] stored as [K][64] rows. - const int b_panel_offset = - static_cast(blockIdx.x) * kTargetK * kBlockN; + // Iteration-5 packed layout: for each 64-K stage and each 128-column block the + // weight is stored as 16 fragment blocks of 512 B in [k32][n16] order, so this + // block's whole 64-K B stage is ONE contiguous kBSwzBytes region at + // ((k0/64) * (N/128) + blockIdx.x) * kBSwzBytes. The staging is therefore a + // flat 16-byte copy per chunk (no transpose, no padding). + const int b_colblocks = kTargetN / kBlockN; const int local_row = wave_row * 64; - const int local_col = wave_col * 32; + const int local_col = wave_col * 64; - __shared__ __align__(16) int8_t a_tile[2][kBlockM * kAStride]; - __shared__ __align__(16) int8_t b_tile[2][kStageK * kBStride]; + // Iteration 5 - square macro-tile with the swizzled B stage. A[128,64] rows + // keep the 80-byte five-bank-phase stride (128 * 80 = 10,240 B) and the + // packed-B 64-K stage is a dense 8,192 B swizzled region, so the block uses + // 18,432 B. Two blocks stay resident (36,864 B of the 64 KiB LDS budget) and + // the 16 accumulator fragments land under the 128-VGPR two-block cap. + __shared__ __align__(16) int8_t a_tile[kBlockM * kAStride]; + __shared__ __align__(16) int8_t b_tile[kBSwzBytes]; DUFragment - a_frag0, a_frag1; - DUFragment - b_frag0, b_frag1; + a_frag0, a_frag1, a_frag2, a_frag3; + DUFragment + b_frag0, b_frag1, b_frag2, b_frag3; DUFragment - acc00, acc01, acc10, acc11, acc20, acc21, acc30, acc31; + acc00, acc01, acc02, acc03, + acc10, acc11, acc12, acc13, + acc20, acc21, acc22, acc23, + acc30, acc31, acc32, acc33; du_fill_fragment(acc00, 0); du_fill_fragment(acc01, 0); + du_fill_fragment(acc02, 0); + du_fill_fragment(acc03, 0); du_fill_fragment(acc10, 0); du_fill_fragment(acc11, 0); + du_fill_fragment(acc12, 0); + du_fill_fragment(acc13, 0); du_fill_fragment(acc20, 0); du_fill_fragment(acc21, 0); + du_fill_fragment(acc22, 0); + du_fill_fragment(acc23, 0); du_fill_fragment(acc30, 0); du_fill_fragment(acc31, 0); + du_fill_fragment(acc32, 0); + du_fill_fragment(acc33, 0); - // Prologue: cooperative vectorized staging of stage 0. Each thread owns one - // int4 in A rows [0,64) and one in A rows [64,128), plus one int4 of packed - // B. A[128,64] is 512 int4s; B[64,64] is 256 int4s. - const int vector_byte_offset = tid * sizeof(int4); + // Prologue: cooperative vectorized staging of stage 0 straight into the one + // LDS tile. Each thread owns one int4 in A rows [0,64), one in A rows + // [64,128) (tid/4 is the row, (tid%4)*16 the K chunk) and two int4 of the + // packed-B stage (chunk tid and chunk tid + 256). A's tiles keep their + // 80-byte row stride; the packed-B stage is contiguous in global memory and + // in LDS, so both of its 16-byte copies are flat and fully coalesced. + constexpr int kInt4 = static_cast(sizeof(int4)); + const int vector_byte_offset = tid * kInt4; const int stage_row = vector_byte_offset / kStageK; const int stage_col = vector_byte_offset - stage_row * kStageK; - *reinterpret_cast(a_tile[0] + stage_row * kAStride + stage_col) = + const int b_stage_base0 = static_cast(blockIdx.x) * kBSwzBytes; + *reinterpret_cast(a_tile + stage_row * kAStride + stage_col) = *reinterpret_cast( x_q + static_cast(m0 + stage_row) * k + stage_col); - *reinterpret_cast(a_tile[0] + + *reinterpret_cast(a_tile + (stage_row + kBlockM / 2) * kAStride + stage_col) = *reinterpret_cast( x_q + static_cast(m0 + stage_row + kBlockM / 2) * k + stage_col); - *reinterpret_cast(b_tile[0] + stage_row * kBStride + stage_col) = - *reinterpret_cast( - weight + b_panel_offset + stage_row * kBlockN + stage_col); + *reinterpret_cast(b_tile + tid * kInt4) = + *reinterpret_cast(weight + b_stage_base0 + tid * kInt4); + *reinterpret_cast(b_tile + (tid + kBlockThreads) * kInt4) = + *reinterpret_cast(weight + b_stage_base0 + + (tid + kBlockThreads) * kInt4); __syncthreads(); - // Loop-carried register payload for the stage after the one being computed - // (stage k0 + kStageK): loaded one full stage ahead and published into the - // idle ping-pong buffer at the TOP of the following stage. The publish's - // LDS store latency then retires under the MMAC burst, and the stage-end - // barrier publishes it a full stage before the next stage's first fragment - // loads - so the structural LDS write -> barrier -> read chain leaves the - // stage boundary (rounds 7/8 falsified in-loop A-fragment prefetch and - // occupancy; this is the prescribed stage-boundary restructure). + // Loop-carried register payload for stage kStageK, the first stage that has + // to be republished into the single buffer. It is consumed by the stage-0 + // burst first and only published after that burst retires, so it keeps a + // one-full-stage lead from load to LDS store, exactly as before. int4 cur_a0{}; int4 cur_a1{}; - int4 cur_b{}; + int4 cur_b0{}; + int4 cur_b1{}; if (k > kStageK) { cur_a0 = *reinterpret_cast( x_q + static_cast(m0 + stage_row) * k + kStageK + stage_col); cur_a1 = *reinterpret_cast( x_q + static_cast(m0 + stage_row + kBlockM / 2) * k + kStageK + stage_col); - cur_b = *reinterpret_cast( - weight + b_panel_offset + (kStageK + stage_row) * kBlockN + stage_col); + cur_b0 = *reinterpret_cast( + weight + (b_colblocks + static_cast(blockIdx.x)) * kBSwzBytes + + tid * kInt4); + cur_b1 = *reinterpret_cast( + weight + (b_colblocks + static_cast(blockIdx.x)) * kBSwzBytes + + (tid + kBlockThreads) * kInt4); } - int current = 0; for (int k0 = 0; k0 < k; k0 += kStageK) { - const int next_k = k0 + kStageK; - const int next = current ^ 1; - // Publish the loop-carried payload (stage next_k) into the idle buffer - // BEFORE the MMAC burst; the stage-end barrier below publishes it a full - // stage before it is read, hiding the store latency under the burst. - if (next_k < k) { - *reinterpret_cast(a_tile[next] + stage_row * kAStride + - stage_col) = cur_a0; - *reinterpret_cast(a_tile[next] + - (stage_row + kBlockM / 2) * kAStride + - stage_col) = cur_a1; - *reinterpret_cast( - b_tile[next] + stage_row * kBStride + stage_col) = cur_b; - } - // Prefetch stage k0 + 2*kStageK into the payload registers (a full stage - // of extra global->LDS lead) so the publish never waits on the loads. - const int load_k = k0 + 2 * kStageK; - if (load_k < k) { - cur_a0 = *reinterpret_cast( - x_q + static_cast(m0 + stage_row) * k + load_k + stage_col); - cur_a1 = *reinterpret_cast( - x_q + static_cast(m0 + stage_row + kBlockM / 2) * k + - load_k + stage_col); - cur_b = *reinterpret_cast( - weight + b_panel_offset + (load_k + stage_row) * kBlockN + - stage_col); - } - - // Each wave consumes its 64x32 quadrant: eight m16n16k32 MMACs per kk. + // Each wave consumes its 64x64 quadrant from the single LDS tile: sixteen + // m16n16k32 MMACs per kk from four A and four B fragments (0.5 LDS loads + // per MMAC instead of the 0.75 of the 128x64 tile). #pragma unroll for (int kk = 0; kk < kStageK; kk += kTileK) { + // Swizzled B block index of this wave's four 16-n fragments: k32 half + // (kk/32) selects the [k32] outer index, and this wave's 64-column + // quadrant starts at n16 group wave_col*4. + const int b_swz = ((kk >> 5) << 3) + (wave_col << 2); + load_b_frag8_swizzled(b_frag0, b_tile + (b_swz + 0) * kSwzBlock, lane); + load_b_frag8_swizzled(b_frag1, b_tile + (b_swz + 1) * kSwzBlock, lane); du_load_matrix_sync( - b_frag0, b_tile[current] + kk * kBStride + local_col, kBStride); - du_load_matrix_sync( - b_frag1, b_tile[current] + kk * kBStride + local_col + kTileN, - kBStride); - du_load_matrix_sync( - a_frag0, a_tile[current] + local_row * kAStride + kk, kAStride); + a_frag0, a_tile + (local_row + 0 * kTileM) * kAStride + kk, + kAStride); du_load_matrix_sync( - a_frag1, a_tile[current] + (local_row + kTileM) * kAStride + kk, + a_frag1, a_tile + (local_row + 1 * kTileM) * kAStride + kk, kAStride); du_mma_sync(acc00, a_frag0, b_frag0, acc00); du_mma_sync(acc01, a_frag0, b_frag1, acc01); du_mma_sync(acc10, a_frag1, b_frag0, acc10); du_mma_sync(acc11, a_frag1, b_frag1, acc11); + load_b_frag8_swizzled(b_frag2, b_tile + (b_swz + 2) * kSwzBlock, lane); + load_b_frag8_swizzled(b_frag3, b_tile + (b_swz + 3) * kSwzBlock, lane); du_load_matrix_sync( - a_frag0, a_tile[current] + (local_row + 2 * kTileM) * kAStride + kk, + a_frag2, a_tile + (local_row + 2 * kTileM) * kAStride + kk, kAStride); du_load_matrix_sync( - a_frag1, a_tile[current] + (local_row + 3 * kTileM) * kAStride + kk, + a_frag3, a_tile + (local_row + 3 * kTileM) * kAStride + kk, kAStride); - du_mma_sync(acc20, a_frag0, b_frag0, acc20); - du_mma_sync(acc21, a_frag0, b_frag1, acc21); - du_mma_sync(acc30, a_frag1, b_frag0, acc30); - du_mma_sync(acc31, a_frag1, b_frag1, acc31); + du_mma_sync(acc02, a_frag0, b_frag2, acc02); + du_mma_sync(acc03, a_frag0, b_frag3, acc03); + du_mma_sync(acc12, a_frag1, b_frag2, acc12); + du_mma_sync(acc13, a_frag1, b_frag3, acc13); + du_mma_sync(acc22, a_frag2, b_frag2, acc22); + du_mma_sync(acc23, a_frag2, b_frag3, acc23); + du_mma_sync(acc32, a_frag3, b_frag2, acc32); + du_mma_sync(acc33, a_frag3, b_frag3, acc33); + du_mma_sync(acc20, a_frag2, b_frag0, acc20); + du_mma_sync(acc21, a_frag2, b_frag1, acc21); + du_mma_sync(acc30, a_frag3, b_frag0, acc30); + du_mma_sync(acc31, a_frag3, b_frag1, acc31); } + // Single-buffer hazard: the tile must not be overwritten while any wave is + // still reading it. This is the extra barrier the single-buffered pipeline + // pays per K stage (the ping-pong version published into the idle buffer + // at the top of the stage and needed only one barrier). + __syncthreads(); + const int next_k = k0 + kStageK; if (next_k < k) { - // The stage-end barrier publishes the payload written at the top of - // this stage (a full stage before the next stage reads it) and protects - // buffer `current` from the publish at the top of the next stage. + // Publish stage next_k into the now-idle single tile. The store latency + // is no longer hidden under a burst, so the second barrier below is + // required before the next stage's first fragment loads. + *reinterpret_cast(a_tile + stage_row * kAStride + stage_col) = + cur_a0; + *reinterpret_cast(a_tile + + (stage_row + kBlockM / 2) * kAStride + + stage_col) = cur_a1; + *reinterpret_cast(b_tile + tid * kInt4) = cur_b0; + *reinterpret_cast(b_tile + (tid + kBlockThreads) * kInt4) = + cur_b1; __syncthreads(); - current = next; + // Refill the register payload one stage further out; the lead from this + // load to its publish at the end of the next iteration is still one full + // stage, so the global->register latency is covered as before. + const int load_k = k0 + 2 * kStageK; + if (load_k < k) { + cur_a0 = *reinterpret_cast( + x_q + static_cast(m0 + stage_row) * k + load_k + + stage_col); + cur_a1 = *reinterpret_cast( + x_q + static_cast(m0 + stage_row + kBlockM / 2) * k + + load_k + stage_col); + const int b_load_base = + ((load_k >> 6) * b_colblocks + static_cast(blockIdx.x)) * + kBSwzBytes; + cur_b0 = *reinterpret_cast(weight + b_load_base + + tid * kInt4); + cur_b1 = *reinterpret_cast( + weight + b_load_base + (tid + kBlockThreads) * kInt4); + } } } const int base_row = m0 + local_row; const int base_col = n0 + local_col; - store_prefill_fragment(acc00, x_scale, weight_scale, out, - base_row, base_col, m, n, lane); - store_prefill_fragment(acc01, x_scale, weight_scale, out, - base_row, base_col + kTileN, m, n, lane); - store_prefill_fragment(acc10, x_scale, weight_scale, out, - base_row + kTileM, base_col, m, n, lane); - store_prefill_fragment(acc11, x_scale, weight_scale, out, - base_row + kTileM, base_col + kTileN, m, n, lane); - store_prefill_fragment(acc20, x_scale, weight_scale, out, - base_row + 2 * kTileM, base_col, m, n, lane); - store_prefill_fragment(acc21, x_scale, weight_scale, out, - base_row + 2 * kTileM, base_col + kTileN, m, n, lane); - store_prefill_fragment(acc30, x_scale, weight_scale, out, - base_row + 3 * kTileM, base_col, m, n, lane); - store_prefill_fragment(acc31, x_scale, weight_scale, out, - base_row + 3 * kTileM, base_col + kTileN, m, n, lane); + store_prefill_fragment_coalesced(acc00, x_scale, weight_scale, out, + base_row, base_col, m, n, lane); + store_prefill_fragment_coalesced(acc01, x_scale, weight_scale, out, + base_row, base_col + kTileN, m, n, lane); + store_prefill_fragment_coalesced(acc02, x_scale, weight_scale, out, + base_row, base_col + 2 * kTileN, m, n, + lane); + store_prefill_fragment_coalesced(acc03, x_scale, weight_scale, out, + base_row, base_col + 3 * kTileN, m, n, + lane); + store_prefill_fragment_coalesced(acc10, x_scale, weight_scale, out, + base_row + kTileM, base_col, m, n, lane); + store_prefill_fragment_coalesced(acc11, x_scale, weight_scale, out, + base_row + kTileM, base_col + kTileN, m, n, + lane); + store_prefill_fragment_coalesced(acc12, x_scale, weight_scale, out, + base_row + kTileM, base_col + 2 * kTileN, m, + n, lane); + store_prefill_fragment_coalesced(acc13, x_scale, weight_scale, out, + base_row + kTileM, base_col + 3 * kTileN, m, + n, lane); + store_prefill_fragment_coalesced(acc20, x_scale, weight_scale, out, + base_row + 2 * kTileM, base_col, m, n, + lane); + store_prefill_fragment_coalesced(acc21, x_scale, weight_scale, out, + base_row + 2 * kTileM, base_col + kTileN, m, + n, lane); + store_prefill_fragment_coalesced(acc22, x_scale, weight_scale, out, + base_row + 2 * kTileM, base_col + 2 * kTileN, + m, n, lane); + store_prefill_fragment_coalesced(acc23, x_scale, weight_scale, out, + base_row + 2 * kTileM, base_col + 3 * kTileN, + m, n, lane); + store_prefill_fragment_coalesced(acc30, x_scale, weight_scale, out, + base_row + 3 * kTileM, base_col, m, n, + lane); + store_prefill_fragment_coalesced(acc31, x_scale, weight_scale, out, + base_row + 3 * kTileM, base_col + kTileN, m, + n, lane); + store_prefill_fragment_coalesced(acc32, x_scale, weight_scale, out, + base_row + 3 * kTileM, base_col + 2 * kTileN, + m, n, lane); + store_prefill_fragment_coalesced(acc33, x_scale, weight_scale, out, + base_row + 3 * kTileM, base_col + 3 * kTileN, + m, n, lane); } // --------------------------------------------------------------------------- -// Generic scalar fallback: one thread per output element. Exact int32 dot -// over K, then fused x_scale * weight_scale, then bf16 store. Handles every -// unmatched (m, n, k), including all small-M API cases. When (k, n) matches -// the packed pair (2048, 4096) the weight is read from the [N/64, K, 64] -// panel layout; otherwise from the identity layout. +// Generic scalar fallback: one thread per output element. Exact int32 dot over +// K, then fused x_scale * weight_scale, then bf16 store. Handles every +// unmatched (m, n, k), including all M < 128. When (k, n) matches the packed +// pair (2048, 4096) the weight is read from the iteration-5 fragment- +// interleaved swizzle; otherwise from the identity layout. // --------------------------------------------------------------------------- __global__ __launch_bounds__(256) void w8a8_gemm_scalar_fallback_kernel( const int8_t* __restrict__ x_q, @@ -323,15 +651,21 @@ __global__ __launch_bounds__(256) void w8a8_gemm_scalar_fallback_kernel( const int col = static_cast(linear - static_cast(row) * n); int32_t acc = 0; if (n == kTargetN && k == kTargetK) { - const int panel = col / kBlockN; - const int panel_col = col - panel * kBlockN; - const int64_t panel_offset = - static_cast(panel) * kTargetK * kBlockN; + // Decode the same swizzle the packer writes (ascending kk, exact order). + const int colblock = col / kBlockN; + const int n16 = (col % kBlockN) / kTileN; + const int r = col % kTileN; for (int kk = 0; kk < k; ++kk) { + const int ks = kk % kStageK; + const int block = ((kk / kStageK) * (kTargetN / kBlockN) + colblock) * + kBSwzBlocks + + (ks / kTileK) * (kBlockN / kTileN) + n16; + const int64_t idx = + static_cast(block) * kSwzBlock + + static_cast((ks % kTileK) / kKGroup) * (kTileN * kKGroup) + + static_cast(r) * kKGroup + (kk % kKGroup); acc += static_cast(x_q[static_cast(row) * k + kk]) * - static_cast(weight[panel_offset + - static_cast(kk) * kBlockN + - panel_col]); + static_cast(weight[idx]); } } else { const int8_t* a_row = x_q + static_cast(row) * k; @@ -341,36 +675,55 @@ __global__ __launch_bounds__(256) void w8a8_gemm_scalar_fallback_kernel( static_cast(b_col[static_cast(kk) * n]); } } - const float scaled = static_cast(acc) * - x_scale[row] * weight_scale[col]; + const float scaled = static_cast(acc) * x_scale[row] * + weight_scale[col]; out[linear] = __float2bfloat16(scaled); } // --------------------------------------------------------------------------- -// Weight packing: exact (k, n) == (2048, 4096) becomes contiguous -// [N/64, K, 64] int8 panels (each int4 stays within one 64-column row, so -// loads are coalesced 4 KiB streams per 64-K stage); identity copy for any -// other (k, n). Scales are always copied identity. +// Weight packing: the exact (k, n) == (2048, 4096) weight is permuted once, +// outside the timed region and outside Graph capture, into the iteration-5 +// fragment-interleaved swizzle. For each 64-K stage and each 128-column block +// there are kBSwzBlocks = 16 fragment blocks of 512 B ordered [k32][n16], each +// stored as [k8 0..3][n 0..15][8 B]. Every packed 16-byte chunk is therefore a +// flat 16-byte store of two 8-byte k-runs (n rows 2*n_pair and 2*n_pair+1 of +// one k8 group), which is exactly the chunk the cooperative stage store later +// copies into LDS. The byte count k*n, the allocation and the graph-stable +// captured addresses are unchanged; every other (k, n) keeps the identity +// device-to-device copy, and the scales are always copied identity. // --------------------------------------------------------------------------- -__global__ __launch_bounds__(256) void w8a8_pack_64n_panel_kernel( +__global__ __launch_bounds__(256) void w8a8_pack_bswz_i8_kernel( const int8_t* __restrict__ raw_weight, int8_t* __restrict__ packed_weight) { - const int vec = static_cast(blockIdx.x) * blockDim.x + threadIdx.x; - constexpr int kVectors = + const int chunk = static_cast(blockIdx.x) * blockDim.x + threadIdx.x; + constexpr int kChunks = kTargetK * kTargetN / static_cast(sizeof(int4)); - if (vec >= kVectors) { + if (chunk >= kChunks) { return; } - constexpr int kPanelBytes = kTargetK * kBlockN; - const int byte_offset = vec * sizeof(int4); - const int panel = byte_offset / kPanelBytes; - const int panel_offset = byte_offset - panel * kPanelBytes; - const int kk = panel_offset / kBlockN; - const int col = panel_offset - kk * kBlockN; - reinterpret_cast(packed_weight)[vec] = - *reinterpret_cast( - raw_weight + static_cast(kk) * kTargetN + - panel * kBlockN + col); + constexpr int kChunksPerBlock = kSwzBlock / static_cast(sizeof(int4)); + constexpr int kPairsPerK8 = kTileN / 2; // 16-byte chunks per k8 group + const int within = chunk % kChunksPerBlock; + const int block = chunk / kChunksPerBlock; + const int kk8 = within / kPairsPerK8; + const int n_pair = within % kPairsPerK8; + const int n16 = block % (kBlockN / kTileN); + const int k32 = (block / (kBlockN / kTileN)) % (kStageK / kTileK); + const int colblock = (block / kBSwzBlocks) % (kTargetN / kBlockN); + const int stage = block / (kBSwzBlocks * (kTargetN / kBlockN)); + const int kk_base = stage * kStageK + k32 * kTileK + kk8 * kKGroup; + const int col0 = colblock * kBlockN + n16 * kTileN + n_pair * 2; + // Transposed gather: one 16-byte chunk = two 8-byte k-runs of adjacent n. + int4 value; + int8_t* bytes = reinterpret_cast(&value); +#pragma unroll + for (int i = 0; i < kKGroup; ++i) { + bytes[i] = raw_weight[static_cast(kk_base + i) * kTargetN + col0]; + bytes[kKGroup + i] = + raw_weight[static_cast(kk_base + i) * kTargetN + col0 + 1]; + } + *reinterpret_cast(packed_weight + + chunk * static_cast(sizeof(int4))) = value; } __global__ __launch_bounds__(256) void w8a8_pack_identity_kernel( @@ -388,7 +741,7 @@ __global__ __launch_bounds__(256) void w8a8_pack_scale_identity_kernel( const float* __restrict__ raw, float* __restrict__ packed, int count) { - const int linear = blockIdx.x * blockDim.x + threadIdx.x; + const int linear = static_cast(blockIdx.x) * blockDim.x + threadIdx.x; if (linear < count) { packed[linear] = raw[linear]; } @@ -397,10 +750,10 @@ __global__ __launch_bounds__(256) void w8a8_pack_scale_identity_kernel( } // namespace // --------------------------------------------------------------------------- -// Stable host launch symbols consumed by csrc/bindings.cpp. -// Both launchers are pure dispatch: no allocation, no packing, no -// synchronization, no default-stream launch; they run on the caller-provided -// PyTorch stream and are CUDA-Graph safe. +// Stable host launch symbols consumed by csrc/bindings.cpp. Both launchers are +// pure dispatch: no allocation, no packing, no synchronization, no +// default-stream launch; they run on the caller-provided PyTorch stream and +// are CUDA-Graph safe. // --------------------------------------------------------------------------- extern "C" void launch_w8a8_gemm( const int8_t* a, @@ -414,28 +767,28 @@ extern "C" void launch_w8a8_gemm( int n, int k, hipStream_t stream) { - (void)workspace; // no split-K: the GEMM does not use the workspace + (void)workspace; // no split-K: the GEMM never touches the workspace (void)workspace_bytes; auto* out_bf16 = reinterpret_cast(out); // Explicit dispatch. The assigned shape (M=4096, N=4096, K=2048) takes the - // packed-panel DUMMA 128x64 path; every other (m, n, k) - including - // small-M API cases and the paired M=2 shape with the same (K, N) - takes - // the scalar fallback, which understands the packed panel layout. + // swizzled-packed DUMMA 128x128 path; every other (m, n, k) - including every + // M < 128 API case and the paired small-M shape with the same (N, K) - takes + // the scalar fallback, which understands the fragment-interleaved swizzle. if (m == kTargetM && n == kTargetN && k == kTargetK) { - const dim3 grid(kTargetN / kBlockN, kTargetM / kBlockM); + const dim3 grid(static_cast(kTargetN / kBlockN), + static_cast(kTargetM / kBlockM)); const dim3 block(kBlockThreads); - hipLaunchKernelGGL(w8a8_dumma_128x64x64_packed_kernel, - grid, block, 0, stream, - a, b, x_scale, weight_scale, out_bf16, m, n, k); + hipLaunchKernelGGL(w8a8_dumma_128x128x64_packed_kernel, grid, block, 0, + stream, a, b, x_scale, weight_scale, out_bf16, m, n, k); } else { constexpr int kBlock = 256; const int64_t total = static_cast(m) * n; const dim3 grid(static_cast((total + kBlock - 1) / kBlock)); const dim3 block(kBlock); - hipLaunchKernelGGL(w8a8_gemm_scalar_fallback_kernel, - grid, block, 0, stream, - a, b, x_scale, weight_scale, out_bf16, m, n, k); + hipLaunchKernelGGL(w8a8_gemm_scalar_fallback_kernel, grid, block, 0, + stream, a, b, x_scale, weight_scale, out_bf16, m, n, + k); } (void)hipGetLastError(); } @@ -455,26 +808,25 @@ extern "C" void launch_pack_w8a8_weight( const dim3 weight_grid( static_cast((kPackVectors + kBlock - 1) / kBlock)); const dim3 block(kBlock); - hipLaunchKernelGGL(w8a8_pack_64n_panel_kernel, - weight_grid, block, 0, stream, - raw_weight, packed_weight); + hipLaunchKernelGGL(w8a8_pack_bswz_i8_kernel, weight_grid, block, 0, + stream, raw_weight, packed_weight); } else { const int64_t weight_count = static_cast(k) * n; - const dim3 weight_grid( - static_cast((weight_count + kBlock - 1) / kBlock)); + if (weight_count > 0) { + const dim3 weight_grid( + static_cast((weight_count + kBlock - 1) / kBlock)); + const dim3 block(kBlock); + hipLaunchKernelGGL(w8a8_pack_identity_kernel, weight_grid, block, 0, + stream, raw_weight, packed_weight, weight_count); + } + } + if (n > 0) { + const dim3 scale_grid(static_cast( + (static_cast(n) + kBlock - 1) / kBlock)); const dim3 block(kBlock); - hipLaunchKernelGGL(w8a8_pack_identity_kernel, - weight_grid, block, 0, stream, - raw_weight, packed_weight, weight_count); + hipLaunchKernelGGL(w8a8_pack_scale_identity_kernel, scale_grid, block, 0, + stream, weight_scale, packed_weight_scale, n); } - - const int64_t scale_count = n; - const dim3 scale_grid( - static_cast((scale_count + kBlock - 1) / kBlock)); - const dim3 block(kBlock); - hipLaunchKernelGGL(w8a8_pack_scale_identity_kernel, - scale_grid, block, 0, stream, - weight_scale, packed_weight_scale, n); (void)hipGetLastError(); } // @@end diff --git a/metainfer/tasks/dcu_kernel_auto_opt/variant/int8w8a8-gemm/hy3/TP4/M4096/qkv_proj.hip b/metainfer/tasks/dcu_kernel_auto_opt/variant/int8w8a8-gemm/hy3/TP4/M4096/qkv_proj.hip index 919b94e1..443ac4bc 100644 --- a/metainfer/tasks/dcu_kernel_auto_opt/variant/int8w8a8-gemm/hy3/TP4/M4096/qkv_proj.hip +++ b/metainfer/tasks/dcu_kernel_auto_opt/variant/int8w8a8-gemm/hy3/TP4/M4096/qkv_proj.hip @@ -1,122 +1,54 @@ -// @@variant shape=hy3_tp4_qkv_proj_m4096 commit=61a0f079ab9622cc6c83a9c44af6ecd626ef0640 added=2026-08-23 -// median_us=756.7 p90_us=760.2 tops=113.5 bandwidth_gb_s=63.78 speedup=31.78 baseline_us=2.405e+04 -// source=hy3-tp4-m4096-dsh-test1-bfdf3002 -// MetaInfer W8A8 INT8 GEMM bootstrap for gfx928 (K500SM_AI). +// @@variant shape=hy3_tp4_qkv_proj_m4096 commit=2eebb654a8b34a0ec8bdca109fa8daef07be1e91 added=2026-09-14 +// median_us=625.3 p90_us=626.8 speedup=38.45 baseline_us=2.405e+04 +// source=test-9-11-1/iteration_002/hy3_tp4_qkv_proj_m4096 (retested A/B) +// MetaInfer W8A8 INT8 GEMM for gfx928 (K500SM_AI), CHILD worker_0. // -// Worker: worker_0 (physical GPU 0) -// Assigned shape: hy3_tp4_qkv_proj_m4096 (M=4096, N=2560, K=4096) +// Assigned shape (iteration 1 / establish_arch round, packed_weight family): +// hy3_tp4_qkv_proj_m4096 : M=4096, N=2560, K=4096 // -// Operator contract (fixed by the control plane): -// out[m, n] = bf16( int32_dot(x_q[m, :], raw_weight[:, n]) +// Frozen logical contract (validated in int8_w8a8_gemm_api.py): +// out[m, n] = bf16( int32_dot(x_q[m, :], weight[:, n]) // * x_scale[m] * weight_scale[n] ) -// with the packed weight layout equal to the logical [K, N] row-major int8 -// weight for the bootstrap (launch_pack_w8a8_weight is an identity D2D copy). +// The bootstrap packed weight was the logical [K, N] row-major int8 weight +// (identity copy). This session keeps that layout for every shape except the +// exact assigned pair (K=4096, N=2560), which launch_pack_w8a8_weight packs +// once (outside the timed region and outside Graph capture) into the +// 16-byte-chunk layout packed16[(k/16) * N + n] with the same k*n byte count. // -// Iteration 3 (harness) strategy: single-buffered 64x128 macro-tile with -// cooperative A+B LDS staging -- the matched control arm of the mandated -// pipeline comparison (single vs double buffering across K tiles). -// * Large-M path (M >= 128, N % 128 == 0, K % 128 == 0): native INT8 DUMMA -// m16n16k32 tiled kernel with a 64x128 output tile per block and 256 -// threads (4 wavefronts). Each wave owns a 32x64 quadrant (eight -// m16n16k32 int32 accumulator fragments). K is staged cooperatively in a -// SINGLE LDS buffer at 64-K granularity: the next stage's 3 x int4 -// (1 A + 2 B vectors) are loaded from global only after the current -// stage's 16-MMAC DUMMA burst and its all-consumption barrier, waited -// on, and committed as int32 LDS stores into the same buffer, followed -// by a store-visibility barrier -- TWO __syncthreads per 64-K stage -// (128 over K=4096). The global-load latency is fully exposed (no -// compute overlap), which isolates the value of the double-buffer -// overlap at the same 64-K stage size, odd-word LDS strides, and -// 2-blocks/CU residency as the accepted iteration-2 double-buffered -// kernel (1108.58 us median; iteration-1 single-buffered 128-K stage: -// 1296.50 us). LDS/block = 64x68 + 64x132 = 12,800 B -> 2 resident -// blocks/CU (VGPR-bound). The grid (N/128) x (ceil(M/64)) supplies 1280 -// blocks for the assigned shape. -// * Comparison evidence (fact ledger): double-64 vs single-128 improved -// the benchmark median 1296.50 -> 1108.58 us, but PMC moved against the -// 'reduced VMEM stalls' reading: vmem_read_instructions stayed flat at -// 1,177,600, lds_wait_instructions rose 19.48M -> 20.69M, bank conflicts -// rose 5.24M -> 7.86M, L2 hit fell 89.46% -> 88.25%. This round re-runs -// the single-buffered pipeline at the SAME 64-K stage to decide -// retention on matched evidence (benchmark median/P90 is authoritative -// per the interpretation guard; PMC counters are diagnostic). -// * Operand-reuse accounting per 64x128 macro-tile (A[64,64] + B[64,128] -// per 64-K stage): cooperative staging loads each A element from global -// once per stage and reuses it for 8 MMAs (512 MMA-uses per element over -// K=4096), and each B element for 2 MMAs per stage (128 MMA-uses over -// K=4096). Without staging, the two waves sharing each A row / B column -// segment would refetch everything (2x global traffic) with per-lane -// 8-byte fragment loads (4x more global-load instructions), so the -// cooperative A+B staging path is kept. -// * LDS row strides stay padded to odd word counts (A: 68 B = 17 words, -// B: 132 B = 33 words). The 64x64 bootstrap used unpadded 128/64-byte -// rows, so every INT8 m16n16k32 fragment load collapsed onto a handful -// of LDS banks (16-way conflicts; 199.2M conflict events over 24.9M LDS -// instructions in PMC). The odd word stride makes the lane row/k-group -// index contribute to the bank, spreading fragment loads across banks -// (A and B: 16-way -> 2-4-way per LDS instruction) at 2 blocks/CU. -// * The epilogue applies float(dot) * x_scale[m] * weight_scale[n] and -// stores bf16 directly from the accumulator fragments using the verified -// gfx928 int8 m16n16k32 lane mapping (row = lane & 15, col_mod4 = -// lane >> 4, frag.x[i] -> columns col_mod4 + 4*i). -// * Fallbacks are preserved unchanged: the 64x64 kernel for large-M shapes -// with N % 64 == 0 but N % 128 != 0, and the scalar int8/int32 -// grid-stride fallback for every other (m, n, k) including the paired -// M=2 API shape. launch_pack_w8a8_weight stays an identity -// device-to-device copy valid for every (K, N). +// Strategy: native INT8 DUMMA m16n16k32 tiled kernel with int32 accumulation. +// * Large-M generic path (m >= 128, n % 128 == 0, k % 64 == 0): one 64x128 +// output macro-tile per block, 256 threads = 4 gfx928 wavefronts of 64 +// lanes. Each wave owns a 32x64 quadrant = eight m16n16k32 int32 +// accumulator fragments (2 row halves x 4 column quarters). K is staged +// cooperatively in a SINGLE LDS buffer at 64-K granularity, consumed by a +// 32-K unrolled fragment/MMA loop. Exactly two __syncthreads per stage; +// no split-K, no double buffering, no raw asm. +// * Exact-shape path (m >= 128, n == 2560, k == 4096): the SAME 64x128 +// macro-tile / 4-wavefront / single-buffered-64-K geometry, but B is +// packed 16-byte-chunk and consumed with one ds_read2_b64 per m16n16k32 +// fragment instead of eight ds_read_u8. Measured motivation (PMC on the +// bootstrap digest): lds_wait_instructions 42,668,821 = 1.76x +// lds_instructions (24,248,320), lds_bank_conflicts 7,864,320 = 32% of +// LDS instructions -> the LDS pipe is the dominant stall class at +// 99.6 TOPS, and the byte-granular row-major B fragment loads are the +// primary contributor. +// * Grid for the assigned shape: (2560 / 128) x (4096 / 64) = 20 x 64 = +// 1280 blocks, 4 waves/block, 13,568 B LDS/block -> 3 resident +// blocks/CU on the 120-CU K500SM_AI. +// * Scalar int8/int32 grid-stride fallback for every unmatched (m, n, k) +// and for small-M API cases (m < 128), which keeps the paired M=2 shape +// on the fallback path; that fallback decodes the packed layout for the +// exact (k, n) pair and the logical [K, N] layout otherwise. // -// Iteration 5 (packing round) strategy: keep the accepted iteration-3 -// pipeline (single-buffered 64-K stage, 64x128 macro-tile, 3 blocks/CU) and -// add ONE weight packing/swizzle for the exact assigned shape (K=4096, -// N=2560): launch_pack_w8a8_weight transposes the logical [K, N] weight to -// an n-major packed[n][k] layout once, outside the timed region and outside -// Graph capture (same byte count k*n, same allocation, graph-stable packed -// layout). The exact shape routes (before the generic 64x128 path) to a new -// w8a8_dumma_prefill_64x128_packedb_kernel that stages B into an n-major LDS -// tile with a 16-byte-aligned 80-byte row stride (20 words, five bank -// phases) and consumes it with col_major m16n16k32 B fragments via an -// explicit 8-byte loader (load_b_frag8): each lane's 8 fragment elements at -// p[row*ldm + col + i] are one contiguous 8-byte-aligned chunk (one -// ds_read2_b64 per B fragment instead of 8 byte-granular ds_read_u8 at -// stride 132 in the accepted kernel's ISA), and each 16-byte staging vector -// commits with one ds_write_b128 instead of four ds_write_b32. A stays -// row-major (already vectorized ds_read2_b32 in the accepted ISA); the int32 -// accumulation order (k0-outer, kk-inner) and the fused scale/bf16 epilogue -// are unchanged, so results stay bit-identical. All other shapes keep the -// identity pack and the accepted iteration-3 kernel byte-for-byte; the 64x64 -// kernel, the scalar fallback (which decodes packed[n][k] when (k, n) -// matches), M tails, and the exact shape guards are preserved. +// The timed operator (launch_w8a8_gemm) only dispatches kernels on the +// caller-provided HIP stream: it never allocates, compiles, autotunes, packs +// weights, synchronizes the host or device, or launches on the default stream. +// launch_pack_w8a8_weight performs the one-time exact-shape chunk packing +// and an identity copy for every other (K, N); both are outside Graph capture. // -// Iteration 6 (epilogue round) strategy: keep the accepted iteration-5 -// packedb kernel's pipeline byte-identical (single-buffered 64-K stage, -// n-major B with 80-B rows, load_b_frag8, 3 blocks/CU) and replace ONLY its -// final store. The scales and bf16 conversion are already fused in the -// kernel and launch_w8a8_gemm has no workspace/combine pass (workspace is -// unused), so the remaining epilogue inefficiency is the store pattern: the -// m16n16k32 accumulator lane mapping (row = lane & 15, column group -// c4 = lane >> 4, frag.x[i] -> column c4 + 4*i) makes the direct store four -// 2-byte scalar stores per lane whose wavefront addresses touch every 32-B -// sector at 25% utilization. store_prefill_fragment_coalesced transposes the -// 4-element groups within each 4-lane column group (lanes r, r+16, r+32, -// r+48) with two 2x2 shuffle steps (8 shfl_xor + 8 v_cndmask; this DTK -// lowers each shfl_xor to one ds_bpermute -- +64 LDS permutes per wavefront -// at the block tail, where the LDS pipe is otherwise idle), so lane (r, c4) -// owns the four CONTIGUOUS columns 4*c4 .. 4*c4+3, then converts to bf16, -// packs 4 bf16 (8 B), and issues ONE 8-byte store per lane: 64 lanes x 8 B = -// 512 B per fragment per wavefront in 16 fully-used 32-B sectors (100% -// store sector efficiency; the assigned shape's vmem_write_instructions drop -// 163,840 -> 40,960). Only the int32 values are re-routed between lanes; the -// per-element float scale/bf16 math is unchanged, so output bits are -// identical. The generic 64x128/64x64 kernels and the scalar fallback are -// untouched (their store_prefill_fragment epilogue is preserved -// byte-for-byte). -// -// The timed operator (launch_w8a8_gemm) performs no allocation, compilation, -// autotuning, packing, host/device synchronization, or default-stream launch: -// it only dispatches kernels on the caller-provided HIP stream. -// -// Include order is fixed by the control plane: hip_runtime, hip_bfloat16, -// then du_mma (this DTK's du_mma.h is not self-contained otherwise). +// Include order is fixed by the control plane: hip_runtime, then +// hip_bfloat16, then du_mma (this DTK's du_mma.h is not self-contained when +// included before the HIP runtime headers). Do not include du_mma_common.h. #include #include @@ -126,57 +58,99 @@ namespace { +using namespace du::dumma; + +// gfx928 native wavefront is 64 lanes; blockDim must be a multiple of 64. constexpr int kWaveSize = 64; + +// Native INT8 DUMMA shape. constexpr int kTileM = 16; constexpr int kTileN = 16; constexpr int kTileK = 32; + +// 64x128 macro-tile, single-buffered 64-K LDS stage, 4 wavefronts. constexpr int kBlockM = 64; -constexpr int kBlockN = 64; -constexpr int kStageK = 128; -// 4 wavefronts; must remain a multiple of the gfx928 wavefront size (64). +constexpr int kBlockN = 128; +constexpr int kStageK = 64; constexpr int kThreadsPerBlock = 256; -// Iteration 3 (pipeline round): 64x128 macro-tile, SINGLE-buffered 64-K K -// stage (matched control for the single-vs-double buffering comparison). -// LDS row strides stay padded to odd word counts (A: 68 B = 17 words, -// B: 132 B = 33 words) so the INT8 m16n16k32 fragment loads spread across -// banks instead of collapsing 16-way onto one bank (128-byte rows are 32 -// words, i.e. 0 mod 32 for every row -> single-bank collisions). -constexpr int kBlockM128 = 64; -constexpr int kBlockN128 = 128; -// Single-buffered K stage: one 64-K buffer = 12,800 B/block (the accepted -// iteration-2 double buffer was 2 x 64 = 25,600 B; the iteration-1 -// single-buffered 128-K stage was 25,344 B). Occupancy stays 2 blocks/CU -// (VGPR-bound at arch_vgpr ~112), identical to both prior kernels. -constexpr int kStageK128 = 64; -constexpr int kAStride128 = kStageK128 + 4; // 68 bytes per A row (17 words) -constexpr int kBStride128 = kBlockN128 + 4; // 132 bytes per B row (33 words) - -// Iteration 5 (packing round): packed n-major B layout for the exact -// assigned shape (K=4096, N=2560). B is transposed once, outside timing, to -// packed[n][k]; the packed kernel stages B into an n-major LDS tile with a -// 16-byte-aligned non-power-of-two row stride (80 B = 20 words, five bank -// phases) so every DUMMA m16n16k32 B fragment load (col_major) reads its -// lane's 8 bytes contiguously (ds_read2_b64) and every 16-byte staging -// vector commits with one ds_write_b128 (b_n*80 + b_k16 is 0 mod 16). -constexpr int kPackedBStride = 80; // 64 data + 16 pad bytes (20 words) +// Odd-word (4-byte-padded) LDS row strides: 17 and 33 words. +constexpr int kAStride = kStageK + 4; // 68 bytes per A row +constexpr int kBStride = kBlockN + 4; // 132 bytes per B row + +// Iteration 2 (operand-reuse round) strategy: keep the accepted iteration-1 +// cooperative A+B LDS staging decision and fix ONLY the packed B LDS layout to +// be bank-safe. Operand-reuse accounting per 64x128 macro-tile per 64-K stage +// (A[64,64] + B[64,128] = 12,288 staged bytes): the 2x2 wave grid reads every +// staged A element 2x and every staged B element 2x (48 m16n16k32 fragment +// loads per block stage, contiguous 512-byte fragments), i.e. the cooperative +// path moves 12,288 global bytes but only 2x that through LDS. Loading the +// DUMMA fragments directly from global would instead refetch both operands +// twice with 4,096-byte-strided (>32x uncoalesced) per-lane 8-byte addresses, +// so cooperative A+B staging is retained and only its LDS layout changes: +// * the weight is now packed once (untimed, outside Graph capture) into a +// 16-byte-chunk layout packed16[(k/16) * N + n] holding +// weight[16*(c/N) .. +15][c%N], same k*n byte count, so the GEMM's B +// staging reads become 4 coalesced 256-byte runs per 64-lane wavefront +// (consecutive lanes -> consecutive 16-byte chunks) at unchanged volume; +// * the B LDS tile uses an 8-byte-aligned 72-byte (18-word) row stride and +// the staging map lane = kq*16 + row4 (row4 = lane & 15 = n-row inside a +// 16-row group, kq = (lane >> 4) & 3 = 16-byte k-chunk), so both the +// col_major m16n16k32 fragment read (n = lane & 15, k = 8*(lane>>4)+i; bank +// = 18*row4 + 2*kq) and the staged commit at n_local*72 + 16*kq spread the +// 16-lane issue phase over 16 distinct even banks plus their odd +// neighbours. The previous 80-byte (20-word) row stride left only 8 +// distinct banks per 16-row phase; the fresh PMC on the iteration-1 digest +// records lds_bank_conflicts 23,592,960 over lds_instructions 3,932,160 +// (6.0 events per LDS read instruction), the largest LDS defect. +// * the chunk commit is two alignment-provable 8-byte qword stores (the tile +// is declared as int64_t), the fragment registers receive byte-identical +// values, and the int32 k0-outer/kk-inner accumulation order is unchanged. +// LDS/block = 64*68 + 128*72 = 13,568 B; the 64x128 geometry, 256 threads / +// 4 wavefronts, single-buffered 64-K stage, two barriers per stage, 3 +// blocks/CU occupancy, fused scale/bf16 epilogue, exact shape guards, the +// generic 64x128 fallback, the scalar fallback (which decodes the chunked +// layout for the exact (k, n)) and every other shape's path are unchanged. +// +// Iteration 3 (fix_build round): the iteration-2 candidate above failed to +// compile for exactly one reason -- launch_pack_w8a8_weight still referenced +// the removed w8a8_pack_nmajor_b_kernel symbol after the pack kernel was +// rewritten as w8a8_pack_k16chunk_b_kernel. The only change this round is +// rebinding that host call to the defined 16-byte-chunk pack kernel (plus +// comment text); the kernel bodies, geometry, guards and layouts are byte-for- +// byte the iteration-2 candidate. +// +// Iteration 1 (establish_arch round, packed_weight family) for the exact +// assigned shape (K=4096, N=2560): launch_pack_w8a8_weight transposed the +// logical [K, N] row-major weight once, outside the timed region and outside +// Graph capture, into an n-major layout with the SAME byte count (k*n), so +// allocation and graph-stable addresses are unchanged. Every DUMMA m16n16k32 B +// fragment collapsed to ONE contiguous 8-byte lane chunk (one ds_read2_b64) +// instead of eight byte-granular ds_read_u8 at the 132-byte row_major stride. +// +// Iteration 2 (operand-reuse round): the pack is now a 16-byte-chunk layout +// packed16[(k/16) * N + n] (chunk c = weight[16*(c/N) .. +15][c%N]) so the +// staging global reads are 4 coalesced 256-byte runs per wavefront, and the B +// LDS tile row stride becomes 72 bytes (18 words, 8-byte aligned) with the +// staging map lane = kq*16 + row4. A 16-row phase then touches 16 distinct +// even banks (18*row4) plus their odd neighbours instead of the 8 banks the +// 20-word stride produced, removing the measured 6.0 bank-conflict events per +// LDS read instruction (23,592,960 conflicts / 3,932,160 LDS instructions). +constexpr int kPackedBStride = 72; // 64 data + 8 pad bytes (18 words) +constexpr int kPackedBWords = kPackedBStride / 8; // 9 qwords per n-row constexpr int kPackedBK = 4096; // exact K of the packed assigned shape constexpr int kPackedBN = 2560; // exact N of the packed assigned shape -// Iteration 18 (fragment-pipeline round): the packedb compute loop splits the -// 64-K stage into two explicit 32-K steps (kk = 0, kk = 32) and hoists ALL -// fragment loads for both steps before the first v_mmac. This requires -// exactly two kTileK steps per stage. -static_assert(kStageK128 == 2 * kTileK, - "pipelined kk loop requires exactly two 32-K steps per stage"); +// Fallback launch geometry. +constexpr int kFallbackThreads = 256; +constexpr int kPackThreads = 256; -using namespace du::dumma; - -// Direct accumulator epilogue for gfx928 INT8 m16n16k32 (verified against -// du_store_matrix_sync): row = lane & 15, col_mod4 = lane >> 4, -// frag.x[i] maps to columns col_mod4 + 4*i. Stores +// Direct accumulator epilogue for the gfx928 INT8 m16n16k32 DUMMA lane +// mapping (verified against du_store_matrix_sync): row = lane & 15, +// col_mod4 = lane >> 4, and frag.x[i] maps to columns col_mod4 + 4*i. Stores // bf16(float(dot) * x_scale[row] * weight_scale[col]); out-of-range rows are -// masked (tail-M handling). +// masked so a partial M tail is handled safely. Adjacent lanes stay on +// adjacent columns in the fastest-changing N dimension. template __device__ __forceinline__ void store_prefill_fragment( const AccFragment& frag, @@ -202,101 +176,10 @@ __device__ __forceinline__ void store_prefill_fragment( } } -// Iteration 6 (epilogue round): coalesced fragment store. The m16n16k32 -// accumulator lane mapping (row = lane & 15, column group c4 = lane >> 4, -// frag.x[i] -> column c4 + 4*i) gives each lane four elements strided by 4 -// columns, so the direct store (store_prefill_fragment) is four 2-byte -// scalar stores per lane whose wavefront addresses touch each 32-B sector at -// 25% utilization (8 B used of 32 B; 4 store instructions per fragment per -// wave). This epilogue transposes the 4-element groups within each 4-lane -// column group (lanes r, r+16, r+32, r+48 -- a 4x4 transpose, two 2x2 steps -// with shfl_xor 16 then 32, one v_cndmask per element per step; no staging -// tile round trip), so lane (r, c4) ends up holding the four CONTIGUOUS -// columns 4*c4 .. 4*c4+3, converts them to bf16, packs 4 bf16 (8 B), and -// writes ONE 8-byte store per lane: 64 lanes x 8 B = 512 B per fragment per -// wavefront in 16 fully-used 32-B sectors (100% store sector efficiency; -// vmem_write_instructions for the assigned shape drops 163,840 -> 40,960). -// Note: this DTK's __shfl_xor lowers to ds_bpermute (one LDS permute per -// shuffle, i.e. +64 LDS instructions per wavefront per block -- accepted -// because it runs at the block tail where the LDS pipe is otherwise idle; -// there is no global round trip and no staging tile). Only the int32 values -// are re-routed between lanes -- the per-element scale multiply order -// (float(dot) * x_scale[row] * weight_scale[col]) and the bf16 rounding are -// unchanged, so the stored bits are identical to store_prefill_fragment. -// The row>=m guard is wavefront-uniform (lane & 15 cycles the same 16 rows -// in every 16-lane group), so the shuffles never mix active and inactive -// lanes; base_col is a multiple of 64 and n*2 a multiple of 8, so the float4 -// weight_scale load (col0 % 4 == 0) and the 8-byte store are aligned. -template -__device__ __forceinline__ void store_prefill_fragment_coalesced( - const AccFragment& frag, - const float* __restrict__ x_scale, - const float* __restrict__ weight_scale, - hip_bfloat16* __restrict__ out, - int m, - int n, - int base_row, - int base_col, - int lane) { - const int row = base_row + (lane & 15); - if (row >= m) { - return; - } - const int c4 = lane >> 4; // 0..3 - const int x0 = frag.x[0]; - const int x1 = frag.x[1]; - const int x2 = frag.x[2]; - const int x3 = frag.x[3]; - // Step 1: transpose the 2x2 in-block pairs (c4 bit0 <-> element bit0). - const int s0 = __shfl_xor(x0, 16, kWaveSize); - const int s1 = __shfl_xor(x1, 16, kWaveSize); - const int s2 = __shfl_xor(x2, 16, kWaveSize); - const int s3 = __shfl_xor(x3, 16, kWaveSize); - const bool lo = (c4 & 1) == 0; - const int a0 = lo ? x0 : s1; - const int a1 = lo ? s0 : x1; - const int a2 = lo ? x2 : s3; - const int a3 = lo ? s2 : x3; - // Step 2: transpose the 2x2 blocks (c4 bit1 <-> element bit1). - const int t0 = __shfl_xor(a0, 32, kWaveSize); - const int t1 = __shfl_xor(a1, 32, kWaveSize); - const int t2 = __shfl_xor(a2, 32, kWaveSize); - const int t3 = __shfl_xor(a3, 32, kWaveSize); - const bool hi = (c4 & 2) == 0; - const int f0 = hi ? a0 : t2; - const int f1 = hi ? a1 : t3; - const int f2 = hi ? t0 : a2; - const int f3 = hi ? t1 : a3; - - // Lane (r, c4) now owns columns base_col + 4*c4 .. +3 (8 B, 8-byte - // aligned: base_col is a multiple of 64, n*2 is a multiple of 8). - const int col0 = base_col + 4 * c4; - const float xs = x_scale[row]; - const float4 ws = *reinterpret_cast(weight_scale + col0); - const float v0 = static_cast(f0) * xs * ws.x; - const float v1 = static_cast(f1) * xs * ws.y; - const float v2 = static_cast(f2) * xs * ws.z; - const float v3 = static_cast(f3) * xs * ws.w; - const uint64_t packed = - static_cast( - static_cast(__float2bfloat16(v0))) | - (static_cast( - static_cast(__float2bfloat16(v1))) - << 16) | - (static_cast( - static_cast(__float2bfloat16(v2))) - << 32) | - (static_cast( - static_cast(__float2bfloat16(v3))) - << 48); - *reinterpret_cast(out + row * n + col0) = packed; -} - -// Large-M prefill kernel: 64x64 output tile per block, K staged in LDS -// (single buffer), four waves each owning a 32x32 quadrant = four -// m16n16k32 int8->int32 DUMMA accumulators. +// Large-M prefill kernel: 64x128 output tile per block, single-buffered 64-K +// K stage, 256 threads / 4 wavefronts. __global__ __launch_bounds__(kThreadsPerBlock) void -w8a8_dumma_prefill_64x64_kernel( +w8a8_dumma_prefill_64x128_kernel( const int8_t* __restrict__ x_q, const int8_t* __restrict__ weight, const float* __restrict__ x_scale, @@ -313,120 +196,8 @@ w8a8_dumma_prefill_64x64_kernel( const int m0 = static_cast(blockIdx.y) * kBlockM; const int n0 = static_cast(blockIdx.x) * kBlockN; - __shared__ __align__(16) int8_t a_tile[kBlockM * kStageK]; - __shared__ __align__(16) int8_t b_tile[kStageK * kBlockN]; - - DUFragment - a_frag0, a_frag1; - DUFragment - b_frag0, b_frag1; - DUFragment - acc00, acc01, acc10, acc11; - du_fill_fragment(acc00, 0); - du_fill_fragment(acc01, 0); - du_fill_fragment(acc10, 0); - du_fill_fragment(acc11, 0); - - for (int k0 = 0; k0 < k; k0 += kStageK) { - // Stage A[64,128] (row-major, zero-filled past M). - constexpr int kAVectors = kBlockM * kStageK / static_cast(sizeof(int4)); - for (int vec = tid; vec < kAVectors; vec += kThreadsPerBlock) { - const int byte_offset = vec * static_cast(sizeof(int4)); - const int local_row = byte_offset / kStageK; - const int kk = byte_offset - local_row * kStageK; - const int global_row = m0 + local_row; - reinterpret_cast(a_tile)[vec] = - global_row < m - ? *reinterpret_cast( - x_q + global_row * k + k0 + kk) - : int4{0, 0, 0, 0}; - } - - // Stage B[128,64] (row-major [K, N] logical weight layout). - constexpr int kBVectors = kStageK * kBlockN / static_cast(sizeof(int4)); - for (int vec = tid; vec < kBVectors; vec += kThreadsPerBlock) { - const int byte_offset = vec * static_cast(sizeof(int4)); - const int kk = byte_offset / kBlockN; - const int col = byte_offset - kk * kBlockN; - reinterpret_cast(b_tile)[vec] = - *reinterpret_cast( - weight + (k0 + kk) * n + n0 + col); - } - __syncthreads(); - - // Consume the stage: each wave does four m16n16k32 MMAs per 32-K step. - const int local_row = wave_row * 32; - const int local_col = wave_col * 32; -#pragma unroll - for (int kk = 0; kk < kStageK; kk += kTileK) { - du_load_matrix_sync( - a_frag0, a_tile + local_row * kStageK + kk, kStageK); - du_load_matrix_sync( - a_frag1, a_tile + (local_row + kTileM) * kStageK + kk, kStageK); - du_load_matrix_sync( - b_frag0, b_tile + kk * kBlockN + local_col, kBlockN); - du_load_matrix_sync( - b_frag1, b_tile + kk * kBlockN + local_col + kTileN, kBlockN); - du_mma_sync(acc00, a_frag0, b_frag0, acc00); - du_mma_sync(acc01, a_frag0, b_frag1, acc01); - du_mma_sync(acc10, a_frag1, b_frag0, acc10); - du_mma_sync(acc11, a_frag1, b_frag1, acc11); - } - // Single buffer: make sure every wave finished reading LDS before the - // next stage overwrites it. - __syncthreads(); - } - - const int base_row = m0 + wave_row * 32; - const int base_col = n0 + wave_col * 32; - store_prefill_fragment( - acc00, x_scale, weight_scale, out, m, n, - base_row, base_col, lane); - store_prefill_fragment( - acc01, x_scale, weight_scale, out, m, n, - base_row, base_col + kTileN, lane); - store_prefill_fragment( - acc10, x_scale, weight_scale, out, m, n, - base_row + kTileM, base_col, lane); - store_prefill_fragment( - acc11, x_scale, weight_scale, out, m, n, - base_row + kTileM, base_col + kTileN, lane); -} - -// Large-M prefill kernel #2 (iteration 3, pipeline round): 64x128 output -// tile per block, 256 threads (4 wavefronts). Each wave owns a 32x64 -// quadrant = eight m16n16k32 int32 accumulators (2 row halves x 4 column -// quarters). K is staged cooperatively in a SINGLE LDS buffer at 64-K -// granularity as the matched control for the mandated single-vs-double -// buffering comparison: it shares the iteration-2 double-buffered kernel's -// 64-K stage size, odd-word LDS strides (A: 68 B = 17 words, B: 132 B = 33 -// words), 16-MMAC DUMMA burst, and 2-blocks/CU residency, but uses ONE -// 12,800-B buffer and TWO __syncthreads per stage (128 over K): the next -// stage's 3 x int4 (1 A + 2 B) are loaded from global only after the -// current burst and its all-consumption barrier, vmcnt-waited, and -// committed into the same buffer, then a store-visibility barrier. The -// global-load latency is fully exposed (no compute overlap), isolating the -// value of the double-buffer overlap at matched stage size and occupancy. -__global__ __launch_bounds__(kThreadsPerBlock) void -w8a8_dumma_prefill_64x128_kernel( - const int8_t* __restrict__ x_q, - const int8_t* __restrict__ weight, - const float* __restrict__ x_scale, - const float* __restrict__ weight_scale, - hip_bfloat16* __restrict__ out, - int m, - int n, - int k) { - const int tid = static_cast(threadIdx.x); - const int wave = tid / kWaveSize; - const int lane = tid % kWaveSize; - const int wave_row = wave >> 1; // 0 or 1 - const int wave_col = wave & 1; // 0 or 1 - const int m0 = static_cast(blockIdx.y) * kBlockM128; - const int n0 = static_cast(blockIdx.x) * kBlockN128; - - __shared__ __align__(16) int8_t a_tile[kBlockM128 * kAStride128]; - __shared__ __align__(16) int8_t b_tile[kStageK128 * kBStride128]; + __shared__ __align__(16) int8_t a_tile[kBlockM * kAStride]; + __shared__ __align__(16) int8_t b_tile[kStageK * kBStride]; DUFragment a_frag0, a_frag1; @@ -443,22 +214,21 @@ w8a8_dumma_prefill_64x128_kernel( du_fill_fragment(acc12, 0); du_fill_fragment(acc13, 0); - const int num_stages = k / kStageK128; + const int num_stages = k / kStageK; // Cooperative staging mapping (fixed per thread, reused every stage): - // A[64,64] -> 256 int4 vectors; thread `tid` owns vector `tid` - // (row = tid/4, 16-B column group = (tid%4)*16), so each - // wavefront covers 16 rows x 64 B contiguous per row. - // B[64,128] -> 512 int4 vectors; thread `tid` owns vectors `tid` and - // `tid+256` (kk = tid/8, column group = (tid%8)*16), so - // each wavefront covers 8 kk rows x 128 B contiguous. + // A[64, 64] -> 256 int4 vectors; thread `tid` owns vector `tid` + // (row = tid/4, 16-B column group = (tid%4)*16). + // B[64, 128] -> 512 int4 vectors; thread `tid` owns vectors `tid` and + // `tid+256` (kk = tid/8, 16-B column group = (tid%8)*16), + // i.e. kk rows 0..31 and 32..63. const int a_row = tid >> 2; const int a_col16 = (tid & 3) << 4; const int b_kk0 = tid >> 3; const int b_col16 = (tid & 7) << 4; - // VGPR payload for one full 64-K stage (3 x int4 = 12 VGPR), loaded just - // before it is committed (no one-stage-ahead overlap in this control). + // One-stage register payload (3 x int4 = 12 VGPR), loaded just before it is + // committed; there is no stage-ahead overlap in this bootstrap. int4 vA, vB0, vB1; // Prologue: load stage 0 into registers, commit it to the single buffer, @@ -471,22 +241,24 @@ w8a8_dumma_prefill_64x128_kernel( vB0 = *reinterpret_cast( weight + b_kk0 * n + n0 + b_col16); vB1 = *reinterpret_cast( - weight + (b_kk0 + kStageK128 / 2) * n + n0 + b_col16); - // Commit as int32 stores (skips the 4-byte pad column; 4 x ds_write_b32 - // per 16-byte vector because the odd strides are 4 B mod 16). + weight + (b_kk0 + kStageK / 2) * n + n0 + b_col16); + // Commit as int32 stores: the odd-word strides leave the rows 4-byte but + // not 16-byte aligned, so four ds_write_b32 per 16-byte vector. int32_t* adst = reinterpret_cast( - a_tile + a_row * kAStride128 + a_col16); + a_tile + a_row * kAStride + a_col16); adst[0] = vA.x; adst[1] = vA.y; adst[2] = vA.z; adst[3] = vA.w; int32_t* bdst0 = reinterpret_cast( - b_tile + b_kk0 * kBStride128 + b_col16); + b_tile + b_kk0 * kBStride + b_col16); bdst0[0] = vB0.x; bdst0[1] = vB0.y; bdst0[2] = vB0.z; bdst0[3] = vB0.w; - int32_t* bdst1 = bdst0 + (kStageK128 / 2) * (kBStride128 / 4); + // kBStride is a whole number of int32 words, so the second half-row is + // exactly (kStageK/2) rows away. + int32_t* bdst1 = bdst0 + (kStageK / 2) * (kBStride / 4); bdst1[0] = vB1.x; bdst1[1] = vB1.y; bdst1[2] = vB1.z; @@ -496,27 +268,23 @@ w8a8_dumma_prefill_64x128_kernel( for (int s = 0; s < num_stages; ++s) { // Consume the single buffer: each wave does eight m16n16k32 MMAs per - // 32-K step over its 32x64 quadrant. + // 32-K step over its own 32x64 quadrant. const int local_row = wave_row * 32; const int local_col = wave_col * 64; #pragma unroll - for (int kk = 0; kk < kStageK128; kk += kTileK) { + for (int kk = 0; kk < kStageK; kk += kTileK) { du_load_matrix_sync( - a_frag0, a_tile + local_row * kAStride128 + kk, kAStride128); + a_frag0, a_tile + local_row * kAStride + kk, kAStride); du_load_matrix_sync( - a_frag1, a_tile + (local_row + kTileM) * kAStride128 + kk, - kAStride128); + a_frag1, a_tile + (local_row + kTileM) * kAStride + kk, kAStride); du_load_matrix_sync( - b_frag0, b_tile + kk * kBStride128 + local_col, kBStride128); + b_frag0, b_tile + kk * kBStride + local_col, kBStride); du_load_matrix_sync( - b_frag1, b_tile + kk * kBStride128 + local_col + kTileN, - kBStride128); + b_frag1, b_tile + kk * kBStride + local_col + kTileN, kBStride); du_load_matrix_sync( - b_frag2, b_tile + kk * kBStride128 + local_col + 2 * kTileN, - kBStride128); + b_frag2, b_tile + kk * kBStride + local_col + 2 * kTileN, kBStride); du_load_matrix_sync( - b_frag3, b_tile + kk * kBStride128 + local_col + 3 * kTileN, - kBStride128); + b_frag3, b_tile + kk * kBStride + local_col + 3 * kTileN, kBStride); du_mma_sync(acc00, a_frag0, b_frag0, acc00); du_mma_sync(acc01, a_frag0, b_frag1, acc01); du_mma_sync(acc02, a_frag0, b_frag2, acc02); @@ -526,18 +294,15 @@ w8a8_dumma_prefill_64x128_kernel( du_mma_sync(acc12, a_frag1, b_frag2, acc12); du_mma_sync(acc13, a_frag1, b_frag3, acc13); } - // All consumption of the single buffer is complete only after every - // wavefront passes this barrier; the stage s+1 stores below overwrite - // that same buffer, so they must wait for it. + // Every wavefront must finish reading the single buffer before the next + // stage overwrites it. __syncthreads(); - // Load stage s+1 and commit it into the single buffer. The global-load - // latency is fully exposed here: the loads are issued after the burst - // (nothing to overlap) and the compiler's vmcnt wait before the DS - // stores is on the critical path. `s + 1 < num_stages` is block-uniform, - // so the branch and its barrier are free of divergence. + // Load stage s+1 from global and commit it into the same buffer. The + // `s + 1 < num_stages` guard is block-uniform, so the barrier below is + // reached by every thread in the block. if (s + 1 < num_stages) { - const int s1 = (s + 1) * kStageK128; + const int s1 = (s + 1) * kStageK; const int g_row = m0 + a_row; vA = (g_row < m) ? *reinterpret_cast(x_q + g_row * k + s1 + a_col16) @@ -545,26 +310,25 @@ w8a8_dumma_prefill_64x128_kernel( vB0 = *reinterpret_cast( weight + (s1 + b_kk0) * n + n0 + b_col16); vB1 = *reinterpret_cast( - weight + (s1 + b_kk0 + kStageK128 / 2) * n + n0 + b_col16); + weight + (s1 + b_kk0 + kStageK / 2) * n + n0 + b_col16); int32_t* adst = reinterpret_cast( - a_tile + a_row * kAStride128 + a_col16); + a_tile + a_row * kAStride + a_col16); adst[0] = vA.x; adst[1] = vA.y; adst[2] = vA.z; adst[3] = vA.w; int32_t* bdst0 = reinterpret_cast( - b_tile + b_kk0 * kBStride128 + b_col16); + b_tile + b_kk0 * kBStride + b_col16); bdst0[0] = vB0.x; bdst0[1] = vB0.y; bdst0[2] = vB0.z; bdst0[3] = vB0.w; - int32_t* bdst1 = bdst0 + (kStageK128 / 2) * (kBStride128 / 4); + int32_t* bdst1 = bdst0 + (kStageK / 2) * (kBStride / 4); bdst1[0] = vB1.x; bdst1[1] = vB1.y; bdst1[2] = vB1.z; bdst1[3] = vB1.w; - // Make the stage s+1 stores visible to every wavefront before the - // next burst. + // Make the stage s+1 stores visible before the next burst. __syncthreads(); } } @@ -572,96 +336,80 @@ w8a8_dumma_prefill_64x128_kernel( const int base_row = m0 + wave_row * 32; const int base_col = n0 + wave_col * 64; store_prefill_fragment( - acc00, x_scale, weight_scale, out, m, n, - base_row, base_col, lane); + acc00, x_scale, weight_scale, out, m, n, base_row, base_col, lane); store_prefill_fragment( - acc01, x_scale, weight_scale, out, m, n, - base_row, base_col + kTileN, lane); + acc01, x_scale, weight_scale, out, m, n, base_row, base_col + kTileN, + lane); store_prefill_fragment( - acc02, x_scale, weight_scale, out, m, n, - base_row, base_col + 2 * kTileN, lane); + acc02, x_scale, weight_scale, out, m, n, base_row, + base_col + 2 * kTileN, lane); store_prefill_fragment( - acc03, x_scale, weight_scale, out, m, n, - base_row, base_col + 3 * kTileN, lane); + acc03, x_scale, weight_scale, out, m, n, base_row, + base_col + 3 * kTileN, lane); store_prefill_fragment( - acc10, x_scale, weight_scale, out, m, n, - base_row + kTileM, base_col, lane); + acc10, x_scale, weight_scale, out, m, n, base_row + kTileM, base_col, + lane); store_prefill_fragment( - acc11, x_scale, weight_scale, out, m, n, - base_row + kTileM, base_col + kTileN, lane); + acc11, x_scale, weight_scale, out, m, n, base_row + kTileM, + base_col + kTileN, lane); store_prefill_fragment( - acc12, x_scale, weight_scale, out, m, n, - base_row + kTileM, base_col + 2 * kTileN, lane); + acc12, x_scale, weight_scale, out, m, n, base_row + kTileM, + base_col + 2 * kTileN, lane); store_prefill_fragment( - acc13, x_scale, weight_scale, out, m, n, - base_row + kTileM, base_col + 3 * kTileN, lane); + acc13, x_scale, weight_scale, out, m, n, base_row + kTileM, + base_col + 3 * kTileN, lane); } -// Iteration 5 (packing round): explicit 8-byte loader for the col_major -// m16n16k32 B fragment (the gate_up-lineage-validated load_fragment8 -// pattern). The col_major fragment mapping is n = lane&15, -// k = 8*(lane>>4) + i; with the n-major LDS tile at row stride 80 the -// lane's 8 elements are contiguous (p[row*ldm + col .. +7], 8-byte aligned -// because ldm=80 and col are multiples of 8), so this compiles to ONE -// ds_read2_b64 instead of 8 ds_read_u8 + mask/OR reassembly. The byte -// placement is identical to du_load_matrix_sync, so the -// v_mmac_i32_16x16x32_i8 fragment registers receive the same values. +// Iteration 1/2 (packed_weight family): explicit 8-byte loader for the +// col_major m16n16k32 B fragment. With the n-major LDS tile at row stride +// ldm (72 bytes in iteration 2, 80 in iteration 1), the col_major fragment +// mapping (n = lane & 15, k = 8 * (lane >> 4) + i) places each lane's 8 +// elements at p[(lane & 15) * ldm + ((lane >> 4) << 3) .. +7]: one contiguous, +// 8-byte-aligned chunk (ldm and every k offset are multiples of 8), so this is +// ONE ds_read2_b64 instead of eight ds_read_u8 + mask/OR reassembly. The bytes +// placed in the fragment registers are exactly what +// du_load_matrix_sync would produce, so the +// v_mmac_i32_16x16x32_i8 operands and the int32 accumulation are unchanged. __device__ __forceinline__ void load_b_frag8( DUFragment& f, const int8_t* __restrict__ p, - int ldm) { - const unsigned row = static_cast(__lane_id()) & 0xfu; - const unsigned col = (static_cast(__lane_id()) >> 4) << 3; + int ldm, + int lane) { + const int row = lane & 15; + const int col = (lane >> 4) << 3; const int64_t v = *reinterpret_cast(p + row * ldm + col); *reinterpret_cast(&f.x[0]) = v; } -// Large-M prefill kernel #3 (iteration 5, packing round): 64x128 macro-tile -// for the exact assigned shape (K=4096, N=2560) with the weight packed -// n-major. The pipeline is byte-identical to the accepted iteration-3 kernel -// (single-buffered 64-K stage, TWO __syncthreads per stage with fully -// exposed global-load latency, 3 x int4 VGPR payload, A staged row-major -// with the 68-byte odd-word stride); ONLY the B operand path changes: -// * pack: the logical [K, N] weight is transposed once (outside the timed -// region and Graph capture) to packed[n][k] by launch_pack_w8a8_weight. -// * staging: thread tid owns rows n_local = tid>>2 and n_local+64, each -// with the 16-byte k-run (tid&3)*16; two global_load_dwordx4 are -// committed with ONE ds_write_b128 each (16-byte-aligned LDS address: -// row stride 80 and the k16 offset are both 0 mod 16), vs four -// ds_write_b32 per vector in the accepted kernel. -// * fragments: load_b_frag8 (col_major mapping n = local_col + lane&15, -// k = kk + 8*(lane>>4) + i) reads each lane's 8 elements as one -// 8-byte-aligned contiguous chunk -> one ds_read2_b64 per B fragment, -// vs 8 byte-granular ds_read_u8 at stride 132 in the accepted kernel's -// ISA. The mapping is the transpose of the accepted row_major mapping, -// so the v_mmac_i32_16x16x32_i8 accumulation order is unchanged and the -// int32 results are bit-identical. Bank safety: the 20-word row stride -// spreads each k-group's 16 n-lanes over 8 distinct banks with five -// bank phases. -// * A path, epilogue, tail masking, and occupancy are unchanged. -// LDS/block = 64x68 + 128x80 = 14,592 B -> 3 resident blocks/CU -// (VGPR-bound at arch_vgpr ~80), same as the accepted kernel. -// -// Iteration 18 (fragment-pipeline round) strategy: keep the accepted -// iteration-6 packedb kernel byte-identical in EVERYTHING except the inner -// fragment schedule of the 64-K-stage compute loop. The accepted kernel's -// exact gfx928 ISA (iteration 7 evidence) shows the compiler already -// software-pipelines the kk=32 A fragment loads under the kk=0 MMAC burst, -// but its four kk=32 B-fragment ds_read_b64 loads are issued only AFTER -// MMACs 0-7, with s_waitcnt lgkmcnt(4..1) interleaved into MMACs 8-11 (16+ -// lgkmcnt waits per stage; PMC lds_wait 6,560,572 = 1.18x lds_instructions, -// the dominant stall class of a SIMD ~36%-issue-busy kernel). This round -// hoists ALL twelve ds_read fragment loads of both 32-K steps before the -// first v_mmac of the stage (explicit two-step unroll of the kk loop with -// separate kk=32 fragment variables), so the LDS latency concentrates in one -// wait and the 16-MMAC stream has no lgkmcnt waits. The MMAC sequence per -// (m,n) output tile and the int32 k0-outer/kk-inner accumulation order are -// unchanged, so output bits stay identical. Occupancy target is retained: -// arch_vgpr 72 -> <= 85 at 3 blocks/CU (65,280 <= 65,536 VGPRs), LDS 14,592 -// B, grid 1280 x 256, TWO __syncthreads per stage, staging mapping, packed -// B layout, coalesced epilogue, and the exact shape guard (m >= 128 && -// n == 2560 && k == 4096) all unchanged; generic 64x128/64x64 kernels, the -// pack kernel, and the scalar fallback are untouched. +// Iteration 2 (operand-reuse round): assemble one 8-byte qword from the two +// low 32-bit halves of a packed 16-byte staging vector. The chunk commit uses +// two of these per 16-byte vector (72-byte LDS rows are only 8-byte aligned), +// which keeps the store side of the B tile bank-safe at identical byte volume. +__device__ __forceinline__ int64_t pack_lo32_pair(int lo, int hi) { + return static_cast(static_cast(lo)) | + (static_cast(static_cast(hi)) << 32); +} + +// Iteration 1/2 (packed_weight family): packed B variant of the 64x128 +// macro-tile for the exact assigned shape (K=4096, N=2560). The geometry, +// K staging depth (64), barrier count (two __syncthreads per stage), A path +// (row-major, 68-byte odd-word stride, 4 x ds_write_b32 per 16-byte vector), +// 8-MMA-per-32-K burst, fused scale/bf16 epilogue and 3-blocks/CU occupancy +// target are the same as the generic kernel. ONLY the B operand path changes: +// * staging (iteration 2): lane = b_kq*16 + b_row4 with b_row4 = lane & 15 +// (n-row inside a 16-row group) and b_kq = (lane >> 4) & 3 (16-byte +// k-chunk of the 64-K stage); wave w owns n-rows w*32 + b_row4 and +// w*32 + b_row4 + 16. Consecutive lanes read consecutive 16-byte chunks of +// packed16[(k/16)*N + n] (4 fully coalesced 256-byte runs per wavefront), +// and each chunk commits as two alignment-provable 8-byte qword stores into +// the bank-safe 72-byte-stride LDS tile at n_local*72 + 16*b_kq. +// * consume: load_b_frag8 reads each lane's 8 B bytes as ONE ds_read2_b64 +// (vs 8 ds_read_u8 at stride 132); with the 18-word row stride the +// 16-lane phase spreads over 16 distinct even banks + their odd +// neighbours (the 20-word stride left 8), removing the measured 6.0 bank +// conflicts per LDS read instruction. +// LDS/block = 64x68 + 128x72 = 13,568 B (fits the single-buffer 48 KiB +// headroom rule); grid = (2560 / 128) x (4096 / 64) = 20 x 64 = 1280 blocks. __global__ __launch_bounds__(kThreadsPerBlock) void w8a8_dumma_prefill_64x128_packedb_kernel( const int8_t* __restrict__ x_q, @@ -677,12 +425,13 @@ w8a8_dumma_prefill_64x128_packedb_kernel( const int lane = tid % kWaveSize; const int wave_row = wave >> 1; // 0 or 1 const int wave_col = wave & 1; // 0 or 1 - const int m0 = static_cast(blockIdx.y) * kBlockM128; - const int n0 = static_cast(blockIdx.x) * kBlockN128; + const int m0 = static_cast(blockIdx.y) * kBlockM; + const int n0 = static_cast(blockIdx.x) * kBlockN; - __shared__ __align__(16) int8_t a_tile[kBlockM128 * kAStride128]; - // n-major B tile: 128 n-rows of 80 bytes (64 data + 16 pad). - __shared__ __align__(16) int8_t b_tile[kBlockN128 * kPackedBStride]; + __shared__ __align__(16) int8_t a_tile[kBlockM * kAStride]; + // Iteration 2: n-major B tile, 128 n-rows of 72 bytes (64 data + 8 pad), + // declared as qwords so each 8-byte chunk commit is alignment-provable. + __shared__ __align__(16) int64_t b_tile[kBlockN * kPackedBWords]; DUFragment a_frag0, a_frag1; @@ -699,173 +448,167 @@ w8a8_dumma_prefill_64x128_packedb_kernel( du_fill_fragment(acc12, 0); du_fill_fragment(acc13, 0); - const int num_stages = k / kStageK128; + const int num_stages = k / kStageK; - // A staging mapping unchanged (row-major x_q, odd-word stride 68). + // A staging mapping unchanged. Iteration 2 B staging mapping for the chunked + // packed layout: lane = b_kq*16 + b_row4 with b_row4 = lane & 15 (n-row + // inside a 16-row group) and b_kq = (lane >> 4) & 3 (16-byte k-chunk of the + // 64-K stage); wave w owns n-rows w*32 + b_row4 and w*32 + b_row4 + 16. const int a_row = tid >> 2; const int a_col16 = (tid & 3) << 4; - // B staging mapping for packed[n][k]: thread tid owns the 16-byte k-run - // (b_k16..b_k16+15) of n-rows b_n and b_n + 64. - const int b_n = tid >> 2; - const int b_k16 = (tid & 3) << 4; + const int b_row4 = lane & 15; + const int b_kq = (lane >> 4) & 3; + const int b_n0 = wave * 32 + b_row4; + const int b_n1 = b_n0 + 16; + const int b_kqo = b_kq << 1; // qword offset of this 16-byte chunk + + // Iteration 18 (consolidate round, final HIP-only polish): every staging + // address inside the 64-stage loop is a loop-invariant base plus a constant + // per-stage byte stride, and every per-stage offset fits in 32 bits. + // * A: the row offset g_row*k (int64) is formed once (iteration 16); the + // stage-dependent part is a 32-bit running offset advanced by kStageK. + // * B: ((kg + b_kq)*n + n0 + b_n) << 4 for stage s is + // ((b_kq*n + n0 + b_n) << 4) + s*(kStageK/16)*n*16. The base is at most + // (3*2560 + 2559 + 127) << 4 = 165,856 and the running offset at most + // 63*163,840 + 165,856 = 10,487,776, both inside int32, so the compiler + // no longer emits a 64-bit v_mad_i64_i32 multiply per B operand per + // stage; only a 32-bit add. The two B pointers differ by exactly + // (b_n1 - b_n0) << 4 = 256 bytes, so one running offset serves both. + // * the LDS commit destinations are loop-invariant and hoisted as well. + // Same bytes from the same addresses into the same registers and LDS slots: + // every fragment byte and every int32 accumulator is bit-identical. + const int g_row = m0 + a_row; + const bool a_row_ok = (g_row < m); + const int64_t a_row_off = static_cast(g_row) * k; + int a_stage_off = a_col16; // s1 + a_col16, advanced by kStageK + const int a_step = kStageK; + int b_off = (b_kq * n + n0 + b_n0) << 4; // 32-bit packed-B byte offset + const int b_off_n1 = b_off + (16 << 4); + const int b_step = ((kStageK >> 4) * n) << 4; + int32_t* const adst = reinterpret_cast( + a_tile + a_row * kAStride + a_col16); + int64_t* const bdst0 = b_tile + b_n0 * kPackedBWords + b_kqo; + int64_t* const bdst1 = b_tile + b_n1 * kPackedBWords + b_kqo; - // VGPR payload for one full 64-K stage (3 x int4 = 12 VGPR). int4 vA, vB0, vB1; // Prologue: load stage 0 into registers, commit it to the single buffers, // and make it visible before the first burst. { - const int g_row = m0 + a_row; - vA = (g_row < m) - ? *reinterpret_cast(x_q + g_row * k + a_col16) - : int4{0, 0, 0, 0}; - vB0 = *reinterpret_cast( - packed_b + (n0 + b_n) * k + b_k16); - vB1 = *reinterpret_cast( - packed_b + (n0 + b_n + kBlockN128 / 2) * k + b_k16); + vA = a_row_ok ? *reinterpret_cast(x_q + a_row_off + a_stage_off) + : int4{0, 0, 0, 0}; + // Chunk c = (k/16)*n + n_idx holds packed_b[c*16 .. +15] = + // weight[16*(c/n) .. 16*(c/n)+15][c%n]; stage 0 uses k-chunks 0..3. + vB0 = *reinterpret_cast(packed_b + b_off); + vB1 = *reinterpret_cast(packed_b + b_off_n1); // Commit A as int32 stores (68-byte rows are 4 mod 16 -> no b128). - int32_t* adst = reinterpret_cast( - a_tile + a_row * kAStride128 + a_col16); adst[0] = vA.x; adst[1] = vA.y; adst[2] = vA.z; adst[3] = vA.w; - // Commit B as 16-byte vector stores: b_n*80 + b_k16 is 0 mod 16. - *reinterpret_cast(b_tile + b_n * kPackedBStride + b_k16) = vB0; - *reinterpret_cast( - b_tile + (b_n + kBlockN128 / 2) * kPackedBStride + b_k16) = vB1; + // Commit B as two 8-byte qword stores per 16-byte chunk (72-byte rows are + // 8 mod 16, so no single b128); n_local*72 + 16*b_kq is a multiple of 8. + bdst0[0] = pack_lo32_pair(vB0.x, vB0.y); + bdst0[1] = pack_lo32_pair(vB0.z, vB0.w); + bdst1[0] = pack_lo32_pair(vB1.x, vB1.y); + bdst1[1] = pack_lo32_pair(vB1.z, vB1.w); __syncthreads(); } for (int s = 0; s < num_stages; ++s) { // Consume the single buffer: each wave does eight m16n16k32 MMAs per - // 32-K step over its 32x64 quadrant. + // 32-K step over its own 32x64 quadrant. const int local_row = wave_row * 32; const int local_col = wave_col * 64; - const int8_t* abase = a_tile + local_row * kAStride128; - const int8_t* bbase = b_tile + local_col * kPackedBStride; - // Iteration 18 (fragment-pipeline round): software-pipeline the kk=32 - // fragment loads UNDER the kk=0 MMAC burst. The accepted kernel's exact - // ISA already prefetches the kk=32 A fragments this way, but its four - // kk=32 B ds_read_b64 loads are issued only AFTER MMACs 0-7, with - // s_waitcnt lgkmcnt(4..1) interleaved into MMACs 8-11 (16+ lgkmcnt waits - // per stage; PMC lds_wait 6,560,572 = 1.18x lds_instructions). Loading - // both 32-K steps' operands before the first v_mmac concentrates the LDS - // latency into one wait and leaves the 16-MMAC stream free of lgkmcnt - // waits. Same 12 ds_read instructions per stage, same fragment values, - // same int32 k0-outer/kk-inner accumulation order (the MMAC sequence per - // output tile is unchanged) -> bit-identical results. - du_load_matrix_sync(a_frag0, abase, kAStride128); - du_load_matrix_sync(a_frag1, abase + kTileM * kAStride128, kAStride128); - load_b_frag8(b_frag0, bbase, kPackedBStride); - load_b_frag8(b_frag1, bbase + kTileN * kPackedBStride, kPackedBStride); - load_b_frag8(b_frag2, bbase + 2 * kTileN * kPackedBStride, kPackedBStride); - load_b_frag8(b_frag3, bbase + 3 * kTileN * kPackedBStride, kPackedBStride); - DUFragment - a2_frag0, a2_frag1; - DUFragment - b2_frag0, b2_frag1, b2_frag2, b2_frag3; - du_load_matrix_sync(a2_frag0, abase + kTileK, kAStride128); - du_load_matrix_sync( - a2_frag1, abase + kTileM * kAStride128 + kTileK, kAStride128); - load_b_frag8(b2_frag0, bbase + kTileK, kPackedBStride); - load_b_frag8( - b2_frag1, bbase + kTileN * kPackedBStride + kTileK, kPackedBStride); - load_b_frag8( - b2_frag2, bbase + 2 * kTileN * kPackedBStride + kTileK, - kPackedBStride); - load_b_frag8( - b2_frag3, bbase + 3 * kTileN * kPackedBStride + kTileK, - kPackedBStride); - // kk = 0 burst (eight m16n16k32 MMAs). - du_mma_sync(acc00, a_frag0, b_frag0, acc00); - du_mma_sync(acc01, a_frag0, b_frag1, acc01); - du_mma_sync(acc02, a_frag0, b_frag2, acc02); - du_mma_sync(acc03, a_frag0, b_frag3, acc03); - du_mma_sync(acc10, a_frag1, b_frag0, acc10); - du_mma_sync(acc11, a_frag1, b_frag1, acc11); - du_mma_sync(acc12, a_frag1, b_frag2, acc12); - du_mma_sync(acc13, a_frag1, b_frag3, acc13); - // kk = 32 burst (same accumulators, same MMAC order as the accepted - // two-iteration kk loop). - du_mma_sync(acc00, a2_frag0, b2_frag0, acc00); - du_mma_sync(acc01, a2_frag0, b2_frag1, acc01); - du_mma_sync(acc02, a2_frag0, b2_frag2, acc02); - du_mma_sync(acc03, a2_frag0, b2_frag3, acc03); - du_mma_sync(acc10, a2_frag1, b2_frag0, acc10); - du_mma_sync(acc11, a2_frag1, b2_frag1, acc11); - du_mma_sync(acc12, a2_frag1, b2_frag2, acc12); - du_mma_sync(acc13, a2_frag1, b2_frag3, acc13); - // All consumption of the single buffer is complete only after every - // wavefront passes this barrier; the stage s+1 stores below overwrite - // that same buffer, so they must wait for it. + const int8_t* abase = a_tile + local_row * kAStride; + const int8_t* bbase = + reinterpret_cast(b_tile) + local_col * kPackedBStride; +#pragma unroll + for (int kk = 0; kk < kStageK; kk += kTileK) { + du_load_matrix_sync(a_frag0, abase + kk, kAStride); + du_load_matrix_sync(a_frag1, abase + kTileM * kAStride + kk, kAStride); + load_b_frag8(b_frag0, bbase + kk, kPackedBStride, lane); + load_b_frag8( + b_frag1, bbase + kTileN * kPackedBStride + kk, kPackedBStride, lane); + load_b_frag8( + b_frag2, bbase + 2 * kTileN * kPackedBStride + kk, kPackedBStride, + lane); + load_b_frag8( + b_frag3, bbase + 3 * kTileN * kPackedBStride + kk, kPackedBStride, + lane); + du_mma_sync(acc00, a_frag0, b_frag0, acc00); + du_mma_sync(acc01, a_frag0, b_frag1, acc01); + du_mma_sync(acc02, a_frag0, b_frag2, acc02); + du_mma_sync(acc03, a_frag0, b_frag3, acc03); + du_mma_sync(acc10, a_frag1, b_frag0, acc10); + du_mma_sync(acc11, a_frag1, b_frag1, acc11); + du_mma_sync(acc12, a_frag1, b_frag2, acc12); + du_mma_sync(acc13, a_frag1, b_frag3, acc13); + } + // Every wavefront must finish reading the single buffer before the next + // stage overwrites it. __syncthreads(); - // Load stage s+1 and commit it into the single buffer (fully exposed - // global-load latency, exactly as in the accepted iteration-3 kernel). + // Load stage s+1 from global and commit it into the same buffers. The + // `s + 1 < num_stages` guard is block-uniform, so the barrier below is + // reached by every thread in the block. if (s + 1 < num_stages) { - const int s1 = (s + 1) * kStageK128; - const int g_row = m0 + a_row; - vA = (g_row < m) - ? *reinterpret_cast(x_q + g_row * k + s1 + a_col16) + // Iteration 18: advance the hoisted 32-bit staging offsets by the + // compile-time per-stage strides instead of re-forming the full row / + // chunk index (and its 64-bit multiply) in every one of the 64 stages. + a_stage_off += a_step; + b_off += b_step; + vA = a_row_ok + ? *reinterpret_cast(x_q + a_row_off + a_stage_off) : int4{0, 0, 0, 0}; - vB0 = *reinterpret_cast( - packed_b + (n0 + b_n) * k + s1 + b_k16); - vB1 = *reinterpret_cast( - packed_b + (n0 + b_n + kBlockN128 / 2) * k + s1 + b_k16); - int32_t* adst = reinterpret_cast( - a_tile + a_row * kAStride128 + a_col16); + vB0 = *reinterpret_cast(packed_b + b_off); + vB1 = *reinterpret_cast(packed_b + b_off + (16 << 4)); adst[0] = vA.x; adst[1] = vA.y; adst[2] = vA.z; adst[3] = vA.w; - *reinterpret_cast(b_tile + b_n * kPackedBStride + b_k16) = vB0; - *reinterpret_cast( - b_tile + (b_n + kBlockN128 / 2) * kPackedBStride + b_k16) = vB1; - // Make the stage s+1 stores visible to every wavefront before the - // next burst. + bdst0[0] = pack_lo32_pair(vB0.x, vB0.y); + bdst0[1] = pack_lo32_pair(vB0.z, vB0.w); + bdst1[0] = pack_lo32_pair(vB1.x, vB1.y); + bdst1[1] = pack_lo32_pair(vB1.z, vB1.w); + // Make the stage s+1 stores visible before the next burst. __syncthreads(); } } const int base_row = m0 + wave_row * 32; const int base_col = n0 + wave_col * 64; - // Iteration 6 (epilogue round): coalesced stores -- register 4x4 - // transpose (8 shfl_xor -> ds_bpermute + 8 v_cndmask, no staging tile) - // then ONE 8-byte store per lane per fragment (4x fewer VMEM store - // instructions, 100% store sector utilization vs 25% for the scalar - // 2-byte stores). - store_prefill_fragment_coalesced( - acc00, x_scale, weight_scale, out, m, n, - base_row, base_col, lane); - store_prefill_fragment_coalesced( - acc01, x_scale, weight_scale, out, m, n, - base_row, base_col + kTileN, lane); - store_prefill_fragment_coalesced( - acc02, x_scale, weight_scale, out, m, n, - base_row, base_col + 2 * kTileN, lane); - store_prefill_fragment_coalesced( - acc03, x_scale, weight_scale, out, m, n, - base_row, base_col + 3 * kTileN, lane); - store_prefill_fragment_coalesced( - acc10, x_scale, weight_scale, out, m, n, - base_row + kTileM, base_col, lane); - store_prefill_fragment_coalesced( - acc11, x_scale, weight_scale, out, m, n, - base_row + kTileM, base_col + kTileN, lane); - store_prefill_fragment_coalesced( - acc12, x_scale, weight_scale, out, m, n, - base_row + kTileM, base_col + 2 * kTileN, lane); - store_prefill_fragment_coalesced( - acc13, x_scale, weight_scale, out, m, n, - base_row + kTileM, base_col + 3 * kTileN, lane); + store_prefill_fragment( + acc00, x_scale, weight_scale, out, m, n, base_row, base_col, lane); + store_prefill_fragment( + acc01, x_scale, weight_scale, out, m, n, base_row, base_col + kTileN, + lane); + store_prefill_fragment( + acc02, x_scale, weight_scale, out, m, n, base_row, + base_col + 2 * kTileN, lane); + store_prefill_fragment( + acc03, x_scale, weight_scale, out, m, n, base_row, + base_col + 3 * kTileN, lane); + store_prefill_fragment( + acc10, x_scale, weight_scale, out, m, n, base_row + kTileM, base_col, + lane); + store_prefill_fragment( + acc11, x_scale, weight_scale, out, m, n, base_row + kTileM, + base_col + kTileN, lane); + store_prefill_fragment( + acc12, x_scale, weight_scale, out, m, n, base_row + kTileM, + base_col + 2 * kTileN, lane); + store_prefill_fragment( + acc13, x_scale, weight_scale, out, m, n, base_row + kTileM, + base_col + 3 * kTileN, lane); } -// Generic scalar int8/int32 fallback for unmatched shapes and small-M API -// cases. One output element per grid-stride step; exact int32 accumulation -// (K <= 4096 keeps the int8 dot well within int32 range), then the fused -// float scale and bf16 store. -__global__ __launch_bounds__(256) void w8a8_scalar_gemm_kernel( +// Generic scalar int8/int32 fallback for every unmatched (m, n, k) and for +// small-M API cases (m < 128, including the paired M=2 shape). One output +// element per grid-stride step; exact int32 accumulation (the assigned K +// keeps the int8 dot well inside int32 range), then the fused float scale and +// bf16 store. +__global__ __launch_bounds__(kFallbackThreads) void w8a8_scalar_gemm_kernel( const int8_t* __restrict__ a, const int8_t* __restrict__ b, const float* __restrict__ x_scale, @@ -883,19 +626,22 @@ __global__ __launch_bounds__(256) void w8a8_scalar_gemm_kernel( const int row = static_cast(idx / n); const int col = static_cast(idx - static_cast(row) * n); const int8_t* a_row = a + static_cast(row) * k; - // Iteration 5 (packing round): the exact assigned shape (K=4096, - // N=2560) uses the packed n-major layout packed[n][k] (set up once by - // launch_pack_w8a8_weight); every other (k, n) keeps the logical - // [K, N] row-major layout. The branch is grid-uniform per launch. + // Iteration 2 (operand-reuse round): the exact assigned shape + // (K=4096, N=2560) is packed by launch_pack_w8a8_weight into the chunked + // packed16[(k/16) * N + n] layout, so this fallback must decode that layout + // for the paired M=2 API shape: element (kk, col) lives at + // packed16[((kk >> 4) * n + col) * 16 + (kk & 15)]. Every other (k, n) + // keeps the logical [K, N] row-major layout. Grid-uniform per launch. const bool packed_b = (k == kPackedBK && n == kPackedBN); - const int8_t* b_col = - packed_b ? b + static_cast(col) * k : b + col; + const int8_t* b_col = b + col; int32_t acc = 0; for (int kk = 0; kk < k; ++kk) { - const int32_t bw = packed_b - ? static_cast(b_col[kk]) - : static_cast( - b_col[static_cast(kk) * n]); + const int32_t bw = + packed_b + ? static_cast( + b[(static_cast(kk >> 4) * n + col) * 16 + + (kk & 15)]) + : static_cast(b_col[static_cast(kk) * n]); acc += static_cast(a_row[kk]) * bw; } const float scaled = static_cast(acc) * @@ -906,7 +652,7 @@ __global__ __launch_bounds__(256) void w8a8_scalar_gemm_kernel( // Identity device-to-device byte copy (pack_weight bootstrap, valid for any // (K, N)). -__global__ __launch_bounds__(256) void w8a8_pack_identity_i8_kernel( +__global__ __launch_bounds__(kPackThreads) void w8a8_pack_identity_i8_kernel( const int8_t* __restrict__ src, int8_t* __restrict__ dst, int64_t count) { @@ -920,7 +666,7 @@ __global__ __launch_bounds__(256) void w8a8_pack_identity_i8_kernel( } // Identity device-to-device float copy for the weight scales. -__global__ __launch_bounds__(256) void w8a8_pack_identity_f32_kernel( +__global__ __launch_bounds__(kPackThreads) void w8a8_pack_identity_f32_kernel( const float* __restrict__ src, float* __restrict__ dst, int64_t count) { @@ -933,45 +679,54 @@ __global__ __launch_bounds__(256) void w8a8_pack_identity_f32_kernel( } } -// Iteration 5 (packing round): one-time n-major transpose of the logical -// [K, N] row-major weight into packed[n][k] for the exact assigned shape -// (K=4096, N=2560). Runs only from launch_pack_w8a8_weight, outside the -// timed region and outside Graph capture. The packed buffer keeps the same -// byte count (k*n) as the identity pack, so allocations and graph-stable -// addresses are unchanged. Element (k, n) of the logical weight lands at -// packed[n * K + k]; each thread copies one 16-byte k-run (coalesced read -// side; the strided write side is off the critical path). The guard -// guarantees n % 16 == 0, so every 16-byte chunk lies inside one logical -// row and the transpose is exact. -__global__ __launch_bounds__(256) void w8a8_pack_nmajor_b_kernel( +// Iteration 2 (operand-reuse round): one-time 16-byte-chunk packing of the +// logical [K, N] row-major weight into packed16[(k/16) * N + n], where chunk +// c holds packed16[c*16 + i] = weight[(c / N) * 16 + i][c % N] for i in 0..15. +// Runs only from launch_pack_w8a8_weight, outside the timed region and outside +// Graph capture. The packed buffer keeps the same byte count (k*n) as the +// identity pack, so allocations and graph-stable addresses are unchanged. Each +// thread owns one 16-byte chunk: the 16 strided byte gathers (stride N) are +// fully coalesced across the wavefront's threads and the 16-byte store is +// contiguous, so the pack stays bandwidth-clean. The guard requires +// k % 16 == 0, true for the exact assigned shape. +__global__ __launch_bounds__(kPackThreads) void w8a8_pack_k16chunk_b_kernel( const int8_t* __restrict__ src, int8_t* __restrict__ dst, int k, int n) { - const int64_t chunks = (static_cast(k) * n) >> 4; + const int64_t chunks = (static_cast(k) >> 4) * n; const int64_t stride = static_cast(gridDim.x) * blockDim.x; for (int64_t c = static_cast(blockIdx.x) * blockDim.x + threadIdx.x; c < chunks; c += stride) { - const int64_t off = c << 4; - const int kk = static_cast(off / n); - const int col = static_cast(off - static_cast(kk) * n); - const int4 v = *reinterpret_cast(src + off); - const int8_t* bytes = reinterpret_cast(&v); + const int64_t kg = c / n; + const int n_idx = static_cast(c - kg * n); + const int8_t* scol = src + (kg << 4) * n + n_idx; + int8_t* d = dst + (c << 4); #pragma unroll for (int i = 0; i < 16; ++i) { - // Logical element (k = kk, n = col + i) lands at packed[(col+i)*K+kk]. - dst[static_cast(col + i) * k + kk] = bytes[i]; + d[i] = scol[static_cast(i) * n]; } } } +inline int64_t capped_blocks(int64_t elements, int threads) { + int64_t blocks = (elements + threads - 1) / threads; + if (blocks > 4096) { + blocks = 4096; + } + if (blocks < 1) { + blocks = 1; + } + return blocks; +} + } // namespace // Stable host launch symbol consumed by csrc/bindings.cpp. Dispatches on the -// caller-provided stream only; never allocates, synchronizes, or touches the -// default stream. +// caller-provided stream only; never allocates, synchronizes, packs, or +// touches the default stream. extern "C" void launch_w8a8_gemm( const int8_t* a, const int8_t* b, @@ -991,16 +746,18 @@ extern "C" void launch_w8a8_gemm( } auto* out_ptr = static_cast(out); - // Iteration 5 (packing round): exact assigned shape (K=4096, N=2560) - // routes to the packed n-major B variant of the 64x128 kernel. The guard - // is exact (m >= 128, n == 2560, k == 4096) and sits BEFORE the generic - // 64x128 path; every other shape keeps its existing path (generic 64x128 - // for n % 128 == 0, 64x64 for n % 64 == 0, scalar fallback otherwise) and - // the identity-packed layout. + // Iteration 1/2 (packed_weight family): the exact assigned shape + // (K=4096, N=2560) routes to the packed 16-byte-chunk B variant of the + // 64x128 kernel, whose B operand comes from the one-time packed16[(k/16)*N+n] + // layout prepared by launch_pack_w8a8_weight. The guard is exact (m >= 128, + // n == 2560, k == 4096) and sits BEFORE the generic path; every other + // (m, n, k) keeps its existing path and the identity-packed layout. The + // m >= 128 guard keeps the paired M=2 API shape on the scalar fallback, + // which decodes the chunked layout for this exact (k, n). if (m >= 128 && n == kPackedBN && k == kPackedBK) { const dim3 grid( - static_cast(n / kBlockN128), - static_cast((m + kBlockM128 - 1) / kBlockM128)); + static_cast(n / kBlockN), + static_cast((m + kBlockM - 1) / kBlockM)); const dim3 block(static_cast(kThreadsPerBlock)); hipLaunchKernelGGL( w8a8_dumma_prefill_64x128_packedb_kernel, @@ -1019,44 +776,17 @@ extern "C" void launch_w8a8_gemm( return; } - // Native INT8 DUMMA m16n16k32 prefill path (2-D macro-tile 64x128, - // single-buffered 64-K K stage; pipeline-round control arm) for every - // large-M shape with compatible geometry (covers the assigned M=4096, - // N=2560, K=4096 shape: 2560 % 128 == 0). The 128-K guard is written - // explicitly so the dispatch condition is bit-identical to the accepted - // iteration-1 kernel even though the internal K stage is now 64 - // (single-buffered). - if (m >= 128 && (n % kBlockN128) == 0 && (k % 128) == 0) { - const dim3 grid( - static_cast(n / kBlockN128), - static_cast((m + kBlockM128 - 1) / kBlockM128)); - const dim3 block(static_cast(kThreadsPerBlock)); - hipLaunchKernelGGL( - w8a8_dumma_prefill_64x128_kernel, - grid, - block, - 0, - stream, - a, - b, - x_scale, - weight_scale, - out_ptr, - m, - n, - k); - return; - } - - // Native INT8 DUMMA m16n16k32 prefill path (64x64 tile) for large-M shapes - // whose N is not a multiple of 128 but is a multiple of 64. + // Native INT8 DUMMA m16n16k32 prefill path for every large-M shape with + // compatible geometry (covers the assigned M=4096, N=2560, K=4096 shape: + // 2560 % 128 == 0 and 4096 % 64 == 0). The explicit m >= 128 guard keeps + // the small-M API shapes on the scalar fallback. if (m >= 128 && (n % kBlockN) == 0 && (k % kStageK) == 0) { const dim3 grid( static_cast(n / kBlockN), static_cast((m + kBlockM - 1) / kBlockM)); const dim3 block(static_cast(kThreadsPerBlock)); hipLaunchKernelGGL( - w8a8_dumma_prefill_64x64_kernel, + w8a8_dumma_prefill_64x128_kernel, grid, block, 0, @@ -1074,15 +804,7 @@ extern "C" void launch_w8a8_gemm( // Scalar int8/int32 fallback for unmatched (m, n, k) and small-M API cases. const int64_t total = static_cast(m) * n; - constexpr int kFallbackThreads = 256; - int64_t blocks = (total + kFallbackThreads - 1) / kFallbackThreads; - if (blocks > 4096) { - blocks = 4096; - } - if (blocks < 1) { - blocks = 1; - } - const dim3 grid(static_cast(blocks)); + const dim3 grid(static_cast(capped_blocks(total, kFallbackThreads))); const dim3 block(kFallbackThreads); hipLaunchKernelGGL( w8a8_scalar_gemm_kernel, @@ -1100,12 +822,19 @@ extern "C" void launch_w8a8_gemm( k); } -// Stable host launch symbol consumed by csrc/bindings.cpp. For the exact -// assigned shape (K=4096, N=2560) it performs the one-time n-major B pack -// (logical [K, N] -> packed[n][k]) outside the timed region and outside -// Graph capture; every other (K, N) keeps the identity device-to-device -// copy, valid for every (K, N). The packed buffer size is k*n in both -// cases, so the allocation and graph-stable packed layout are unchanged. +// Stable host launch symbol consumed by csrc/bindings.cpp. Iteration 2 +// (operand-reuse round) behaviour: for the exact assigned pair (K=4096, +// N=2560) it performs the one-time 16-byte-chunk packing +// packed16[(k/16) * N + n] (outside the timed region and Graph capture) and +// for every other (K, N) the identity device-to-device copy of the logical +// [K, N] int8 weight. The N fp32 weight scales are copied identically in both +// cases. Both layouts keep the same byte count as the raw weight, so +// allocation and graph-stable addresses are unchanged. +// +// Iteration 3 (fix_build round) correction: this host call previously +// referenced the removed w8a8_pack_nmajor_b_kernel symbol, which was the only +// compile error of the iteration-2 candidate. It now binds the defined +// w8a8_pack_k16chunk_b_kernel; nothing else in this file changed. extern "C" void launch_pack_w8a8_weight( const int8_t* raw_weight, const float* weight_scale, @@ -1114,19 +843,22 @@ extern "C" void launch_pack_w8a8_weight( int k, int n, hipStream_t stream) { - constexpr int kPackThreads = 256; const int64_t weight_count = static_cast(k) * n; if (weight_count > 0) { if (k == kPackedBK && n == kPackedBN) { + // Iteration 2 (operand-reuse round): exact assigned shape + // (K=4096, N=2560) -> one-time 16-byte-chunk packing + // packed16[(k/16) * N + n]. Same k*n byte count as the identity pack, so + // the allocation and the graph-stable packed layout are unchanged. + // Outside the timed region. const int64_t chunks = weight_count >> 4; - int64_t blocks = (chunks + kPackThreads - 1) / kPackThreads; - if (blocks > 4096) { - blocks = 4096; - } + const dim3 grid( + static_cast(capped_blocks(chunks, kPackThreads))); + const dim3 block(kPackThreads); hipLaunchKernelGGL( - w8a8_pack_nmajor_b_kernel, - dim3(static_cast(blocks)), - dim3(kPackThreads), + w8a8_pack_k16chunk_b_kernel, + grid, + block, 0, stream, raw_weight, @@ -1134,14 +866,13 @@ extern "C" void launch_pack_w8a8_weight( k, n); } else { - int64_t blocks = (weight_count + kPackThreads - 1) / kPackThreads; - if (blocks > 4096) { - blocks = 4096; - } + const dim3 grid( + static_cast(capped_blocks(weight_count, kPackThreads))); + const dim3 block(kPackThreads); hipLaunchKernelGGL( w8a8_pack_identity_i8_kernel, - dim3(static_cast(blocks)), - dim3(kPackThreads), + grid, + block, 0, stream, raw_weight, @@ -1150,20 +881,18 @@ extern "C" void launch_pack_w8a8_weight( } } if (n > 0) { - int64_t blocks = (static_cast(n) + kPackThreads - 1) / - kPackThreads; - if (blocks > 4096) { - blocks = 4096; - } + const dim3 grid( + static_cast(capped_blocks(n, kPackThreads))); + const dim3 block(kPackThreads); hipLaunchKernelGGL( w8a8_pack_identity_f32_kernel, - dim3(static_cast(blocks)), - dim3(kPackThreads), + grid, + block, 0, stream, weight_scale, packed_weight_scale, - n); + static_cast(n)); } } // @@end diff --git a/metainfer/tasks/dcu_kernel_auto_opt/variant/int8w8a8-gemm/hy3/TP8/M16/qkv_proj.hip b/metainfer/tasks/dcu_kernel_auto_opt/variant/int8w8a8-gemm/hy3/TP8/M16/qkv_proj.hip index e0c06dd2..3c5fe22e 100644 --- a/metainfer/tasks/dcu_kernel_auto_opt/variant/int8w8a8-gemm/hy3/TP8/M16/qkv_proj.hip +++ b/metainfer/tasks/dcu_kernel_auto_opt/variant/int8w8a8-gemm/hy3/TP8/M16/qkv_proj.hip @@ -1,459 +1,467 @@ -// @@variant shape=hy3_tp8_qkv_proj_m16 commit=f2b771e350c9fea3b13d59b5fec4219786976bcc added=2026-09-09 -// median_us=19.3 p90_us=19.32 speedup=3.875 baseline_us=74.78 -// source=hy3-dsh-tp8-m16-9-8-0161e718 -// W8A8 INT8 GEMM HIP implementation for Hygon K500SM_AI / gfx928. +// @@variant shape=hy3_tp8_qkv_proj_m16 commit=a1271e87052a802cf1a620ad5f58ba9d526103cd added=2026-09-17 +// median_us=14.43 p90_us=14.44 speedup=5.183 baseline_us=74.78 +// source=hy3-tp8-m16-test9-16-619177f8 +// Initial correctness-first INT8 W8A8 GEMM for gfx928 (K500SM_AI). // -// Worker: worker_0 (physical GPU 0), shape hy3_tp8_qkv_proj_m16 -// M = 16, N = 1280, K = 4096 (logical weight layout [K, N]). +// Bootstrap strategy (iteration 0): one plain scalar kernel computes the +// complete int32 dot product for every output element. There is no DUMMA, no +// split-K, no LDS staging, no inline assembly, no software pipelining and no +// autotuning. The only goals are: +// * compile fast and portably for gfx928, +// * launch on PyTorch's current HIP stream with no host/device sync, +// * be exact against bf16((A.float() @ B.float()) * x_scale * w_scale.T). // -// Iteration 0 (bootstrap): correctness-first scalar kernel (one thread per -// output element) served every shape and measured 336 us on this shape. +// Layout contract for this iteration (identity pack): +// a : int8 [M, K] row-major (x_q) +// b : int8 [K, N] row-major (packed_weight) +// x_scale : fp32 [M] +// weight_scale : fp32 [N] +// out : bf16 [M, N] row-major // -// Iteration 1 (DUMMA bootstrap, this round): replace the scalar kernel for -// the exact guard (m, n, k) == (16, 1280, 4096) with the minimal native -// gfx928 DUMMA INT8 m16n16k32 tile per the round mandate: -// - one 64-lane wavefront per block, one 16x16 output tile per block -// (grid = N/16 = 80 blocks), no cross-wave barrier; -// - A and B fragments are loaded DIRECTLY from their row-major global -// tensors every 32-K step (no LDS staging, no split-K, no workspace, -// no combine kernel in the timed region); -// - explicit int32 accumulation: du_fill_fragment(acc, 0) then one -// du_mma_sync per ascending 32-K step (exact int32 dot, same sum as -// the scalar kernel since int32 addition is exact and order-free); -// - single-wave LDS epilogue: du_store_matrix_sync materializes the -// 16x16 int32 tile in LDS (library-defined ownership), one -// __syncthreads() reached by all 64 threads, then the 64 lanes scale -// with x_scale[row] * weight_scale[n0+col] (scalar-path float order) -// and store bf16 through the manual round-to-nearest-even conversion. -// This establishes the exact DUMMA API family below so later rounds can -// tune geometry (wavefronts/block, LDS staging, split-K planes). +// launch_pack_w8a8_weight is an identity device-to-device copy of both the +// int8 weight and the fp32 weight scale. Later Parallel explore rounds may +// change that packing and the matching GEMM interpretation. // -// Iteration 2 (architecture round, this round): measure grid parallelism -// before polishing. The iteration-1 geometry (80 one-wave blocks over 120 -// CUs, 0.67 blocks/CU) is far below the two-blocks-per-CU latency-hiding -// target, and the measured 130.43 us median (vs 74.782 us Triton Graph -// baseline) is dominated by underfilled CUs and per-block serial load->mma -// chains (128 direct-load 32-K steps per block). This round replaces the -// exact-guard dispatch with a grid-level split-K=3 geometry, still one -// 64-lane wavefront per block and one 16x16 N tile per wave (zero in-block -// barriers): -// - grid = (N/16) * SPLIT_K = 80 * 3 = 240 blocks = exactly 2 blocks per -// device CU (120 CUs), split id on gridDim.y, tile id on gridDim.x; -// - the 4,096-long K axis is partitioned into three legal 32-aligned, -// non-uniform, ascending slices of {1376, 1376, 1344} rows (43/43/42 -// m16n16k32 steps), each block du_fill_fragment(acc,0) then runs its -// own ascending 32-K DUMMA chain over its slice only (bit-exact int32 -// partial dot: |partial| <= 6.6e7 << 2^31, order-free); -// - each block materializes its 16x16 int32 partial (LDS epilogue, same -// single-wave store/barrier pattern as iteration 1) into its own plane -// of the caller's int32 workspace: partials[split][16][N]; every launch -// overwrites every plane, so no workspace clear is needed; -// - a small combine kernel (80 blocks x 64 lanes, second launch on the -// same stream inside the timed API) sums the SPLIT_K planes ascending -// per output element (exact int32, identical to the scalar k-ascending -// sum) and applies x_scale[row] * weight_scale[col] then bf16 RNE, -// keeping every output bit-identical to the CPU reference. -// Split-K combine is part of the operator wall and stays inside the timed -// region/Graph. Workspace need = SPLIT_K*M*N*4 = 245,760 B, well under the -// contract's 16-plane 1,310,720 B allocation for this shape. +// Iteration 1 established the minimal native gfx928 DUMMA INT8 m16n16k32 +// tile: ONE 64-lane wavefront per block, one 16x16 output tile per block, +// grid = N/16 = 80 one-wave blocks, blockDim 64, A/B fragments loaded +// directly from row-major global at every 32-K step, explicit int32 +// accumulation, and a single-wave LDS epilogue. It reached 119.72 us +// (official best), well behind the 74.78 us Triton graph baseline. // -// Iteration 3 (architecture round, this round): continue the mandated -// split-K occupancy sweep. Iteration 2's falsifiable claim held (240 -// one-wave blocks = 2 blocks/CU cut the wall from 130.43 to 68.65 us -// median, 1.089x over the 74.782 us Triton Graph baseline), but the -// measured logical rate (86 GB/s, 2.44 TOPS) is still far below the -// headroom of this decode family, so the sweep goes finer inside the -// trusted CU-aligned probe set {2,3,4,5,6,8,9,12}: SPLIT_K=6 -> grid = -// 80 N tiles x 6 K-slices = 480 one-wave zero-in-block-barrier blocks = -// exactly 4 blocks per device CU, and each block's serial 32-K chain -// halves to {22,22,21,21,21,21} steps = rows {704,704,672,672,672,672} -// (the existing generic slice formula; disjoint, ascending, 32-aligned, -// exact cover of [0,4096)). The partial and combine kernels are -// UNCHANGED code -- both were already generic over gridDim.y / -// split_count -- so this round only moves the launcher geometry constant -// kSplitK from 3 to 6. Workspace need = 6*M*N*4 = 491,520 B, still -// inside the caller's 16-plane 1,310,720 B allocation; the combine kernel -// (80 blocks x 64 lanes) sums the six planes ascending per output element -// (exact int32 -> bit-identical to the scalar k-ascending sum) and stays -// in the timed Graph. +// Iteration 2 (architecture: launch-geometry exploration). +// PMC/ISA evidence for that 80-block grid: +// * grid_blocks = 80 for device_cu_count = 120 -> 40 CUs idle and at most +// one resident wave per busy CU, far below the two-blocks-per-CU +// latency-hiding target; +// * the disassembled K-loop is a serial chain -- 16 x global_load_ubyte, +// a run of s_waitcnt vmcnt(N...) drains, then exactly one v_mmac -- so +// each wave stalls on global latency every 32-K step with no other wave +// on the CU to cover it; +// * counter-derived HBM bandwidth is ~45 GB/s, i.e. the device is nowhere +// near memory-bandwidth bound: the kernel is latency/underfill bound. +// The N dimension only has N/16 = 80 tiles and M is already the full 16, so +// the single geometry that can raise independent block count is split-K. +// This round therefore keeps the complete one-wave / one-N-tile geometry +// (waves_per_block = 1, adjacent_n_tiles = 1, no cross-wave barrier, direct +// global fragment loads, one v_mmac per 32-K step) and adds a 3-way split of +// the 128 stage-aligned (k/32) K-steps: +// * grid = (N/16) * 3 = 240 one-wave blocks for 120 CUs => exactly two +// independent blocks per CU, the mandated latency-hiding target; +// * split s owns stages [s*T/S, (s+1)*T/S) with T = k/32 = 128 and S = 3, +// i.e. 42/43/43 stages of 32 K each -- every range stays 32-aligned, so +// the stage-alignment constraint and the workspace constraint (S <= 16 +// planes) both hold; +// * each block accumulates its K sub-range exactly in int32 and stores the +// 16x16 partial into plane s of the caller-provided workspace +// (int32 [S][M][N]); a separate 128-thread combine kernel sums the S +// int32 planes (exact, order-free integer addition), applies +// static_cast(acc) * x_scale[row] * weight_scale[col] in the same +// order as the scalar fallback, and writes bf16 -- so the output stays +// bit-identical while the combine is a cheap L2-resident second launch. // -// Iteration 4 (architecture/pipeline round, this round): multi-N-tile -// reuse. The iteration-3 code object (PMC digest 26d6df45...) shows the -// poison direct-load pattern per 32-K step: 16 global_load_ubyte (8 per -// A/B fragment) with ~10 scattered s_waitcnt vmcnt before byte reassembly -// and one v_mmac_i32_16x16x32_i8, and no cross-iteration load overlap, so -// at 4 resident one-wave blocks/CU every block's serial chain is one -// ~load-latency step per m16n16k32 mma (52.19 us median; the wall is the -// per-step latency of a one-mma step, not raw issue or HBM traffic). -// This round keeps the same split-K partial + combine architecture and -// the same 4 blocks/CU co-residency but widens each one-wave block to TWO -// adjacent 16-wide N tiles (32 columns): per 32-K step the wave loads the -// A fragment ONCE and issues two du_mma_sync (independent accumulators), -// so the x_q A bytes [16 x 32] of the shared step are reused across the -// two output tiles (per-output A global loads/reassembly halve) while -// each B byte is still read exactly once (no B reuse). To keep 480 -// one-wave blocks = exactly 4/CU over 120 CUs the grid becomes 40 N -// groups x SPLIT_K=12 K-slices ({11x8, 10x4} 32-K steps = rows -// {352x8, 320x4}, disjoint ascending exact cover of [0,4096), every -// boundary a DUMMA-k multiple; existing generic slice formula). Per-block -// serial chain drops to <= 11 steps of TWO mmas, so if the iteration-3 -// wall is per-step load->mma latency the per-output step latency halves -// and the unprofiled Graph median/P90 must drop materially below -// 52.194/52.269 us. Workspace need = 12*M*N*4 = 983,040 B, still inside -// the caller's 16-plane 1,310,720 B allocation; the combine kernel (80 -// blocks x 64 lanes) is UNCHANGED code and sums the twelve planes -// ascending per output element (exact int32 -> bit-identical to the -// scalar k-ascending sum), staying in the timed Graph. The only edits -// are the widened per-wave N tile in the partial kernel plus the launcher -// geometry; the workspace-less fallback keeps the iteration-1 one-wave -// direct kernel byte-for-byte and the scalar fallback still serves every -// unmatched shape including the paired M=2 API shape. +// Iteration 3 (architecture: split-K occupancy sweep). +// The iteration-2 falsifiable claim held: the 240 one-wave blocks (2 blocks +// per CU) cut the wall to 47.81 us median, 1.564x over the 74.78 us Triton +// Graph baseline, so the 80-block underfill was real. The current-best +// PMC still reports grid_blocks = 240 for device_cu_count = 120 -- only TWO +// of the four SIMDs of every CU ever hold a resident wave -- and the +// counter-derived HBM rate is still just 123.8 GB/s, far below device +// bandwidth, so the kernel remains latency/occupancy bound rather than +// bandwidth bound. The trusted CU-aligned probe set for this shape is +// {2,3,4,5,6,8,9,12}; with N/16 = 80 one-wave N tiles the grid is 80*S, so +// the CU-aligned splits are the multiples of 3 (80*S must be a multiple of +// 120). split-K=2 yields only 160 blocks = 1.33 blocks/CU -- still below +// the mandated two-blocks-per-CU target -- so it is rejected, and the next +// CU-aligned candidate is split-K=6: +// * grid = (N/16) * 6 = 480 one-wave blocks for 120 CUs => exactly FOUR +// independent one-wave blocks per CU, i.e. one resident wave on each of +// the four SIMDs, instead of 2 of 4 in iteration 2; +// * the 128 32-aligned K-stages are partitioned 21/21/22/21/21/22 (rows +// 672/672/704/672/672/704) -- disjoint, ascending, exact cover of +// [0, 4096), every boundary a multiple of 32 -- so the stage-alignment +// and exact-int32 contracts are unchanged; +// * workspace need = 6 * M * N * 4 = 491,520 B, inside the contract's +// 16-plane 1,310,720 B allocation; the partial kernel, its plane +// indexing and the combine+scale kernel are already generic in split_k, +// so the only source change is the launcher geometry constant +// kSplitKTarget 3 -> 6 (the combine stays inside the timed Graph). +// Falsifiable claim: if the two idle SIMDs per CU are the limiter, the +// per-block serial 32-K chain halves (43 -> ~21 steps) while the number of +// resident waves doubles, so the official Graph median/P90 must drop +// materially below the 47.81/47.85 us iteration-2 best (expected ~34-40 us, +// with the extra 2 partial planes and the slightly longer combine cost +// included). If it stays flat or regresses, the wall is the per-step +// load->wait->reassemble->mma latency or per-CU issue rather than SIMD +// occupancy, and the next round must attack the 16 direct global byte loads +// per stage instead of the split factor. // -// Iteration 5 (packed-weight/staging round, this round): B-only LDS slice -// staging. The iteration-4 code object (PMC digest 1a4e30d0..., vmem_read -// 122,880 = 24 byte-granular global_load_ubyte per 32-K step x 5120 -// block-steps) shows the remaining wall is the poison direct-global B half -// of the per-step load->wait->reassembly->mma chain: per step the wave -// still pays 16 global byte loads (B0/B1) plus ~10 scattered s_waitcnt -// vmcnt on the MMA critical path even though every byte is L2-hot in the -// timed Graph replay. Per the lineage rule, direct-global fragment loads -// are poison: this round removes the B half by staging each block's whole -// 32-column B slice (<= 352 rows x 32 B = 11,264 B) into LDS ONCE at block -// start with 16-B int4 cooperative loads (64 lanes x 16 B per wavefront -// instruction, 4-deep source batching so one vmcnt drain covers 4 -// in-flight round trips), one same-wave __syncthreads(), then every -// 32-K step reads the B0/B1 fragments from LDS with du_load_matrix_sync -// (~30-cycle LDS latency, no global round trip) at row stride 32 -- the -// staged tile is an exact byte mirror of the block's global B strip, so -// fragment contents, the k-ascending int32 order and the partial planes -// are bit-identical to iteration 4. A stays direct-global on purpose: -// this round isolates the B-side hypothesis (A-only staging and the -// packed [N,K] layout remain untested axes). Geometry is UNCHANGED (40 -// N groups x SPLIT_K=12 = 480 one-wave blocks = exactly 4 blocks/CU over -// 120 CUs; per-block LDS 11,264 B + 2 x 1,024 B acc tiles = 13,312 B, -// 65,536/13,312 = 4.9, so the 4/CU co-residency is preserved and LDS does -// not become the occupancy limiter). If the per-step wall is the B-load -// half of the global wait ladder, the official unprofiled Graph median/P90 -// must drop materially below 41.154/41.233 us (partial-kernel profiled -// 36.32 us); if instead the median stays flat or regresses, the wall at 4 -// waves/CU is A-side latency or per-CU issue (16 vs 24 loads immaterial), -// and the next round must stage A too (LDS-only K loop) or raise -// co-residency -- never a scalar-fallback change. Exactness is preserved -// for every shape: the (m,n,k)==(16,1280,4096) guard is unchanged; the -// staged bytes are a copy of the same global bytes the direct loader -// read, the workspace-less fallback keeps the iteration-1 direct kernel -// byte-for-byte (still correct for this shape), the scalar fallback still -// serves every unmatched shape including the paired M=2 API shape, and no -// pack/layout/env/stream/config change is made; all launches stay on the -// caller stream inside the timed API/Graph. +// Iteration 4 (accepted, architecture/pipeline: multi-N-tile reuse). +// The iteration-3 result falsified its own prediction: moving kSplitKTarget +// 3 -> 6 cut the wall only 47.81 -> 45.08 us (-5.7%) instead of the +// predicted ~34-40 us, so doubling the resident one-wave blocks per CU did +// NOT remove the limiter. The reason is an invariance the split factor +// cannot break: the total m16n16k32 mma count is fixed at (N/16)*(K/32) = +// 80*128 = 10,240, so per device SIMD (480 of them) the number of serial +// load->wait->reassemble->mma steps is 10,240/480 = 21.3 for ANY split +// factor -- finer splits only shorten each block while multiplying the +// block count by the same amount. The measured PMC still shows the poison +// per-step pattern: 16 global_load_ubyte (8 for the A fragment + 8 for the +// single B fragment) followed by a ladder of scattered s_waitcnt vmcnt and +// exactly ONE v_mmac per 32-K step, with no cross-iteration load overlap. +// This round therefore keeps the split-K partial+combine architecture and +// the 480-block / 4-blocks-per-CU co-residency but widens every one-wave +// block to TWO adjacent 16-wide N tiles (32 N columns): +// * per 32-K step the wave loads the A fragment (16 m-rows x 32 K bytes = +// 512 B of x_q) EXACTLY ONCE and issues TWO independent du_mma_sync +// calls -- one per 16-wide N tile -- so those A bytes are REUSED across +// both output tiles (the per-output-tile A global loads and their +// wait/reassembly cost halve); +// * every B byte (k x 32 N bytes per block slice) is still read exactly +// once -- B has no reuse (each N column belongs to exactly one block); +// * the 128 32-aligned K stages are split 12 ways and each block owns TWO +// output tiles, so the grid is (N/(16*2)) * 12 = 40 * 12 = 480 one-wave +// blocks for 120 CUs = exactly 4 blocks/CU, i.e. one resident wave per +// SIMD, identical co-residency to iteration 3 (enough blocks to cover +// every CU); +// * each block's serial chain drops from ~21 single-mma steps to <= 11 +// two-mma steps (the 128 stages partition 8 x 11 + 4 x 10 = rows +// 352 x 8 + 320 x 4, disjoint, ascending, exact 32-aligned cover of +// [0, 4096)), so if the wall is the per-step global latency then the +// latency paid per m16n16k32 mma is halved; +// * workspace need = 12 * M * N * 4 = 983,040 B, inside the contract's +// 16-plane 1,310,720 B allocation; the partial kernel body, its plane +// indexing and the combine+scale kernel stay generic in split_k (the +// runtime clamp still shrinks split_k to the supplied workspace), and +// the combine stays inside the timed Graph. +// Falsifiable claim: if the per-step direct-global load->wait->mma latency +// is the limiter, the per-output A-load cost halves and the serial chain +// per block halves, so the official Graph median/P90 must drop materially +// below the 45.08/45.17 us iteration-3 best (expected ~33-39 us, with the +// six extra partial planes and the longer 12-way combine included). If it +// stays flat or regresses, the limiter is L2/HBM-side byte traffic or the +// per-step issue of the byte-granular loads themselves rather than latency +// amortization, and the next round must stage the operands in LDS. // -// Iteration 6 (infrastructure failure, no valid artifact): the round-6 -// attempt was killed by the agent infrastructure before it produced a -// candidate result; no round-6 source change was accepted and the session -// was reset to the iteration-5 accepted best source below. The failure does -// not count as a completed optimization round. +// Iteration 5 (measured and NOT accepted: B-only LDS staging). +// Staging only the B operand into a per-block LDS mirror (16-B int4 +// cooperative copy + one same-wave barrier, then du_load_matrix_sync from LDS +// at row stride 32) left the loop time unchanged and paid the serial staging +// prologue on top: the official Graph median/P90 moved 30.374/30.420 -> +// 31.309/31.350 us (+3.1%), so the round was rejected and the source was +// reverted to the iteration-4 best recorded here. The falsification is +// informative: removing two thirds of the per-step global byte loads (the 16 +// B loads of the 24) did NOT shorten the per-step chain, so the residual +// serialized operand fetch on the MMA critical path is the A half, and the +// B-only design additionally left a mixed dual-latency step (LDS B + global +// A) whose LDS reads replay the same bank 4x at the iteration-5 row pitch. // -// Iteration 7 (staging round, this round): A+B whole-slice LDS staging -// (LDS-only K loop). The iteration-5 result (39.404/39.485 us official -// Graph median/P90 vs iteration-4 41.154/41.233) falsified its own claim: -// the predicted 25-33 us for the B-half hypothesis did not appear, so per -// the iteration-5 contingency text this round stages the shared A operand -// too. The iteration-5 PMC (source digest 5fb42aff..., grid 480 = 40 N -// groups x SPLIT_K=12, vmem_read 46,080, l2_misses 98,879, counter-derived -// HBM ~187.9 GB/s << peak) still shows 8 byte-granular global_load_ubyte of -// the shared A fragment plus ~8 scattered s_waitcnt vmcnt before shift/or -// reassembly on the MMA path every 32-K step (~40,960 of the 46,080 -// vmem_read over 5,120 block-steps): pure per-step A-side global latency -// serialization, not traffic. This round stages BOTH operands once per -// block with 16-B int4 cooperative loads (A slice: 16 m-rows x 256 B + 16 B -// pad/row = 4,352 B at row stride 272 for bank skew; B strip: 256 x 32 = -// 8,192 B), one same-wave __syncthreads(), then an LDS-only K loop (A -// fragment at row stride 272, B0/B1 at row stride 32 via -// du_load_matrix_sync) with zero per-step global loads/waits. Staged tiles -// are exact byte mirrors of the global operands, so fragment bytes, the -// k-ascending int32 order and the partial planes are bit-identical to -// iteration 5. Geometry forced by LDS capacity: a whole-slice A stage does -// not fit at SPLIT_K=12 with 4 resident blocks/CU (A 16 x 352 = 5,632 B + -// B 11,264 B + acc 2,048 B = 18,944 B -> floor(65,536/18,944) = 3), so the -// guard launch moves to SPLIT_K=16: sixteen UNIFORM 8-step slices (128 == -// 16 x 8, rows {256 x 16}; every boundary a DUMMA-k multiple), grid = 40 N -// groups x 16 = 640 one-wave blocks ~ 5.33/CU over 120 CUs, per-block LDS -// 4,352 + 8,192 + 2,048 = 14,592 B -> floor(65,536/14,592) = 4 resident -// blocks/CU, i.e. the iteration-5 co-residency is preserved. Workspace -// need = 16 int32 planes = 1,310,720 B = the caller's full fixed 16-plane -// allocation for this shape, still gated by the launcher's workspace_bytes -// >= kWorkspaceNeeded check (undersized workspaces keep the workspace-less -// fallback). The combine kernel (80 blocks x 64 lanes, second same-stream -// launch in the timed API/Graph) is UNCHANGED code, generic over -// split_count = 16, and sums the sixteen planes ascending per output -// element (exact int32 -> bit-identical to the scalar k-ascending sum), -// staying in the timed Graph. All fallbacks, guards, pack/layout/env/ -// stream/config behavior are unchanged. +// Iteration 6 (pipeline round, this round): A+B whole-slice LDS staging. +// Mandated staging-family decision, made from the L2/VMEM evidence: +// * VMEM: the exact-source PMC of the iteration-4 code object (digest +// d5784ae4...) shows the rolled K loop issuing 24 byte-granular +// global_load_ubyte per 32-K step (vmem_read 122,880 = 24 x 5,120 +// block-steps) behind an 18-entry scattered s_waitcnt vmcnt ladder, all +// on the MMA path with no cross-iteration load overlap. +// * L2: the same replay reports 61.237% L2 hits, and the per-kernel TCC/EA +// traffic ledger shows the partial kernel reading only 5,359,488 B while +// its load instructions request ~7.9 MB (A 2.62 MB + B 5.24 MB). The +// 40x-reused x_q A operand is therefore ~95% L2-resident (A HBM misses +// ~= 5,359,488 - 5,242,880 = 116,608 B) and the streaming B operand is +// the compulsory HBM traffic read exactly once. No staging family can +// change the HBM byte count: direct, A-only, B-only and A+B all fetch the +// same 5,242,880 B of B and the same ~116,608 B of A misses. The choice +// is therefore only about WHERE the operand fetch latency sits. +// * B-only was already measured (iteration 5) and regressed, so between the +// remaining families the evidence selects A+B: it is the ONLY one that +// removes the A-side global wait (which the B-only result proves is the +// serialization point) AND the B-side ladder at the same time, replacing +// both with one batched cooperative prologue of 16-B loads. +// Design (one-wave geometry, blockDim 64, unchanged): +// * Each 64-lane block stages BOTH operands of its whole K slice once: +// stage_a = the 16 m-rows x 256 slice bytes of x_q at row stride 272 +// (256 data + 16 pad for bank skew, 4,352 B) and stage_b = the +// 256 k-rows x 32 B B-strip at row stride 36 (32 data + 4 pad, 9,216 B). +// Both are EXACT byte mirrors of the global regions the direct loader +// read, filled by 16-B int4 cooperative copies (A: rows/16 = 16 chunks +// per m-row -> 256 chunks / 64 lanes = 4 chunks per lane, one 4-deep +// batched group; B: 2 chunks per k-row -> 512 chunks / 64 lanes = 8 +// chunks per lane, two 4-deep batched groups), one same-wave +// __syncthreads(), and then a K loop with ZERO per-step global loads and +// ZERO vmcnt waits: A from LDS at stride 272, B0/B1 from LDS at stride 36. +// * Pad choice is deliberate: the row_major B fragment loader reads each +// lane's bytes at (8*(lane>>4) + i)*ldm, so a 32-B ldm puts the four lane +// groups 256 B (== 0 mod 128) apart and replays one LDS bank 4x, while +// 36 B puts them 288 B (== 32 mod 128) apart -> banks b, b+8, b+16, b+24; +// the A pad 272 B (== 16 mod 128) likewise skews its 16 row reads. The +// pads are never written and never read. +// * Geometry moves 12 -> 16 splits because a whole-slice A stage does not +// fit at split 12 with 4 resident blocks/CU (A 16x368 + B 352x36 + 2,048 +// = 20,608 B -> floor(65,536/20,608) = 3 resident waves/CU, below the +// measured 4). At S = 16 the slices are uniform 8 x 32 stages = 256 rows +// (128 == 16*8), per-block LDS is 4,352 + 9,216 + 2,048 = 15,616 B -> +// floor(65,536/15,616) = 4 resident blocks/CU (62,464 B <= 65,536), and +// the grid is (N/32) x 16 = 40 x 16 = 640 one-wave blocks = 5.33/CU over +// 120 CUs, i.e. the measured 4-blocks/CU co-residency is preserved. +// The workspace need is 16 int32 planes = 1,310,720 B = the caller's full +// fixed allocation for this shape (the existing clamp still shrinks +// split_k for any smaller buffer, and a slice larger than the staging +// capacity keeps the byte-for-byte direct-global loop below). +// * Every partial is still an exact int32 ascending 32-aligned sub-sum over +// the SAME fragment bytes (the staged mirrors are exact copies), the +// combine kernel is unchanged, and no cross-wave synchronization or +// cross-block state is introduced. +// Byte ledger: HBM read/write bytes UNCHANGED (B 5,242,880 B read once + A +// misses ~116,608 B + 1,310,720 B of partial planes written/read); LDS added +// per block = 4,352 (stage_a) + 9,216 (stage_b) = 13,568 B written and +// re-read 8 times as fragments (8 x 1,536 B = 12,288 B), with the bank-skew +// pads chosen so the fragment reads are conflict-free. +// Falsifiable claim: if the per-step serialized operand fetch (not the +// compulsory HBM bytes) is the wall, the LDS-only K loop plus the batched +// 16-B staging prologue must drop the official Graph median/P90 materially +// below 30.374/30.420 us (expected ~21-27 us, combine's +4 planes included). +// If it instead stays flat or regresses, the wall at 4 waves/CU is the +// DRAM/LDS service rate or per-CU issue rather than the per-step wait ladder, +// and the next round must attack the operand stream's DRAM granularity (a +// packed [N/32][K][32] weight layout) rather than the staging family. // -// Iteration 8 (occupancy/resource round, this round): raise the waves-per- -// block occupancy limiter from 1 to 2 wavefronts per block (blockDim 64 -> -// 128). PMC of the accepted iteration-7 code object (source digest -// b64e3209..., grid 640, workgroup 64, arch_vgpr 40, sgpr 48, scratch 0, -// LDS/block 14,592 B -> floor(65,536/14,592) = 4 resident blocks/CU = -// 5.33 waves/CU by grid) shows the residual wall is per-SIMD single-wave -// latency exposure, not issue or HBM traffic: each resident wave is alone -// on its SIMD (4 blocks x 1 wave -> 4 SIMDs), so every per-step -// s_waitcnt lgkmcnt after the staged-fragment du_load_matrix_sync and every -// du_mma accumulator dependency stalls that SIMD with no second independent -// wave to issue (VALU issue utilization ~3-6% of the 26.56 us profiled -// kernel; only 12 one-time staging global loads per block, vmem_read -// 7,680). The fix keeps the iteration-7 geometry byte-for-byte in every -// respect that affects partials (SPLIT_K=16, 40 x 16 = 640 blocks, 8-step -// slices rows {256 x 16}, staged exact byte mirrors, k-ascending int32 -// order, 16-plane workspace) and splits the one-time staging ACROSS the two -// waves (wave 0 copies the A slice, wave 1 copies the B strip; exactly the -// same global bytes read once -- no repeated HBM reads traded for -// occupancy), then each wave runs its OWN 8-step accumulator chain for one -// n16 tile of the block's 32 columns (one du_mma per step per wave; total -// MMA count unchanged). LDS per block stays 14,592 B -> still 4 resident -// blocks/CU, but now 8 resident waves/CU = 2 per SIMD, so a stalling wave -// is covered by its co-resident sibling. The only deliberately added cost -// is LDS re-reads: the shared A fragment is now loaded by both waves -// (predicted lds_instructions 99,840 -> ~125-135k with conflicts scaling at -// the measured ~2.56/instr); LDS is not the wall at ~3-6% issue -// utilization. The combine kernel, guards, workspace-less fallback, scalar -// fallback and pack identity are all unchanged. +// Iteration 7 (measured and NOT accepted: depth-1 register double buffering). +// Giving the rolled LDS-only K loop two fragment register sets moved the +// official Graph median 21.412 -> 20.938 us but the P90 21.427 -> 25.333 us +// (+18%, max 26.557), so the round was rejected on the tail and the source was +// reverted to the iteration-6 best recorded here. The lesson: at ONE resident +// wave per SIMD there is no sibling wave to absorb the tail, so the fix must +// add wave-level parallelism rather than reorder a single wave's own chain. // -// Iteration 9 (combine restructure round, this round): attack the OTHER half -// of the operator wall. PMC of the accepted iteration-8 code object (source -// digest beb25b76..., official Graph median/P90 28.884/28.933 us) profiles -// BOTH timed dispatches of the operator replay: the 640-block partial kernel -// at 19.361 us AND the 80-block combine kernel at 16.960 us -- the combine is -// 46.7% of the 36.321 us profiled operator aggregate, not a small tail. Its -// structure explains the cost: 80 one-wave 64-lane blocks (<= 80 of 480 -// SIMDs busy, ~40 CUs fully idle) whose lanes each run FOUR output elements -// through a DEPENDENT per-plane chain -- one 4-B global load + s_waitcnt -// vmcnt + int32 add per plane, 16 serial DRAM round trips per element (the -// exact code object's static body is one load/wait/add per plane iteration) -// -- against plane data that is L2-cold at combine start (combine -// l2_hit_rate 6.9%, l2_misses 21,596 ~= the whole 1.31 MB of partial planes, -// evicted by the partial kernel's 5.37 MB streaming read): DRAM-latency -// serialization on only 1.34 MB of reads, plus underfill. This round -// restructures ONLY the combine kernel (the partial kernel, SPLIT_K=16 -// 640-block geometry, staged byte mirrors, k-ascending int32 partials, -// 16-plane 1,310,720 B workspace contract, workspace-less fallback, exact -// guard and scalar fallback are byte-for-byte unchanged): the guard launcher -// switches the combine grid from 80 x 1 one-wave blocks to a 2-D grid of 80 -// n-tiles x 4 four-row m-groups = 320 one-wave blocks, and the kernel's -// split_count == 16 specialization gives every lane exactly ONE output -// element that issues ALL sixteen plane loads as INDEPENDENT reads into a -// 16-int register array (one vmcnt drain, 16-way memory-level parallelism -// per lane), then sums the registers in ascending split order s = 0..15 -- -// the identical int32 sequence as the old ascending loop -- and applies the -// unchanged float scale order and f32_to_bf16_rne, so every output element -// stays bit-identical (this mirrors the validated gate_up S=16 combine -// recipe: issue all 16 plane loads into a register array before the -// ascending sum). Same stream, same two launches, no allocation or sync -// inside the timed API/Graph. +// Iteration 8 (occupancy/resource round, this round): raise the waves-per-block +// occupancy limiter from 1 to 2. The exact-source PMC of the accepted +// iteration-6 code object (digest 4fe713e6..., grid 640 = 40 N groups x 16 +// splits, workgroup 64, arch_vgpr 80, sgpr 64, scratch 0, LDS 15,616 B/block +// -> floor(65,536/15,616) = 4 resident blocks/CU, i.e. exactly ONE resident +// wave per SIMD) shows the residual wall is per-SIMD single-wave latency +// exposure, not issue or HBM traffic: every per-step s_waitcnt lgkmcnt after +// the staged-fragment du_load_matrix_sync and every du_mma accumulator +// dependency stalls that SIMD with no second independent wave to issue +// (lds_wait_instructions 11,842, lds_bank_conflicts 133,120, vmem_read 7,680 = +// only the one-time staging loads). This round makes each block TWO 64-lane +// wavefronts (blockDim 64 -> 128, __launch_bounds__(128)) and splits the block +// by ownership: wave w computes ONLY n-tile w (cols [n0+16w, n0+16w+16)) over +// the shared 8-step slice with its own independent m16n16k32 accumulator chain, +// one du_mma per step per wave (same total MMA count). LDS per block is +// UNCHANGED (stage_a 4,352 + stage_b 9,216 + 2 x 1,024 acc tiles = 15,616 B), +// so 4 resident blocks/CU still fit but now carry 8 resident waves/CU = 2 per +// SIMD: one wave's LDS->MMA stall is covered by its co-resident sibling. The +// one-time staging is PARTITIONED, not duplicated: wave 0 copies the shared A +// slice, wave 1 copies the B strip (the iteration-6 cooperative loops +// unchanged), so exactly the same global bytes are read once per block -- no +// repeated HBM reads are traded for occupancy -- and the staging barrier +// becomes a cross-wave __syncthreads(). The only deliberately added cost is +// LDS re-reads: the shared A fragment is now loaded from the staged mirror by +// both waves (LDS bandwidth is not the wall at this issue utilization). Every +// partial int32 is the identical ascending sum over the identical staged bytes +// for its own 16-column tile, so the output stays bit-identical and the combine +// kernel, the 16-plane workspace contract, the exact (m,n,k) == (16,1280,4096) +// guard, the workspace-less direct-global path and the scalar fallback for +// every unmatched shape (paired M=2 included) are all unchanged. +// Falsifiable claim: if the per-SIMD single-wave latency exposure is the wall, +// the official Graph median/P90 must drop below 21.411954/21.427149 us +// (expected ~19.5-20.9 us median) with mismatch_count 0; if it stays flat or +// regresses, the wall at 8 waves/CU is the LDS service rate or per-CU issue +// rather than wave-level coverage. // -// Iterations 10-15 (five-wave occupancy arc on this guard, all archived at -// iterations/hy3_tp8_qkv_proj_m16/iterationN): iteration 10 (direct-register -// epilogue on the two-wave mapping: acc tiles + epilogue barrier removed, -// LDS 14,592 -> 12,544 B = 5 resident blocks/CU = 10 waves/CU) measured -// 20.827/21.585 us and was NOT accepted; iteration 11 raised waves-per-block -// 2 -> 5 (blockDim 320, grid 16 x 16 = 256 blocks, LDS 29,952 B = 2 resident -// blocks/CU = 10 waves/CU) but FAILED exact correctness (its source declared -// no __launch_bounds__ while the launcher requests 320 threads, so the -// compiled descriptor capped the workgroup at 256 and the fifth wavefront's -// tile was absent: 4,091/20,480 mismatches, first at (m,n)=(0,64)); the -// iteration-11 repair was completed in iteration 13 by declaring -// __launch_bounds__(320) (exact correctness passed, 0 mismatches; measured -// 23.764/24.086 us); iteration 14 (ping/pong fragment double-buffer) passed -// correctness but the toolchain spilled the second fragment set to the -// private segment on every K step (.private_segment_fixed_size 48) -> 39.472 -// us, closing the register-pipelining axis; iteration 15 replaced the -// five-wave epilogue __syncthreads() with __threadfence_block() (the acc -// tiles are wave-private and the waves' plane stores are disjoint, so the -// cross-wave join was pure skew cost) and measured 20.613/20.641 us, correct -// but still not below the accepted two-wave 20.179/20.216. +// Iteration 9 (combine-restructure round, this round): attack the OTHER timed +// dispatch of the operator -- the split-K combine. Every previous round +// optimized only the 640-block partial kernel, but the operator replay runs +// TWO kernels on the same stream and the combine is a real part of the wall: +// its exact-source PMC (source digest e2a23702..., 160 x 128-thread +// one-element-per-thread blocks) profiles at 3.52 us against 19.52 us for the +// partial kernel, and the static code object's rolled plane loop is a +// DEPENDENT load -> s_waitcnt vmcnt -> int32 add chain of 16 DRAM round trips +// per output element (one 4-B global load per plane iteration) with only 160 +// blocks (~1.33 warps/SIMD) to hide it. This round restructures ONLY the +// combine kernel and its launch: +// * the guard launcher switches the combine from 160 linear 128-thread +// blocks to a 2-D grid of (n / 64) column groups x m rows = 20 x 16 = 320 +// ONE-wave 64-lane blocks, and the kernel's split_k == 16 specialization +// gives every lane exactly ONE output element (row = blockIdx.y, +// col = blockIdx.x * 64 + lane): all sixteen plane loads are issued as +// INDEPENDENT reads into a 16-int register array (one vmcnt drain, +// 16-way memory-level parallelism per lane) and only then summed in the +// unchanged ascending split order s = 0..15; +// * each per-plane wavefront load now reads 64 lanes x 4 B = 256 B +// CONTIGUOUS (four consecutive 64-B lines of one m-row) instead of four +// runs at the 5,120-B m-row stride. +// The partial kernel, the 640-block / 16-split geometry, the staged byte +// mirrors, the k-ascending int32 partials, the 16-plane 1,310,720 B workspace +// contract, the workspace-less fallback and the scalar fallback are +// byte-for-byte unchanged. Integer addition is exact and order-free +// (|acc| <= K*127*127 ~= 6.6e7 << 2^31), the fp32 scale order and the bf16 +// conversion are element-for-element identical, so every output byte is +// bit-identical. A clamped split_k < 16 still takes the unchanged generic +// combine path with its original 128-thread launch, so an undersized +// workspace cannot change behavior. +// Falsifiable claim: if the combine's per-element dependent plane-load chain +// (and its underfilled 160-block grid) is a material part of the 19.51 us +// operator wall, the official Graph median/P90 must drop materially below +// 19.510394/19.535989 us (expected ~17.3-18.9 us); if it stays flat, the +// combine was already fully hidden behind the partial kernel's drain and the +// remaining wall is entirely partial-kernel side. // -// Iteration 16 (this round): close the five-wave occupancy axis with the -// measured occupancy evidence and apply the repair arc's validated epilogue -// discovery to the accepted mapping. Occupancy ledger of the partial -// kernel, all correct and bit-identical: 8 resident waves/CU (4 x 2-wave -// blocks, LDS 14,592 B) = 20.179/20.216 us (iteration 9, accepted); the two -// independent 10 resident-waves/CU configurations -- iteration 10 (5 x -// 2-wave blocks) 20.827/21.585 and iteration 15 (2 x 5-wave blocks) -// 20.613/20.641 -- BOTH trail the 8-wave configuration, i.e. per-SIMD wave -// density 2.5 (some SIMDs carry 3 waves while others idle) is worse than a -// clean 2.0 on this kernel, and the five-wave mapping (2 or 1 resident -// 5-wave blocks/CU = 10 or 5 waves/CU only) can never reach the 8-wave -// sweet spot. The accepted iteration-9 source below still carries the SAME -// provably-unnecessary epilogue __syncthreads() cross-wave join whose -// five-wave form cost ~3.15 us (iteration 13 23.764 -> iteration 15 20.613 -// with the join removed). Fix (the ONLY code change of this round; the -// two-wave geometry, blockDim 128, grid 40 x 16 = 640 blocks, staging -// partition, stage byte mirrors, 8-step K loop, combine kernel and its -// 320-block launch, workspace, guards and fallbacks are byte-for-byte the -// accepted iteration-9 candidate, verified by diff): replace the partial -// kernel's epilogue __syncthreads() with __threadfence_block(), exactly as -// iteration 15 validated on the five-wave form of this same kernel. -// acc_tile0/acc_tile1 are each written by du_store_matrix_sync and read -// back only by the SAME wave (w == 0 -> acc_tile0, w == 1 -> acc_tile1), and -// the two waves' plane stores touch disjoint 16-column ranges, so no -// epilogue stage has any cross-wave dependency; __threadfence_block() still -// orders each wave's own outstanding LDS writes before its read-back -// ds_reads (wavefront-wide lgkmcnt drain) while costing zero cross-wave -// wait. The staging __syncthreads() above stays (wave 0 stages the A slice -// that wave 1 also reads). Every partial byte and combine output byte is -// unchanged, so the output is bit-identical to iteration 9. +// Iteration 11 (DS-side staging-latency round, this round): shorten the +// one-time staging join of the accepted iteration-9/10 partial kernel. The +// exact-source PMC of that object (source digest bd963ec6..., grid 640 = 40 N +// groups x 16 splits, workgroup 128, LDS 15,616 B, arch_vgpr 80, scratch 0, +// profiled 20.159 us) shows the kernel is deeply latency-bound, not +// issue/bandwidth-bound: only 486,400 VALU + 112,640 LDS wave-instructions for +// 1,280 resident wavefronts x 8 K-steps, while vmem_read_instructions is just +// 7,680 (= the one-time staging loads, 12 int4 loads per block) and the +// block's critical path therefore still contains the operand staging prologue. +// The staging is PARTITIONED (wave 0 copies the 4-chunk-per-lane A slice, +// wave 1 copies the 8-chunk-per-lane B strip) and the cross-wave +// __syncthreads() joins both, so the join costs the SLOWER wave. Wave 0 +// already issues its whole A copy as one 4-deep batch, but wave 1's 512-chunk +// B strip (rows*2 = 256*2 = 512 chunks == 8 x 64 lanes) is emitted as two +// serialized 4-deep iterations: iteration k+1's four 16-B global loads cannot +// issue until iteration k's four dependent LDS stores have consumed their +// registers, so every block pays TWO dependent global round trips on the +// staging join instead of one. Change (one focused HIP edit, +// csrc/w8a8_gemm_hip.hip only): when the B strip is exactly the guard's +// 8-chunk-per-lane shape (b_chunks == 8 * kDummaWaveSize == 512, which the +// exact (m,n,k) == (16,1280,4096) guard always produces because rows == +// kStageMaxRows == 256), wave 1 now runs ONE straight-line 8-deep batched +// group: all eight 16-B source loads are issued before the first LDS store, +// so a single vmcnt drain covers the whole strip and the second global round +// trip leaves every block's staging join. The generic rolled 4-deep loop +// stays verbatim as the else path for any non-guard chunk count. The staged +// bytes, their LDS addresses (the same 36-B row pitch and store decomposition) +// and therefore every fragment byte, every k-ascending int32 partial and the +// combine output stay bit-identical; the 16-plane 1,310,720 B workspace +// contract, the workspace-less direct-global fallback, the exact shape guard, +// the scalar fallback for every unmatched shape (paired M=2 included), the +// combine kernel and its 320-block one-row-per-block launch, the pack identity +// and every stream/Graph property are unchanged. +// Falsifiable claim: if the block's staging join is a material part of the +// 18.980 us wall, removing the second global round trip from wave 1's B copy +// must move the official Graph median/P90 below 18.980430/19.183630 us +// (expected ~18.3-18.9 us median) with mismatch_count 0; if it stays flat or +// regresses, the staging join is already fully covered by the co-resident +// blocks and the residual wall is the LDS-only K loop. // -// Iteration 18 (this round): compile-time 8-step K-loop specialization of -// the partial kernel, applied to the current checked-in source (the -// iteration-16 candidate; working-tree digest cbc40932... = the iteration-16 -// archive, verified by diff). Round 17 drafted exactly this edit and -// verified its codegen locally, but the agent session was killed by the -// round infrastructure before a valid proposal landed -- no source change -// was ever applied, no build/benchmark ran, and per policy that -// infrastructure failure does not count toward the valid-round ledger, so -// this round re-submits the same HIP-only change as a fresh valid round. -// The iteration-11 repair item is closed since iteration 13 (archived -// iteration-11 five-wave source + __launch_bounds__(320) ONLY, exact-correct -// with 0 mismatches; iterations 13-16 completed that arc and iteration 16 -// re-based on the accepted two-wave mapping). Status of the current source: -// the iteration-16 epilogue barrier-decoupled two-wave partial kernel -// (blockDim 128, grid 40 x 16 = 640 blocks, LDS 14,592 B = 4 resident -// blocks/CU = 8 resident waves/CU) measured official Graph median/P90 -// 19.9885/20.0133 us (min 19.9557 / max 21.3734, correctness 0 mismatches, -// Graph capture and paired M=2 fallback passed), improving the accepted -// 20.179/20.216 by 0.95%; it is kept as the experimental base ("official -// best is unchanged until cumulative improvement reaches the normal -// acceptance threshold"), and this round keeps building on that source. -// The partial kernel dominates the operator wall, and the exact-source code -// object shows its K loop is ROLLED: the generic loop is runtime-bounded -// (my_steps from the runtime k and gridDim.y args), so the backend emits one -// static body of [ds_read2_b32 (A) + 8 x ds_read_u8 (B) + ~25-VALU byte -// reassembly + 4 lgkmcnt drains + one v_mmac_i32_16x16x32_i8] with a -// loop-carried WAR on the single fragment register set: step s+1's LDS -// reads cannot issue until step s's v_mmac has consumed the same fragment -// registers, so every step's full LDS latency sits serially on the -// accumulator chain with only 2 resident waves/SIMD to cover it (reused PMC -// of this exact geometry: lds_instructions 107,520, lds_wait_instructions -// 19,597, lds_bank_conflicts 276,480). The guard launch is the kernel's -// ONLY call site and always runs the uniform 8-step slice (k == 4096, -// gridDim.y == 16 -> my_steps == 8 == kStageMaxSteps, rows == 256 == -// kStageMaxRows, staged capacity exactly filled), so this round specializes -// that case: when my_steps == kStageMaxSteps the K loop runs with a -// COMPILE-TIME trip count of 8 under #pragma unroll; the backend then fully -// unrolls the chain, renames the fragment registers per step and issues -// each step's LDS reads after the previous step's v_mmac has consumed its -// registers, overlapping the next step's LDS round trip with the current -// v_mmac's execution (no loop-carried WAR; verified in the local exact-flag -// code object of this edit: 8 static v_mmac + 72 static ds_reads in the -// specialized path vs 1 static v_mmac / 10 static ds_reads in the rolled -// predecessor, vgpr 34 / sgpr 39 / private_segment 0 / group_segment 14,592 -// B and exactly ONE s_barrier unchanged -> the 4-blocks/CU = 8-waves/CU -// occupancy is preserved). Both branches issue the identical ascending-s -// du_mma sequence into the same accumulator (v_mmac acc operand v[1:4] -// throughout) over the same staged byte mirrors, so every partial byte and -// every combine output byte is bit-identical to iteration 16 and exact -// correctness must pass with 0 mismatches; the generic rolled loop stays -// verbatim as the else path for any non-guard (my_steps != 8) -// configuration. Geometry, staging partition, stage byte mirrors, the -// blockDim 128 / grid 40 x 16 = 640 blocks launch, the epilogue -// (acc-tile store + __threadfence_block + read-back plane store), the -// combine kernel and its 320-block launch, the 16-plane 1,310,720 B -// workspace need, the workspace-less iteration-1 fallback, the exact -// (m,n,k)==(16,1280,4096) guard and the scalar fallback for every unmatched -// shape (including paired M=2) are all unchanged. +// Iteration 13 (HIP-only consolidation round, this round): halve the split-K +// partial-plane round trip, the only non-compulsory component of the +// operator's memory traffic. The exact-source PMC of the accepted iteration-11 +// object (source digest 210fbfcfde28bc26d9c1c93a8a5f91353db51c5426853e85b5701dd44fa81fa8, +// primary kernel 640 blocks x 128 threads, LDS 15,616 B, arch_vgpr 80, sgpr 48, +// scratch 0, profiled 20.0 us; combine 320 one-wave blocks, profiled 2.72 us; +// official unprofiled Graph median/P90 18.145450/18.166250 us) reports, per +// operator replay, 6,716,736 B read + 1,351,680 B written = 8,068,416 B, and +// the memory_traffic ledger splits it into 5,368,320 B for the partial kernel +// (B 5,242,880 compulsory + A + the 1,310,720 B of int32 partial planes it +// writes) and 1,348,416 B read + 40,960 B written for the combine (the same +// 1,310,720 B of planes re-read). The combine's own L2 hit rate is just +// 6.849% (l2_hits 1,597 / l2_misses 21,721) against the partial kernel's +// 54.392%, i.e. the 16 int32 planes are written through L2, evicted by the +// streaming B strip, and pulled back from DRAM by the immediately following +// combine: 2,621,440 B of the 8,068,416 B total (32.5%) is pure split-K +// bookkeeping against a compulsory A 65,536 B + B 5,242,880 B + out 40,960 B +// = 5,349,376 B. Change (one focused HIP edit, csrc/w8a8_gemm_hip.hip only): +// move kSplitKTarget 16 -> 8 so the split-K bookkeeping halves to 8 planes +// (655,360 B written + 655,360 B re-read), removing 1,310,720 B (16.2%) of the +// per-replay traffic. +// * Geometry: grid = (N/(16*T)) * 8 = 40 * 8 = 320 blocks of 128 threads; +// the 128 32-K stages partition into uniform 16-stage (512-row) slices +// (128 == 8 x 16), every boundary still a multiple of 32, so the +// stage-alignment and exact-int32 contracts are unchanged. The workspace +// need is 8 x 80 KiB = 655,360 B, well inside the contract's fixed +// 1,310,720 B / 16-plane allocation, and the existing clamp still shrinks +// split_k for a smaller buffer. +// * Staging: a 512-row slice is twice the unchanged 256-row staging +// capacity, so the kernel now stages and consumes its slice in ascending +// 256-row PHASES (exactly two at the guard). Each phase is byte-for-byte +// the accepted iteration-11 geometry: wave 0 stages the 256-chunk A phase +// as one 4-deep batch and wave 1 stages the 512-chunk B strip as ONE +// straight-line 8-deep batch behind a single vmcnt drain, then the phase's +// 8 K-steps accumulate into the SAME acc_frag in ascending k order, and an +// end-of-phase __syncthreads() releases the LDS for the next phase. The +// staged bytes, their bank-skewed LDS addresses, the 36-B B row pitch, the +// 8-deep batch and therefore every fragment byte are unchanged, so every +// k-ascending int32 partial and the combine output stay bit-identical. +// * LDS per block is unchanged at 15,616 B (stage_a 4,352 + stage_b 9,216 + +// 2 x 1,024 acc tiles), so four resident blocks/CU still fit by capacity +// (2 waves/block = 8 waves/CU = 2 per SIMD by capacity); the 320-block +// grid is a single resident generation. +// * The combine guard split and its 2-D one-wave launch move to +// kCombineSplits = 8 (the same 20 x 16 = 320 one-wave blocks, eight +// independent plane loads per lane in registers); a clamped split_k < 8 +// keeps the unchanged generic 128-thread combine path. +// The exact shape guard, the workspace-less fall-through, the scalar fallback +// for every unmatched shape (paired M=2 included), the pack identity, the +// stream/Graph contract and every fallback path are unchanged; no raw inline +// asm and no ISA skill is used (phase hip_only, plateau false, +// raw_inline_asm_allowed false, skill_allowed false). +// Falsifiable claim: if the 2,621,440 B split-K plane round trip is a material +// part of the 18.145 us wall, halving it must move the official Graph +// median/P90 below 18.145450/18.166250 us (expected ~16.5-17.5 us median, +// ~4-9% faster, from the 1,310,720 B traffic reduction minus the reduced grid +// parallelism) with mismatch_count 0 and unchanged Graph capture; if it stays +// flat or regresses, the plane traffic was hidden behind the operand stream +// and the residual wall is the compulsory 5.35 MB operand/out stream. // -// Iteration 20 (DS-side staging-latency round, accepted): the exact-guard -// launch always stages the full 512-chunk B strip (chunks == 512 == 8 x 64 -// lanes), so wave 1's B copy now runs as ONE straight-line 8-deep batched -// group (all eight 16-B source loads issue before the first LDS store, one -// vmcnt drain covers the whole strip) instead of two serialized 4-deep -// iterations -- the second global round trip is removed from every block's -// staging join critical path (official Graph median/P90 19.967 -> 19.541/ -// 19.558 us, accepted). The generic rolled loop stays verbatim as the else -// path for any non-guard (chunks != 512) configuration. +// The scalar kernel below stays byte-for-byte as the generic fallback for +// every unmatched shape, including the paired M=2 API shape with the same +// (N, K); the (m, n, k) == (16, 1280, 4096) guard is the only call site of +// the split-K DUMMA path, and if the caller supplies no usable workspace the +// exact shape safely falls through to that scalar fallback. // -// Iteration 21 (this round): DS-side bank-skew round for the B-fragment -// ds_read_u8 structure (pre-registered in iterations 18 and 20: the next -// increment must attack the PMC-documented DS ledger of this exact object -- -// lds_bank_conflicts 276,480 = ~2.57 replays per LDS instruction, dominated -// by the 8 byte-granular B-fragment reads per step per wave, 128 per block). -// The row-major B fragment loader reads each lane's 8 bytes at addresses -// base + (8*(lane>>4) + i) * ldm for i = 0..7, so the four row groups -// g = lane>>4 of the fragment's 32 k rows sit 8*ldm bytes apart; with the -// stage_b rows packed at the old ldm == 32, every group sat 256 B (== 0 -// mod 128 B) apart and each ds_read_u8 replayed the SAME LDS bank 4x per -// wavefront. This round pads each staged B row to kStageBRowStride = 36 B -// (32 data + 4 pad): the groups then sit 288 B (== 32 mod 128 B) apart and -// hit banks {b, b+8, b+16, b+24} -- the B-fragment reads become -// conflict-free while every staged data byte, the fragment contents, the -// k-ascending int32 order and the partial planes stay bit-identical (the -// pads are never written or read; only the loader's ldm argument and the -// wave-1 store offsets change). Wave-1 staging stores become four 4-B -// words per 16-B chunk (36*r is only 4-B aligned) at int32 offsets -// 9*r + 4*h + {0,1,2,3}; LDS store bandwidth is unchanged. LDS per block -// grows 14,592 -> 15,616 B (stage_b 8,192 -> 9,216): 4 x 15,616 = 62,464 -// <= 65,536, so the 4-resident-blocks/CU = 8-resident-waves/CU occupancy -// sweet spot is preserved. The workspace-less fallback, the exact -// (m,n,k)==(16,1280,4096) guard, the scalar fallback for every unmatched -// shape (including paired M=2), the combine kernel and its 320-block -// launch, the pack identity and the 16-plane 1,310,720 B workspace need are -// all unchanged. The iteration-11 repair item stays closed since -// iteration 13 (five-wave archive + __launch_bounds__(320) ONLY, exact- -// correct; the five-wave axis closed on measurement: best 20.613/20.641 us -// vs the accepted two-wave mapping 19.541/19.558 us). -// -// Iterations 22-25 (rounds 22 and 23 measured and rejected, tree reverted to -// the iteration-21 source e7ae27d0...; round 24 was killed mid-round before -// any measurement -- its archive draft was never compiled or run): round 22 -// removed the acc-tile LDS round trip from the partial epilogue at pinned -// occupancy (direct-register plane stores) and REGRESSED (+0.82 us -> -// 20.276/20.298 us) because the register-distributed acc values force 16-B -// store granules per wavefront instruction instead of the read-back path's -// four full 64-B lines -- rejected and reverted; round 23 widened each -// combine block to a 32-column n-strip with dwordx2 plane loads (160 blocks) -// and measured FLAT (median 19.4504 us vs the 19.4528 shadow, within its own -// +/-0.05 us falsification band, plus a 41.1 us max outlier) -- rejected and -// reverted. Round 23's pre-registered falsification branch (a) therefore -// committed THIS round to a combine block mapping whose per-plane reads run -// in longer contiguous runs per row (or a partial-plane store-order change), -// never occupancy / fragment duplication / raw asm / a scalar-fallback -// change. -// -// Iteration 25 (this round): combine mapping v3 -- one output row per -// block. The combine byte stream is fixed (every plane word is read -// exactly once, 64-B sectors fully consumed), so v1 (iteration 9: blocks of -// 4 rows x 16 cols) and v2 (iteration 23: 4 rows x 32 cols) both issue -// every per-plane wavefront load as FOUR 64-B (v1) or 128-B (v2) runs at -// the 5,120-B m-row stride -- four different DRAM rows, i.e. up to four row -// activations per load instruction -- which is exactly why widening the -// per-row run 64 -> 128 B in v2 could not move the combine (it never -// reduced the rows-per-instruction scatter). The guard combine launch now -// uses one-wave blocks that each own ONE m-row x 64 consecutive columns -// (kDummaCombineColsPerBlock = 64; grid 20 x 16 = 320 blocks, the same -// 320-block count and the same one-output-element-per-lane, 16-independent- -// plane-load structure as iteration 9): every per-plane wavefront load then -// reads 64 lanes x 4 B = 256 B CONTIGUOUS = four consecutive 64-B lines of -// ONE DRAM row (single run, ~4x fewer DRAM row activations for the same -// byte stream). Each element's ascending-s int32 sum, the float scale -// order and every output bf16 word are unchanged element-for-element, so -// the output is bit-identical to the iteration-21 shadow; the partial -// kernel, the 16-plane workspace contract, the exact guard, the -// workspace-less fallback and the scalar fallback (paired M=2 included) are -// byte-for-byte untouched. -// -// launch_pack_w8a8_weight (optional, out of the timed region) is an -// identity device-to-device copy: the logical [K, N] int8 weight and the -// [N, 1] fp32 scales are copied unchanged, so the DUMMA direct loader -// reads the exact logical [K, N] row-major layout. Later rounds may -// change the packed layout here together with the matching GEMM -// interpretation. -// -// This file is compiled with hipcc by torch.utils.cpp_extension; it must -// stay free of torch/ATen dependencies. Only the two extern "C" launchers -// below are the host contract consumed by csrc/bindings.cpp. -// -// Include order is the known-good DTK order: HIP runtime first, then the -// bfloat16 header, then du_mma.h (consumed by the DUMMA kernel below). +// Iteration 17 (HIP-only consolidation round, this round): complete the B +// memory path by packing the cold once-read weight into the col_major +// fragment layout, the lineage rule that the sibling TP4/TP8 M=16 shapes +// validated ("one ds_read_b64 per step, was 8 ds_read_u8"). Evidence for the +// accepted iteration-13 object (source digest 920506893c362e1bcefa5c36ffb0776fbfee00a9dd004943b66fc55d386ae572, +// primary kernel 320 blocks x 128 threads, LDS 15,616 B, arch_vgpr 80, sgpr 48, +// scratch 0, profiled 18.24 us; combine 320 one-wave blocks, profiled 2.72 us; +// official unprofiled Graph median/P90 16.614190340042114/16.642990112304688 +// us): the exact-source code object's rolled LDS-only K loop issues ONE +// ds_read2_b32 for the A fragment, but EIGHT scatter ds_read_u8 per lane for +// the B fragment (immediates i*36 on the k-major 36-B row pitch) plus the +// byte-mask/shift/OR reassembly that packs those eight bytes into the v_mmac +// operand -- ~10 of the ~11 LDS wave-instructions and ~2/3 of the ~41 VALU +// wave-instructions per wave-step, with lds_wait_instructions 15,137 and +// lds_bank_conflicts 117,760 against lds_instructions 108,800. The B +// fragment's 8 bytes per lane are contiguous in memory if and only if the +// staged B tile is n-major, because the library col_major matrix_b loader +// reads p[(lane&15)*ldm + 8*(lane>>4) + i]. Change (one focused HIP edit, +// csrc/w8a8_gemm_hip.hip only): +// * for the exact (K, N) == (4096, 1280) weight pair, launch_pack_w8a8_weight +// now writes the n-major transpose packed[n*K + k] = raw[k*N + n] instead +// of the identity copy (same buffer, same k*n bytes, outside the timed +// region/Graph; the packed layout is opaque per the API contract). Every +// other (K, N) keeps the identity copy. +// * the guard kernel stages its 32-column B strip n-major: +// stage_b[n * kStageBColStride + k] = packed[n0 + n][phase_begin + k] at +// the 16-B-aligned 272-B column stride, so each 16-B cooperative chunk is +// one ds_write_b128 and each column's phase is one contiguous run. +// * the B fragment is declared col_major and loaded from the n-major mirror +// (ldm = 272), so the K loop's B fragment becomes ONE contiguous 8-byte +// LDS read per step. +// * the scalar fallback (paired M=2 and every unmatched shape) and the +// DUMMA kernel's direct-global safety net decode the same permutation for +// this (K, N); all other shapes are untouched. +// The same logical (k, n) element lands in the same v_mmac fragment register, +// every staged byte is a copy of the same packed byte, and the k-ascending +// int32 accumulation is unchanged, so every partial and every output byte stay +// bit-identical; LDS per block drops 15,616 -> 15,104 B (still 4 resident +// blocks/CU), and the grid, plane geometry, combine kernel, workspace +// contract, stream/Graph properties and fallbacks are unchanged. No raw +// inline asm and no ISA skill is used (phase hip_only, plateau false, +// raw_inline_asm_allowed false, skill_allowed false). +// Falsifiable claim: if the B fragment's 8-scatter-byte LDS read plus its +// reassembly VALU is a material part of the 16.614 us wall, replacing it with +// one 8-byte LDS read (and ~4x fewer LDS wave-instructions) must move the +// official Graph median/P90 below 16.614190/16.642990 us (expected +// ~15.9-16.3 us median, 2-4% faster) with mismatch_count 0 and unchanged +// Graph capture; if it stays flat or regresses, the LDS instruction count is +// not the wall and the residual wall is the compulsory operand byte stream. #include #include @@ -463,662 +471,629 @@ namespace { -// IEEE round-to-nearest-even conversion of an fp32 value to a bf16 bit -// pattern, implemented with pure integer ops. This is exactly the -// conversion applied by the CPU reference (`scaled.to(torch.bfloat16)`), -// independent of which bf16 type family the DTK headers provide. -__device__ __forceinline__ uint16_t f32_to_bf16_rne(float f) { - constexpr uint32_t kRoundBias = 0x7FFFu; - uint32_t u; - __builtin_memcpy(&u, &f, sizeof(u)); - const uint32_t lsb = (u >> 16) & 1u; // tie goes to even - u += kRoundBias + lsb; - return static_cast(u >> 16); -} +// 128 threads = 2 waves of the native gfx928 64-lane wavefront, and a +// multiple of 64 as required by this DTK. +constexpr int kBlockThreads = 128; -} // namespace +// Iteration 17: the exact-guard weight pair (K, N) == (4096, 1280) whose +// packed weight buffer is the n-major transpose packed[n*K + k] = +// raw[k*N + n] instead of the identity copy. launch_pack_w8a8_weight writes +// that permutation (outside the timed region / Graph), the split-K DUMMA +// kernel consumes it through its n-major LDS stage with the library col_major +// matrix_b fragment loader, and every other consumer of the packed weight +// (the scalar fallback that serves the paired M=2 shape and every unmatched +// (m, n, k), plus the DUMMA kernel's direct-global safety net) decodes the +// same permutation. Every other (K, N) keeps the byte-for-byte identity pack. +constexpr int kGuardK = 4096; +constexpr int kGuardN = 1280; -// --------------------------------------------------------------------------- -// Scalar generic GEMM: out[m, n] = bf16(int32(sum_k a[m,k]*b[k,n]) -// * x_scale[m] * weight_scale[n]). -// One thread per output element. Adjacent lanes walk adjacent columns n -// (the fastest-changing output/weight dimension). gfx928 wavefront = 64; -// the launcher always uses blockDim 128 (a multiple of 64). -// --------------------------------------------------------------------------- -__global__ void w8a8_scalar_gemm_kernel( - const int8_t* __restrict__ a, // x_q [M, K] row-major - const int8_t* __restrict__ b, // packed_weight [K, N] row-major - const float* __restrict__ x_scale, // [M] (logical [M, 1]) - const float* __restrict__ w_scale, // [N] (logical [N, 1]) - uint16_t* __restrict__ out, // [M, N] bf16 payload +// One thread -> one output element. Consecutive threads stay adjacent in N, +// the fastest-changing dimension, so weight loads are coalesced. +__global__ __launch_bounds__(kBlockThreads) void w8a8_scalar_kernel( + const int8_t* __restrict__ a, + const int8_t* __restrict__ b, + const float* __restrict__ x_scale, + const float* __restrict__ weight_scale, + __hip_bfloat16* __restrict__ out, int m, int n, int k) { - const int tid = - static_cast(blockIdx.x) * static_cast(blockDim.x) + - static_cast(threadIdx.x); - const int total = m * n; - if (tid >= total) { + const int64_t total = + static_cast(m) * static_cast(n); + const int64_t idx = + static_cast(blockIdx.x) * blockDim.x + threadIdx.x; + if (idx >= total) { return; } - const int row = tid / n; // M is small for decode; N is the fast axis - const int col = tid - row * n; - const int8_t* a_row = a + static_cast(row) * k; - const int8_t* b_col = b + col; // stride n along k + const int row = static_cast(idx / n); + const int col = static_cast(idx - static_cast(row) * n); + const int8_t* a_ptr = a + static_cast(row) * k; int32_t acc = 0; -#pragma unroll 4 - for (int kk = 0; kk < k; ++kk) { - acc += static_cast(a_row[kk]) * - static_cast(b_col[static_cast(kk) * n]); + if (k == kGuardK && n == kGuardN) { + // Iteration 17: the exact-guard weight buffer holds the n-major + // transpose packed[n*K + k] = raw[k*N + n] (a pure byte permutation), + // so the fallback decodes that layout here. The k-ascending int32 sum + // and therefore every output byte are unchanged. + const int8_t* b_col = b + static_cast(col) * k; + for (int kk = 0; kk < k; ++kk) { + acc += static_cast(a_ptr[kk]) * + static_cast(b_col[kk]); + } + } else { + const int8_t* b_ptr = b + col; + for (int kk = 0; kk < k; ++kk) { + acc += static_cast(a_ptr[kk]) * static_cast(*b_ptr); + b_ptr += n; + } } - const float scaled = static_cast(acc) * x_scale[row] * w_scale[col]; - out[tid] = f32_to_bf16_rne(scaled); + // Integer dot stays exact in int32; the two fp32 scales are applied once at + // the very end and the result is stored as bf16. + const float scaled = static_cast(acc) * x_scale[row] * + weight_scale[col]; + out[idx] = __float2bfloat16(scaled); } // --------------------------------------------------------------------------- -// Exact-guard DUMMA path for hy3_tp8_qkv_proj_m16 (M=16, N=1280, K=4096). +// Exact-guard DUMMA split-K path for hy3_tp8_qkv_proj_m16 +// (M=16, N=1280, K=4096). // -// Minimal native gfx928 INT8 tile: DUFragment fragments with a DUFragment -// accumulator, one 64-thread wavefront per block, one 16x16 output tile per -// block (grid = N/16 = 80 blocks). Every K step is an explicit int32 -// du_mma_sync over a 32-aligned ascending k chunk, so the int32 dot is -// identical to the scalar kernel's sum (exact integer addition, no -// overflow: |out| <= K*127*127 ~= 6.6e7 << 2^31). +// Minimal native gfx928 INT8 tile: DUFragment fragments with a DUFragment accumulator, one 64-lane wavefront per N tile (two wavefronts per block +// since iteration 8). Iteration 4 gives every block kTilesPerWave = 2 +// adjacent 16x16 output tiles, and the 128 stage-aligned (k/32) K-steps are +// split kSplitKTarget ways so that the grid becomes (N/(16*T))*S blocks +// instead of N/16. Every K step is an explicit int32 du_mma_sync over a +// 32-aligned ascending k chunk, so each partial is an exact int32 sub-sum and +// the combine below is bit-identical to the scalar kernel's total (exact +// integer addition, order-free; |total| <= K*127*127 ~= 6.6e7 << 2^31). // -// The gfx928 int8 m16n16k32 accelerator fragment ownership is opaque, so -// the epilogue materializes the tile with du_store_matrix_sync into a -// single-wave LDS staging array (library-defined mem_row_major placement, -// 256 x int32 = 1,024 B) and only afterwards applies the per-element float -// scaling and the bf16 RNE store. BlockDim is one wavefront, so the single -// __syncthreads() below is a same-wave barrier (all 64 threads reach it) -// and there is no cross-wave synchronization anywhere in the kernel. +// Iteration 6 adds whole-slice A+B LDS staging: each block first mirrors its +// whole A slice and its whole 32-column B strip into LDS with 16-B cooperative +// copies (exact byte mirrors, bank-skewed row pitches), and the K loop then +// reads every fragment from LDS with no global load and no vmcnt wait on the +// MMA path. See the staging block below for the capacity, bank-skew and +// bit-exactness arguments. +// +// Iteration 8 raises waves-per-block from 1 to 2: wave w owns N tile w of the +// block's 32 columns and runs its own independent accumulator chain, the +// one-time staging is partitioned across the two waves (no operand byte is +// fetched twice), and LDS per block is unchanged, so 4 resident blocks/CU +// become 8 resident waves/CU = 2 per SIMD. +// +// The gfx928 int8 m16n16k32 accelerator fragment ownership is opaque, so the +// epilogue materializes each tile with du_store_matrix_sync into a per-wave +// LDS staging array (16x16 int32 = 1,024 B each, library-defined mem_row_major +// placement) and from there stores the raw int32 partials into plane `split` +// of the caller workspace. blockDim is two wavefronts, so the staging +// __syncthreads() is a genuine cross-wave barrier inside one block (wave 0's A +// slice is read by wave 1 and vice versa for the B strip); there is no +// cross-block synchronization anywhere. // --------------------------------------------------------------------------- constexpr int kDummaM = 16; constexpr int kDummaN = 16; constexpr int kDummaK = 32; constexpr int kDummaWaveSize = 64; -// Combine constants. Iteration 9: the exact-guard launcher (SPLIT_K == 16) -// launched the combine kernel as 80 n-tiles x four-row m-groups of one-wave -// blocks and the kernel specialized split_count == kDummaCombineSplits with -// one output element per lane. Iteration 25 (mapping v3): the guard launch -// becomes 20 column-groups x 16 m-rows of one-wave blocks -- each block owns -// ONE output row x kDummaCombineColsPerBlock = 64 consecutive columns (4 x -// kDummaN), so every per-plane wavefront load reads a single 256-B -// contiguous run (four consecutive 64-B lines inside one DRAM row) instead -// of four 64/128-B runs at the 5,120-B row stride (see the kernel comment -// below for the DRAM row-activation mechanism). -constexpr int kDummaCombineSplits = 16; // == the guard launcher kSplitK -constexpr int kDummaCombineColsPerBlock = 64; // n-columns per combine block - -__global__ void w8a8_dumma_m16_tile_kernel( - const int8_t* __restrict__ a, // x_q [M, K] row-major - const int8_t* __restrict__ b, // packed_weight [K, N] row-major - const float* __restrict__ x_scale, // [M] - const float* __restrict__ w_scale, // [N] - uint16_t* __restrict__ out, // [M, N] bf16 payload - int n, - int k) { - const int lane = static_cast(threadIdx.x); // blockDim == 64 - const int n0 = static_cast(blockIdx.x) * kDummaN; - - __shared__ __align__(16) int32_t acc_tile[kDummaM * kDummaN]; - - du::dumma::DUFragment - a_frag; - du::dumma::DUFragment - b_frag; - du::dumma::DUFragment - acc_frag; - du::dumma::du_fill_fragment(acc_frag, 0); - - // A is [16, k] (row stride k) and B is [k, 16] (row stride n); both stay - // row-major in their global tensors, so each 32-K step loads straight - // from global with the library row_major loaders. - for (int k0 = 0; k0 < k; k0 += kDummaK) { - du::dumma::du_load_matrix_sync(a_frag, a + k0, k); - du::dumma::du_load_matrix_sync( - b_frag, b + static_cast(k0) * n + n0, n); - du::dumma::du_mma_sync(acc_frag, a_frag, b_frag, acc_frag); - } +// Iteration 4: two adjacent 16-wide N tiles per block (32 N columns). +// Iteration 8: one 64-lane wave owns each of those two tiles, so the block is +// two wavefronts and the A fragment of a 32-K step is shared by both waves +// through LDS; B has no reuse (every B byte belongs to exactly one block). +constexpr int kTilesPerWave = 2; - du::dumma::du_store_matrix_sync(acc_tile, acc_frag, kDummaN, - du::dumma::mem_row_major); - __syncthreads(); +// Iteration 13 target split of the K dimension: 8. With kTilesPerWave = 2 the +// grid is (N / (16*2)) * S = 40 * S blocks, so S = 8 gives 320 one-generation +// blocks over the 120-device-CU K500SM_AI (2.67 blocks/CU by grid, all resident +// because the unchanged 15,616 B/block LDS footprint still fits four blocks/CU +// = 8 resident waves/CU = 2 per SIMD by capacity). The move 16 -> 8 halves the +// ONLY non-compulsory component of the operator's memory traffic: the split-K +// partial planes (8 x 80 KiB = 655,360 B written and re-read instead of +// 16 x 80 KiB = 1,310,720 B), i.e. 1,310,720 B of the measured 8,068,416 B +// per-operator-replay traffic. Each split now owns 16 x 32 K stages = 512 +// rows, which is twice the unchanged 256-row staging capacity, so the kernel +// stages and consumes its slice in ascending 256-row PHASES (exactly two at +// the guard); see the phase loop below. The slices stay UNIFORM +// (128 == 8 x 16) and every boundary stays a multiple of 32, so the +// stage-alignment and exact-int32 contracts are untouched. The workspace +// holds 16 int32 planes for this shape, so 8 planes fit with room to spare and +// the runtime clamp still covers smaller buffers. +constexpr int kSplitKTarget = 8; - // 256 elements / 64 lanes: each lane scales and stores four outputs. -#pragma unroll - for (int linear = lane; linear < kDummaM * kDummaN; linear += kDummaWaveSize) { - const int row = linear / kDummaN; - const int col = linear - row * kDummaN; - const int out_col = n0 + col; - const float scaled = static_cast(acc_tile[linear]) * - x_scale[row] * w_scale[out_col]; - out[static_cast(row) * n + out_col] = f32_to_bf16_rne(scaled); - } -} +// Iteration 6 staging capacity, tied to the guard launch (k == 4096): 8 +// stages x 32 K = 256 K rows of the slice are staged per phase. Kept +// deliberately independent of kSplitKTarget since iteration 13: the 512-row +// slice of the 8-way split is staged as two 256-row phases, so the staging +// capacity, the A bank-skew pad and the 8-deep B batch are the accepted +// iteration-11 geometry (iteration 17 only re-lays the B mirror n-major). +constexpr int kStageMaxSteps = 8; +constexpr int kStageMaxRows = kStageMaxSteps * kDummaK; // 256 rows +// stage_a mirrors the block's A slice: 16 m-rows x rows bytes of x_q stored +// m-major at kStageARowStride = rows + 16 pad bytes. 272 B (== 16 mod 128 B) +// skews the row_major A fragment loader's 16 row reads instead of replaying +// one bank, and 272 is a multiple of 16 so the cooperative 16-B stores stay +// 16-B aligned. +constexpr int kStageARowStride = kStageMaxRows + 16; // 272 B +// Iteration 17: stage_b is an n-MAJOR mirror of the block's 32-column B strip. +// Element B[k][n0 + n] lives at stage_b[n * kStageBColStride + k], i.e. each +// N column owns a contiguous run of kStageBColStride bytes (the phase's k rows +// followed by 16 pad bytes), and the stride is 16-B aligned so every 16-B +// staged chunk is one aligned ds_write_b128. The library col_major matrix_b +// fragment loader reads lane l's 8 fragment bytes as +// p[(lane & 15) * ldm + 8 * (lane >> 4) + i] (i = 0..7) -- EIGHT CONTIGUOUS +// bytes -- so on the n-major tile the K loop's B fragment becomes ONE 8-byte +// LDS read per step instead of the iteration-13 row_major loader's 8 scatter +// ds_read_u8 plus the byte-reassembly VALU that fed them. The same logical +// (k, n) element lands in the same v_mmac fragment register in both layouts, +// so every k-ascending int32 partial and the whole output stay bit-identical. +constexpr int kStageBColStride = kStageMaxRows + 16; // 272 B +constexpr int kStageABytes = kDummaM * kStageARowStride; // 4,352 B +constexpr int kStageBBytes = + kDummaN * kTilesPerWave * kStageBColStride; // 8,704 B -// --------------------------------------------------------------------------- -// Split-K partial kernel (iteration 2 probe, widened in iteration 4 to -// multi-N-tile reuse, B-staged in iteration 5): same m16n16k32 int8 DUMMA -// tile as iteration 1, but gridDim = (N/(16*kTilesPerWave), SPLIT_K) so -// every one-wave block owns kTilesPerWave adjacent 16-wide N tiles AND one -// ascending 32-aligned K slice, and writes its exact int32 partials into -// workspace planes partials[split][16][n]. Iteration 4: kTilesPerWave = 2, -// so per 32-K step the wave loads the A fragment once and shares it between -// the two du_mma_sync calls (A bytes of the step reused across both output -// tiles; every B byte is still read exactly once). Iteration 5 (staging -// round, B-only): each block first stages its whole 32-column B slice -// (my_steps*32 rows x 32 B, <= 352 x 32 = 11,264 B under the SPLIT_K=12 -// guard launch) into an LDS tile stage_b that is an EXACT byte mirror of -// the block's global B strip (16-B int4 cooperative loads, 4-deep source -// batching so one vmcnt drain covers 4 in-flight round trips), then after -// one same-wave __syncthreads() the K loop reads the B0/B1 fragments from -// LDS with du_load_matrix_sync (row stride 32) -- same fragment bytes as -// the iteration-4 direct global loads, so the int32 partials stay -// bit-identical -- while A stays on the direct-global path (this round -// isolates the B side only). BlockDim is one 64-lane wavefront: the two -// barriers are the staging barrier above and the single same-wave -// __syncthreads() after the LDS materialization of both partial tiles. -// LDS per block: 11,264 B (stage_b) + 2 x 1,024 B (acc tiles) = 13,312 B -// -> 65,536 / 13,312 = 4.9, so the 4-blocks-per-CU launch co-residency is -// preserved (LDS is not the occupancy limiter). -// -// Iteration 7 (this round): A+B whole-slice LDS staging. Each one-wave -// block now stages BOTH operands of its whole K slice once: stage_a holds -// the 16 m-rows x 256 slice bytes at row stride 272 (256 data + 16 pad for -// bank skew, 4,352 B) and stage_b holds the 256 x 32 B strip (8,192 B); -// both use 16-B int4 cooperative loads with the same 4-deep batching, one -// same-wave __syncthreads(), and then the K loop reads every fragment from -// LDS (A at row stride 272, B0/B1 at row stride 32) -- zero per-step global -// loads and zero vmcnt waits on the MMA path. Geometry moved to SPLIT_K=16 -// (uniform 8-step slices, 40 x 16 = 640 blocks): per-block LDS 4,352 + -// 8,192 + 2,048 = 14,592 B -> floor(65,536/14,592) = 4, so the 4-blocks/CU -// co-residency of iteration 5 is preserved. Barriers per block stay two -// (staging, epilogue). Staged tiles are exact byte mirrors, so partials -// are bit-identical to iteration 5 and the combine kernel is unchanged. -// -// Iteration 8 (occupancy/resource round, this round): tune the waves-per- -// block occupancy limiter. PMC of the accepted iteration-7 code object -// (source digest b64e3209..., grid 640, workgroup 64, arch_vgpr 40, sgpr 48, -// scratch 0, LDS/block 14,592 B -> floor(65,536/14,592) = 4 resident blocks/ -// CU, 5.33 waves/CU by grid) shows a per-SIMD latency-exposure wall: every -// block is ONE wavefront, so at 4 resident blocks/CU the four waves land one -// per SIMD and each SIMD has no second independent wave to issue while the -// resident wave stalls on its per-step s_waitcnt lgkmcnt (lds_wait 8,359) -// after the staged-fragment du_load_matrix_sync and on its du_mma accum -// chain (VALU issue utilization ~3-6% of the 26.56 us profiled kernel; -// vmem_read 7,680 = only the 12 one-time staging loads per block). Fix: -// blockDim 64 -> 128, i.e. TWO 64-lane wavefronts per block, and the block's -// 32-column strip is split by ownership: wave w computes ONLY n-tile w (cols -// [n0+16w, n0+16w+16)) over the shared 8-step slice with its own independent -// accumulator chain, one du_mma per step per wave (same total MMA count). -// LDS per block is UNCHANGED (stage_a 4,352 + stage_b 8,192 + 2 x 1,024 acc -// tiles = 14,592 B), so 4 blocks/CU still fit but now carry 8 resident waves -// = 2 per SIMD: one wave's LDS->MMA stall is filled by the other. The -// one-time staging is partitioned, NOT duplicated: wave 0 copies the shared -// A slice and wave 1 copies the B strip (their iteration-7 loops unchanged), -// so exactly the same global bytes are read once (no repeated HBM reads for -// occupancy) and the staging barrier becomes a cross-wave __syncthreads(). -// Deliberate added cost: the A fragment is re-read from LDS by each wave -// (lds_instructions 99,840 -> ~125-135k predicted; LDS pipe/conflicts are -// not the wall at ~3-6% issue utilization -- the trade is LDS re-reads, not -// HBM traffic). Geometry, planes, combine kernel, guards and fallbacks are -// otherwise unchanged. -// --------------------------------------------------------------------------- -__global__ void w8a8_dumma_m16_splitk_partial_kernel( - const int8_t* __restrict__ a, // x_q [16, k] row-major - const int8_t* __restrict__ b, // packed_weight [k, n] row-major - int32_t* __restrict__ partials, // [gridDim.y][16][n] int32 +__global__ __launch_bounds__(kDummaWaveSize * kTilesPerWave) +void w8a8_dumma_m16_splitk_kernel( + const int8_t* __restrict__ a, // x_q [M, K] row-major + const int8_t* __restrict__ b, // packed_weight [N, K] n-major (guard) + int32_t* __restrict__ partials, // workspace [S][M, N] int32 int n, - int k) { - constexpr int kTilesPerWave = 2; // two adjacent 16-wide N tiles per block - // Iteration 8 (occupancy round): each block is TWO 64-lane wavefronts - // (blockDim 128). Wave w owns N tile w of the block's 32 columns (cols - // [n0 + 16*w, n0 + 16*w + 16)) and runs an independent 8-step accumulator - // chain over the shared K slice for that tile alone. - const int tid = static_cast(threadIdx.x); // blockDim == 128 - const int w = tid >> 6; // wave id inside block 0/1 - const int lane = tid & (kDummaWaveSize - 1); // lane id within the wave - const int group = static_cast(blockIdx.x); // 32-wide n-group id - const int split = static_cast(blockIdx.y); // k-slice id + int k, + int split_k, + int groups /* n / (kDummaN * kTilesPerWave) */) { + // Iteration 8: blockDim is TWO 64-lane wavefronts. Wave w owns N tile w of + // the block's 2 x 16 columns (cols [n0 + 16*w, n0 + 16*w + 16)) and runs its + // own independent m16n16k32 accumulator chain for that tile; the staging of + // each ascending 256-row phase is partitioned across the waves, not + // duplicated (wave 0 copies the shared A phase, wave 1 the B strip phase), + // so every global byte is still read exactly once per block -- no repeated + // HBM reads are traded for occupancy. + const int tid = static_cast(threadIdx.x); // blockDim == 128 + const int w = tid / kDummaWaveSize; // wave id inside block: 0/1 + const int lane = tid - w * kDummaWaveSize; // lane id within the wave + const int group = static_cast(blockIdx.x) % groups; + const int split = static_cast(blockIdx.x) / groups; const int n0 = group * (kDummaN * kTilesPerWave); - // Stage-buffer sizing is tied to the ONLY launch site of this kernel: the - // exact (m,n,k)==(16,1280,4096) guard with gridDim.y == 16 (iteration 7), - // so total_steps == 128 and every split owns exactly 8 steps = 256 rows - // (128 == 16 * 8; the generic slice formula below degenerates to uniform - // 8-step slices). Compile-time capacities: 256 rows of B per block and - // 16 m-rows x (256 + 16 pad) bytes of A per block. - constexpr int kStageSplitCount = 16; // gridDim.y of the guard launch - constexpr int kStageMaxSteps = - (128 + kStageSplitCount - 1) / kStageSplitCount; // 8 (uniform) - constexpr int kStageMaxRows = kStageMaxSteps * kDummaK; // 256 rows x 32 - constexpr int kStageARowStride = - kStageMaxRows + 16; // 272 B per A m-row: 256 data + 16 pad for banks - // Iteration 21 (DS-side bank-skew round): each staged B k-row now occupies - // kStageBRowStride = 36 B (32 data bytes + 4 pad bytes). The row-major B - // fragment loader reads a lane's 8 fragment bytes at addresses - // base + (8*(lane>>4) + i) * ldm, i = 0..7: the four lane groups - // g = lane>>4 sit 8*ldm bytes apart, so with the old ldm == 32 every group - // landed 256 B (== 0 mod 128 B) apart and each of the 8 ds_read_u8 per - // fragment replayed the SAME LDS bank 4x (the PMC-documented - // lds_bank_conflicts 276,480 = ~2.57 replays/instr on this object, ~89% of - // it from the B-fragment byte reads: 128 per block). ldm == 36 puts the - // groups 288 B (== 32 mod 128 B, 8 banks) apart, so the four groups hit - // banks {b, b+8, b+16, b+24} -- fully conflict-free -- while the 4-B pad - // per row keeps every row start 4-B aligned for the staging stores. - // stage_b grows 8,192 -> 9,216 B and the block total becomes 4,352 - // (stage_a) + 9,216 (stage_b) + 2,048 (acc tiles) = 15,616 B/block: - // 4 x 15,616 = 62,464 <= 65,536, so the 4-resident-blocks/CU = - // 8-resident-waves/CU occupancy sweet spot is preserved unchanged. - constexpr int kStageBRowStride = kDummaN * 2 + 4; // 36 B per staged B row - + // Two LDS staging tiles, one per 16-wide N tile of this block, plus the + // iteration-6 whole-slice operand mirrors: stage_a (the shared A slice) and + // stage_b (the block's 32-column n-major B strip). Static LDS = 2 x 1,024 + // + 4,352 + 8,704 = 15,104 B/block, so floor(65,536 / 15,104) = 4 resident + // blocks/CU = 8 resident waves/CU (2 per SIMD) since iteration 8. __shared__ __align__(16) int32_t acc_tile0[kDummaM * kDummaN]; __shared__ __align__(16) int32_t acc_tile1[kDummaM * kDummaN]; - __shared__ __align__(16) int8_t stage_a[kDummaM * kStageARowStride]; - __shared__ __align__(16) int8_t stage_b[kStageMaxRows * kStageBRowStride]; + __shared__ __align__(16) int8_t stage_a[kStageABytes]; + __shared__ __align__(16) int8_t stage_b[kStageBBytes]; du::dumma::DUFragment a_frag; + // Iteration 17: the B fragment is col_major over the n-major stage_b, so its + // 8 bytes per lane are contiguous in LDS (one 8-byte read per step). du::dumma::DUFragment - b_frag; // this wave's B half of the strip (b_col == 16*w bytes) + signed char, du::dumma::col_major> + b_frag; // this wave's own 16-wide B half of the block's 32-column strip du::dumma::DUFragment acc_frag; // this wave's accumulator for its n16 tile du::dumma::du_fill_fragment(acc_frag, 0); - // 32-aligned non-uniform K slicing over the gridDim.y splits: split s owns - // my_steps ascending 32-K steps beginning at step_begin, slices are - // disjoint and cover [0, k) exactly once in ascending order, so the int32 - // partial sums and the ascending combine below reproduce the scalar - // k-ascending sum bit-for-bit. - const int total_steps = k / kDummaK; - const int split_count = static_cast(gridDim.y); - const int base_steps = total_steps / split_count; - const int rem_steps = total_steps - base_steps * split_count; - const int my_steps = base_steps + (split < rem_steps ? 1 : 0); - const int step_begin = - split * base_steps + (split < rem_steps ? split : rem_steps); - const int k_base = step_begin * kDummaK; // first global k row of slice - const int rows = my_steps * kDummaK; // slice rows: my_steps x 32 + // Stage-aligned split: split s owns stages [s*T/S, (s+1)*T/S) of the + // T = k/kDummaK 32-K stages, so every k range is a multiple of 32 and the + // ranges tile [0, k) exactly with no gap and no overlap. + const int total_stages = k / kDummaK; + const int stage_begin = (split * total_stages) / split_k; + const int stage_end = ((split + 1) * total_stages) / split_k; + const int k_begin = stage_begin * kDummaK; + const int k_end = stage_end * kDummaK; - // Stage the block's operands (iteration 8: PARTITIONED across the two - // wavefronts, not duplicated -- the union reads exactly the same global - // bytes as the iteration-7 single wave, so no byte is fetched twice and - // HBM traffic is unchanged). Wave 0 copies the shared A slice: the 16 - // m-rows x rows bytes of A (global columns [k_base, k_base + rows)), - // stored m-major in stage_a at row stride kStageARowStride (an exact byte - // mirror of global a[m][k_base .. k_base + rows)). Wave 1 copies the - // block's 32-column B strip: my_steps*32 rows x 32 B, stored row-major in - // stage_b at the 36-B row pitch kStageBRowStride (32 data + 4 pad bytes - // per row; iteration 21 bank skew -- an exact byte mirror of the global - // data rows [k_base, k_base + rows) x columns [n0, n0 + 32), pads never - // written or read). Each copy uses the - // unchanged iteration-7 cooperative code: 64 lanes x 16 B per wavefront - // instruction, chunks in LDS int4 units, loads batched 4-deep in source so - // one vmcnt drain covers 4 in-flight global round trips (the two waves' - // round trips overlap further across the SIMDs). Global m-rows are - // k-major at stride k and global B rows are n-major at stride n (k % 16 == - // 0 and n % 16 == 0 under the guard; k_base and n0 are 32-aligned), so - // every 16-B global load is 16-B aligned (the wave-1 LDS stores are - // 4-B-granular since iteration 21: 36*row is only 4-B aligned). - const int a_chunks = (rows * kDummaM) / 16; // rows/16 int4 per m-row - const int chunks = rows * (kDummaN * 2) / 16; // rows*2 int4 chunks - if (w == 0) { - const int per_row = rows / 16; - const int4* __restrict__ a4 = reinterpret_cast(a); - int4* __restrict__ ald4 = reinterpret_cast(stage_a); - const int kb4 = k_base >> 4; // global int4 column of the slice start - const auto ld_chunk = [&](int e) { - const int m = e / per_row; - const int t = e - m * per_row; - return a4[static_cast(m) * (k >> 4) + kb4 + t]; - }; - const auto st_chunk = [&](int e, const int4& v) { - const int m = e / per_row; - const int t = e - m * per_row; - ald4[m * (kStageARowStride / 16) + t] = v; - }; - int e = lane; - for (; e + 3 * kDummaWaveSize < a_chunks; e += 4 * kDummaWaveSize) { - const int4 v0 = ld_chunk(e); - const int4 v1 = ld_chunk(e + kDummaWaveSize); - const int4 v2 = ld_chunk(e + 2 * kDummaWaveSize); - const int4 v3 = ld_chunk(e + 3 * kDummaWaveSize); - st_chunk(e, v0); - st_chunk(e + kDummaWaveSize, v1); - st_chunk(e + 2 * kDummaWaveSize, v2); - st_chunk(e + 3 * kDummaWaveSize, v3); - } - for (; e < a_chunks; e += kDummaWaveSize) { - st_chunk(e, ld_chunk(e)); - } - } else { - const int4* __restrict__ b4 = reinterpret_cast(b); - const auto ld_chunk = [&](int e) { - const int r = e >> 1; - // Global int4 index of (global row k_base + r, col half e&1). - return b4[static_cast(k_base + r) * (n >> 4) + (n0 >> 4) + - (e & 1)]; - }; - // Iteration 21: stage_b rows now live at the 36-B pitch - // kStageBRowStride (32 data bytes + 4 pad per row), so a 16-B chunk - // (row r = e >> 1, col half h = e & 1 of that row) can no longer be - // stored with one 16-B-aligned ds_write_b128 (36*r is 16-B aligned only - // for r % 4 == 0). Each chunk is stored as four consecutive int32 words - // at word offset 9*r + 4*h + {0,1,2,3} == (36*r + 16*h)/4 + {0,1,2,3}: - // every data byte lands at exactly the same byte offset (row*36 + ncol) - // the loader reads back, so the staged data byte mirror of the global B - // strip is unchanged -- only the pad bytes (row*36 + 32..35) are never - // written and never read (the fragment loader only touches ncol < 32). - // LDS store bandwidth is unchanged (4 x 4-B words per chunk); the four - // words of one chunk are consecutive, so a lane's store stream stays - // dense per row half. - int32_t* __restrict__ lds32 = reinterpret_cast(stage_b); - const auto st_chunk = [&](int e, const int4& v) { - const int word0 = (e >> 1) * (kStageBRowStride >> 2) + (e & 1) * 4; - lds32[word0 + 0] = v.x; - lds32[word0 + 1] = v.y; - lds32[word0 + 2] = v.z; - lds32[word0 + 3] = v.w; - }; - // Iteration 20 (guard-slice staging latency specialization): the guard - // launch is the ONLY call site of this kernel and always stages the - // full 512-chunk B strip (chunks == rows*2 == 512 == 8 x 64 lanes, rows - // == 256 == kStageMaxRows, staged capacity exactly filled). The - // runtime-bounded 4-deep loop below then compiles to TWO serialized - // 4-deep iterations (each iteration issues four 16-B source loads and - // drains them with vmcnt waits before the next iteration's loads can - // issue -- two full global round trips on the block's staging critical - // path, one per iteration), and wave 0's A copy finishes in a single - // 4-deep group and then idles at the __syncthreads() join while wave 1 - // completes its second round trip. When chunks == 8*kDummaWaveSize the - // loop now runs as ONE straight-line 8-deep batched group: all eight - // 16-B source loads per lane are issued before the first LDS store, so - // one drain window covers the whole strip and the second round trip is - // removed from the join's critical path. Every chunk id [0, 512) is - // still loaded and stored exactly once by exactly one lane to the same - // stage_b byte (the chunk set, the store order and the byte mirror are - // identical to the rolled loop), so every staged byte, every partial - // and every combine output byte is bit-identical; the generic rolled - // loop stays verbatim as the else path for any non-guard (chunks != - // 512) configuration, which never executes at the only launch site. - if (chunks == 8 * kDummaWaveSize) { - const int4 v0 = ld_chunk(lane); - const int4 v1 = ld_chunk(lane + kDummaWaveSize); - const int4 v2 = ld_chunk(lane + 2 * kDummaWaveSize); - const int4 v3 = ld_chunk(lane + 3 * kDummaWaveSize); - const int4 v4 = ld_chunk(lane + 4 * kDummaWaveSize); - const int4 v5 = ld_chunk(lane + 5 * kDummaWaveSize); - const int4 v6 = ld_chunk(lane + 6 * kDummaWaveSize); - const int4 v7 = ld_chunk(lane + 7 * kDummaWaveSize); - st_chunk(lane, v0); - st_chunk(lane + kDummaWaveSize, v1); - st_chunk(lane + 2 * kDummaWaveSize, v2); - st_chunk(lane + 3 * kDummaWaveSize, v3); - st_chunk(lane + 4 * kDummaWaveSize, v4); - st_chunk(lane + 5 * kDummaWaveSize, v5); - st_chunk(lane + 6 * kDummaWaveSize, v6); - st_chunk(lane + 7 * kDummaWaveSize, v7); - } else { - int e = lane; - for (; e + 3 * kDummaWaveSize < chunks; e += 4 * kDummaWaveSize) { - const int4 v0 = ld_chunk(e); - const int4 v1 = ld_chunk(e + kDummaWaveSize); - const int4 v2 = ld_chunk(e + 2 * kDummaWaveSize); - const int4 v3 = ld_chunk(e + 3 * kDummaWaveSize); - st_chunk(e, v0); - st_chunk(e + kDummaWaveSize, v1); - st_chunk(e + 2 * kDummaWaveSize, v2); - st_chunk(e + 3 * kDummaWaveSize, v3); - } - for (; e < chunks; e += kDummaWaveSize) { - st_chunk(e, ld_chunk(e)); + // Iteration 13: split_k == 8 makes each split's slice 16 x 32-K stages = 512 + // rows, twice the unchanged 256-row staging capacity, so the slice is staged + // and consumed in ascending 256-row PHASES (exactly two at the exact-shape + // guard). Each phase reproduces the accepted iteration-11 staging geometry + // byte-for-byte -- A: 256 16-B chunks = 4 per lane on wave 0 (one 4-deep + // batch); B: 512 chunks = 8 per lane on wave 1 (ONE straight-line 8-deep + // batch behind a single vmcnt drain) -- and the phase's 8 K-steps accumulate + // into the SAME acc_frag in ascending k order. The staged bytes, their LDS + // addresses and therefore every fragment byte are identical to what the + // 16-way split staged per 32-K step; only the number of int32 partial planes + // written and re-read by the split-K round trip halves. + for (int phase_begin = k_begin; phase_begin < k_end; + phase_begin += kStageMaxRows) { + const int remain = k_end - phase_begin; + const int phase_rows = (remain < kStageMaxRows) ? remain : kStageMaxRows; + + // Iteration 6 A+B phase staging, PARTITIONED across the two wavefronts + // since iteration 8 (B mirror is n-major since iteration 17). Every + // global operand byte of this phase is loaded exactly once, in 16-B int4 + // granules, into an EXACT byte mirror of the operand region the fragment + // loaders read: stage_a holds a[m][phase_begin .. phase_begin+phase_rows) + // at row stride kStageARowStride, and stage_b holds the block's 32 + // B-columns' phase as b_lds[n][k], i.e. element + // packed[n0 + n][phase_begin + k] at n * kStageBColStride + k (the 16 pad + // bytes per column are never written and never read). Wave 0 copies the A + // phase and wave 1 copies the B strip -- the union reads exactly the same + // global bytes as the single-wave iteration-6 prologue, so no operand byte + // is fetched twice. Each copy uses 64 lanes x 16 B per wavefront + // instruction; the A phase is one 4-deep batch and the exact-guard B strip + // is one 8-deep batch, so one vmcnt drain covers each wave's whole phase + // and the two waves' round trips overlap across the SIMDs. n0 and + // phase_begin are 32-aligned under the exact-shape guard and k % 16 == 0, + // n % 16 == 0, so every 16-B source load is 16-B aligned, and every staged + // B chunk lands at a 16-B-aligned n-major address (one ds_write_b128). + const bool staged_fits = phase_rows <= kStageMaxRows; + if (staged_fits) { + if (w == 0) { + // A slice phase: 16 m-rows x phase_rows bytes = phase_rows int4 + // chunks, per_row = phase_rows/16 chunks per m-row (phase_rows is a + // multiple of 32, hence of 16). + const int per_row = phase_rows / 16; + const int a_chunks = kDummaM * per_row; // == phase_rows + const int4* __restrict__ a4 = reinterpret_cast(a); + int4* __restrict__ ald4 = reinterpret_cast(stage_a); + const int kb4 = phase_begin >> 4; // phase start in global int4 columns + const int a4_stride = k >> 4; // global A int4 columns per m-row + const auto ld_a = [&](int e) { + const int m = e / per_row; + const int t = e - m * per_row; + return a4[static_cast(m) * a4_stride + kb4 + t]; + }; + const auto st_a = [&](int e, const int4& v) { + const int m = e / per_row; + const int t = e - m * per_row; + ald4[m * (kStageARowStride / 16) + t] = v; + }; + int e = lane; + for (; e + 3 * kDummaWaveSize < a_chunks; e += 4 * kDummaWaveSize) { + const int4 v0 = ld_a(e); + const int4 v1 = ld_a(e + kDummaWaveSize); + const int4 v2 = ld_a(e + 2 * kDummaWaveSize); + const int4 v3 = ld_a(e + 3 * kDummaWaveSize); + st_a(e, v0); + st_a(e + kDummaWaveSize, v1); + st_a(e + 2 * kDummaWaveSize, v2); + st_a(e + 3 * kDummaWaveSize, v3); + } + for (; e < a_chunks; e += kDummaWaveSize) { + st_a(e, ld_a(e)); + } + } else { + // Iteration 17 B strip: the n-major packed weight stores each N + // column's K axis contiguously, so a phase is + // chunks_per_n = phase_rows / 16 int4 chunks PER N column (16 at the + // guard) and consecutive lanes cover consecutive 16-B chunks of one + // column's 256-B phase run. Each chunk is written CONTIGUOUSLY into + // the n-major LDS tile at stage_b[nl * kStageBColStride + t * 16], i.e. + // one aligned ds_write_b128 per chunk, exactly the bytes the col_major + // fragment loader reads back. + const int chunks_per_n = phase_rows / 16; // 16 at the guard + const int b_chunks = (kDummaN * kTilesPerWave) * chunks_per_n; + const int4* __restrict__ b4 = reinterpret_cast(b); + int4* __restrict__ bld4 = reinterpret_cast(stage_b); + const int b4_per_n = k >> 4; // packed int4 chunks per N column + const auto ld_b = [&](int e) { + const int nl = e / chunks_per_n; // local N column 0..31 + const int t = e - nl * chunks_per_n; // k-chunk in this phase + return b4[static_cast(n0 + nl) * b4_per_n + + (phase_begin >> 4) + t]; + }; + // 272-B n-row stride and 16-B chunks keep every store 16-B aligned. + const auto st_b = [&](int e, const int4& v) { + const int nl = e / chunks_per_n; + const int t = e - nl * chunks_per_n; + bld4[nl * (kStageBColStride >> 4) + t] = v; + }; + int e = lane; + // Iteration 11: the exact-guard B strip is exactly 8 chunks per lane + // (phase_rows == kStageMaxRows == 256 -> b_chunks == 512 == 8 x 64 + // lanes), so that case runs as ONE straight-line 8-deep batched group: + // all eight 16-B source loads issue before the first LDS store, one + // vmcnt drain covers the whole strip, and the second dependent global + // round trip leaves the block's staging join. Any other chunk count + // keeps the unchanged rolled 4-deep loop below. + if (b_chunks == 8 * kDummaWaveSize) { + const int4 v0 = ld_b(e + 0 * kDummaWaveSize); + const int4 v1 = ld_b(e + 1 * kDummaWaveSize); + const int4 v2 = ld_b(e + 2 * kDummaWaveSize); + const int4 v3 = ld_b(e + 3 * kDummaWaveSize); + const int4 v4 = ld_b(e + 4 * kDummaWaveSize); + const int4 v5 = ld_b(e + 5 * kDummaWaveSize); + const int4 v6 = ld_b(e + 6 * kDummaWaveSize); + const int4 v7 = ld_b(e + 7 * kDummaWaveSize); + st_b(e + 0 * kDummaWaveSize, v0); + st_b(e + 1 * kDummaWaveSize, v1); + st_b(e + 2 * kDummaWaveSize, v2); + st_b(e + 3 * kDummaWaveSize, v3); + st_b(e + 4 * kDummaWaveSize, v4); + st_b(e + 5 * kDummaWaveSize, v5); + st_b(e + 6 * kDummaWaveSize, v6); + st_b(e + 7 * kDummaWaveSize, v7); + e += 8 * kDummaWaveSize; + } + for (; e + 3 * kDummaWaveSize < b_chunks; e += 4 * kDummaWaveSize) { + const int4 v0 = ld_b(e); + const int4 v1 = ld_b(e + kDummaWaveSize); + const int4 v2 = ld_b(e + 2 * kDummaWaveSize); + const int4 v3 = ld_b(e + 3 * kDummaWaveSize); + st_b(e, v0); + st_b(e + kDummaWaveSize, v1); + st_b(e + 2 * kDummaWaveSize, v2); + st_b(e + 3 * kDummaWaveSize, v3); + } + for (; e < b_chunks; e += kDummaWaveSize) { + st_b(e, ld_b(e)); + } } } - } - __syncthreads(); // stage_a and stage_b ready for every lane of both waves - - // LDS-only K loop (iteration 8): every 32-K step wave w reads the A - // fragment from the staged A mirror at row stride kStageARowStride and its - // own B half from the staged B strip at the 36-B row pitch - // kStageBRowStride (byte column b_col; iteration 21 bank skew) - // -- the same fragment bytes the iteration-7 loads produced, with no - // global round trip and no vmcnt wait on the MMA path -- then issues ONE - // du_mma on its private accumulator. The two resident wavefronts carry - // independent 8-step chains, so a SIMD holding both can issue one wave's - // instructions while the other stalls on its s_waitcnt lgkmcnt / du_mma - // dependency (per-SIMD latency exposure was the iteration-7 wall). - const int b_col = w * kDummaN; // byte offset of this wave's n16 tile + // Cross-wave barrier: wave 0's staged A phase is read by wave 1 (and vice + // versa for the B strip), so both tiles are ready for both waves here, and + // it also orders this phase's staging after every lane's previous-phase + // LDS reads. + __syncthreads(); // stage_a / stage_b ready for every lane of the block - // Iteration 18 (compile-time 8-step K-loop specialization): the guard - // launch is the ONLY call site of this kernel and always runs the uniform - // 8-step slice (k == 4096, gridDim.y == 16 -> my_steps == 8 == - // kStageMaxSteps, rows == 256 == kStageMaxRows, staged capacity exactly - // filled). The runtime-bounded generic loop below compiles to a ROLLED - // body (verified in the exact-source code object: one static v_mmac per - // step with per-step ds_read2_b32 + 8 x ds_read_u8 + ~25-VALU byte - // reassembly + 4 lgkmcnt drains on the accumulator chain and a - // loop-carried WAR on the single fragment set), so every step's full LDS - // latency sits serially on the MMA path with only 2 resident waves/SIMD - // to cover it (PMC of the exact object: lds_instructions 107,520, - // lds_wait_instructions 19,597). Making the guard trip count - // compile-time 8 lets the backend fully unroll the chain, rename the - // fragment registers per step and hoist later steps' LDS reads across the - // current step's v_mmac (local exact-flag codegen check of this edit: 8 - // static v_mmac in the specialized path with the next steps' LDS reads - // issued before the current v_mmac; vgpr 34 / private_segment 0 / - // group_segment 14,592 B unchanged -> the 4-blocks/CU = 8-waves/CU - // occupancy is preserved). Both branches issue the identical - // ascending-s du_mma sequence into the same accumulator over the same - // staged byte mirrors, so every partial byte and every combine output - // byte is bit-identical to iteration 16; the generic rolled loop stays - // verbatim as the else path for any non-guard (my_steps != 8) - // configuration. - if (my_steps == kStageMaxSteps) { -#pragma unroll - for (int s = 0; s < kStageMaxSteps; ++s) { - du::dumma::du_load_matrix_sync( - a_frag, stage_a + static_cast(s) * kDummaK, - kStageARowStride); - du::dumma::du_load_matrix_sync( - b_frag, - stage_b + - static_cast(s) * kDummaK * kStageBRowStride + b_col, - kStageBRowStride); - du::dumma::du_mma_sync(acc_frag, a_frag, b_frag, acc_frag); - } - } else { - for (int s = 0; s < my_steps; ++s) { - du::dumma::du_load_matrix_sync( - a_frag, stage_a + static_cast(s) * kDummaK, - kStageARowStride); - du::dumma::du_load_matrix_sync( - b_frag, - stage_b + - static_cast(s) * kDummaK * kStageBRowStride + b_col, - kStageBRowStride); - du::dumma::du_mma_sync(acc_frag, a_frag, b_frag, acc_frag); + // Iteration 8 LDS-only K loop, one independent chain per wavefront. Each + // 32-K step reads the shared A fragment from the staged A mirror at row + // stride kStageARowStride and this wave's own 16-wide B half from the + // iteration-17 n-major B mirror at column stride kStageBColStride, where + // the library col_major loader takes the lane's 8 fragment bytes as ONE + // contiguous 8-byte read -- no global load and no vmcnt wait on the MMA + // path. The ascending k0 order keeps each partial's int32 accumulation + // exact, and the staged tiles are exact byte copies of the global bytes the + // iteration-4 path read, so this branch is bit-identical to the direct-global + // loop below. + if (staged_fits) { + for (int k0 = phase_begin; k0 < phase_begin + phase_rows; + k0 += kDummaK) { + const int local_row = k0 - phase_begin; + du::dumma::du_load_matrix_sync(a_frag, stage_a + local_row, + kStageARowStride); + du::dumma::du_load_matrix_sync( + b_frag, stage_b + local_row + w * kDummaN * kStageBColStride, + kStageBColStride); + du::dumma::du_mma_sync(acc_frag, a_frag, b_frag, acc_frag); + } + } else { + // Safety net kept for a runtime-clamped split whose phase would exceed + // the staging capacity (an undersized workspace clamps split_k < 8): the + // iteration-4 direct-global path, here consuming the same n-major packed + // weight through the col_major loader at column stride K, so a clamped + // launch stays bit-exact and can never overrun stage_a / stage_b. This + // branch never executes at the exact-shape guard launch, where every + // phase is exactly kStageMaxRows rows. + for (int k0 = phase_begin; k0 < phase_begin + phase_rows; + k0 += kDummaK) { + du::dumma::du_load_matrix_sync(a_frag, a + k0, k); + du::dumma::du_load_matrix_sync( + b_frag, b + static_cast(n0 + w * kDummaN) * k + k0, k); + du::dumma::du_mma_sync(acc_frag, a_frag, b_frag, acc_frag); + } } + // End-of-phase barrier: no lane may overwrite stage_a / stage_b for the + // next phase until every lane has consumed this phase's staged fragments. + __syncthreads(); } - // Epilogue (iteration 16, barrier-decoupled): each wave materializes its - // own tile into its own LDS acc buffer (wave 0 -> acc_tile0, wave 1 -> - // acc_tile1, 1,024 B each), then its 64 lanes read back that same buffer - // and store the 256 int32 partials (4 stores per lane) to - // plane[split][m][n0 + b_col + col]. acc_tile{w} is written and read ONLY - // by wave w, and the two waves' plane stores touch disjoint 16-column - // ranges, so nothing in the epilogue needs cross-wave convergence. The - // former epilogue __syncthreads() was a 128-thread cross-wave join whose - // release was gated by the slower of the block's two waves on a kernel - // that runs only 8 resident waves/CU (4 x 2-wave blocks) to hide the skew - // -- the same join whose five-wave form measured ~3.15 us of pure cost - // (iteration 13 23.764/24.086 -> iteration 15 20.613/20.641 us with the - // join removed, exact correctness passing in both). It is replaced by - // __threadfence_block(), which only orders THIS wave's own outstanding LDS - // writes (the du_store_matrix_sync ds_writes of its private acc tile) - // before its read-back ds_reads and costs no cross-wave wait. The staging - // __syncthreads() above stays (wave 0 stages the A slice and wave 1 stages - // the B strip; every wave reads bytes the other wave staged). Every - // partial byte, the k-ascending int32 order and every plane store address - // are unchanged, so the output is bit-identical to iteration 9 and exact - // correctness must pass with 0 mismatches. - if (w == 0) { - du::dumma::du_store_matrix_sync(acc_tile0, acc_frag, kDummaN, - du::dumma::mem_row_major); - } else { - du::dumma::du_store_matrix_sync(acc_tile1, acc_frag, kDummaN, - du::dumma::mem_row_major); - } - __threadfence_block(); // iteration 16: wave-local store->read ordering - // only; no cross-wave join (was __syncthreads()) + // Each wave materializes ONLY its own n16 tile (its private acc_tile slot) + // and publishes that tile's 16 columns to the plane; the two waves' plane + // ranges are disjoint, so no cross-wave dependency exists in the epilogue + // (the barrier below only orders each wave's own LDS store/read-back). + int32_t* my_tile = (w == 0) ? acc_tile0 : acc_tile1; + du::dumma::du_store_matrix_sync(my_tile, acc_frag, kDummaN, + du::dumma::mem_row_major); + __syncthreads(); - int32_t* plane = partials + static_cast(split) * kDummaM * n; - const int32_t* tile = (w == 0) ? acc_tile0 : acc_tile1; - const int out_col0 = n0 + b_col; + // 256 int32 elements / 64 lanes: each lane writes four partials of its own + // tile, kept row-major within the plane so the combine below reads them + // coalesced. Wave 1's tile lands kDummaN columns to the right of wave 0's. + int32_t* plane = + partials + static_cast(split) * kDummaM * n; #pragma unroll for (int linear = lane; linear < kDummaM * kDummaN; linear += kDummaWaveSize) { const int row = linear / kDummaN; const int col = linear - row * kDummaN; - plane[static_cast(row) * n + out_col0 + col] = tile[linear]; + const int64_t dst = + static_cast(row) * n + n0 + w * kDummaN + col; + plane[dst] = my_tile[linear]; } } -// --------------------------------------------------------------------------- -// Split-K combine kernel (iteration 2, restructured in iteration 9, mapping -// v3 in iteration 25): sums the gridDim.y int32 partial planes ascending per -// output element (exact int32, no overflow, bit-identical to the scalar -// k-ascending dot), applies x_scale[row] * weight_scale[col] in the scalar -// float order and stores bf16 through f32_to_bf16_rne. Iteration 9 added -// the guard specialization (split_count == 16): one output element per lane, -// all sixteen plane loads issued as INDEPENDENT reads into a 16-int register -// array (single vmcnt drain instead of a dependent load->wait->add chain of -// 16 DRAM round trips per element), then the ascending-s register sum and -// the scaled bf16 store. Iteration 25 (this round) keeps that per-lane -// structure and changes ONLY the block-to-output mapping: every one-wave -// block owns ONE m-row x kDummaCombineColsPerBlock = 64 consecutive columns -// (guard grid = 20 x 16 = 320 one-wave blocks, same block count as -// iteration 9). In the iteration-9/23 mappings each block covered 4 rows x -// 16/32 columns, so every per-plane wavefront load issued four 64/128-B runs -// at the 5,120-B m-row stride -- four different DRAM rows, i.e. up to four -// row activations per load instruction -- which is why widening the per-row -// run 64 -> 128 B in iteration 23 could not move the combine (it never -// reduced the rows-per-instruction scatter). With ONE row per block every -// per-plane load instruction is 64 lanes x 4 B = 256 B CONTIGUOUS = four -// consecutive 64-B lines of one DRAM row (a single run: ~4x fewer DRAM row -// activations for the identical 1.31 MB byte stream, identical 64-B sectors, -// identical wavefront-load count). The element set is unchanged, so every -// element's ascending-s int32 sum, the float scale order and every output -// bf16 word are bit-identical to the pre-iteration-25 combine; the generic -// loop below stays verbatim for every other call site / split count / grid -// shape. -// --------------------------------------------------------------------------- -__global__ void w8a8_dumma_m16_combine_kernel( - const int32_t* __restrict__ partials, // [split_count][16][n] int32 - const float* __restrict__ x_scale, // [16] - const float* __restrict__ w_scale, // [n] - uint16_t* __restrict__ out, // [16, n] bf16 payload +// Combine pass: sum the S exact int32 partial planes (order-free integer +// addition), then apply the two fp32 scales in the same order as the scalar +// fallback and store bf16. Every plane is M*N int32 = 80 KiB for this shape, +// so the whole combine is L2-resident. +// +// Iteration 9 (combine restructure, HIP-only): the guard launch +// (split_k == kCombineSplits == 8 since iteration 13) now runs a 2-D grid of +// one-wave 64-lane blocks -- (n / kCombineColsPerBlock) column groups x m rows +// -- with exactly one output element per lane (row = blockIdx.y, +// col = blockIdx.x * 64 + lane). The eight plane loads are issued as +// INDEPENDENT reads into a register array (one vmcnt drain, 8-way +// memory-level parallelism per lane) and summed afterwards in the unchanged +// ascending split order s = 0..7, instead of the rolled dependent load -> +// wait -> int32 add chain of 8 DRAM round trips per element. Each per-plane +// wavefront load also reads 64 lanes x 4 B = 256 B contiguous (four +// consecutive 64-B lines of one m-row). Every output element's int32 sum, +// fp32 scale order and bf16 conversion are unchanged, so the output is +// bit-identical; the generic path below (any clamped split_k < 8) keeps the +// original 128-thread linear mapping and the original dependent loop. +constexpr int kCombineThreads = 128; +constexpr int kCombineSplits = 8; // == kSplitKTarget of the guard +constexpr int kCombineWaveSize = 64; // one gfx928 wavefront per block +constexpr int kCombineColsPerBlock = 64; // n-columns per one-wave block + +__global__ __launch_bounds__(kCombineThreads) void w8a8_splitk_combine_kernel( + const int32_t* __restrict__ partials, // [S][M, N] int32 + const float* __restrict__ x_scale, // [M] + const float* __restrict__ weight_scale, // [N] + __hip_bfloat16* __restrict__ out, // [M, N] bf16 + int total /* m * n */, int n, - int split_count) { - const int lane = static_cast(threadIdx.x); // blockDim == 64 - const int n0 = static_cast(blockIdx.x) * kDummaN; + int split_k) { + const int lane = static_cast(threadIdx.x); - // --------------------------------------------------------------------- - // Iteration 25 specialization (mapping v3; guard launch only: - // split_count == 16 and gridDim.y == kDummaM == 16 -> blockIdx.x = - // 64-column group in [0, 20), blockIdx.y = one m-row in [0, 16)). Each - // lane owns output element (row, col) with row = blockIdx.y and col = - // blockIdx.x * 64 + lane: the sixteen independent 4-B plane reads (plane - // stride kDummaM * n int32) batch into one vmcnt drain, and because the - // 64 lanes of the block are 64 CONSECUTIVE columns of ONE row, every - // per-plane wavefront load covers 256 contiguous bytes (4 consecutive - // 64-B lines of one DRAM row) instead of four 5,120-B-strided runs; the - // ascending-s register sum then reproduces the exact int32 sequence for - // this element. - // --------------------------------------------------------------------- - if (split_count == kDummaCombineSplits && gridDim.y == kDummaM) { + // Iteration 9 guard specialization: one output element per lane, all sixteen + // plane reads issued as independent loads before the ascending sum. The + // launcher pairs this branch with the 2-D one-wave grid (blockDim == 64, + // grid == (n/64, m)); the blockDim test keeps any other launch shape on the + // generic path. + if (split_k == kCombineSplits && + static_cast(blockDim.x) == kCombineColsPerBlock) { const int row = static_cast(blockIdx.y); const int col = - static_cast(blockIdx.x) * kDummaCombineColsPerBlock + lane; + static_cast(blockIdx.x) * kCombineColsPerBlock + lane; const int64_t elem = static_cast(row) * n + col; - int32_t v[kDummaCombineSplits]; + int32_t plane[kCombineSplits]; #pragma unroll - for (int s = 0; s < kDummaCombineSplits; ++s) { - v[s] = partials[static_cast(s) * (kDummaM * n) + elem]; + for (int s = 0; s < kCombineSplits; ++s) { + plane[s] = partials[static_cast(s) * total + elem]; } int32_t acc = 0; #pragma unroll - for (int s = 0; s < kDummaCombineSplits; ++s) { - acc += v[s]; // ascending split order: identical int32 sum + for (int s = 0; s < kCombineSplits; ++s) { + acc += plane[s]; // ascending split order: identical exact int32 sum } const float scaled = - static_cast(acc) * x_scale[row] * w_scale[col]; - out[elem] = f32_to_bf16_rne(scaled); + static_cast(acc) * x_scale[row] * weight_scale[col]; + out[elem] = __float2bfloat16(scaled); return; } -#pragma unroll - for (int linear = lane; linear < kDummaM * kDummaN; - linear += kDummaWaveSize) { - const int row = linear / kDummaN; - const int col = linear - row * kDummaN; - const int out_col = n0 + col; - int32_t acc = 0; - for (int s = 0; s < split_count; ++s) { - acc += partials[(static_cast(s) * kDummaM + row) * n + - out_col]; - } - const float scaled = - static_cast(acc) * x_scale[row] * w_scale[out_col]; - out[static_cast(row) * n + out_col] = f32_to_bf16_rne(scaled); + const int i = static_cast(blockIdx.x) * kCombineThreads + lane; + if (i >= total) { + return; + } + + int32_t acc = 0; + for (int s = 0; s < split_k; ++s) { + acc += partials[static_cast(s) * total + i]; } + + const int row = i / n; + const int col = i - row * n; + const float scaled = + static_cast(acc) * x_scale[row] * weight_scale[col]; + out[i] = __float2bfloat16(scaled); } -// --------------------------------------------------------------------------- -// Identity device-to-device packing (bootstrap): grid-stride byte copies on -// the caller stream. Valid for every (K, N), including unmatched shapes. -// --------------------------------------------------------------------------- -__global__ void w8a8_identity_copy_i8_kernel( +// Identity pack: raw int8 weight -> packed int8 weight. +__global__ __launch_bounds__(kBlockThreads) void identity_i8_kernel( const int8_t* __restrict__ src, int8_t* __restrict__ dst, int64_t count) { - const int64_t stride = static_cast(gridDim.x) * blockDim.x; - for (int64_t i = static_cast(blockIdx.x) * blockDim.x + - static_cast(threadIdx.x); - i < count; i += stride) { + const int64_t i = + static_cast(blockIdx.x) * blockDim.x + threadIdx.x; + if (i < count) { dst[i] = src[i]; } } -__global__ void w8a8_identity_copy_f32_kernel( +// Identity pack: fp32 weight scale -> packed fp32 weight scale. +__global__ __launch_bounds__(kBlockThreads) void identity_f32_kernel( const float* __restrict__ src, float* __restrict__ dst, int64_t count) { - const int64_t stride = static_cast(gridDim.x) * blockDim.x; - for (int64_t i = static_cast(blockIdx.x) * blockDim.x + - static_cast(threadIdx.x); - i < count; i += stride) { + const int64_t i = + static_cast(blockIdx.x) * blockDim.x + threadIdx.x; + if (i < count) { dst[i] = src[i]; } } -// --------------------------------------------------------------------------- -// Host launcher consumed by csrc/bindings.cpp (gemm_out, timed region). -// The exact (16, 1280, 4096) guard dispatches the split-K DUMMA partial + -// combine kernels (two wavefronts per partial block since iteration 8); -// the scalar fallback below launches 128 threads per 128 outputs. -// --------------------------------------------------------------------------- +// Iteration 17 exact-guard n-major transpose pack: +// packed[n * K + k] = raw[k * N + n] ((K, N) == (4096, 1280) only) +// The pack runs once in launch_pack_w8a8_weight, outside the timed operator +// region and outside Graph capture, so a plain 32 x 32 LDS-tiled transpose is +// more than enough. Same byte count and the same packed_weight buffer as the +// identity pack; the packed layout is opaque per the API contract and every +// consumer below decodes this permutation for the guard pair. +__global__ __launch_bounds__(256) void pack_nmajor_i8_kernel( + const int8_t* __restrict__ src, // raw weight [K, N] row-major + int8_t* __restrict__ dst, // packed n-major [N, K] row-major + int k, + int n) { + __shared__ int8_t tile[32][33]; + const int tx = static_cast(threadIdx.x); // 0..31 + const int ty = static_cast(threadIdx.y); // 0..7 + const int col0 = static_cast(blockIdx.x) * 32; // N (output row) + const int row0 = static_cast(blockIdx.y) * 32; // K (input row) + // Load tile[k_local][n_local] = src[(row0 + k_local) * n + col0 + n_local]. + for (int j = ty; j < 32; j += 8) { + const int row = row0 + j; + if (row < k && col0 + tx < n) { + tile[j][tx] = src[static_cast(row) * n + (col0 + tx)]; + } + } + __syncthreads(); + // Store its transpose: dst[(col0 + n_local) * k + row0 + k_local]. + for (int j = ty; j < 32; j += 8) { + const int col = col0 + j; + if (col < n && row0 + tx < k) { + dst[static_cast(col) * k + (row0 + tx)] = tile[tx][j]; + } + } +} + +void launch_identity_i8( + const int8_t* src, + int8_t* dst, + int64_t count, + hipStream_t stream) { + if (count <= 0) { + return; + } + const int64_t blocks = (count + kBlockThreads - 1) / kBlockThreads; + hipLaunchKernelGGL( + identity_i8_kernel, + dim3(static_cast(blocks)), + dim3(kBlockThreads), + 0, + stream, + src, + dst, + count); +} + +void launch_pack_nmajor_i8( + const int8_t* src, + int8_t* dst, + int k, + int n, + hipStream_t stream) { + if (k <= 0 || n <= 0) { + return; + } + const dim3 block(32, 8); + const dim3 grid( + static_cast((n + 31) / 32), + static_cast((k + 31) / 32)); + hipLaunchKernelGGL( + pack_nmajor_i8_kernel, grid, block, 0, stream, src, dst, k, n); +} + +void launch_identity_f32( + const float* src, + float* dst, + int64_t count, + hipStream_t stream) { + if (count <= 0) { + return; + } + const int64_t blocks = (count + kBlockThreads - 1) / kBlockThreads; + hipLaunchKernelGGL( + identity_f32_kernel, + dim3(static_cast(blocks)), + dim3(kBlockThreads), + 0, + stream, + src, + dst, + count); +} + +} // namespace + extern "C" void launch_w8a8_gemm( const int8_t* a, const int8_t* b, @@ -1131,118 +1106,147 @@ extern "C" void launch_w8a8_gemm( int n, int k, hipStream_t stream) { - (void)workspace; - (void)workspace_bytes; - if (m <= 0 || n <= 0 || k <= 0 || a == nullptr || b == nullptr || - x_scale == nullptr || weight_scale == nullptr || out == nullptr) { + // The timed op may only use the caller-provided out/workspace on the + // caller stream; no host/device sync and no allocation happen here. + if (a == nullptr || b == nullptr || x_scale == nullptr || + weight_scale == nullptr || out == nullptr) { + return; + } + if (m <= 0 || n <= 0 || k <= 0) { return; } - // --------------------------------------------------------------------- - // Exact-shape DUMMA guard: hy3_tp8_qkv_proj_m16 (M=16, N=1280, K=4096). - // Lineage: iteration 4 widened each one-wave block to TWO adjacent - // 16-wide N tiles (32 columns) sharing one A-fragment load per 32-K step - // at SPLIT_K=12 (grid 40 x 12 = 480 blocks = 4/CU over 120 CUs); - // iteration 5 staged each block's 32-column B slice into LDS once and - // read the B0/B1 fragments from LDS per step, A direct-global - // (39.404/39.485 us official Graph median/P90, from 41.154/41.233). - // Iteration 7: BOTH operands are staged per K slice -- the A fragment - // also comes from LDS, so the K loop is global-load-free -- which forces - // SPLIT_K=16: sixteen UNIFORM 8-step slices (128 == 16 x 8, rows - // {256 x 16}, exact ascending cover of [0, 4096), every boundary a - // DUMMA-k multiple), grid = 40 N groups x 16 = 640 one-wave blocks - // (~5.33 waves/CU, 4 resident blocks/CU by LDS: 14,592 B/block -> - // 65,536/14,592 = 4.4), each block writing exact int32 partial planes - // (cols [n0, n0+16) and [n0+16, n0+32) of plane[split]), then one - // UNCHANGED 80-block combine kernel (generic over split_count = 16) that - // sums the planes ascending and emits the scaled bf16 output. Staged - // tiles are exact byte mirrors of the global operands, so fragment - // bytes, K-slice bounds, the k-ascending int32 order and the partial - // plane layout are unchanged: the combine+scale output is bit-identical - // to iteration 5. - // Iteration 8 (this round): occupancy/resource change only -- the same - // 640-block SPLIT_K=16 geometry now runs TWO 64-lane wavefronts per block - // (blockDim 128): wave w computes tile w (cols [n0+16w, n0+16w+16)) of - // the block's 32-column strip with its own independent accumulator chain - // over the shared slice. LDS per block is UNCHANGED (14,592 B -> 4 - // resident blocks/CU), so co-residency rises from 4 to 8 resident waves - // per CU = 2 per SIMD, hiding each wave's per-step LDS->MMA stall behind - // the other wave. Staging is partitioned across the two waves (wave 0 = - // A slice, wave 1 = B strip; identical global bytes read exactly once). - // The combine kernel, the 16-plane workspace need (1,310,720 B), the - // k-ascending partial order and the bit-identical output are unchanged. - // Both launches stay on the caller stream inside the timed API/Graph; - // the combine cost is part of the operator wall. Requires 16 int32 - // partial planes (1,310,720 B) of the caller's workspace -- the full - // fixed 16-plane 1,310,720 B allocation for this shape; when the - // workspace is missing/undersized the exact guard falls back to the - // iteration-1 one-wave direct geometry (still no workspace). Every - // unmatched (m, n, k) -- including the paired M=2 API shapes with the - // same (N, K) -- keeps reaching the scalar fallback below. - // Iteration 9 (this round): combine-side restructure only. PMC of the - // accepted iteration-8 code object profiles the combine kernel at 16.960 - // us vs 19.361 us for the partial kernel (46.7% of the 36.321 us profiled - // operator aggregate) with a 6.9% L2 hit rate and only 80 one-wave blocks, - // so the guard now launches the SAME combine kernel on a 2-D grid (80 - // n-tiles x 4 four-row m-groups = 320 one-wave blocks) and the kernel's - // split_count==16 specialization gives each lane one output element that - // batches all sixteen plane reads into a register array before the - // ascending sum. Partials, output bytes, bit-exactness and the partial - // kernel launch are unchanged. - // --------------------------------------------------------------------- - if (m == 16 && n == 1280 && k == 4096) { - constexpr int kDummaThreads = 64; // one wavefront (tile/combine) - constexpr int kPartialThreads = 2 * kDummaThreads; // 2 waves per block - constexpr int kTilesPerWave = 2; // two 16-wide N tiles per block (n32) - constexpr int kSplitK = 16; // 40 groups x 16 = 640 blocks, 4/CU - const int ntiles = n / kDummaN; // 1280 / 16 = 80 combine tiles - const int ngroups = ntiles / kTilesPerWave; // 80 / 2 = 40 n32 groups - const int64_t kWorkspaceNeeded = - static_cast(kSplitK) * kDummaM * n * sizeof(int32_t); - auto* out_bf16 = reinterpret_cast(out); - if (workspace != nullptr && workspace_bytes >= kWorkspaceNeeded) { - auto* partials = reinterpret_cast(workspace); - hipLaunchKernelGGL(w8a8_dumma_m16_splitk_partial_kernel, - dim3(ngroups, kSplitK), dim3(kPartialThreads), 0, - stream, a, b, partials, n, k); - // Iteration 25 (mapping v3): 2-D combine grid = 20 column-groups x 16 - // one-row blocks = 320 one-wave blocks (one output row x 64 consecutive - // columns per block, one output element per lane, 16-deep independent - // plane-load register batch in the kernel's split_count==16 path; every - // per-plane wavefront load is one 256-B contiguous run of one DRAM row - // instead of four 5,120-B-strided runs). - hipLaunchKernelGGL(w8a8_dumma_m16_combine_kernel, - dim3(n / kDummaCombineColsPerBlock, kDummaM), - dim3(kDummaThreads), 0, stream, - partials, x_scale, weight_scale, out_bf16, n, - kSplitK); + __hip_bfloat16* out_bf16 = reinterpret_cast<__hip_bfloat16*>(out); + + // Exact-shape DUMMA split-K specialization (iteration 8): the only call + // site of w8a8_dumma_m16_splitk_kernel. Multi-N-tile reuse plus whole-slice + // A+B LDS staging, with two 64-lane wavefronts per block (iteration 8): + // wave w owns one of the block's kTilesPerWave = 2 adjacent 16-wide N tiles + // and runs its own accumulator chain, while the block stages its A slice and + // its whole 32-column B strip into LDS (partitioned across the two waves) + // and runs an LDS-only K loop; since iteration 13 a 512-row slice is staged + // as two ascending 256-row phases. The K dimension is split S ways, so + // grid = (N/(16*T))*S blocks of 128 threads: the 8-way split gives + // 40*8 = 320 blocks for the 120 device CUs with uniform 16-stage (512-row) + // slices, each staged as two 256-row phases in the iteration-17 + // 15,104 B/block LDS footprint (four resident blocks/CU by capacity). S is + // clamped to the workspace capacity (the API guarantees 16 int32 planes) and + // to the number of 32-K stages; a clamped split whose phase exceeded the + // staging capacity would use the kernel's byte-for-byte direct-global loop + // instead (unreachable at the guard, where every phase is exactly 256 rows). + // Guarded by the exact (m, n, k) + // triple so every other shape -- notably the paired M=2 API shape with the + // same (N, K) -- keeps the scalar fallback below; if no usable workspace is + // supplied the exact shape also falls through. Iteration 9 changes only the + // second timed dispatch: at the unclamped guard split the 8-plane combine + // runs as a 2-D one-wave grid (n/64 x m blocks of 64 lanes, one output + // element per lane, all 8 plane loads independent in registers) while any + // runtime-clamped split_k < 8 keeps the original generic combine launch. + if (m == 16 && n == 1280 && k == 4096 && workspace != nullptr) { + const int64_t plane_elems = static_cast(m) * n; + const int64_t plane_bytes = + plane_elems * static_cast(sizeof(int32_t)); + const int total_stages = k / kDummaK; + int split_k = kSplitKTarget; + if (plane_bytes > 0) { + const int64_t max_split = workspace_bytes / plane_bytes; + if (split_k > max_split) { + split_k = static_cast(max_split < 0 ? 0 : max_split); + } } else { - // Workspace-less fallback: iteration-1 one-wave direct geometry. - hipLaunchKernelGGL(w8a8_dumma_m16_tile_kernel, - dim3(ntiles), dim3(kDummaThreads), 0, stream, - a, b, x_scale, weight_scale, out_bf16, n, k); + split_k = 0; + } + if (split_k > total_stages) { + split_k = total_stages; + } + + if (split_k >= 1) { + int32_t* partials = reinterpret_cast(workspace); + // 32-column groups: n / 32 = 40 for the guarded exact shape. Both + // factors are exact divisors of the guard's (n, k), so no tail path is + // needed. + const int groups = n / (kDummaN * kTilesPerWave); + const dim3 grid(static_cast(groups * split_k)); + const dim3 block(static_cast(kDummaWaveSize * kTilesPerWave)); + hipLaunchKernelGGL( + w8a8_dumma_m16_splitk_kernel, + grid, + block, + 0, + stream, + a, + b, + partials, + n, + k, + split_k, + groups); + + // Iteration 9: the unclamped guard split (split_k == 8 since iteration + // 13) launches the combine on the 2-D one-wave grid (20 column groups x + // 16 rows = 320 blocks of 64 lanes) that gives every lane one output + // element and all eight plane loads as independent register reads; a + // runtime-clamped split_k < 8 keeps the original linear 128-thread launch + // and the kernel's generic dependent-plane loop. + const int total = m * n; + if (split_k == kCombineSplits) { + hipLaunchKernelGGL( + w8a8_splitk_combine_kernel, + dim3(static_cast(n / kCombineColsPerBlock), + static_cast(m)), + dim3(static_cast(kCombineWaveSize)), + 0, + stream, + partials, + x_scale, + weight_scale, + out_bf16, + total, + n, + split_k); + } else { + const int combine_blocks = + (total + kCombineThreads - 1) / kCombineThreads; + hipLaunchKernelGGL( + w8a8_splitk_combine_kernel, + dim3(static_cast(combine_blocks)), + dim3(static_cast(kCombineThreads)), + 0, + stream, + partials, + x_scale, + weight_scale, + out_bf16, + total, + n, + split_k); + } + return; } - return; } - // --------------------------------------------------------------------- - // Scalar generic fallback: every shape outside the exact guard above. - // --------------------------------------------------------------------- + const int64_t total = + static_cast(m) * static_cast(n); + const int64_t blocks = (total + kBlockThreads - 1) / kBlockThreads; - constexpr int kBlockThreads = 128; // multiple of the gfx928 wavefront 64 - const int total = m * n; - const int grid = (total + kBlockThreads - 1) / kBlockThreads; - auto* out_bf16 = reinterpret_cast(out); - hipLaunchKernelGGL(w8a8_scalar_gemm_kernel, - dim3(grid), dim3(kBlockThreads), 0, stream, - a, b, x_scale, weight_scale, out_bf16, m, n, k); + // Generic scalar fallback for every unmatched (m, n, k), including the + // paired M=2 API shape with the same (N, K). + hipLaunchKernelGGL( + w8a8_scalar_kernel, + dim3(static_cast(blocks)), + dim3(kBlockThreads), + 0, + stream, + a, + b, + x_scale, + weight_scale, + out_bf16, + m, + n, + k); } -// --------------------------------------------------------------------------- -// Host launcher for the optional out-of-timed-region weight packing. -// Bootstrap: identity device-to-device copy of the int8 [K, N] weight and -// the fp32 [N, 1] scales; valid for every (K, N), unmatched shapes included. -// --------------------------------------------------------------------------- extern "C" void launch_pack_w8a8_weight( const int8_t* raw_weight, const float* weight_scale, @@ -1251,27 +1255,28 @@ extern "C" void launch_pack_w8a8_weight( int k, int n, hipStream_t stream) { - constexpr int kPackBlockThreads = 256; - - const int64_t i8_count = static_cast(k) * n; - if (i8_count > 0 && raw_weight != nullptr && packed_weight != nullptr) { - const int i8_grid = - static_cast((i8_count + kPackBlockThreads - 1) / - kPackBlockThreads); - hipLaunchKernelGGL(w8a8_identity_copy_i8_kernel, - dim3(i8_grid), dim3(kPackBlockThreads), 0, stream, - raw_weight, packed_weight, i8_count); + if (raw_weight == nullptr || weight_scale == nullptr || + packed_weight == nullptr || packed_weight_scale == nullptr) { + return; + } + if (k <= 0 || n <= 0) { + return; } - const int64_t f32_count = static_cast(n); - if (f32_count > 0 && weight_scale != nullptr && - packed_weight_scale != nullptr) { - const int f32_grid = - static_cast((f32_count + kPackBlockThreads - 1) / - kPackBlockThreads); - hipLaunchKernelGGL(w8a8_identity_copy_f32_kernel, - dim3(f32_grid), dim3(kPackBlockThreads), 0, stream, - weight_scale, packed_weight_scale, f32_count); + const int64_t weight_count = + static_cast(k) * static_cast(n); + if (k == kGuardK && n == kGuardN) { + // Iteration 17: the exact-guard weight pair is transposed into the + // n-major layout packed[n*K + k] = raw[k*N + n] consumed by the guard + // DUMMA kernel's n-major LDS stage and by every fallback that decodes the + // packed buffer for this (K, N). Same element count and buffer as the + // identity pack. + launch_pack_nmajor_i8(raw_weight, packed_weight, k, n, stream); + } else { + // Generic fallback for every unmatched (K, N): identity copy. + launch_identity_i8(raw_weight, packed_weight, weight_count, stream); } + launch_identity_f32( + weight_scale, packed_weight_scale, static_cast(n), stream); } // @@end diff --git a/metainfer/tasks/dcu_kernel_auto_opt/variant/int8w8a8-gemm/hy3/TP8/M16/shared_gate_up_proj.hip b/metainfer/tasks/dcu_kernel_auto_opt/variant/int8w8a8-gemm/hy3/TP8/M16/shared_gate_up_proj.hip index 1f40f800..b4247bad 100644 --- a/metainfer/tasks/dcu_kernel_auto_opt/variant/int8w8a8-gemm/hy3/TP8/M16/shared_gate_up_proj.hip +++ b/metainfer/tasks/dcu_kernel_auto_opt/variant/int8w8a8-gemm/hy3/TP8/M16/shared_gate_up_proj.hip @@ -1,393 +1,206 @@ -// @@variant shape=hy3_tp8_shared_gate_up_proj_m16 commit=2321c42b8618aae23700bc2cf3532b59c8f6c8d8 added=2026-08-27 -// baseline_us=66.527 speedup=6.40915 -// median_us=10.38 p90_us=10.42 -// source=hy3-dsh-tp8-m16-2-dc53295a +// @@variant shape=hy3_tp8_shared_gate_up_proj_m16 commit=d36c2aa242f6657f2fbb136f2edc7e0bf394900a added=2026-09-17 +// median_us=9.17 p90_us=9.427 speedup=7.255 baseline_us=66.53 +// source=hy3-tp8-m16-test9-16-619177f8 // MetaInfer W8A8 INT8 GEMM - HIP implementation (gfx928 / K500SM_AI). // -// worker_2 iteration 8: split-K=15 one-wave LDS-staged partial GEMM + -// split-15 combine for the assigned shape hy3_tp8_shared_gate_up_proj_m16: -// M = 16, N = 384, K = 4096 (M*N = 6144 output elements) +// worker_2 iterations 3-5 for hy3_tp8_shared_gate_up_proj_m16: +// M = 16, N = 384, K = 4096. Iteration 3 = mandated architecture round +// (accepted). Iteration 5 = mandated packed-weight/staging round: +// the DUMMA-fragment-order weight pack + register-direct B transport +// (the B LDS stage of iteration 3 is removed). // -// Operator contract: -// out[m, n] = bf16( int32_dot(x_q[m, :], weight[:, n]) +// Contract: +// out[m, n] = bf16( int32_dot(x_q[m, :], raw_weight[:, n]) // * x_scale[m, 0] * weight_scale[n, 0] ) // -// Iteration 1 = minimal gfx928 DUMMA INT8 bootstrap (exact mandatory tile): -// * one 64-lane wavefront per block, one 16x16 output tile per wave, -// explicit int32 accumulation, grid = N/16 = 24 blocks, direct global -// fragment loads, LDS-drain epilogue; measured 117.91 us median with -// only 24 of 120 CUs active (24 waves, 1 per CU, no co-residency). +// State after iteration 2: the 16-way uniform split-K launch measured +// 11.350 us median / 11.389 us p90 with grid = (N/16) x 16 = 384 one-wave +// blocks (3.2 blocks/CU), 9,216 B LDS/block, and a SEPARATE 24-block combine +// kernel (two dispatches per replay). Fresh PMC on that exact source reports +// lds_bank_conflicts = 254,976 (664 per block) and lds_wait_instructions = +// 6,112 concentrated in the B fragment path, because B is staged k-major +// ([k][16], leading dimension 16) and the row_major fragment loader reads each +// lane's 8 elements as 8 ds_read_u8 at stride 16 -> a structural 8-way bank +// conflict per B fragment read (verified in the exact code object: +// ds_read_u8 v3, v38 offset:4096 / 4112 / 4128 / ... stride 16). The A +// fragment already reads one ds_read2_b32 because A is staged row-major with +// the 8 elements consecutive. The same PMC block shows B staging issues 16-B +// loads at a 384-B stride (half of every 32-B sector wasted) and the second +// dispatch pays a full kernel-launch tail after the partial grid drains. // -// Iteration 4 = accepted split-K=10 grid-parallelism probe (19.84 us): -// * K=4096 split into 10 non-uniform 32-aligned slices, grid = 24 N-tiles -// x 10 splits = 240 one-wave blocks (2 per CU); direct global fragment -// loads in the K loop; separate 24-block combine kernel sums the 10 -// int32 planes in ascending split order (bit-identical int32 order); -// both launches inside the timed Graph region. -// * PMC: 49,152 vmem_read instructions for the partial kernel (~205 per -// block = byte-level fragment loads + reassembly, one vmcnt wait per -// load before each mmac), lds_instructions = 0 -> the K loop is -// serialized on global load latency (issue/latency-bound). +// This round is the mandated architecture change (one-wave, ZERO-barrier, +// CU-aligned, non-power-of-two split; trusted probe set [2,5,10,15]) plus ONE +// consolidation of the two measured B/combine bottlenecks, using the exact +// recipe previously validated on this shape: // -// Iteration 6 = accepted split-K=16 LDS-staged kernel (18.73 us median / -// 19.04 us p90): block tile 16x32 (2 waves x 1 N-tile of 16 columns), 128 -// thr, grid = 12 N-groups x split-K=16 = 192 blocks = 1.6 blocks/CU = 3.2 -// waves/CU; whole uniform 256-row slice staged once (A 16x256 stride 272, -// B 32x256 stride 48), one __syncthreads, zero-barrier LDS-only K loop -// (8 steps x 1 v_mmac per wave); workspace = 16 int32 planes = 393,216 B = -// the caller's workspace_split_k_capacity(16,384,4096) budget; combine = -// 24 blocks x 128 thr, ascending split sum; PMC: 192 blocks < 2/CU target. +// * K is split kSplitK = 15 ways with 32-aligned non-uniform slices +// (8 x 288 = 9 DUMMA steps + 7 x 256 = 8 steps; 8*288 + 7*256 = 4096). +// Grid = 24 N-groups x 15 splits = 360 one-wave blocks = EXACTLY 3 +// blocks/CU on the 120 measured device CUs - integer balanced, no tail +// wave, and a shrink of the per-block slice (288/256 vs 256) that keeps +// 3 co-resident blocks inside the LDS budget (3 x 4,868 B << 64 KiB). +// * [iteration 3 - pack superseded by the iteration-5 block below] the +// weight is packed once, OUTSIDE the timed region and outside the +// Graph, by launch_pack_w8a8_weight into the per-tile slice-major-chunk +// layout for (k,n)==(4096,384) ONLY: every 16-B chunk +// holds 16 consecutive k of one n column and the 4,096 chunks of a tile +// are stored in ascending (slice, n, k-chunk) order. Every other (K,N) +// keeps the identity copy. +// * [iteration 3 - B transport superseded by the iteration-5 block below] +// the B slice is staged into an n-major LDS layout b_lds[n][k] (element +// (k,n) at n*304 + k) from the contiguous packed region, so each staged +// wave-load covers 1,024 contiguous bytes (full 32-B sectors, sequential +// DRAM pages) and each lane's 8 B fragment elements are 8 CONSECUTIVE +// bytes -> exactly ONE ds_read2_b32 per DUMMA step (was 8 bank-conflicted +// ds_read_u8). The col_major loader reads the same elements as the old +// row_major loader (x[i] = element (k0+col+i, n_base+row)), so every +// fragment register - and therefore the exact int32 accumulation order - +// is unchanged. +// * A is NOT staged through LDS: each lane loads its own 8-B fragment slice +// for every DUMMA step into a per-lane register array (int2 global loads +// issued back-to-back) and the K loop consumes it straight from +// registers; A is 64 KiB total and L2-resident, and removing its LDS +// round trip drops the per-block LDS footprint to 4,868 B. +// * the 24-block combine is FUSED into the partial kernel as a +// last-arrival tail: every block publishes its int32 plane tile, lane 0 +// releases with __threadfence() + atomicAdd on a per-tile counter, the +// 15th arriver acquires, sums the 15 planes in ascending split order +// (exact integer order), applies the reference float order +// (float(acc) * x_scale[row]) * weight_scale[col], stores bf16 and resets +// its counter. Exactly ONE dispatch per replay. // -// Iteration 7 = measured split-K=10 one-wave grid (this file's round-7 -// candidate, REJECTED by the p90 guard): block tile 16x16 (1 wavefront, 64 -// thr), grid = 24 N-groups x split-K=10 = 240 one-wave blocks = exactly -// 2 blocks/CU; K=4096 -> 10 non-uniform 32-aligned slices (8 x 416 + 2 x -// 384); whole slice staged once (A stride 432, B stride 48; 26,880 B/block -// = 2 x 26,880 = 53,760 B/CU at 2 blocks, fits 64 KiB), 0 s_barrier (the -// single-wave __syncthreads is eliminated; lgkmcnt waits cover the ds -// hazard, verified in the exact gfx928 code object); 10 planes = 245,760 B; -// combine = 24 blocks x 128 thr. Measured median 15.432 us (-21% vs -// iteration 6) but p90 19.434 > best 19.040 -> p90_guard_passed = false; -// the 30 samples were bimodal (4 slow windows 19.4-23.2 us then 26 fast -// windows 15.4-15.9 us): a machine-state artifact, not kernel behavior. +// --------------------------------------------------------------------------- +// Iteration 5 (this revision) - mandated packed-weight/staging round: the +// DUMMA-fragment-order weight pack + register-direct B transport. +// +// The accepted iteration-3 source (digest 77a1ea90..., 9.674 us median / +// 9.751 us p90 / 9.631 us min, 360 one-wave blocks = exactly 3 blocks/CU, +// fused last-arrival tail, ONE dispatch) carried B through LDS: 5 batched +// 16-B global loads -> `s_waitcnt vmcnt(0)` (every B byte had to arrive +// before ANY ds_write) -> 5 ds_write_b128 with per-chunk index math -> 2 full +// `s_waitcnt lgkmcnt(0)` drains -> 9 ds_read2_b32 back into the fragment +// registers. The exact iteration-4 code object shows that whole path +// (load_wait0_ds_write_windows = 5, 15 lgkmcnt(0) drains, lds_instructions +// 5,424 = 15/block, lds_bank_conflicts 14,592, lds_bytes 4,868) while the PMC +// shows only ~199 VALU instructions per 64-lane wave in ~42,829 cycles: with +// just 3 co-resident waves per CU there is no other work to hide a +// serialization stage, so every turn of the B path is exposed latency. // -// Iteration 8 (this file's active M=16 path) = mandatory HIP-only -// occupancy round: tune one occupancy limiter (LDS footprint -> blocks/CU) -// with PMC evidence. The split-K=10 kernel's 26,880 B LDS/block caps it at -// exactly 2 blocks/CU (3 x 26,880 = 80,640 > 64 KiB). split-K=15 (trusted -// probe set [2,5,10,15]) shortens every slice (288/256 vs 416/384 rows), -// shrinking the per-block LDS footprint to 18,688 B -> 3 x 18,688 = 56,064 -// B/CU <= 64 KiB, so 3 one-wave blocks/CU become legal, and the grid = -// 24 N-groups x 15 splits = 360 one-wave blocks = EXACTLY 3 blocks/CU on -// 120 CUs (above the 2-blocks/CU latency-hiding target, integer-balanced, -// no tail wave). No repeated HBM reads: B stays once-read (1,572,864 B), -// A stays L2-hot, every block still stages its own slice exactly once. -// * block tile 16x16 (1 wavefront x 1 N-tile), 64 thr; grid = 24 x 15 = -// 360 one-wave blocks = exactly 3 blocks/CU; -// * K=4096 -> 15 non-uniform 32-aligned slices: 8 x 288 (9 DUMMA steps) -// + 7 x 256 (8 steps); k0-ascending within each slice and ascending -// split sum in the combine preserve the bit-identical int32 order; -// * whole slice staged once into bank-skewed LDS (A 16x288 stride 304, -// B 288x16 stride 48; 18,688 B/block), batched 16-B vector loads, one -// __syncthreads (0 s_barrier for the 1-wave block), then a zero-barrier -// LDS-only K loop (9/8 steps x 1 A-frag + 1 B-frag ds_read + 1 v_mmac); -// * workspace = 15 int32 planes [15][16][384] = 368,640 B (<= the -// caller's 393,216 B capacity; guard updated, unsplit DUMMA fallback -// when workspace < 15 planes); combine = 24 blocks x 128 thr, all 15 -// plane loads issued into a register array before the ascending sum; -// * total replay traffic ~2.32 MB (B 1.57 MB + planes RW 737,280 B + -// out 12,288 B) vs 2.47 MB for the accepted S=16 kernel (-6%); vs S=10 -// the plane RW grows +245,760 B (+50%) - the combine cost of more -// splits being benchmarked against the occupancy gain. +// This round changes ONE thing (the weight pack), and one stage disappears +// with it: launch_pack_w8a8_weight (outside the timed region and outside the +// Graph, same 1,572,864-byte buffer, (k,n)==(4096,384) ONLY) now emits the +// weight in the exact m16n16k32 matrix_b fragment order of the 64-lane +// wavefront, so a block's whole B slice becomes a straight sequential +// register stream and the B LDS stage is deleted: // -// Iteration 9 (this file's active M=16 path) = HIP-only two-launch attack. -// Round 8 measured 15.529 us median / 16.024 us p90 and its falsifiable -// branch ("if median stays >= 15.4, the next round attacks the two-launch -// structure - fused/single-kernel combine - instead of geometry") fired. -// Change: fuse the 24-block combine INTO the partial kernel as a -// last-arrival tail using the validated TP4 qkv lineage protocol - every -// block writes its 16x16 int32 plane tile, __threadfence() (release), then -// atomicAdd(&counters[tile], 1); the block whose atomicAdd returns -// kSplitK-1 (the 15th arriver of its N-group) __threadfence()s (acquire), -// sums the 15 planes in ascending split order (exact int32 order preserved -// -> outputs bit-identical), applies both scales, stores bf16, and resets -// its counter to 0 (atomicExch) so the next replay restarts from 0. The 24 -// counters live in the workspace tail (96 B right after the 15 planes; -// guard updated to 368,736 B) and are zeroed once by the host before the -// first launch (a static-flag hipMemsetAsync on the caller stream - issued -// in the eager precheck, outside the timed Graph in the normal flow). -// Result: ONE kernel launch per replay (grid 360 one-wave blocks, 3/CU, -// LDS unchanged 18,688 B); the combine no longer pays a second dispatch -// and each N-group's combine tail overlaps the still-running partials of -// other groups instead of waiting for full grid completion + relaunch. +// * the 65,536 B of one 16-column tile are the 15 split slices in +// ascending split order; inside a slice the bytes are the fragment slots +// of the wave, TWO k32 steps per 1,024-B pair slot: byte = +// slice_base(s) + (step>>1)*1024 + lane*16 + (step&1)*8 + i with +// lane = ((k - k_base)>>3 & 3)*16 + (col&15), i = (k - k_base)&7; +// * a 9-step (288-k) slice is 4 pair slots plus one 512-B single slot for +// the odd 9th step (8 B/lane at lane*8); an 8-step (256-k) slice is 4 +// pair slots; 8*4,608 + 7*4,096 = 65,536 B per tile exactly, so the byte +// count and the graph-stable buffer are unchanged; +// * the timed kernel then fetches B fragment registers with 4 x int4 (one +// per step pair, 16 B/lane, 1,024 CONTIGUOUS bytes per wave-load) plus, +// for the 9-step slices, one int2 (8 B/lane, 512 contiguous bytes) - the +// SAME 4/5 load instructions and the same bytes per lane as the accepted +// staged path, but each load address is lane*16/lane*8 inside a +// contiguous slot: no chunk-index math, no ds_write, no __syncthreads, no +// lgkmcnt drain and no ds_read is needed, and the K loop can consume each +// step pair as soon as it arrives instead of after the whole slice; +// * b_slice[s].x[i] is the same element the accepted col_major LDS loader +// produced, x[i] = element(k0 + (lane>>4)*8 + i, n_base + (lane&15)), so +// every matrix_b fragment register - and therefore every v_mmac input and +// the exact int32 accumulation order - is unchanged; +// * A is unchanged (register-only, 8/9 batched int2 loads, zero LDS), the +// grid is unchanged (24 N-groups x 15 splits = 360 one-wave blocks = +// exactly 3 blocks/CU), and the partial-plane store, the fused +// last-arrival combine tail, the workspace guard (15 planes + 24 counters +// = 368,736 B <= 393,216 B) and both fallbacks are unchanged; the generic +// scalar and unsplit M=16 DUMMA fallbacks decode the new pack elementwise +// through the updated packed384_byte_offset, so they stay bit-exact +// (including the paired M=2 shape). // -// Repair 1/4 (correctness): the round-9 draft let ALL 64 lanes of every -// block execute atomicAdd(&counters[tile], 1) (64 increments per block = -// 960 per tile per replay), so the kSplitK-1 condition fired on the 15th -// increment inside the FIRST-arriving block - before its 14 siblings had -// stored their planes - and the unguarded atomicExch reset made several -// blocks combine per replay. Exact check failed: 6048/6144 mismatched, -// first_mismatch (0,0) actual=0.0 (premature combiner summed unwritten, -// zero planes). Fix: exactly one arrival per block (lane 0 fence + -// atomicAdd, LDS s_is_last flag, barriers) + lane-0-only atomicExch reset, -// mirroring the validated TP4 qkv fused tail. GEMM/plane-store body and the -// ascending 15-plane sum are unchanged (bit-identical int32 order). +// Predicted signature if the mechanism is real: LDS per block drops from +// 4,868 B to the 4-B s_is_last flag; lds_instructions 5,424 -> ~0; +// lds_bank_conflicts 14,592 -> 0; lds_wait_instructions 461 -> 0; +// group_segment_fixed_size ~4,868 -> ~4 B; waitcnt drops by ~20 (all +// lgkmcnt drains and the staging vmcnt(0) pair vanish); vmem_read_instructions +// stays ~5,832 (same B bytes and the same 4/5 B load instructions per block); +// memory_traffic.total_bytes_per_operator_replay stays ~2.05 MB. If the +// median does not improve, the B LDS round trip was fully hidden by the 3 +// co-resident waves and the kernel sits at its launch/streaming floor. +// --------------------------------------------------------------------------- // -// Iteration 10 (this file's active M=16 path) = final HIP-only -// consolidation round (raw inline asm not allowed by the ISA policy: -// plateau=false, valid HIP rounds < 8). Round 9 measured 14.516 us median / -// 14.544 us p90 and its falsifiable branch ("if median stays >= 14.5 ... the -// single-wave combine tail (60 loads/lane) is the new critical path - the -// next round vectorizes the combiner's plane loads (int4) or shrinks the -// plane count instead") fired (14.516 >= 14.5). Change: vectorize ONLY the -// fused combine tail's plane loads - each lane now owns 4 CONSECUTIVE -// columns (row = lane>>2, col0 = (lane&3)*4) and reads each of the 15 -// planes with one aligned int4 (16-B) load instead of 4 scalar 4-B loads -// (60 -> 15 vmem loads per lane, 4x fewer tail load instructions, same -// bytes, same L2-request footprint); weight_scale is read once as float4 -// and the 4 adjacent bf16 stores merge into one 8-B store. Per-element -// int32 accumulation order is unchanged (ascending split sum per output -// element) -> outputs remain bit-identical (0 mismatches). Everything else -// is untouched: grid = 24 N-groups x 15 splits = 360 one-wave blocks = 3 -// blocks/CU, per-block LDS 18,688 B, zero-barrier LDS-only K loop, -// workspace = 15 planes + 24 counters = 368,736 B, one launch per replay, -// exact (m,n,k) guard, unsplit DUMMA fallback, generic scalar fallback. -// Falsifiable: (1) median_us < 14.516 (target ~13.6-14.2) with p90 <= 14.544 -// (guard vs the accepted best); (2) 0 mismatches + graph_capture_passed; -// (3) PMC: grid 360 / workgroup 64 / LDS 18,688 / s_barrier 0 unchanged, -// arch_vgpr stays ~40-60 with 0 spills, the tail portion of vmem_read drops -// ~4x (staging ~3,264 unchanged; tail 60 -> 15 loads/lane); (4) replay -// traffic unchanged ~2.07 MB. If median stays >= 14.516 with p90 <= 14.544, -// the tail was not the critical path and the kernel sits at its -// streaming/launch floor -> the round documents the plateau evidence. +// Iteration 7 (this revision) - mandated pipeline/buffering decision plus the +// removal of the only avoidable term left in the HBM byte ledger. // -// Iteration 11 (this file's active M=16 path) = final conditional inline-asm -// round -> mandatory HIP-only consolidation (raw asm is NOT allowed: isa_policy -// says phase=hip_only, plateau=false, raw_inline_asm_allowed=false, -// valid_hip_rounds=7 < 8 - the control plane has not confirmed a HIP plateau). -// Round 10 measured 13.925 us median / 13.972 us p90 (< 14.516 target), so the -// int4-vectorized tail branch did NOT fire and the tail is not the critical -// path anymore. The accepted kernel's fresh exact gfx928 code object exposes -// the next concrete compiler limitation: the slice staging is SERIALIZED - -// every global_load_dwordx4 is followed by s_waitcnt vmcnt(0) before its -// ds_write_b128 (load_wait0_ds_write_windows = 18 across the two slice -// instantiations, 10 long + 8 short), so each block pays ~10/8 full -// global-memory latencies one after another before the K loop's first -// ds_read (the last write is followed by lgkmcnt(0) waits). Change: in -// split15_slice_body, issue ALL staging global loads into one register array -// (int4 s_buf[10/8]) BEFORE any ds_write - the compiler is then forced to put -// the loads in flight back-to-back and move the vmcnt waits to the write -// side, collapsing the staging critical path to one memory latency. Same -// addresses, same per-lane order, same LDS layout, same __syncthreads, same -// zero-barrier LDS-only K loop, same fused last-arrival tail: the int32 -// accumulation order is untouched (outputs bit-identical, 0 mismatches -// expected). Everything else is untouched: grid = 24 N-groups x 15 splits = -// 360 one-wave blocks = exactly 3 blocks/CU (trusted probe set member 15), -// per-block LDS 18,688 B (still the occupancy binder: 3 x 18,688 = 56,064 B -// <= 64 KiB), workspace = 15 int32 planes + 24 arrival counters = 368,736 B -// (<= 393,216 B budget; unsplit DUMMA fallback when smaller), one launch per -// replay, fence/atomic/barrier protocol unchanged, exact (m,n,k) guard, -// generic scalar fallback for every unmatched shape including the paired M=2. -// Falsifiable predictions: (1) median_us < 13.925 (target ~11.5-12.8) with -// p90_us <= 13.972 (guard vs the accepted best); (2) correctness 0 mismatches -// + graph_capture_passed (single-launch structure, write-in-place planes, -// self-resetting counters, no clear); (3) ISA: the first staging vmcnt(0) -// now covers ~7 outstanding global_load_dwordx4 (old kernel: exactly 1) - -// memory-level parallelism per block rises 1 -> ~7-10 and the staging -// critical path collapses from ~10 serialized latencies to ~1-2; vmcnt(0) -// 20 -> ~19, arch_vgpr ~69-75 (locally verified 69 VGPR / 40 SGPR) with 0 -// spills, grid 360 / LDS 18,692 / s_barrier 0 / v_mmac 17 / ds_write_b128 18 -// / ds_read2_b32 17 / ds_read_u8 136 unchanged; (4) PMC vmem_read ~4,392 and -// replay traffic ~2.06 MB unchanged (same loads, same bytes). If -// median stays >= 13.925 with p90 <= 13.972, the 3-blocks/CU overlap already -// hid the staging latency and the kernel sits at its streaming/launch floor - -// the round then documents the plateau evidence for this shape. +// MANDATED DECISION (single vs double buffering). The gate is evaluated on the +// exact accepted source: K = 4096 >= 1024 (true), l2_hit_rate_percent 51.521 +// < 70 (true), doubled LDS budget 2 x 4 B = 8 B << 48 KiB (true). The round +// nevertheless keeps SINGLE buffering, because the kernel has no LDS pipeline +// stage to buffer: LDS is 4 B/block, the fused tail's s_is_last flag +// (lds_instructions 720 = 2/block, lds_bank_conflicts 0, lds_wait 8). Both +// operands travel global -> fragment registers directly; the exact gfx928 +// code object of the accepted source shows 9 x global_load_dwordx2 (A) and +// 4 x global_load_dwordx4 + 1 x global_load_dwordx2 (B) issued back-to-back +// ahead of the chain, with the compiler already software-pipelining the +// consumption (s_waitcnt vmcnt(7) -> first v_mmac ... vmcnt(0) -> last +// v_mmac). BARRIERS PER K STEP = 0 for the current kernel (zero s_barrier in +// the K loop). The double-buffered alternative is the iterations 1-3 LDS +// stage, which needed 2 s_barrier and 2 full s_waitcnt lgkmcnt(0) per stage +// and whose deletion in iteration 5 measured +3.2% (9.674 -> 9.365 us). The +// single/double comparison therefore rejects double buffering on measured +// evidence, not on the gate. // -// Iteration 15 (this file's active M=16 path) = final conditional inline-asm -// round -> mandatory HIP-only consolidation (raw asm is NOT allowed: isa_policy -// says phase=hip_only, plateau=false, raw_inline_asm_allowed=false - the -// control plane has NOT confirmed a HIP plateau: the three recent valid -// improvements were -28.62% / -0.033% / +3.86% (iterations 12/13/14), the -// -28.62% being the rejected round-12 regression and +3.86% the round-14 -// packed-B median gain, and no prior ISA-guided round recorded a compiler -// limitation plus target instructions). Round 14 (n-major-packed B staging -// loads only) measured median 12.512 us / p90 13.064 us - best median ever, -// rejected only by the p90 guard (13.064 > 13.052) - and the source was -// restored to the accepted iteration-11 digest. The accepted kernel's exact -// gfx928 code object exposes the remaining half of the B path: the K loop -// still reads each lane's 8 B-fragment elements as 8 ds_read_u8 from the -// k-major stride-48 LDS (4-way bank conflicted; the persistent PMC signature -// since iteration 9), while the A fragment already reads its 8 consecutive -// bytes as ONE ds_read2_b32. The TP4 gate_up/down_proj lineage validated the -// fix for exactly this ("one ds_read_b64 per step, was 8 ds_read_u8"; rule 3: -// 'Pack the cold once-read weight (B) ... into the fragment's layout'). -// Change (one consolidation): complete the round-14 B-pack into a per-tile -// n-major transpose packed[tile][n][kk] = raw[kk*384 + tile*16 + n] -// ((k,n)==(4096,384) ONLY; every other (k,n) keeps the identity pack), stage -// B into an n-major LDS layout b_lds[n][k] (element (k,n) at n*304 + k; -// column stride 304 = 288 + 16 bank skew, 16-B aligned so the staging writes -// stay ds_write_b128), and load the B fragment with the library col_major -// loader: a.x[i] = p[row*304 + col + i] reads the lane's 8 elements as 8 -// CONSECUTIVE bytes -> one ds_read2_b32 per step (was 8 ds_read_u8 at stride -// 48). Same elements, same x[i] register assignment (x[i] = element -// (k0+col+i, n_base+row) in both layouts), same v_mmac, so the int32 -// accumulation order is untouched (k-ascending within each slice, ascending -// split sum - outputs bit-identical, 0 mismatches expected). The B staging -// loads also get the round-14 locality win: each lane holds one 16-B -// row-chunk of 16 consecutive k values of one n column (chunk c -> n = c&15, -// k-chunk = c>>4) - 64 lanes cover 16 x 64-B contiguous sectors per wave-load -// (was 64 scattered 16-B requests), sequential HBM pages, no straddles. LDS -// per block drops 18,688 -> 9,728 B (A 16x304 = 4,864 + B 16x304 = 4,864), -// so 3 x 9,728 = 29,184 B/CU << 64 KiB (LDS is no longer the occupancy -// binder, but the grid stays 360 = exactly 3 blocks/CU, so residency is -// unchanged); per-block LDS instructions drop ~4x (136 ds_read_u8 + 17 -// ds_read2_b32 -> 34 ds_read2_b32), attacking the per-CU LDS-pipe pressure -// that round 13's evidence identified as the shared per-CU resource. -// Everything else is untouched: grid = 24 N-groups x 15 splits = 360 -// one-wave blocks = exactly 3 blocks/CU (trusted probe-set member 15), -// workspace = 15 int32 planes + 24 arrival counters = 368,736 B (<= the -// caller's 393,216 B budget; unsplit DUMMA fallback when smaller), one launch -// per replay, fence/atomic/barrier protocol unchanged (lane-0 atomicAdd, LDS -// s_is_last flag, lane-0 atomicExch reset), exact (m,n,k) guard, generic -// scalar fallback for every unmatched shape including the paired M=2 (both -// fallbacks decode the packed layout for (k,n)==(4096,384): the scalar path -// reads element (kk, col) at (col>>4)*65536 + (col&15)*4096 + kk; the unsplit -// DUMMA path loads B with the row_major loader at ldm = K from -// packed + tile*65536 + k0, giving p[(col+i)*4096 + row] = element -// (k0+col+i, n_base+row) - identical values to the raw layout). -// Falsifiable predictions: (1) median_us < 12.995 (target ~12.0-12.7; the -// round-14 pack alone measured 12.512 and this also cuts the per-block LDS -// read instructions 8x and the per-CU LDS-pipe work ~4x) with p90_us <= -// 13.052 (guard vs the accepted best p90); (2) correctness 0 mismatches -// (bit-identical int32 order) and graph_capture_passed (single-launch -// structure unchanged, write-in-place planes, self-resetting counters, pack -// out-of-timed/out-of-Graph, no clear); (3) PMC/ISA: grid_blocks 360 / -// workgroup_size 64 / s_barrier 0 / v_mmac 17 / ds_write_b128 18 unchanged, -// ds_read_u8 136 -> 0, ds_read2_b32 17 -> 34 (A + B), group_segment_fixed_size -// 18,692 -> ~9,732, arch_vgpr ~69 +/- a few with 0 spills, occupancy 3 -// blocks/CU (now grid-bound), lds_instructions ~31,632 -> ~20k, -// lds_bank_conflicts ~63,744 -> ~15-25k (the B u8 4-way reads disappear), -// vmem_read_instructions ~4,392 unchanged (same loads, same bytes); -// (4) memory_traffic.total_bytes_per_operator_replay unchanged ~2.06 MB (no -// HBM delta). If median stays >= 12.995 with p90 <= 13.052, the LDS pipe was -// not on the critical path either (the kernel is at its launch/memory floor) -// and this final round documents the plateau evidence for this shape instead -// of claiming a gain. +// The round's ONE change attacks the partial planes. The accepted source's +// memory_traffic is read 1,665,856 B (= weight 1,572,864 + A 65,536 + 27,456) +// and write 381,024 B (= partials 368,640 + out 12,288 + counters 96), total +// 2,046,880 B per replay at 218.56 GB/s counter-derived; the 368,640 B of +// partial-plane write-back is 18% of all HBM bytes for values that are read +// back exactly once by the fused tail. Those 15 int32 planes are replaced by +// ONE hot per-tile int32 accumulator (24 tiles x 256 int32 = 24,576 B, +// L2-resident) updated with global atomicAdd, and the fused last-arrival tail +// reads that single accumulator, applies the reference float order +// (float(acc) * x_scale[row]) * weight_scale[col], stores bf16 and resets the +// tile to zero. Integer addition is exact and order-independent, so the summed +// int32 - and therefore every output bf16 - is bit-identical to the 15-plane +// ascending-sum chain. Unchanged: grid (24 N-groups x 15 splits = 360 one-wave +// blocks = exactly 3.0 blocks/CU on 120 CUs), slices (8 x 288 + 7 x 256, +// 32-aligned), weight pack, A register path, counter protocol, fallbacks. The +// workspace requirement drops from 368,736 B to 24,672 B, which frees the +// 393,216-B partial-plane cap that held the split at 15 - the hand-off for a +// later blocks/CU probe. // -// Iteration 17 (this file's active M=16 path) = HIP-only B-pack ORDER -// consolidation (isa_policy: phase=hip_only, plateau=false, -// raw_inline_asm_allowed=false, skill_allowed=false - the three recent valid -// HIP rounds are NOT all within [-2%, +2%): -0.033% / +3.86% / +8.35%, so the -// plateau is not proven and one more HIP-only change is mandated; the -// round-16 attempt died in the agent infrastructure before a valid proposal, -// so this round returns to the accepted iteration-15 source (digest -// b39a985c...) and starts ONE new bounded experiment). Round 15 (n-major B -// pack + n-major LDS) measured median 11.994 us / p90 12.293 us - accepted - -// and left the remaining half of the B memory path on the table: the n-major -// pack places chunk (n, k-chunk) at n*4096 + k-chunk*16, so each staged B -// wave-load touches 16 x 64-B sectors at 4,096-B stride (16 scattered L2 -// requests / up to 16 DRAM pages) - the round-14 contiguous-load locality -// was traded away to buy the one-ds_read2_b32 B fragment read. Change (one -// consolidation, pack ORDER only): the pack kernel now emits the SAME 16-B -// chunks (16 consecutive k of one n column) in ascending (slice, n, k-chunk) -// order - chunk index = slice_chunk_base(s) + n*cps(s) + k-chunk, byte -// address = tile*65536 + chunk*16 - so each staged wave-load (64 consecutive -// chunks) covers 1,024 CONTIGUOUS bytes (sequential L2 sectors and -// sequential DRAM pages; was 16 scattered 64-B sectors at 4,096-B stride). -// Chunk content, the n-major LDS destination b_lds[n][k] (n*304 + k), the -// one-ds_read2_b32 B fragment read, the v_mmac count, the zero-barrier K -// loop and the fused last-arrival tail are all UNTOUCHED: every staged byte -// and every fragment register is identical to iteration 15, so the int32 -// accumulation order is bit-identical (0 mismatches expected). The -// long-slice staging write index changes from (c&15, c>>4) to (c/18, c%18) -// (the short 256-row instantiation keeps (c>>4, c&15); divisions resolved -// by the compiler, staging path only); the generic scalar fallback and the -// unsplit DUMMA fallback decode the new pack order through a shared -// packed384_byte_offset helper (per-element gather in the unsplit path, -// fallback-only: x[i] = element (k0 + (lane>>4)*8 + i, n_base + (lane&15)), -// the same fragment registers as the library row_major loader). -// Everything else is untouched: grid = 24 N-groups x 15 splits = 360 -// one-wave blocks = exactly 3 blocks/CU (trusted probe-set member 15), -// per-block LDS 9,732 B, workspace = 15 int32 planes + 24 arrival counters -// = 368,736 B (<= 393,216 B budget; unsplit DUMMA fallback when smaller), -// one launch per replay, fence/atomic/barrier protocol unchanged, exact -// (m,n,k) guard, generic scalar fallback for every unmatched shape -// including the paired M=2. -// Falsifiable predictions: (1) median_us < 11.994 (target ~11.2-11.8; the B -// staging wave-loads become fully sequential, removing the 16-scattered- -// sector DRAM page pattern) with p90_us <= 12.293 (guard vs the accepted -// best p90); (2) correctness 0 mismatches (bit-identical int32 order) and -// graph_capture_passed (single-launch structure unchanged, write-in-place -// planes, self-resetting counters, pack out-of-timed/out-of-Graph, no -// clear); (3) PMC/ISA: grid_blocks 360 / workgroup_size 64 / lds_bytes -// 9,732 / s_barrier 0 / v_mmac 17 / ds_read2_b32 34 / ds_write_b128 18 -// unchanged, arch_vgpr ~69-72 with 0 spills, vmem_read ~4,392 unchanged -// (same loads, same bytes), the B staging address stream becomes sequential -// ((k_base + c)*16 per chunk) so l2_misses ~32,321 may drop (fewer sector -// straddles / page effects) while lds_bank_conflicts may shift (the -// long-slice staging write bank pattern changes; instruction count and -// bytes identical); (4) memory_traffic.total_bytes_per_operator_replay -// unchanged ~2.06 MB (same bytes, better DRAM locality). If median stays -// >= 11.994 with p90 <= 12.293, the B staging DRAM pattern was not on the -// critical path either (the kernel is at its launch/latency floor) and this -// round documents the next plateau datum for this shape. +// Predicted signature if the mechanism is real: write_bytes_per_operator_replay +// 381,024 -> ~37,000 (out 12,288 + counters 96 + accumulator write-back +// <= 24,576), read_bytes 1,665,856 -> ~1,638,400, total 2,046,880 -> ~1,675,000 +// (-18%); the kernel body's 4 global_store_dword per lane become 4 no-return +// global_atomic_add; lds_bytes stays 4 and ds_read/ds_write stay 1/1 (the +// flag); vmem_read_instructions unchanged; the last arriver's 15 int4 plane +// reads collapse to one int4 accumulator read per lane. If the median does not +// move, the 368,640 B of partial write-back was absorbed by L2 and the kernel +// is latency-bound at 3 warps/CU - which is what the freed 24,672-B workspace +// then lets the next round attack with a >= 4-6 blocks/CU split. +// --------------------------------------------------------------------------- // -// Iteration 21 (this file's active M=16 path) = HIP-only, ONE consolidation: -// A register-only transport (the validated TP4 o_proj lineage recipe -// "register-only K-loop transport with depth-1 prefetch"; raw asm is NOT -// allowed: isa_policy says phase=hip_only, plateau=false, -// raw_inline_asm_allowed=false, skill_allowed=false - the three recent valid -// HIP improvements +3.86% / +8.35% / +5.35% are not within [-2%, +2%), so the -// HIP plateau is not proven and one more HIP-only change is mandated; rounds -// 18/19/20 died in the agent infrastructure before valid proposals, so this -// round returns to the accepted iteration-17 source (digest -// 88ea95a832fbc79e4d625b2aa22df3a0d533d6eb2c6aff08529c7e60a92942c4, verified -// sha256-identical before editing) and starts ONE new bounded experiment - no -// round-18/19/20 partial candidate was repaired or replayed). The accepted -// kernel (11.385 us median / 11.954 us p90) still stages the small L2-hot A -// slice (16 x 288/256 int8) through LDS: per block that is 5/4 16-B global -// loads + 5/4 ds_write_b128 + 9/8 ds_read2_b32 (2-way bank conflicted, -// structural for any 16-B-aligned stride) that the K loop then reads back. -// Every block reads its own A rows exactly once, so the LDS round trip only -// adds instructions and a serialization point; the A values are 64 KiB total -// and L2-resident (re-read by all 360 blocks), so direct global fragment -// loads are cheap. Change (one consolidation): (1) split15_slice_body no -// longer stages A; each lane preloads its own 8-B fragment slice for EVERY -// DUMMA step into a per-lane register array a_slice[kStepsT] (9/8 -// global_load_dwordx2 per lane, issued back-to-back BEFORE any ds_write - -// the iteration-11 register-array batching pattern - so the A slice costs -// ONE memory latency and zero LDS instructions); (2) the K loop consumes the -// A fragments from registers (du_mma_sync(acc, a_slice[s], b_frag, acc)) - -// same elements per lane per step as the LDS row_major loader (verified -// against du_mma.hpp: x[i] = p[row*ldm + col + i], row = lane&15, col = -// (lane>>4)*8, ldm = k), so the int32 accumulation order is untouched -// (k-ascending per split, ascending split sum -> outputs bit-identical, 0 -// mismatches expected); (3) the shared a_lds array is deleted -> per-block -// LDS drops 9,732 -> 4,868 B (B 4,864 + s_is_last 4). B staging, the B -// col_major LDS reads, the zero-barrier structure (one __syncthreads before -// the B reads, unchanged), the slice-major pack, the fused last-arrival tail -// and every fallback are UNTOUCHED. Everything else is unchanged: grid = 24 -// N-groups x 15 splits = 360 one-wave blocks = exactly 3 blocks/CU (trusted -// probe-set member 15; LDS is no longer the binder at 3 x 4,868 = 14,604 -// B/CU), workspace = 15 int32 planes + 24 arrival counters = 368,736 B (<= -// the caller's 393,216 B budget; unsplit DUMMA fallback when smaller), one -// launch per replay, fence/atomic/barrier protocol unchanged (lane-0 -// atomicAdd, LDS s_is_last flag, lane-0 atomicExch reset), exact (m,n,k) -// guard, generic scalar fallback for every unmatched shape including the -// paired M=2 (the packed384_byte_offset decode is untouched). -// Falsifiable predictions: (1) median_us < 11.385 (target ~10.8-11.3; the -// per-block staging loses the A LDS round trip and the K loop loses 9/8 -// LDS reads) with p90_us <= 11.954 (guard vs the accepted best p90); -// (2) correctness 0 mismatches (bit-identical int32 order; M=2 paired -// fallback exact via the packed decode) and graph_capture_passed (single- -// launch structure unchanged, write-in-place planes, self-resetting -// counters, no clear); (3) PMC/ISA: grid_blocks 360 / workgroup_size 64 / -// s_barrier 0 / v_mmac 17 unchanged; group_segment_fixed_size 9,732 -> -// ~4,868 B; ds_write_b128 18 -> ~9/8 (B staging only); ds_read2_b32 34 -> -// ~17 (B only); ds_read_u8 stays 0; the A slice appears as 9/8 batched -// global_load_dwordx2 before the first B ds_write (explicit int2 loads; if -// instead the ISA shows the A loads sunk into the K loop or scalarized to -// global_load_ubyte, the compiler refused the batching/vectorization and -// this round documents that finding); arch_vgpr ~69 -> ~85-95 (a_slice -// holds 18/16 extra VGPR) with 0 spills (per-wave ~90 x 64 = 5,760 VGPR << -// 65,536/CU, occupancy stays 3 blocks/CU = grid-bound); lds_instructions -// ~10,128 -> ~5-6k and lds_bank_conflicts ~29,184 -> ~15k (the A 2-way -// fragment-read conflicts disappear); vmem_read_instructions ~4,392 -> -// ~10,464 (A loads become 8-B per step instead of 16-B staged chunks - SAME -// bytes, more instructions, all issued in one batch); (4) -// memory_traffic.total_bytes_per_operator_replay unchanged ~2.06 MB (same -// A+B bytes; A stays L2-hot). If median stays >= 11.385 with p90 <= 11.954, -// the A LDS round trip was not on the critical path either (the kernel is -// at its launch/latency floor with 3 waves/CU) and this round documents the -// next plateau datum for this shape. +// The int32 accumulation is exact (integer addition is order independent), so +// the split cannot change the result bit-for-bit: k-ascending inside each +// slice, ascending split sum at the tail = the scalar chain. // -// The generic scalar path remains the fallback for every unmatched -// (m,n,k), including the paired M=2 shape with the same (N,K); the M=16 -// DUMMA path is guarded by the exact (m,n,k) triple. +// The generic scalar kernel remains the fallback for every unmatched (m,n,k) +// including the paired M=2 shape with the same (N,K) = (384,4096); it decodes +// the (k,n)==(4096,384) pack so that fallback stays exact. The unsplit M=16 +// DUMMA kernel is the fallback when the caller workspace cannot hold the +// per-tile int32 accumulator + 24 counters (24,672 B as of iteration 7; it was +// 15 partial planes + counters = 368,736 B through iteration 6). The M=16 +// split-K path is guarded by the full (m,n,k) triple. // // Host symbols (matched against csrc/bindings.cpp): -// launch_w8a8_gemm(...) -// launch_pack_w8a8_weight(...) +// launch_w8a8_gemm(...) -- assigned-shape split-K / fallbacks +// launch_pack_w8a8_weight(...) -- fragment-order pack / identity // // Known-good include order for this DTK: HIP runtime first, then the HIP // bfloat16 header, then du_mma.h. @@ -400,59 +213,86 @@ namespace { +// --------------------------------------------------------------------------- +// Layout constants of the exact (k,n)==(4096,384) DUMMA-fragment-order weight +// pack (iteration 5). One 16-column tile is 65,536 B = 15 split slices in +// ascending split order; a slice is the concatenation of the wavefront's +// matrix_b fragment slots, TWO k32 steps per 1,024-B pair slot: +// offset_in_slice(step, lane, i) = +// (step>>1)*1024 + lane*16 + (step&1)*8 + i +// with lane = ((k - k_base)>>3 & 3)*16 + n_local, i = (k - k_base)&7, +// n_local = col&15. The 9-step (288-k) slices have 4 pair slots plus one +// 512-B single slot for the odd 9th step (8 B/lane at lane*8); the 8-step +// (256-k) slices have 4 pair slots. 8*4,608 + 7*4,096 = 65,536 B per tile, so +// the buffer size, the pack launch and the graph-stable address are unchanged. +// --------------------------------------------------------------------------- +constexpr int kFragPackNumLong = 8; // 8 x 288-k + 7 x 256-k +constexpr int kFragPackSliceLong = 288; +constexpr int kFragPackSliceShort = 256; +constexpr int kFragPackStepK = 32; // kDummaK +constexpr int kFragPackPairBytes = 1024; // 64 lanes * 16 B (2 steps) +constexpr int kFragPackLongBytes = 4608; // 4*1024 + 512 (9 steps) +constexpr int kFragPackShortBytes = 4096; // 4*1024 (8 steps) +constexpr int kFragPackLongRegion = 8 * 4608; // 36,864 B of long slices +constexpr int kFragPackTileBytes = 65536; // 4096 * 16 + // Byte offset of element (k, col) inside the exact (k,n)==(4096,384) -// slice-major-chunk weight pack (iteration 17; see -// w8a8_pack_weight_gateup_slicemajor_kernel): each 16-B chunk holds 16 -// CONSECUTIVE k values of one n column, and chunks are stored in ascending -// (slice, n, k-chunk) order. With tile = col>>4, n_local = col&15 and -// kchunk = k>>4: -// kchunk < 144 (the 8 long 288-k slices): s = kchunk/18, kk' = kchunk%18, -// chunk base = s*288, chunks per n = 18; -// kchunk >= 144 (the 7 short 256-k slices): c2 = kchunk-144, -// s = 8 + c2/16, kk' = c2%16, chunk base = 2304 + (c2/16)*256, -// chunks per n = 16; -// offset = tile*65536 + (chunk_base + n_local*cps + kk')*16 + (k & 15). -// Fallback paths only (the timed M=16 staging path uses the same mapping -// implicitly through the pack's chunk order). 144 = 8 long slices * 18 -// k-chunks per 288-k slice. +// DUMMA-fragment-order weight pack (see the constants above and the pack +// kernel below): byte = tile*65536 + slice_base(s) + (step>>1)*1024 + +// lane*16 + (step&1)*8 + i, with s/step/i/lane derived from (k, col) exactly +// as the pack kernel derives them from the byte index. Fallback paths only +// (generic scalar and unsplit M=16 DUMMA); the timed split-K kernel reads the +// same slots directly through its own template-indexed addresses. __device__ __forceinline__ int64_t packed384_byte_offset(int k, int col) { const int64_t tile = static_cast(col) >> 4; // col / 16 const int64_t n_local = static_cast(col) & 15; - const int kchunk = k >> 4; - int64_t base; - int kk_p, cps; - if (kchunk < 144) { - const int s = kchunk / 18; - base = static_cast(s) * 288; - kk_p = kchunk % 18; - cps = 18; + const int long_end = kFragPackNumLong * kFragPackSliceLong; // 2304 + int s, k_base; + if (k < long_end) { + s = k / kFragPackSliceLong; + k_base = s * kFragPackSliceLong; } else { - const int c2 = kchunk - 144; - const int s = 8 + c2 / 16; - base = 2304 + static_cast(c2 / 16) * 256; - kk_p = c2 % 16; - cps = 16; + s = kFragPackNumLong + (k - long_end) / kFragPackSliceShort; + k_base = long_end + (s - kFragPackNumLong) * kFragPackSliceShort; } - return tile * 65536 + (base + n_local * cps + kk_p) * 16 + (k & 15); + const int kk = k - k_base; + const int step = kk >> 5; // kk / kFragPackStepK + const int within = kk & 31; // kk % kFragPackStepK + const int lane = (within >> 3) * 16 + static_cast(n_local); + const int i = within & 7; + const int64_t base = + (s < kFragPackNumLong) + ? static_cast(s) * kFragPackLongBytes + : static_cast(kFragPackLongRegion) + + static_cast(s - kFragPackNumLong) * + kFragPackShortBytes; + const int64_t slot = + (s < kFragPackNumLong && step == kFragPackSliceLong / kFragPackStepK - 1) + ? 4 * kFragPackPairBytes + static_cast(lane) * 8 + : static_cast(step >> 1) * kFragPackPairBytes + + static_cast(lane) * 16 + (step & 1) * 8; + return tile * kFragPackTileBytes + base + slot + i; } -// Scalar W8A8 GEMM: one thread computes one output element (m, n). -// -// Adjacent threads map to adjacent N columns (the fastest-changing dimension -// of both `out` and the [K, N] weight), so global reads coalesce in N. -// The K loop is executed exactly once per output element in ascending k and -// accumulated in int32; every assigned K keeps the exact int8 dot within -// int32 range (max |dot| = 128*128*4096 = 67,108,864 < 2^31). The result is -// converted to float only for `dot * x_scale[m] * weight_scale[n]`, then -// stored as bfloat16, matching the contract reference exactly. + +// --------------------------------------------------------------------------- +// Generic scalar fallback: one thread per output element. // -// gfx928: wavefront = 64, blockDim must be a multiple of 64 (128 here). -__global__ __launch_bounds__(128) void w8a8_gemm_scalar_kernel( - const int8_t* __restrict__ a, // x_q [M, K] row-major - const int8_t* __restrict__ b, // weight [K, N] row-major - const float* __restrict__ x_scale, // [M, 1] contiguous - const float* __restrict__ weight_scale, // [N, 1] contiguous - hip_bfloat16* __restrict__ out, // [M, N] row-major +// The linear index is row-major over [M, N], so adjacent lanes write adjacent +// N columns in `out` and read adjacent N columns of the weight in the raw +// layout: both streams coalesce in the fastest-changing N dimension. For +// (k,n)==(4096,384) the weight buffer instead holds the DUMMA-fragment-order +// pack, decoded per element via packed384_byte_offset so this fallback stays +// exact for the paired M=2 shape and every unmatched m. +// --------------------------------------------------------------------------- +constexpr int kScalarBlockThreads = 128; + +__global__ __launch_bounds__(kScalarBlockThreads) void w8a8_gemm_scalar_kernel( + const int8_t* __restrict__ a, // x_q [M, K] row-major + const int8_t* __restrict__ b, // weight [K, N] row-major / pack + const float* __restrict__ x_scale, // [M, 1] contiguous + const float* __restrict__ weight_scale, // [N, 1] contiguous + hip_bfloat16* __restrict__ out, // [M, N] row-major int m, int n, int k) { @@ -467,17 +307,14 @@ __global__ __launch_bounds__(128) void w8a8_gemm_scalar_kernel( const int col = static_cast(idx - static_cast(row) * n); const int8_t* __restrict__ a_row = a + static_cast(row) * k; - // For (k,n)==(4096,384) the weight buffer holds the per-tile - // slice-major-chunk pack (iteration 17; see packed384_byte_offset); every - // other (k,n) keeps the raw [K, N] row-major layout (identity pack). - // Decode so this fallback stays exact for the paired M=2 shape and all - // unmatched m. const bool packed_384 = (k == 4096 && n == 384); const int64_t b_row_stride = packed_384 ? 0 : static_cast(n); int32_t acc = 0; #pragma unroll 8 for (int kk = 0; kk < k; ++kk) { + // Exact int32 accumulation: max |dot| = 128 * 128 * 4096 = 67,108,864 + // which is < 2^31 for every assigned K. const int8_t b_val = packed_384 ? b[packed384_byte_offset(kk, col)] : b[static_cast(col) + @@ -486,39 +323,30 @@ __global__ __launch_bounds__(128) void w8a8_gemm_scalar_kernel( } // Reference order: (float(dot) * x_scale[m]) * weight_scale[n], then bf16. - // hip_bfloat16's float constructor performs the supported RNE conversion. - const float scaled = static_cast(acc) * x_scale[row] * - weight_scale[col]; + const float scaled = + static_cast(acc) * x_scale[row] * weight_scale[col]; out[idx] = hip_bfloat16(scaled); } // --------------------------------------------------------------------------- -// Minimal gfx928 DUMMA INT8 path (assigned shape M=16, N=384, K=4096). -// Tile: 16x16x32, int32 accumulation, one 64-lane wavefront per block, -// one 16x16 output tile per wave (grid = N/16 = 24 blocks). -// --------------------------------------------------------------------------- - // gfx928 DUMMA INT8 m16n16k32 tile constants. +// --------------------------------------------------------------------------- constexpr int kDummaM = 16; constexpr int kDummaN = 16; constexpr int kDummaK = 32; -constexpr int kDummaBlockThreads = 64; // one wavefront per block +constexpr int kDummaBlockThreads = 64; // one 64-lane wavefront per block -// M=16 W8A8 GEMM with the minimal 16x16x32 DUMMA tile: one wavefront owns one -// independent 16x16 output tile. A (x_q [16, K] row-major) and B (weight -// [K, N] row-major) fragments are loaded directly from global memory every -// DUMMA step with the library loaders; the int32 accumulator is explicit and -// drained through LDS in a coalesced epilogue that applies x_scale and -// weight_scale before the bf16 store. No LDS staging, no double buffering, -// and no cross-wave barrier in the K loop (a single wavefront per block; -// the one __syncthreads in the epilogue is reached by every block thread). +// Unsplit M=16 DUMMA fallback (used only when the caller workspace cannot hold +// the per-tile int32 accumulator + counters). One wavefront owns one 16x16 tile +// with direct library fragment loads and an LDS-drained epilogue. For +// (k,n)==(4096,384) the B fragment is gathered per element from the pack. __global__ __launch_bounds__(kDummaBlockThreads) void w8a8_gemm_m16_dumma_kernel( - const int8_t* __restrict__ a, // x_q [16, K] row-major - const int8_t* __restrict__ b, // weight [K, N] row-major - const float* __restrict__ x_scale, // [16, 1] contiguous - const float* __restrict__ weight_scale, // [N, 1] contiguous - hip_bfloat16* __restrict__ out, // [16, N] row-major + const int8_t* __restrict__ a, // x_q [16, K] row-major + const int8_t* __restrict__ b, // weight [K, N] row-major / pack + const float* __restrict__ x_scale, // [16, 1] contiguous + const float* __restrict__ weight_scale, // [N, 1] contiguous + hip_bfloat16* __restrict__ out, // [16, N] row-major int n, int k) { const int lane = static_cast(threadIdx.x); // 0..63 @@ -536,31 +364,20 @@ w8a8_gemm_m16_dumma_kernel( acc_frag; du::dumma::du_fill_fragment(acc_frag, 0); - // K is divisible by kDummaK for every assigned shape. A fragment rows are - // x_q rows (ldm = k); B fragment rows are weight k-rows (ldm = n), with the - // tile's 16 columns at n_base. The explicit int32 accumulation is - // k0-ascending over exact 32-element dot products, so the final int32 total - // is bit-identical to the scalar chain (integer arithmetic is exact). - // Iteration 17: this fallback is only reachable for (m,n,k)==(16,384,4096), - // whose weight buffer holds the per-tile slice-major-chunk pack. The - // library row_major loader (a.x[i] = p[(col+i)*ldm + row], row = lane&15, - // col = (lane>>4)*8) cannot decode that non-linear order, so the B - // fragment is filled with per-element gathers from the pack: x[i] = - // element (k0 + (lane>>4)*8 + i, n_base + (lane&15)) - the SAME fragment - // registers the raw-layout loader would produce (the round-15 n-major - // decode asserted the same mapping). Fallback path only - never on the - // timed M=16 split-K kernel. const bool packed_384 = (k == 4096 && n == 384); #pragma unroll 4 for (int k0 = 0; k0 < k; k0 += kDummaK) { du::dumma::du_load_matrix_sync(a_frag, a + k0, k); if (packed_384) { + // The library row_major loader (x[i] = p[(col+i)*ldm + row], row = + // lane&15, col = (lane>>4)*8) cannot decode the pack, so gather the + // SAME fragment registers per element: x[i] = element + // (k0 + (lane>>4)*8 + i, n_base + (lane&15)). const int n_local = lane & 15; const int k_off = (lane >> 4) * 8; #pragma unroll for (int i = 0; i < 8; ++i) { - b_frag.x[i] = - b[packed384_byte_offset(k0 + k_off + i, n_base + n_local)]; + b_frag.x[i] = b[packed384_byte_offset(k0 + k_off + i, n_base + n_local)]; } } else { du::dumma::du_load_matrix_sync( @@ -587,45 +404,32 @@ w8a8_gemm_m16_dumma_kernel( } // --------------------------------------------------------------------------- -// Iteration 8 (HIP round): split-K=15 one-wave LDS-staged partial GEMM + -// split-15 combine (mandatory occupancy round - one limiter: LDS footprint -// -> blocks/CU; trusted probe set [2,5,10,15]). +// Split-K=15 one-wave, zero-barrier, CU-aligned GEMM + fused last-arrival tail +// (iteration 3) with register-direct A/B transport (iteration 5) and one hot +// per-tile int32 accumulator (iteration 7). // -// Round 7 measured the split-K=10 one-wave grid (240 blocks = exactly -// 2 blocks/CU, 26,880 B LDS/block = 53,760 B/CU) at 15.43 us median but was -// rejected by the p90 guard (19.43 > 19.04; bimodal samples - machine -// state, not kernel behavior). Its 26,880 B/block LDS caps occupancy at -// exactly 2 blocks/CU (3 x 26,880 = 80,640 B > 64 KiB). split-K=15 shortens -// every slice (288/256 vs 416/384 rows), shrinking the per-block LDS -// footprint to 18,688 B -> 3 x 18,688 = 56,064 B/CU <= 64 KiB, so 3 one-wave -// blocks/CU become legal, and the grid = 24 N-groups x 15 splits = 360 -// one-wave blocks = EXACTLY 3 blocks/CU on 120 CUs (above the 2-blocks/CU -// latency-hiding target, integer-balanced, no tail wave). B stays once-read -// from HBM and A stays L2-hot (no repeated global reads - each block stages -// its own slice exactly once). -// * block tile 16x16 (1 wavefront x 1 N-tile), 64 thr; grid = 24 x 15 = -// 360 one-wave blocks = exactly 3 blocks/CU; -// * K=4096 -> 15 non-uniform 32-aligned slices: 8 x 288 (9 DUMMA steps) -// + 7 x 256 (8 steps); k0-ascending within each slice, ascending split -// sum -> bit-identical int32 order (0 mismatches expected); -// * whole slice staged once into bank-skewed LDS (A 16x288 stride 304, -// B 288x16 stride 48; 18,688 B/block), batched 16-B vector loads, one -// __syncthreads (emitted as 0 s_barrier for the 1-wave block), then a -// zero-barrier LDS-only K loop (9/8 steps x 1 A-frag + 1 B-frag ds_read -// + 1 v_mmac, 9/8 v_mmac per block); -// * workspace = 15 int32 planes [15][16][384] = 368,640 B (<= the -// caller's 393,216 B workspace_split_k_capacity budget; every launch -// overwrites every partial tile - no workspace clear); -// * combine kernel: 24 blocks x 128 thr, all 15 plane loads issued into a -// register array before the ascending split sum (exact int32 order), -// then (float(acc) * x_scale[row]) * weight_scale[col] -> bf16; -// * replay traffic ~2.32 MB (B 1.57 MB + planes RW 737,280 B + out 12,288 -// B) vs 2.47 MB for the accepted S=16 kernel (-6%); vs S=10 the plane -// RW grows +245,760 B (+50%) - the combine cost of more splits is the -// explicit trade being benchmarked against the occupancy gain. +// * block tile 16x16 (1 wavefront x 1 N-tile), 64 thr; grid = 24 N-groups x +// 15 splits = 360 one-wave blocks = EXACTLY 3 blocks/CU on 120 CUs; +// * K=4096 -> 15 non-uniform 32-aligned slices: 8 x 288 (9 DUMMA steps) + +// 7 x 256 (8 steps); k0-ascending within each slice, exact order-free +// int32 reduction at the tail -> bit-identical int32 sum; +// * A is register-only (per-lane int2 fragment slice per step, no LDS); +// * B (the once-read, cold weight) is packed DUMMA-fragment-order outside +// the timed region (iteration 5) and read straight into its fragment +// registers with 4 x int4 (+ 1 x int2 for the 9-step slices) per lane - +// NO LDS stage, no barrier, no lgkmcnt drain; +// * no __syncthreads in the block body (0 s_barrier for a 1-wave block) and +// a zero-barrier register-only K loop (0 barriers per K step); +// * workspace = ONE int32 accumulator per tile [24][16][16] = 24,576 B plus +// 24 arrival counters = 24,672 B (<= the caller's 393,216 B budget); each +// slice adds its 256 int32 with global atomicAdd into its tile (hot in L2, +// no per-replay write-back of 368,640 B of planes); +// * fused tail: exactly ONE arrival per block (lane 0 fence + atomicAdd, +// acquisition fence, LDS s_is_last flag, barriers); the last arriver reads +// the single accumulated tile, applies both scales, stores bf16, resets +// the tile accumulator and the counter. One dispatch per replay. // --------------------------------------------------------------------------- - -constexpr int kSplitK = 15; // 15 planes; workspace 368,640 B +constexpr int kSplitK = 15; // 15 slices; grid = 24 x 15 = 360 constexpr int kNumNGroups = 24; // N=384 / 16 (partial block tiles) constexpr int kNumLongSplits = 8; // 8 x 288-row slices + 7 x 256-row constexpr int kSliceLong = 288; // 9 x kDummaK steps (non-uniform) @@ -633,70 +437,61 @@ constexpr int kSliceShort = 256; // 8 x kDummaK steps constexpr int kStepsLong = kSliceLong / kDummaK; // 9 constexpr int kStepsShort = kSliceShort / kDummaK; // 8 constexpr int kPartialBlockThreads = 64; // one wavefront per block -constexpr int kCounterInts = kNumNGroups; // one arrival counter per N-group -constexpr int kStagedAStride = 304; // 288 + 16 bank skew (16-B aligned) -// Iteration 15: B is staged n-major (element (k,n) at n*kStagedBStride + k, -// the transpose of the A staging convention), so the column stride must hold -// the longest k-slice (288) plus bank skew; 304 = 288 + 16 keeps every -// staging ds_write_b128 and every 8-byte fragment read 16-/8-B aligned. -constexpr int kStagedBStride = 304; // 288 + 16 bank skew (16-B aligned) - -// Partial GEMM: block (tile, split) computes one 16x16 output tile for the -// split's contiguous K slice and stores the int32 partial to -// partials[split][row][n]. The whole slice is staged into LDS once with -// batched 16-B vector loads, one barrier, then a zero-barrier LDS-only K -// loop. k0-ascending within the slice; the combine kernel sums slices in -// ascending split order, so the total int32 accumulation order is -// bit-identical to the scalar chain. The slice body is a compile-time -// template (kSliceKT, kStepsT) so the staging loops and the K loop are -// fully unrolled; the grid-uniform `split < kNumLongSplits` branch picks the -// 288- or 256-row instantiation for each block (one launch total). +constexpr int kCounterInts = kNumNGroups; // one arrival counter per N-group + +// Iteration 7 workspace: ONE int32 accumulator per 16-column tile instead of +// 15 int32 partial planes. 24 tiles x 256 int32 = 24,576 B total, hot in L2, +// so the 368,640 B of per-replay partial-plane write-back disappears. The +// 24 arrival counters stay after the accumulator. Every accumulator element is +// zeroed once by the host (like the counters) and is reset by the tile's fused +// last-arrival tail at the end of every replay, so no clear is needed inside +// the timed region. +constexpr int kAccumIntsPerTile = kDummaM * kDummaN; // 256 +constexpr int64_t kAccumInts = + static_cast(kNumNGroups) * kAccumIntsPerTile; // 6,144 +constexpr int64_t kAccumBytes = + kAccumInts * static_cast(sizeof(int32_t)); // 24,576 B + +// One 16x16 tile for one 32-aligned K slice. The slice body is a compile-time +// template (kSliceKT, kStepsT) so the load loops and the K loop are fully +// unrolled; the grid-uniform `split < kNumLongSplits` branch picks the 288- or +// 256-row instantiation for each block (one launch total). +// +// Iteration 5: both operands go straight into their fragment registers and NO +// LDS is used at all, so the body is just "load registers, then kStepsT +// v_mmac". A comes from the raw row-major activation; B comes from the +// DUMMA-fragment-order pack, whose per-tile slice region is the wavefront's +// matrix_b fragment slots in k-ascending order (two k32 steps per 1,024-B +// pair slot, one 512-B single slot for an odd 9th step). +// Iteration 7: the slice's 16x16 int32 result is added straight into the +// tile's single accumulator with four per-lane global atomicAdd (no plane, no +// per-split store base). Integer addition is exact and order-independent, so +// the reduction is bit-identical to the ascending 15-plane sum. template __device__ __forceinline__ void split15_slice_body( const int8_t* __restrict__ a, // x_q [16, K] row-major - const int8_t* __restrict__ b, // weight [K, N] row-major - int32_t* __restrict__ partials, // [kSplitK][16][N] int32 - int n, + const int8_t* __restrict__ b, // weight fragment-order pack + int32_t* __restrict__ accum, // [24][16][16] int32 int k, - int split, int tile, - int n_base, - int k_base, - int8_t (*b_lds)[kStagedBStride]) { + int split, + int k_base) { const int tid = static_cast(threadIdx.x); - // Iteration 21 (HIP-only, A register-only transport; the validated TP4 - // o_proj lineage recipe "register-only K-loop transport"). The A slice is - // small (16 x kSliceKT = 4,608 / 4,096 B, L2-hot 64-KiB activation) and - // every block reads its own rows exactly once, so staging it through LDS - // only buys the LDS round trip: per block it costs kALoads 16-B global - // loads + kALoads ds_write_b128 + kStepsT ds_read2_b32 (2-way bank - // conflicted). This round removes the A LDS stage entirely: each lane - // loads its own 8-B fragment slice for EVERY DUMMA step into a per-lane - // register array a_slice[kStepsT] (the library matrix_a row_major loader - // with ldm = k reads x[i] = p[row*k + k_base + s*kDummaK + col + i], row = - // lane&15, col = (lane>>4)*8 - the SAME 8 elements per lane per step the - // LDS version read, so every fragment register is identical and the int32 - // accumulation order is bit-identical). All kStepsT A loads are issued in - // one register-array block BEFORE any ds_write (the iteration-11 batching - // pattern that forced the staging loads back-to-back in the accepted code - // object), so the A slice costs one memory latency and zero LDS - // instructions; the K loop then consumes the fragments straight from - // registers. Guarded lanes are impossible (kStepsT * 64 lanes * 8 B = - // kSliceKT * 16 B exactly, no tail). + // Register-only A transport (iteration 3, unchanged). The A slice is small + // (16 x kSliceKT = 4,608/4,096 B, L2-hot 64-KiB activation) and every block + // reads its own rows exactly once, so staging it through LDS only buys the + // LDS round trip. Each lane loads its own 8-B fragment slice for EVERY DUMMA + // step into a per-lane register array: the library matrix_a row_major loader + // with ldm=k reads x[i] = p[row*k + k_base + s*kDummaK + col + i], row = + // lane&15, col = (lane>>4)*8. The 8 elements are 8 CONSECUTIVE bytes of one + // row (8-B aligned), so one explicit int2 load per lane per step and a + // register-to-register memcpy fill the same fragment registers the LDS + // loader produced. All kStepsT loads are issued back-to-back, ahead of the B + // loads, so the A slice costs one memory latency and zero LDS ops. du::dumma::DUFragment a_slice[kStepsT]; - // Explicit vector loads: the library loader assigns the 8 bytes - // individually, which LLVM scalarizes to 8 global_load_ubyte per step per - // lane from GLOBAL memory (verified in the round-21 draft code object: - // 136 ubyte loads = the old "direct fragment byte-load" poison). The - // fragment's 8 elements are 8 CONSECUTIVE bytes of one row (m = lane&15) - // at k = k_base + s*kDummaK + (lane>>4)*8, 8-B aligned (k_base, kDummaK - // and the col offset are all multiples of 8; x_q is tensor-aligned), so - // one explicit int2 load per lane per step forces ONE global_load_dwordx2 - // and the __builtin_memcpy (8 bytes, register-to-register) fills the same - // fragment registers the LDS row_major loader produced. const int a_row = tid & 15; // m index (library loader row) const int a_col = (tid >> 4) * 8; // k offset (library loader col) const int8_t* a_base = @@ -707,114 +502,91 @@ __device__ __forceinline__ void split15_slice_body( __builtin_memcpy(a_slice[s].x, &raw, 8); } - // Stage the B slice (kSliceKT rows x 16 cols) with coalesced 16-B vector - // loads, then one barrier (B only - the A stage is gone). B chunks - // (iteration 17) come from the per-tile slice-major-chunk pack (see - // w8a8_pack_weight_gateup_slicemajor_kernel): chunk c holds 16 CONSECUTIVE - // k values of one n column (n = c/18 long, c/16 short; k-chunk = c%18 / - // c%16) and the chunks are stored in ascending c order, so the slice's - // chunks occupy the CONTIGUOUS byte range [(k_base + c)*16] of the tile - - // 64 lanes cover 1,024 CONTIGUOUS bytes per wave-load (the round-14 - // locality win). Same chunk content, same n-major LDS destination, same - // per-lane order -> the fragment reads and the int32 accumulation stay - // bit-identical. - // - // Iteration 11: issue ALL staging global loads into one register array - // BEFORE any ds_write. The accepted kernel's exact gfx928 code object - // serialized the slice staging: every global_load_dwordx4 was followed by - // s_waitcnt vmcnt(0) before its ds_write_b128 (load_wait0_ds_write_windows - // = 18 across the two slice instantiations, 10 long + 8 short), so each - // block's staging paid ~10 (long) / 8 (short) full global-memory latencies - // one after another before the K loop could issue its first ds_read - // (lgkmcnt(0)). Holding the 16-B chunks in a register array first forces - // the compiler to issue the loads back-to-back (all in flight - // concurrently) and move the vmcnt waits to the write side, collapsing the - // staging critical path to one memory latency. Guarded lanes (the unrolled - // tail beyond the chunk count) write a zero placeholder so no - // uninitialized register is ever consumed. - constexpr int kChunksPerBRow = kSliceKT / 16; // 18 / 16 (per n column) - constexpr int kBLoads = (kSliceKT + 63) / 64; // 5 / 4 - int4 s_buf[kBLoads]; + // Register-direct B transport (iteration 5). This block's split owns a + // CONTIGUOUS region of its tile: slice_base(s) + 4 x 1,024-B pair slots, + // followed (9-step slices only) by one 512-B single slot for the odd last + // step. One int4 load per lane per pair slot covers 1,024 CONTIGUOUS bytes + // per wave-load and lands BOTH matrix_b fragments of that pair in their + // final registers: its low 8 bytes are x[i] of step 2m and its high 8 bytes + // x[i] of step 2m+1, i.e. the same x[i] = element(k0 + (lane>>4)*8 + i, + // n_base + (lane&15)) the accepted col_major LDS loader produced. The loads + // are issued back-to-back with no ds_write, no __syncthreads and no lgkmcnt + // drain in between, so the K loop can consume each step pair as soon as it + // arrives instead of after the whole slice. + du::dumma::DUFragment + b_slice[kStepsT]; + constexpr int kPairSlots = kStepsT / 2; // 4 + const int8_t* b_split = + b + static_cast(tile) * (kDummaN * k) + + ((split < kNumLongSplits) + ? static_cast(split) * kFragPackLongBytes + : static_cast(kFragPackLongRegion) + + static_cast(split - kNumLongSplits) * + kFragPackShortBytes); #pragma unroll - for (int i = 0; i < kBLoads; ++i) { - const int c = tid + 64 * i; - // Iteration 17: slice-major-chunk pack - chunk c (n = c/kChunksPerBRow, - // k-chunk = c%kChunksPerBRow) lives at byte (k_base + c)*16 of the - // tile, so a wave-load reads 1,024 contiguous bytes. - s_buf[i] = (c < kSliceKT) - ? *reinterpret_cast( - b + static_cast(tile) * (kDummaN * k) + - (static_cast(k_base) + c) * 16) - : int4{0, 0, 0, 0}; + for (int m = 0; m < kPairSlots; ++m) { + const int4 raw = *reinterpret_cast( + b_split + static_cast(m) * kFragPackPairBytes + tid * 16); + __builtin_memcpy(b_slice[2 * m].x, &raw, 8); + __builtin_memcpy(b_slice[2 * m + 1].x, + reinterpret_cast(&raw) + 8, 8); } -#pragma unroll - for (int i = 0; i < kBLoads; ++i) { - const int c = tid + 64 * i; - if (c < kSliceKT) { - // n-major LDS destination (iteration 15, UNCHANGED): element (k,n) at - // n*kStagedBStride + k. Chunk c maps to (n = c/kChunksPerBRow, - // k-chunk = c%kChunksPerBRow): for the 256-row instantiation this is - // the same (c&15, c>>4) as iteration 15; for the 288-row - // instantiation the chunk order is n-major (18 chunks per column) and - // the division is resolved by the compiler (staging path only). Still - // one ds_write_b128 per 16-B chunk. - const int b_row = c / kChunksPerBRow; - *reinterpret_cast( - &b_lds[b_row][(c - b_row * kChunksPerBRow) * 16]) = s_buf[i]; - } + if (kStepsT % 2 != 0) { + // 9-step slices: the odd last step is a 512-B single slot (8 B/lane). + const int2 raw = *reinterpret_cast( + b_split + kPairSlots * kFragPackPairBytes + tid * 8); + __builtin_memcpy(b_slice[kStepsT - 1].x, &raw, 8); } - __syncthreads(); - - // The block's one wavefront owns the whole 16x16 tile. Zero-barrier K - // loop: kStepsT steps of kDummaK=32; the A fragment comes from the - // preloaded register array a_slice[s] (iteration 21, no LDS), the B - // fragment from b_lds (iteration 15: col_major on the n-major layout, ldm - // 304), one v_mmac per step (9 or 8 v_mmac per block). The col_major - // loader computes b.x[i] = p[row*304 + col + i] (row = n_local = lane&15, - // col = k offset (lane>>4)*8), i.e. the lane's 8 elements B[k0+col+i] - // [n_base+row] are 8 CONSECUTIVE bytes of one n column -> ONE ds_read2_b32 - // per step (was 8 ds_read_u8 at stride 48). x[i] gets the same element as - // the old row_major loader, so the fragment registers (and the int32 - // accumulation order) are bit-identical. - du::dumma::DUFragment - b_frag; + // The block's one wavefront owns the whole 16x16 tile. Zero-barrier K loop: + // kStepsT steps of kDummaK=32, A and B both straight from the preloaded + // register arrays, one v_mmac per step (9 or 8 v_mmac per block), zero + // s_barrier per K step. The int32 accumulation inside the slice is untouched + // (k-ascending); across slices the addition is exact and order-free. du::dumma::DUFragment acc_frag; du::dumma::du_fill_fragment(acc_frag, 0); #pragma unroll for (int s = 0; s < kStepsT; ++s) { - const int k0 = s * kDummaK; - du::dumma::du_load_matrix_sync(b_frag, &b_lds[0][k0], kStagedBStride); - du::dumma::du_mma_sync(acc_frag, a_slice[s], b_frag, acc_frag); + du::dumma::du_mma_sync(acc_frag, a_slice[s], b_slice[s], acc_frag); } - // Direct int32 partial store: partials[split][row][n_base .. n_base+15]. - // Every launch overwrites every partial tile, so no workspace clear is - // needed before capture or between replays. - int32_t* partial_base = - partials + static_cast(split) * kDummaM * n + n_base; - du::dumma::du_store_matrix_sync(partial_base, acc_frag, n, - du::dumma::mem_row_major); + // Direct int32 accumulation into the per-tile accumulator (iteration 7). + // The library mem_row_major store owns one element per lane at row = tid&15, + // column = (tid>>4), then x[1], x[2], x[3] at +4, +8, +12 columns (the exact + // mapping the iteration-6 candidate verified against the exact code object: + // one 64-bit base with 0/16/32/48-byte offsets). Four no-return atomicAdd + // publish exactly those values into accum[tile][row][col], which stays hot + // in L2 (24,576 B total) - the 368,640-B partial-plane round trip of + // iterations 3-6 is gone. Integer addition is exact and order-independent, + // so the tile sum, and therefore every bf16 output, is unchanged. + int32_t* accum_lane = + accum + static_cast(tile) * kAccumIntsPerTile + + (tid & 15) * kDummaN + (tid >> 4); +#pragma unroll + for (int i = 0; i < 4; ++i) { + atomicAdd(accum_lane + 4 * i, acc_frag.x[i]); + } } -// Fused last-arrival combine tail (iteration 9): removes the separate -// 24-block combine kernel -> exactly ONE kernel launch per replay. +// Fused last-arrival combine tail: removes the separate 24-block combine +// kernel -> exactly ONE kernel launch per replay. // -// Protocol (the validated TP4 qkv lineage pattern): -// * every block writes its 16x16 int32 plane tile, then __threadfence() -// (release) and atomicAdd(&counters[tile], 1); -// * the block whose atomicAdd returns kSplitK-1 is the 15th (last) -// arriver of its N-group: it __threadfence()s (acquire), sums the 15 -// planes in ascending split order (exact int32 order), applies both -// scales, stores bf16, and resets its counter to 0 (atomicExch) so the -// next replay restarts from 0. The host zeroes the 24 counters once -// before the first launch; the self-reset keeps every later replay -// deterministic (stream-ordered, no racing increments). +// Protocol (iteration 7): every block adds its 16x16 int32 result into the +// tile's single accumulator with global atomicAdd, then lane 0 +// __threadfence()es (release) and atomicAdd(&counters[tile], 1); the block +// whose atomicAdd returns kSplitK-1 is the 15th (last) arriver of its +// N-group: it acquires, reads the summed accumulator, applies both scales, +// stores bf16, resets the tile's accumulator to zero and resets its counter to +// 0 (atomicExch) so the next replay restarts from zero. The host zeroes the +// whole scratch region once before the first launch; the tail's self-reset +// keeps every later replay deterministic. Only lane 0 ever increments the +// counter (the round-9 lineage lost correctness by letting all 64 lanes +// increment). __device__ __forceinline__ void split15_combine_tail( - const int32_t* __restrict__ partials, // [kSplitK][16][N] int32 + int32_t* __restrict__ accum, // [24][16][16] int32 (hot) const float* __restrict__ x_scale, // [16, 1] contiguous const float* __restrict__ weight_scale, // [N, 1] contiguous hip_bfloat16* __restrict__ out, // [16, N] row-major @@ -823,24 +595,13 @@ __device__ __forceinline__ void split15_combine_tail( int n) { const int lane = static_cast(threadIdx.x); // 0..63 (one wavefront) const int n_base = tile * kDummaN; - const int plane = kDummaM * n; // int32 stride per split - // Repair 1/4 (iteration 9): exactly ONE arrival per block. Only lane 0 - // fences, atomically increments the tile counter and publishes the result - // through LDS; the other 63 lanes must NOT atomicAdd. The round-9 draft had - // all 64 lanes incrementing -> 64 x 15 = 960 increments per tile per - // replay, so the 15th increment (arrived == kSplitK-1) fired inside the - // FIRST-arriving block, before its 14 siblings had stored their planes: - // the premature combiner summed mostly-zero planes (first_mismatch - // actual=0.0 at (0,0)) and the unguarded atomicExch reset made several - // blocks combine per replay (6048/6144 mismatched). This mirrors the - // validated TP4 qkv fused tail: tid-0 fence + atomicAdd, LDS flag, barriers. __syncthreads(); __shared__ int s_is_last; if (lane == 0) { - __threadfence(); // release plane stores + __threadfence(); // release this slice const int arrived = atomicAdd(&counters[tile], 1); - __threadfence(); // acquire other planes + __threadfence(); // acquire other slices s_is_last = (arrived == kSplitK - 1) ? 1 : 0; } __syncthreads(); @@ -848,93 +609,84 @@ __device__ __forceinline__ void split15_combine_tail( return; // not the last arriver } - // 256 outputs / 64 lanes = 4 CONSECUTIVE columns per lane (row = lane>>2, - // col0 = (lane & 3) * 4): one aligned int4 (16-B) plane load per split - // replaces the round-9 loop's 4 scalar 4-B loads per element, cutting the - // tail's vmem load instruction count 4x (60 -> 15 per lane) with no byte - // or L2-request increase (each 16-B load is fully consumed; the 4 lanes - // of one row cover a contiguous 64-B segment). The per-element int32 - // accumulation order is UNCHANGED: for each output element the 15 plane - // values are still summed in ascending split order, so the outputs stay - // bit-identical to the scalar chain. weight_scale is read once as a - // float4 (n_base + col0 is 16-B aligned) and the 4 adjacent bf16 stores - // merge into one 8-B store. Fence/atomic/barrier protocol untouched. + // 256 accumulators / 64 lanes = 4 CONSECUTIVE columns per lane (row = lane>>2, + // col0 = (lane & 3) * 4): ONE aligned int4 (16-B) accumulator load per lane + // replaces the 15 dependent int4 plane loads (15 -> 1 per lane), and the + // accumulator is only 24,576 B (L2-resident) instead of 368,640 B of planes. + // The value read is the exact int32 sum of all 15 slices (integer addition + // is order-independent), so the reference float order + // (float(acc) * x_scale[row]) * weight_scale[col] produces bit-identical + // bf16 outputs. weight_scale is read once as a float4 (16-B aligned) and the + // 4 adjacent bf16 stores merge into one 8-B store. const int row = lane >> 2; // 0..15 (4 lanes per row) const int col0 = (lane & 3) * 4; // 0,4,8,12 (16-B aligned) - const int32_t* p = partials + row * n + n_base + col0; + int32_t* p = + accum + static_cast(tile) * kAccumIntsPerTile + + row * kDummaN + col0; + const int4 v = *reinterpret_cast(p); const float4 ws = *reinterpret_cast(weight_scale + n_base + col0); const float xs = x_scale[row]; - int32_t acc0 = 0, acc1 = 0, acc2 = 0, acc3 = 0; -#pragma unroll - for (int s = 0; s < kSplitK; ++s) { - const int4 v = *reinterpret_cast( - p + static_cast(s) * plane); - acc0 += v.x; - acc1 += v.y; - acc2 += v.z; - acc3 += v.w; - } hip_bfloat16* o = out + static_cast(row) * n + n_base + col0; - o[0] = hip_bfloat16(static_cast(acc0) * xs * ws.x); - o[1] = hip_bfloat16(static_cast(acc1) * xs * ws.y); - o[2] = hip_bfloat16(static_cast(acc2) * xs * ws.z); - o[3] = hip_bfloat16(static_cast(acc3) * xs * ws.w); - - // All kSplitK arrivals of this replay have happened; restart for the next - // replay (the kernel's completion on the stream orders this reset before - // the next replay's first atomicAdd). One lane is enough; all 15 blocks - // have already passed their atomicAdd, so no racing increment survives. + o[0] = hip_bfloat16(static_cast(v.x) * xs * ws.x); + o[1] = hip_bfloat16(static_cast(v.y) * xs * ws.y); + o[2] = hip_bfloat16(static_cast(v.z) * xs * ws.z); + o[3] = hip_bfloat16(static_cast(v.w) * xs * ws.w); + + // Every slice of this tile has already arrived (that is what made this block + // the last arriver), so the reset cannot race with another atomicAdd: the + // accumulator restarts at zero for the next replay, exactly like the + // counter, and the next replay's kernel is stream-ordered after this one. + // The store is to the same address just read, so the compiler keeps it after + // the load that produced v. + *reinterpret_cast(p) = make_int4(0, 0, 0, 0); if (lane == 0) { atomicExch(&counters[tile], 0); } } +// Iteration 5: no LDS slice stage at all - both operands are fetched straight +// into fragment registers (A from the raw activation, B from the +// DUMMA-fragment-order pack), so the only shared storage left in this kernel is +// the 4-B s_is_last flag of the fused combine tail. +// Iteration 7: the per-slice result is accumulated into accum[tile] with +// global atomicAdd (no per-split planes); split/n_base are only needed to pick +// the slice and its K base. __global__ __launch_bounds__(kPartialBlockThreads) void w8a8_gemm_m16_dumma_splitk15_lds_staged_partial_kernel( const int8_t* __restrict__ a, // x_q [16, K] row-major - const int8_t* __restrict__ b, // weight [K, N] row-major - int32_t* __restrict__ partials, // [kSplitK][16][N] int32 + const int8_t* __restrict__ b, // weight fragment-order pack + int32_t* __restrict__ accum, // [24][16][16] int32 const float* __restrict__ x_scale, // [16, 1] contiguous const float* __restrict__ weight_scale, // [N, 1] contiguous hip_bfloat16* __restrict__ out, // [16, N] row-major int32_t* __restrict__ counters, // [kNumNGroups] monotonic int n, int k) { - const int tile = static_cast(blockIdx.x) / kSplitK; // 0..kNumNGroups-1 - const int split = static_cast(blockIdx.x) % kSplitK; // 0..kSplitK-1 - const int n_base = tile * kDummaN; + const int tile = static_cast(blockIdx.x) / kSplitK; // 0..23 + const int split = static_cast(blockIdx.x) % kSplitK; // 0..14 const int k_base = (split < kNumLongSplits) ? split * kSliceLong : kNumLongSplits * kSliceLong + (split - kNumLongSplits) * kSliceShort; - // Iteration 15: n-major B stage, element (k,n) at n*kStagedBStride + k - // (16 n columns x 304 B = 4,864 B; the k-slice length is carried by the - // template instantiation, not by the shared array). Iteration 21: the A - // slice is no longer staged in LDS (register-only transport), so the - // per-block LDS is B + the combine flag only. - __shared__ __align__(16) int8_t b_lds[kDummaN][kStagedBStride]; - if (split < kNumLongSplits) { - split15_slice_body(a, b, partials, n, k, split, - tile, n_base, k_base, b_lds); + split15_slice_body(a, b, accum, k, tile, split, + k_base); } else { - split15_slice_body(a, b, partials, n, k, split, - tile, n_base, k_base, b_lds); + split15_slice_body(a, b, accum, k, tile, split, + k_base); } - // Fused combine tail (iteration 9): one launch per replay; the last - // arriver of each N-group sums the 15 planes and stores bf16. - split15_combine_tail(partials, x_scale, weight_scale, out, counters, tile, + split15_combine_tail(accum, x_scale, weight_scale, out, counters, tile, n); } -// Identity device-to-device weight pack (iteration 1: the DUMMA kernel reads -// B with the raw [K, N] row-major layout, so identity packing is exact). -// Kept valid for every (K, N) including unmatched shapes; the exact -// (k,n)==(4096,384) shape uses the per-tile slice-major-chunk pack below -// instead (iteration 17). +// --------------------------------------------------------------------------- +// Weight packing (outside the timed region). +// --------------------------------------------------------------------------- +constexpr int64_t kGateUpTotal = 4096 * 384; // 1,572,864 bytes + __global__ void w8a8_pack_weight_identity_kernel( const int8_t* __restrict__ src, int8_t* __restrict__ dst, @@ -946,60 +698,73 @@ __global__ void w8a8_pack_weight_identity_kernel( } } -// Per-tile slice-major-chunk weight pack for the exact (k,n)==(4096,384) -// shape (iteration 17): each 16-B chunk holds 16 CONSECUTIVE k values of one -// n column, and the 4,096 chunks of a tile are stored in ascending (slice, -// n, k-chunk) order: -// chunk index = slice_chunk_base(s) + n_local*chunks_per_slice(s) + kk' -// with the 8 long 288-k slices (s < 8: base s*288, 18 chunks per n) followed -// by the 7 short 256-k slices (s >= 8: base 8*288 + (s-8)*256, 16 chunks per -// n); byte address = tile*65536 + chunk_index*16, chunk content = -// raw[k*384 + tile*16 + n_local] for k = k_base(s) + kk'*16 .. +15 (k_base -// numerically equals the chunk base: 288 k and 288 chunks per long slice). -// This is the iteration-15 n-major pack with the same chunk CONTENT but -// reordered memory (n*4096 + kk*16 -> slice-major chunk order), so a staged -// wave-load (64 consecutive chunks) covers 1,024 CONTIGUOUS bytes -// (sequential L2 sectors and sequential DRAM pages; was 16 x 64-B sectors -// at 4,096-B stride) while the n-major LDS destination, the -// one-ds_read2_b32 B fragment read and the int32 accumulation order are all -// untouched (outputs bit-identical to iteration 15). One-time, -// out-of-timed-region, out-of-Graph, same byte count and same buffer -// (graph-stable addresses unchanged). Every other (K, N) keeps the identity -// pack. Fallbacks decode this order via packed384_byte_offset. +// Per-tile DUMMA-fragment-order weight pack for the exact (k,n)==(4096,384) +// shape (iteration 5): byte idx inside a tile is the matrix_b fragment byte +// that the timed split-K kernel loads, so each block's B slice is a straight +// sequential register stream (4 x 16-B pair slots + one 8-B odd slot per lane) +// with no LDS stage in between. The inverse mapping (element -> byte) is +// packed384_byte_offset; the layout constants above define the geometry: +// 15 split slices in ascending split order; +// slice s byte base: s*4608 (8 long 288-k slices, 9 steps) +// 8*4608 + (s-8)*4096 (7 short 256-k slices, 8 steps); +// inside a slice: (step>>1)*1024 + lane*16 + (step&1)*8 + i, with +// lane = ((k-k_base)>>3 & 3)*16 + n_local, i = (k-k_base)&7. The odd 9th +// step of a long slice is a single 512-B slot: 4*1024 + lane*8 + i. +// One-time, out-of-timed-region, out-of-Graph; same byte count and the same +// graph-stable buffer. Every other (K, N) keeps the identity pack. Fallbacks +// decode this order via packed384_byte_offset. __global__ void w8a8_pack_weight_gateup_slicemajor_kernel( const int8_t* __restrict__ src, int8_t* __restrict__ dst) { const int64_t idx = static_cast(blockIdx.x) * blockDim.x + threadIdx.x; - constexpr int64_t kTotal = 4096 * 384; // 1,572,864 - if (idx >= kTotal) { + if (idx >= kGateUpTotal) { return; } const int64_t tile = idx >> 16; // / 65536 const int64_t rem = idx - (tile << 16); // % 65536 (byte in tile) - const int64_t chunk = rem >> 4; // 16-B chunk index in tile - const int64_t in_chunk = rem & 15; // byte within the chunk - int64_t base; // chunk index of the slice start - int64_t n_local, kk_p; - if (chunk < 8 * 288) { // long slice (18 chunks per n) - const int64_t r = chunk % 288; - base = (chunk / 288) * 288; - n_local = r / 18; - kk_p = r % 18; - } else { // short slice (16 chunks per n) - const int64_t c2 = chunk - 8 * 288; - const int64_t r = c2 % 256; - base = 8 * 288 + (c2 / 256) * 256; - n_local = r / 16; - kk_p = r % 16; + int s; // split index + int step; // DUMMA k32 step inside the slice + int lane; // wavefront lane of the fragment byte + int i; // byte inside the lane's 8-B fragment + if (rem < kFragPackLongRegion) { // long 288-k slices + s = static_cast(rem / kFragPackLongBytes); + const int64_t r = rem - static_cast(s) * kFragPackLongBytes; + if (r < 4 * kFragPackPairBytes) { // 4 pair slots (2 steps each) + const int m = static_cast(r >> 10); // r / 1024 + const int rr = static_cast(r & 1023); + lane = rr >> 4; + const int pos = rr & 15; + step = 2 * m + (pos >> 3); + i = pos & 7; + } else { // odd 9th step: 512-B single slot + const int rr = static_cast(r - 4 * kFragPackPairBytes); + lane = rr >> 3; + i = rr & 7; + step = 8; + } + } else { // short 256-k slices + const int64_t c2 = rem - kFragPackLongRegion; + const int64_t sidx = c2 / kFragPackShortBytes; + s = kFragPackNumLong + static_cast(sidx); + const int64_t r = c2 - sidx * kFragPackShortBytes; + const int m = static_cast(r >> 10); + const int rr = static_cast(r & 1023); + lane = rr >> 4; + const int pos = rr & 15; + step = 2 * m + (pos >> 3); + i = pos & 7; } - // base == k_base (k index of the slice start) numerically: 288 k / 288 - // chunks per long slice, 256 / 256 per short slice. - const int64_t k = base + kk_p * 16 + in_chunk; + const int n_local = lane & 15; + const int k_base = (s < kFragPackNumLong) + ? s * kFragPackSliceLong + : kFragPackNumLong * kFragPackSliceLong + + (s - kFragPackNumLong) * kFragPackSliceShort; + const int64_t k = static_cast(k_base) + step * kFragPackStepK + + (lane >> 4) * 8 + i; dst[idx] = src[k * 384 + tile * 16 + n_local]; } -// Identity device-to-device scale pack (bootstrap). __global__ void w8a8_pack_scale_identity_kernel( const float* __restrict__ src, float* __restrict__ dst, @@ -1012,20 +777,19 @@ __global__ void w8a8_pack_scale_identity_kernel( } // namespace -// Timed GEMM entry point (inside the CUDA/HIP Graph region). -// -// Graph-safety contract: no allocation, no compilation, no autotuning, no -// weight packing, no host/device synchronization, no default-stream launch. -// Launches only on the caller-provided stream and uses only caller-provided -// tensors. The iteration-9 split-K=15 path uses the caller's workspace as the -// int32 partial planes (write-in-place every launch, no clear needed) plus 24 -// arrival counters in the workspace tail and issues exactly ONE kernel per -// replay (LDS-staged partial GEMM with a fused last-arrival combine tail); -// the iteration-1 unsplit kernel is the workspace-too-small fallback. +// --------------------------------------------------------------------------- +// launch_w8a8_gemm // -// Exact-shape specializations are guarded HERE, before the generic scalar -// fallback, so the paired M=2 API shape with the same (N, K) and every other -// shape still reach the scalar path below. +// Timed operator: launches on the caller-provided (PyTorch current) stream. +// No allocation, no compilation, no autotuning, no packing, no host or device +// synchronization and no default-stream launch. The assigned-shape path uses +// the caller-provided workspace for ONE int32 accumulator per 16-column tile +// (24 x 256 x 4 = 24,576 B) plus 24 arrival counters (96 B) and issues exactly +// ONE kernel per replay (split-K=15 one-wave zero-barrier GEMM with global +// atomicAdd accumulation and a fused last-arrival tail, register-direct A/B +// transport, no LDS slice stage); every accumulator element is reset by its +// tile's last arriver, so no clear is needed between replays. +// --------------------------------------------------------------------------- extern "C" void launch_w8a8_gemm( const int8_t* a, const int8_t* b, @@ -1038,53 +802,41 @@ extern "C" void launch_w8a8_gemm( int n, int k, hipStream_t stream) { - (void)workspace; - (void)workspace_bytes; - - if (m <= 0 || n <= 0 || k <= 0) { - return; - } - const int64_t total = static_cast(m) * n; - if (total <= 0) { + if (m <= 0 || n <= 0 || k <= 0 || a == nullptr || b == nullptr || + out == nullptr) { return; } - // Exact-shape M=16 specialization (iteration 9): split-K=15 LDS-staged - // partial GEMM with a FUSED last-arrival combine tail -> exactly one - // launch per replay (the round-8 falsifiable branch fired: median - // 15.53 >= 15.4, so the two-launch structure is attacked instead of - // geometry). grid = 24 N-groups (16x16 tiles) x 15 K-slices = 360 one-wave - // blocks = exactly 3 blocks/CU on 120 CUs; K=4096 -> 15 non-uniform - // 32-aligned slices (8 x 288 = 9 DUMMA steps + 7 x 256 = 8 steps); each - // block stages its whole slice in LDS once (18,688 B/block -> 3 x 18,688 = - // 56,064 B/CU <= 64 KiB), runs a zero-barrier LDS-only K loop, stores its - // int32 plane, then the 15th arriver of its N-group sums the 15 planes in - // ascending k order, applies both scales and stores bf16. The 24 arrival - // counters live in the 96 B right after the 15 planes and are zeroed once - // before the first launch (outside the timed Graph in the normal - // eager-first flow). The iteration-1 unsplit DUMMA kernel remains the - // fallback when the caller's workspace cannot hold 15 partial planes plus - // the counters (368,640 + 96 B <= the workspace_split_k_capacity budget of - // 393,216 B); the paired M=2 API shape with the same (N,K) and every - // other shape still reach the scalar fallback below. + hip_bfloat16* out_bf16 = reinterpret_cast(out); + + // Exact-shape M=16 dispatch (hy3_tp8_shared_gate_up_proj_m16): split-K=15 + // one-wave zero-barrier partial GEMM + fused last-arrival combine tail. + // grid = 24 N-groups x 15 K-slices = 360 one-wave blocks = exactly 3 + // blocks/CU on 120 CUs; K=4096 -> 8 x 288 + 7 x 256 (32-aligned). + // Any other (m, n, k) - including the paired M=2 shape with the same + // (N, K) = (384, 4096), and every other M=16 (N, K) - falls through to the + // generic scalar fallback (which decodes the (4096,384) pack). if (m == 16 && n == 384 && k == 4096) { - constexpr int64_t kPartialBytes = - static_cast(kSplitK) * kDummaM * 384 * 4; // 368,640 B constexpr int64_t kCounterBytes = static_cast(kCounterInts) * sizeof(int32_t); // 96 B - if (workspace != nullptr && - workspace_bytes >= kPartialBytes + kCounterBytes) { - auto* partials = reinterpret_cast(workspace); - int32_t* counters = partials + kSplitK * kDummaM * 384; - // Zero the 24 arrival counters once before the first launch. Issued on + // Workspace is the per-tile int32 accumulator (24,576 B) followed by the + // 24 arrival counters. The caller's validated workspace is >= 393,216 B + // (16 partial planes), so this guard is strictly more permissive than the + // pre-iteration-7 requirement and the same exact-shape caller keeps the + // split-K path. + constexpr int64_t kScratchBytes = kAccumBytes + kCounterBytes; // 24,672 B + if (workspace != nullptr && workspace_bytes >= kScratchBytes) { + int32_t* accum = reinterpret_cast(workspace); + int32_t* counters = accum + kAccumInts; + // Zero the whole scratch region once before the first launch. Issued on // the caller stream in the eager precheck, so the captured Graph - // contains no memset node; if the first call is captured, the memset - // becomes a replayable Graph node (still exact). The kernel's - // self-reset keeps every later replay deterministic. - static bool counters_zeroed = false; - if (!counters_zeroed) { - counters_zeroed = true; - hipMemsetAsync(counters, 0, kCounterBytes, stream); + // contains no memset node in the normal eager-first flow; the kernel's + // fused tail self-resets every accumulator tile and counter, so every + // later replay restarts from zero. + static bool scratch_zeroed = false; + if (!scratch_zeroed) { + scratch_zeroed = true; + hipMemsetAsync(accum, 0, kScratchBytes, stream); } hipLaunchKernelGGL( w8a8_gemm_m16_dumma_splitk15_lds_staged_partial_kernel, @@ -1094,50 +846,63 @@ extern "C" void launch_w8a8_gemm( stream, a, b, - partials, + accum, x_scale, weight_scale, - reinterpret_cast(out), + out_bf16, counters, n, k); - } else { - hipLaunchKernelGGL(w8a8_gemm_m16_dumma_kernel, dim3(n / kDummaN), - dim3(kDummaBlockThreads), 0, stream, a, b, x_scale, - weight_scale, reinterpret_cast(out), - n, k); + return; } + // Workspace too small: unsplit M=16 DUMMA fallback. + hipLaunchKernelGGL( + w8a8_gemm_m16_dumma_kernel, + dim3(n / kDummaN), + dim3(kDummaBlockThreads), + 0, + stream, + a, + b, + x_scale, + weight_scale, + out_bf16, + n, + k); return; } - constexpr int kBlockThreads = 128; // multiple of the gfx928 wavefront (64) - const int grid = static_cast((total + kBlockThreads - 1) / kBlockThreads); - + const int64_t total = static_cast(m) * n; + const int64_t blocks = + (total + kScalarBlockThreads - 1) / kScalarBlockThreads; + const dim3 grid(static_cast(blocks)); + const dim3 block(static_cast(kScalarBlockThreads)); hipLaunchKernelGGL( w8a8_gemm_scalar_kernel, - dim3(grid), - dim3(kBlockThreads), + grid, + block, 0, stream, a, b, x_scale, weight_scale, - reinterpret_cast(out), + out_bf16, m, n, k); } -// Optional out-of-timed-region weight packing. Iteration 1: identity -// device-to-device copy of raw_weight -> packed_weight and weight_scale -> -// packed_weight_scale, valid for any (K, N) and any unmatched shape (the -// DUMMA kernel reads the raw [K, N] row-major layout). Iteration 17: the -// exact (k,n)==(4096,384) shape is packed per-tile slice-major-chunk -// instead (see w8a8_pack_weight_gateup_slicemajor_kernel: same 16-B chunks -// as iteration 15, chunk order (slice, n, k-chunk) so staged wave-loads are -// 1,024-B contiguous); every other (K, N) keeps the identity copy. Same -// byte count, same buffer, one-time and outside the timed Graph region. +// --------------------------------------------------------------------------- +// launch_pack_w8a8_weight +// +// Optional out-of-timed-region weight packing. Identity device-to-device copy +// of raw_weight -> packed_weight and weight_scale -> packed_weight_scale valid +// for any (K, N); the exact (k,n)==(4096,384) shape is packed per-tile +// DUMMA-fragment-order instead (same byte count, same buffer, one-time and +// outside the timed Graph region). The packed copies are asynchronous on the +// caller stream. +// --------------------------------------------------------------------------- extern "C" void launch_pack_w8a8_weight( const int8_t* raw_weight, const float* weight_scale, @@ -1147,7 +912,7 @@ extern "C" void launch_pack_w8a8_weight( int n, hipStream_t stream) { const int64_t total = static_cast(k) * n; - if (total > 0) { + if (raw_weight != nullptr && packed_weight != nullptr && total > 0) { constexpr int kPackBlock = 256; const int grid = static_cast((total + kPackBlock - 1) / kPackBlock); if (k == 4096 && n == 384) { @@ -1166,7 +931,7 @@ extern "C" void launch_pack_w8a8_weight( total); } } - if (n > 0) { + if (weight_scale != nullptr && packed_weight_scale != nullptr && n > 0) { constexpr int kScaleBlock = 64; const int grid = (n + kScaleBlock - 1) / kScaleBlock; hipLaunchKernelGGL(