Skip to content

Gfx11 - #60

Closed
Annieren wants to merge 85 commits into
masterfrom
gfx11
Closed

Annieren wants to merge 85 commits into
masterfrom
gfx11

Conversation

@Annieren

Copy link
Copy Markdown

MMVDQ change summary

What it adds: a dequantize-to-float matvec path (mmvdq) for K-quant weights
during decode (n=1), as an alternative to the existing mul_mat_vec_q (mmvq)
path.

Why: mmvq quantizes the activation vector to q8_1 first (a quantize_q8_1
pass) so it can do int8 dot products. On RDNA3.5 (gfx1151) int8 has no
throughput advantage over f16, and these matvecs are DRAM-bandwidth-bound on
the weights, so the q8_1 quantize pass is pure overhead. mmvdq dequantizes
the weight blocks straight to float and does the dot in float, skipping the
quantize pass entirely.

Scope (files):

  • ggml/src/ggml-cuda/mmvdq.cu (+579), mmvdq.cuh (+27) — new kernel +
    launchers
  • ggml/src/ggml-cuda/ggml-cuda.cu (+33) — two routing hooks

Kernel design (mmvdq.cu): one warp per output block, 16 threads process one
super-block, activations loaded once (as float4/float2) and reused across the
block. Covers Q4_K, Q5_K, Q6_K. Includes a fused gate+up SwiGLU variant
(mul_mat_vec_dq_glu) that computes silu(gate)*up in one dispatch sharing the
activation load.

Routing / gating (ggml-cuda.cu):

  • Non-fused path in ggml_cuda_mul_mat: triggers when ne11==1, type is
    Q4_K/Q5_K (or Q6_K if enabled), all tensors contiguous, ne02/03/12/13==1, and
    ne[0] % QK_K == 0.
  • Fused SwiGLU path in ggml_cuda_try_fuse: requires ids==nullptr,
    GGML_GLU_OP_SWIGLU, matching gate/up shapes+type, single row.

Enablement (env overrides):

  • GGML_CUDA_DQ_MMV — master switch: unset = arch default (on for RDNA3.5), 0
    = force off, non-zero = force on
  • GGML_CUDA_DQ_Q6K — Q6_K gate, same semantics (Q6_K is bandwidth-bound and
    ~neutral vs mmvq)

Default is on only for RDNA3.5; every other arch keeps mmvq.

Full decode comparison (38 models)

