You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Follow-up to #397 (Part C). #398, #399 and #400 are merged; this is the remaining piece.
Update: the unfused R4 decode approach has been measured and rejected (+16.56% on decode, 3/9 paired wins) — the dispatch fusion is worth ~17%. A separate finding means byte-identical output is not achievable by any decode-side approach. Revised acceptance criteria and the llama.cpp comparison are in this comment, which supersedes the criteria below.
The approaches below were originally numbered "option 1/2/3"; that earlier comment still uses those numbers. The mapping is: option 1 = R4 fused decode, option 2 = unfused R4 decode, option 3 = partial retention.
Goal
Stop holding Q8_0 linear weights in two layouts at once. Today every repackable weight exists as both the memory-mapped row-major original and an R4-interleaved copy in committed memory, roughly doubling peak footprint.
Measured on bartowski/Llama-3.2-1B-Instruct-GGUF Q8_0 (1,260 MiB file), sampling the process every 25 ms:
reported Weights (mapped)
1,252 MiB
reported Repacked (R4, committed)
1,252 MiB
OS peak private
1,414 MiB
OS peak working set
2,690 MiB
Dropping the row-major copy for Q8_0 would take peak working set to roughly 1.4 GB — about half.
#400 routed Q8_0 prefill through GemmR4TiledQ8_0, so multi-token work no longer touches row-major. That was the prerequisite for this issue, but it did not finish the job.
Auditing every consumer of the row-major pointers in TransformerModel:
So 5 of 7 projections per layer still read row-major on every decode step. The fused decode kernels exist to batch three (or two) GEMVs into a single pool.Dispatch(), saving dispatch overhead per layer — and they take row-major pointers (MatMulFusedDecode.cs, e.g. case QuantizationType.Q8_0: GemvQ8_0(weights, ...)).
The row-major copy therefore cannot be dropped today. Removing it would break decode.
Candidate approaches
R4 fused decode — the target
Port FusedDecodeGemv3/FusedDecodeGemv2 to consume the interleaved layout, dispatching to ComputeRowsQ8_0Interleaved per projection inside one pool dispatch. Keeps the fusion benefit and frees row-major. Most work, best outcome.
Unfused R4 decode — measured, rejected
Route Q/K/V/Gate/Up decode through the existing GemmInterleaved n=1 path, which is already R4, giving up the fused dispatch. Much smaller change. Measured at +16.56% on decode (3/9 paired wins) — not viable. Its value was diagnostic: it established that the dispatch batching is worth ~17%, which is the bar the R4 fused decode has to clear.
Partial retention — not recommended
Keep row-major only for the tensors the fused path needs and drop it for the rest. Saves little and complicates the loader; listed for completeness.
Also still holding row-major
Independent of decode, these keep row-major alive regardless:
Rows below InterleavedMinRowBytes (1024) fall back in both GemmInterleaved and GemvInterleaved. For Q8_0 that is roughly K < 964.
token_embd.weight is deliberately excluded from repacking (random row access) and should stay mapped. For this model it is ~266 MiB and a 611-token prompt touches at most 611 of 128,256 rows.
Note that for tied-embedding models (TransformerWeights.cs, LM head aliases token_embd.weight) that tensor is currently resident in both layouts.
Acceptance criteria
For a Q8_0 model where every linear weight is repackable and above the row-stride threshold, no forward-pass path reads the row-major linear weights.
The repacked buffers are built without requiring the row-major copy to stay resident afterwards — i.e. the mapping may be released or left cold for those tensors.
Peak working set for Llama-3.2-1B-Instruct Q8_0 drops materially against the 2,690 MiB baseline above; before/after recorded in the PR using the same sampling method.
Decode tok/s does not regress — must be measured, paired and interleaved, on both .NET 10 and .NET 11. The unfused R4 decode approach failed this by +16.56%.
Byte-identical generated output for a fixed-seed greedy run.Disproven — not achievable by any decode-side option, because the two layouts require kernels with different accumulation order and decode is autoregressive. See the linked comment for the replacement criterion.
Existing kernel tests pass; new coverage for whichever decode path is introduced.
Follow-up to #397 (Part C). #398, #399 and #400 are merged; this is the remaining piece.
Goal
Stop holding Q8_0 linear weights in two layouts at once. Today every repackable weight exists as both the memory-mapped row-major original and an R4-interleaved copy in committed memory, roughly doubling peak footprint.
Measured on
bartowski/Llama-3.2-1B-Instruct-GGUFQ8_0 (1,260 MiB file), sampling the process every 25 ms:Dropping the row-major copy for Q8_0 would take peak working set to roughly 1.4 GB — about half.
Where things stand after #400
#400 routed Q8_0 prefill through
GemmR4TiledQ8_0, so multi-token work no longer touches row-major. That was the prerequisite for this issue, but it did not finish the job.Auditing every consumer of the row-major pointers in
TransformerModel:FusedQkvDecode->MatMul.FusedDecodeGemv3FusedGateUpDecode->MatMul.FusedDecodeGemv2GemmInterleaved, n=1)So 5 of 7 projections per layer still read row-major on every decode step. The fused decode kernels exist to batch three (or two) GEMVs into a single
pool.Dispatch(), saving dispatch overhead per layer — and they take row-major pointers (MatMulFusedDecode.cs, e.g.case QuantizationType.Q8_0: GemvQ8_0(weights, ...)).The row-major copy therefore cannot be dropped today. Removing it would break decode.
Candidate approaches
R4 fused decode — the target
Port
FusedDecodeGemv3/FusedDecodeGemv2to consume the interleaved layout, dispatching toComputeRowsQ8_0Interleavedper projection inside one pool dispatch. Keeps the fusion benefit and frees row-major. Most work, best outcome.Unfused R4 decode — measured, rejected
Route Q/K/V/Gate/Up decode through the existing
GemmInterleavedn=1 path, which is already R4, giving up the fused dispatch. Much smaller change. Measured at +16.56% on decode (3/9 paired wins) — not viable. Its value was diagnostic: it established that the dispatch batching is worth ~17%, which is the bar the R4 fused decode has to clear.Partial retention — not recommended
Keep row-major only for the tensors the fused path needs and drop it for the rest. Saves little and complicates the loader; listed for completeness.
Also still holding row-major
Independent of decode, these keep row-major alive regardless:
InterleavedMinRowBytes(1024) fall back in bothGemmInterleavedandGemvInterleaved. For Q8_0 that is roughly K < 964.GemmR4TiledQ8_0is Q8_0-only, and the K-quant interleaved wrappers were a measured regression in Step 26 (Step 26: Outer-product tiled matmul — kernels + investigation (blocked on AVX2) #61). Porting the perf(cpu): route Q8_0 prefill through an L2-tiled R4 GEMM #400 tiling toComputeRowsQ4_KInterleavedand friends is the equivalent follow-up for those formats and would benefit Q4_K_M models, which is what most users run.token_embd.weightis deliberately excluded from repacking (random row access) and should stay mapped. For this model it is ~266 MiB and a 611-token prompt touches at most 611 of 128,256 rows.Note that for tied-embedding models (
TransformerWeights.cs, LM head aliasestoken_embd.weight) that tensor is currently resident in both layouts.Acceptance criteria
Llama-3.2-1B-InstructQ8_0 drops materially against the 2,690 MiB baseline above; before/after recorded in the PR using the same sampling method.dotllm run --jsonmemory reporting (added in fix(cli): count R4 repacked buffers in reported memory #398) stays accurate after the change.Byte-identical generated output for a fixed-seed greedy run.Disproven — not achievable by any decode-side option, because the two layouts require kernels with different accumulation order and decode is autoregressive. See the linked comment for the replacement criterion.References
src/DotLLM.Models/Architectures/TransformerModel.cs—FusedQkvDecode,FusedGateUpDecode,GemmInterleaved,GemvInterleaved,InterleavedMinRowBytessrc/DotLLM.Cpu/Kernels/MatMulFusedDecode.cs—FusedDecodeGemv3,FusedDecodeGemv2src/DotLLM.Cpu/Kernels/MatMul.cs—GemmR4TiledQ8_0(perf(cpu): route Q8_0 prefill through an L2-tiled R4 GEMM #400),ComputeRowsQ8_0Interleavedsrc/DotLLM.Models/Architectures/TransformerWeights.cs—RepackWeights,RepackedBytes