Model Base t/s MMVDQ t/s Δ%
Gemma-4-E2B-IT_Q4_K_M_GGUF_128 95.15 105.09 +10.4%
Gemma-4-E2B-IT_Q4_K_M_GGUF_3968 89.86 98.22 +9.3%
Gemma-2B_Q4_K_M_GGUF_128 106.68 115.00 +7.8%
Janus-Pro-1B_VLM 148.35 158.28 +6.7%
SmolLM2-1.7B-Instruct_Q4_K_M_GGUF_128 143.74 153.18 +6.6%
Qwen3-1.7B_Q4_K_M_GGUF_128 139.69 147.69 +5.7%
Gemma-4-31B-IT_VLM 10.43 10.95 +4.9%
Qwen3-4B_Q4_K_M_GGUF_128 71.07 74.46 +4.8%
Qwen3-VL-4B-Instruct_VLM 66.35 69.31 +4.5%
Gemma-3-4B-IT_VLM 67.67 70.45 +4.1%
Qwen3.5-4B_Q4_K_M_GGUF_128 60.36 62.84 +4.1%
Qwen3-1.7B_Q4_K_M_GGUF_3968 106.89 111.27 +4.1%
Qwen3-4B_Q4_K_M_GGUF_3968 59.31 61.70 +4.0%
Llama-2-7B_Q4_K_M_GGUF_128 46.14 47.84 +3.7%
Qwen2.5-7B-Instruct_Q4_K_M_GGUF_128 44.86 46.41 +3.5%
Janus-Pro-7B_VLM 43.50 44.81 +3.0%
Gemma-3-12B-IT_Q4_K_M_GGUF_VLM 25.73 26.49 +3.0%
Llama-2-7B_Q4_K_M_GGUF_1920 33.86 34.84 +2.9%
Gemma-3-12B-IT_Q4_K_M_GGUF_4096 * 23.71 24.37 +2.8%
Qwen3-8B_Q4_K_M_GGUF_3968 36.27 37.28 +2.8%
Qwen3-30B-A3B-Instruct-2507_Q4_K_M_GGUF_128 74.51 76.35 +2.5%
Gemma-2B_Q4_K_M_GGUF_8000 104.26 106.59 +2.2%
Qwen2.5-3B-Instruct_Q4_K_M_GGUF_128 87.44 89.27 +2.1%
Qwen2.5-3B-Instruct_Q4_K_M_GGUF_3968 82.55 84.09 +1.9%
Qwen3.5-9B_Q4_K_M_GGUF_128 36.51 37.11 +1.6%
SmolLM2-1.7B-Instruct_Q4_K_M_GGUF_8000 36.19 36.77 +1.6%
Qwen3-8B_Q4_K_M_GGUF_128 41.46 42.12 +1.6%
Llama-3.1-8B-Instruct_Q4_K_M_GGUF_128 42.48 43.13 +1.5%
Qwen2.5-VL-3B-Instruct_VLM 84.45 85.56 +1.3%
Qwen2.5-7B-Instruct_Q4_K_M_GGUF_3968 43.26 43.64 +0.9%
MiniCPM-V-2_6_VLM 45.17 45.47 +0.7%
Qwen2.5-VL-7B-Instruct_VLM 44.63 44.90 +0.6%
MiniCPM-o-2_6_VLM 45.21 45.47 +0.6%
Qwen3.5-35B-A3B_Q4_K_M_GGUF_128 * 54.58 54.69 +0.2%
Qwen3.6-27B_Q4_K_M_GGUF_4096 * 12.22 12.24 +0.2%
Qwen2.5-0.5B-Instruct_Q4_K_M_GGUF_128 318.81 318.94 +0.0%
Qwen2.5-0.5B-Instruct_Q4_K_M_GGUF_3968 291.28 290.39 -0.3%
Gemma-4-26B-A4B-IT_VLM 47.30 47.07 -0.5%

Mean +3.09%, median +2.78% (n=38). * = controlled DQ-toggle run (DQ-off/mmvq
vs DQ-on/mmvdq) substituted for the three thermally-throttled full-suite
rows.

yaoliu13 and others added 30 commits June 10, 2026 13:12
Build llama.cpp from this branch's source against the latest TheRock
nightly ROCm tarball for gfx1151/gfx1150 (Ubuntu), then smoke-test each
artifact on the matching self-hosted Strix Halo GPU runner with llama-cli.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
The 7.14.0a20260609+ nightlies regressed libhsa-runtime64 (build
1b2a555677), segfaulting in GpuAgent::InitDma on gfx115x runners.
See ROCm/TheRock#5763.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
The previous gfx1150 runner could not reach huggingface.co (curl 35,
connection reset) during model download. Try a different runner.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
Self-hosted runners (e.g. the gfx1150 box) can't reach huggingface.co
(curl 35, connection reset) but can reach GitHub. Fetch the GGUF from a
fixed release asset (jimw567/llamacpp-test-assets) with retries instead.
Revert gfx1150 runner to the working label (runs-on matches labels, not
machine names, so the prior machine-name value never scheduled).

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
…tput

The binary failed to start on runners lacking libatomic1 (exit 127); bundle
libatomic.so.1 with the artifact so $ORIGIN RPATH resolves it.

Replace stale assertions ("offloaded 29/29 layers to GPU", "</think>") with
functional checks against current output: ROCm GPU selected, all layers +
output offloaded to ROCm0, and tokens actually generated (eval timing).
Verified on a Strix Halo gfx1151 box: passes on real GPU output, fails on
CPU-only/no-generation output.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
The previous assertion only checked GPU offload + that >=1 token was
emitted. Add functional checks that the model produced a non-empty answer
and non-empty reasoning trace, and generated >=20 tokens. Verified against
real gfx1151 output: passes on a genuine response, fails on empty/one-token.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
Switch the llama-cli smoke test to a single-correct-answer prompt
("What is 2 + 2?") with greedy decoding (--temp 0), and assert the
parsed assistant content is 4. This checks the GPU math path end to
end rather than only that some text was generated.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
- Publish a dated GitHub Release (b<YYYYMMDD>) with gfx1151 + gfx1150
  tar.gz binaries, gated to the nightly workflow_dispatch (create_release
  input) so push/PR/manual runs never publish.
- Wrap the llama-cli smoke test in a 180s timeout that dumps GPU/driver
  diagnostics on hang, so a stuck runner fails fast with visible logs.
- Temporarily disable the gfx1150 on-hardware test (its runner hangs in
  GPU inference); gfx1150 binaries are still built and released.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
Mirror the lemonade llamacpp-rocm setup: build the grouped gfx110X
target (gfx1100;gfx1101;gfx1102;gfx1103) from the gfx110X-all ROCm
tarball, covering desktop RDNA3 and the Hawk Point/Phoenix iGPU
(Radeon 760M/780M). Decouple the build matrix into gfx_target /
s3_target / gpu_targets so per-target tarball suffixes and GPU target
lists are explicit. Publish the gfx110X binary in the nightly release.
No on-hardware test leg (no Hawk Point runner).

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
gfx1153 ships as its own standalone TheRock tarball (no bundle suffix),
same as gfx1151/gfx1150. Build+release only — no on-hardware test runner.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
ci: add gfx110X (Hawk Point/Phoenix RDNA3) build + release target
Collapse the 4-leg per-family matrix (gfx1151/gfx1150/gfx1153/gfx110X)
into a single build sourced from TheRock's multi-arch tarball. One fat
binary covers all current CI arches (gfx1100-1103, gfx1150/1151/1153)
and ships as one universal release archive instead of four mostly-
duplicate per-family archives.

The multi-arch tarball is streamed and pruned at the tar level: drop all
.kpack and the Tensile DBs of every non-target arch. The GEMM path
llama.cpp uses works from the per-arch Tensile DB alone (validated on
gfx1151 hardware: rocBLAS sgemm succeeds with ROCM_KPACK_DISABLE=1), so
no .kpack files are bundled. The gfx1151 hardware test job is the
end-to-end safety net.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
The "universal" vs "multi-arch" wording was redundant — both describe one
package covering many arches. Standardize on "multiarch" to match TheRock's
upstream vocabulary. Renames the artifact/archive to
llama-<TAG>-ubuntu-rocm-multiarch-x64 and updates comments + release body.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
Follow-up to the package rename: drop the hyphenated "multi-arch" prose in
comments, echoes, and the release body for one consistent spelling. The
upstream nightlies endpoint (tarball-multi-arch) and the therock-dist-linux-
multiarch- filenames are external names and left untouched.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
ci(gfx11): build one universal multi-arch ROCm package
Sync fork master with upstream llama.cpp (211 commits)
Build llama.cpp from this branch's source against the latest TheRock
nightly ROCm tarball for gfx1151/gfx1150 (Ubuntu), then smoke-test each
artifact on the matching self-hosted Strix Halo GPU runner with llama-cli.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
The 7.14.0a20260609+ nightlies regressed libhsa-runtime64 (build
1b2a555677), segfaulting in GpuAgent::InitDma on gfx115x runners.
See ROCm/TheRock#5763.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
The previous gfx1150 runner could not reach huggingface.co (curl 35,
connection reset) during model download. Try a different runner.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
Self-hosted runners (e.g. the gfx1150 box) can't reach huggingface.co
(curl 35, connection reset) but can reach GitHub. Fetch the GGUF from a
fixed release asset (jimw567/llamacpp-test-assets) with retries instead.
Revert gfx1150 runner to the working label (runs-on matches labels, not
machine names, so the prior machine-name value never scheduled).

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
…tput

The binary failed to start on runners lacking libatomic1 (exit 127); bundle
libatomic.so.1 with the artifact so $ORIGIN RPATH resolves it.

Replace stale assertions ("offloaded 29/29 layers to GPU", "</think>") with
functional checks against current output: ROCm GPU selected, all layers +
output offloaded to ROCm0, and tokens actually generated (eval timing).
Verified on a Strix Halo gfx1151 box: passes on real GPU output, fails on
CPU-only/no-generation output.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
The previous assertion only checked GPU offload + that >=1 token was
emitted. Add functional checks that the model produced a non-empty answer
and non-empty reasoning trace, and generated >=20 tokens. Verified against
real gfx1151 output: passes on a genuine response, fails on empty/one-token.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
Switch the llama-cli smoke test to a single-correct-answer prompt
("What is 2 + 2?") with greedy decoding (--temp 0), and assert the
parsed assistant content is 4. This checks the GPU math path end to
end rather than only that some text was generated.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
- Publish a dated GitHub Release (b<YYYYMMDD>) with gfx1151 + gfx1150
  tar.gz binaries, gated to the nightly workflow_dispatch (create_release
  input) so push/PR/manual runs never publish.
- Wrap the llama-cli smoke test in a 180s timeout that dumps GPU/driver
  diagnostics on hang, so a stuck runner fails fast with visible logs.
- Temporarily disable the gfx1150 on-hardware test (its runner hangs in
  GPU inference); gfx1150 binaries are still built and released.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
jimw567 and others added 27 commits July 13, 2026 13:29
ci(gfx11): install Node.js so llama-ui builds from source
gfx11 saves the cache only on push (infrequent, plus the daily nightly), so
a 1d eviction window prunes still-useful objects between runs. 7d keeps hot
objects across the branch's lower push cadence.

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
The per-op roofline profiler dlopens librocprofiler-sdk.so.1 at runtime
(ggml-cuda-roofline.cpp), but the release only bundled librocprofiler-register.so,
so the profiler silently no-op'd and wrote no artifact. Pack librocprofiler-sdk.so*
plus its one bundle-missing dep librocm_sysdeps_dw.so.1 (libdw, a hard DT_NEEDED of
the sdk lib); the existing $ORIGIN RPATH step lets the dlopen resolve both locally
with no LD_LIBRARY_PATH.

Add a `roofline` workflow_dispatch input (default true, preserving the prior
always-on behavior) gating both -DGGML_HIP_ROOFLINE and the extra lib packing, so
lean builds can opt out.

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
The comment pointed at utils/gather_required_libs.py to regenerate the ROCm lib
copy list, but that script does not exist in the repo; the list is maintained by
hand. Remove the dead reference.

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
ci(gfx11): bundle librocprofiler-sdk for roofline; add roofline input
ci(gfx11): cache HIP builds with ggml-org/ccache-action
Fused rows previously carried a group HBM byte total computed in-tree. Instead,
emit raw per-fused-node facts -- each tensor's byte count, a storage id
(view_src root, so aliasing is visible) and a source tensor id -- and let the
consumer (rocm-scripts tools/roofline) compute external in/out.

This lets the consumer apply the exclusion policy it owns: skip no-op views
(VIEW/RESHAPE), drop in-place destinations (e.g. SET_ROWS/CPY writing their own
dst), exclude internal tensors, and dedup reads by source. It also matches how
non-fused rows already work (producer reports facts, consumer sums).

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
Non-fused rows already emit dst/src byte counts; also emit the dst storage id,
per-source ids and per-source storage ids (all already captured by
fill_head_record). This lets the consumer apply the same in-place-destination
rule to single kernels as to fused ones -- e.g. a standalone CPY or SET_ROWS no
longer has its write target (dst passed in as a source) double-counted.

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
Address review: define "tensor id" (a ggml_tensor's address, dedup key) and
"storage id" (the view_src-root buffer address, shared by aliasing tensors) at
the op_record fields, and drop the vague word "facts" from the surrounding
comments in favour of concrete wording.

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
Address review: the emitted keys and op_record members used cryptic names.
Rename dst_sid -> dst_storage_id, src_ids -> src_tensor_ids, src_sids ->
src_storage_ids (both the C++ members and the JSON keys). No behavior change.

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
… operand swap

A mul_mat of a single-column f32 weight (ne01 == 1) against a wide activation
(ne11 > MMVF_MAX_BATCH_SIZE) misses the mmvf vector kernel because ne11 is the
batch dimension, and falls back to an f32 cuBLAS/rocBLAS GEMM whose 32x32 macro
tile computes just one useful output column. Since the output is a vector, the
operands can be swapped transpose-free: the activation becomes the matrix and the
single-column weight the lone F32 vector, with dst reinterpreted as [ne11, 1].

This runs the op as a proper GEMV. The graph is unchanged, so LoRA and any other
consumer are unaffected. On Qwen3.6-35B-A3B (gfx1151, pp128) the shared-expert
gate (weight f32[2048,1]) drops from ~78 us/instance on rocBLAS Sgemm to ~3.8 us
on mmvf (~20x), ~3.0 ms saved across the 40 layers.

Adds test-backend-ops regression cases (m=1, n sweeping across MMVF_MAX_BATCH_SIZE)
that validate both the direct and swapped paths against the CPU reference.

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
Extend the op_record comment: the storage id alone is not enough. The storage id
(buffer identity) drives the view/in-place/internal exclusions; the tensor id
drives read dedup. They diverge when distinct sources alias one buffer (e.g. two
ops reading the two halves of a tensor: one storage id, two tensor ids) --
deduping by storage id would undercount, so dedup keys on the tensor id.

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
The full build pulls in the llama-ui asset target, which fetches a
prebuilt UI tarball from HF and validates it; that step can fail
independently of the HIP backend this check exists to verify. Build
the two configs with -DLLAMA_BUILD_UI=OFF -DLLAMA_USE_PREBUILT_UI=OFF.

Assisted-by: Claude Opus 4
CUDA: route single-column f32 mul_mat through mmvf (transpose-free operand swap)
fix: ci(hip): drop web UI provisioning from hip-quality-check
roofline: emit raw per-tensor data for DDR byte accounting (single + fused)
MUL_MAT_ID reads only the experts a launch actually routes, but the byte model
had no way to know that count and the consumer fell back to the shape bound
min(M*top_k, E) -- which saturates to all experts and over-counts HBM traffic
when routing leaves experts idle, pushing memory-bound MoE kernels past 100% of
peak (the working set dwarfs L2, so it is not cache reuse).

Read the routing ids tensor back per invocation and emit the distinct
active-expert count as experts_used. begin_op takes the op's stream and
hipStreamSynchronize's it so the ids are final before the copy; the read-back is
wrapped in a sentinel correlation id (0) so any copy kernel it lowers to is
attributed to no op and skipped in the total. Device-timestamp durations are
unaffected, so the report's figures are unchanged apart from the new field.
roofline: capture per-launch MoE active-expert count (experts_used)
Two correctness fixes for the GGML_CUDA_GRAPH_OPT fork/join concurrency so that
work assigned to a non-default stream actually runs there:

- ggml_cuda_op_mul_mat (the legacy tiled dispatcher) hardcoded ctx.stream(id, 0)
  for the src0 setup and ctx.stream(id, is) for the per-column compute loop. For a
  single GPU both resolve to stream 0 regardless of curr_stream_no, so any op on
  this path ran on the main stream while its aux-stream consumers only waited the
  fork event and read stale data. Use ctx.stream() for the non-split case; the
  multi-GPU split path keeps its per-device stream 0 for peer synchronization. This
  is a no-op when concurrency is off (curr_stream_no == 0).

- The cuBLAS handle was shared across streams (one per device). The handle carries
  a workspace that concurrent GEMMs on different streams corrupt, so give each
  (device, stream) its own handle.

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
The attention QKV concurrency (GGML_CUDA_GRAPH_OPT) produced incorrect results
because it relied on interleaving the branch nodes in the graph so that ggml-alloc
would keep them non-overlapping. ggml-alloc sees the interleaved order while the
executor restores the original order, and is_valid() only checks the branches
against each other, not against tensors read across the region (e.g. the fork
output every branch consumes concurrently). ggml-alloc could therefore recycle
such a tensor's memory for a branch scratch, corrupting a concurrent read.

Replace the interleave with a dedicated compute buffer for the concurrent branch
nodes. Their data/buffer is pre-assigned into one CUDA buffer, reused across layers
(which run sequentially) with distinct per-node offsets within a region, so the
branches are structurally disjoint from each other and from the rest of the graph.
is_valid() then accepts the event without any graph reordering.

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
Add a shared-expert detector to ggml_backend_cuda_graph_optimize (after the
attention pass, under GGML_CUDA_GRAPH_OPT). It matches the diamond
join = ggml_add(ffn_moe_out, ffn_shexp*) by callback names and finds the FFN-input
norm that forks into both branches.

Following vLLM's shared-expert model, the routed experts stay on the main stream
and only the shared expert forks onto a single aux stream, joined at the add.
Keeping the large routed branch on the main stream (region nodes not in the
stream map default to the main stream) avoids migrating it and needs just one
fork/join. The shared-expert branch reuses the concurrent-branch scratch buffer
so it is disjoint from the routed branch without any graph reordering.

Gated to decode (single token). The overlap only helps when the routed branch
leaves the GPU underutilized for the shared expert to run alongside it: that
holds in decode (batch 1, latency/occupancy-bound) but not in prefill, where the
routed matmuls already saturate the GPU and the overlap only adds contention.
Output is byte-identical to sequential. Requires GGML_CUDA_GRAPH_OPT, CUDA graphs
and a single GPU, so it is off by default.

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
ggml-cuda: overlap the MoE shared expert on a separate stream
Materialize the transposed qkv_mixed before ggml_concat so the conv-state
prepend takes the coalesced concat_cont path instead of concat_non_cont,
which scaled super-linearly with chunk size (CONCAT ~1166ms -> ~34ms at
ub=4096, -899ms total prefill).
Delta-net fed ggml_ssm_conv a transposed (time-major) conv input, forcing a
physical transpose. Option A removed the quadratic concat but left a 221ms
CONT (transpose) kernel at ub=4096.

Add an opt-in channels-major layout to ggml_ssm_conv (op_params[0]=1):
input is [d_inner, d_conv-1+n_t, n_s] (channels contiguous); output layout
unchanged. Implemented on CPU and CUDA (both short and long-token kernels,
coalesced smem load); other backends' supports_op reject mode!=0 so the
scheduler falls back to CPU. build_conv_state now keeps qkv_mixed channels-
major, prepends the recurrent conv state along the time axis (concat dim 1),
and calls ggml_ssm_conv_channels_major (qwen35/qwen35moe/qwen3next).

Prefill pp4096 ub=4096: CONT 221->10ms, total 2925->2717ms (-1108ms vs the
pre-optimization baseline). Validated: test-backend-ops SSM_CONV channels-
major cases pass (CUDA vs CPU); Qwen3.6 perplexity 3.40 (healthy).
ssm_conv: channels-major input mode to drop the delta-net transpose
…oE launches

count_active_experts already reads the MUL_MAT_ID ids tensor back to host once
per invocation to count distinct routed experts; the same pass now also fills a
per-expert token-count array. begin_op stores it per invocation and write_report
emits it as "tokens_per_expert" (length n_experts, index e = routings to expert e,
Σ = n_tokens*top_k) alongside experts_used.

No new device work or sync — the ids read-back already happens for experts_used.
The consumer renders it as the MoE routing-load distribution under --show-histograms.
roofline: emit per-expert routing histogram (tokens_per_expert) for MoE launches
@Annieren
Annieren requested a review from jimw567 July 20, 2026 05:18
@Annieren

Copy link
Copy Markdown
Author

Wrong base branch.

@Annieren Annieren closed this Jul 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants