Repository navigation
hrx: run a batched dense MUL_MAT as one dispatch per plane (engine #991) - #207
Conversation
…ml-org#991) A MUL_MAT whose activation carries batch planes against an unbatched weight is the same 2-D contraction repeated per plane, and it is how the SSM/Mamba ports write their small projections. Every format-agnostic matcher refused it in common_match_mul_mat_any_format on common_is_2d(*input), before any size rule, so those nodes fell back to the CPU and split the graph. This is not a kernel gap: the two kernels involved (ggml_mul_mat_f32_f32_wmma for prefill, ..._decode_wave64 for token 1) already compute a 2-D tile from a buffer base, so the planes are bound one dispatch at a time. dispatch-gated-delta-net.cpp already binds { id, t * nb[2], byte_count - t * nb[2] } per token, which is the same mechanism. - common_match_mul_mat_any_format takes allow_batched: the activation and output may carry planes when the output's planes match the activation's. ggml pairs the operands' planes when both carry them and broadcasts when only the activation does, so the weight may be one matrix shared by every plane or one matrix per plane (with the same (dim2, dim3) factorisation, since pairing follows that index order). plane_count and batched_weight ride on the match. - common_plane_offset maps a plane index to its byte offset in a contiguous value. - build_batched_mul_mat_dispatch emits one dispatch per plane: activation, weight and output bindings offset to the plane, the token count is the per-plane one. - Two registrations, common.mul_mat.f32_f32_wmma_batched (82) and ..._decode_batched (62), one above each plain dense registration they mirror. The K contract is unchanged: still the plain element-wise predicate, so K % 32 != 0 stays on the CPU until the padded tile lands. Test cases are the shapes the port actually emits (m=264,n=8,k=3712 and m=464,n=8,k=256 with a shared weight; m=264,n=8/1,k=3712 with a BF16 matrix per plane; plus the unaligned k=232 rows that must stay refused).
|
#207 ready for review at 1f27684 Engine ggml-org#991, the batched half. rank>2 activations were refused by Measured on one binary, two arms (
|
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 1f276843 on the code — the broadcast case is handled where it would have gone wrong, and the acceptance gate refuses the combinations it cannot express. No device run from me.
Read the two places that can be wrong. The acceptance is precise: output planes must equal input planes, and the weight must be either a single matrix (broadcast) or one matrix per plane (paired) —
const int64_t plane_count = input->ne[2] * input->ne[3];
const int64_t weight_plane_count = weight->ne[2] * weight->ne[3];
… weight_plane_count == 1 || weight_plane_count == plane_count …
match.plane_count = allow_batched ? plane_count : 1;
match.batched_weight = allow_batched && paired_weight;and the emission gets the one case that silently corrupts results right:
const size_t input_offset = common_plane_offset(*match.input, plane);
const size_t output_offset = common_plane_offset(*match.output, plane);
const size_t weight_offset = match.batched_weight ? common_plane_offset(*match.weight, plane) : 0;A broadcast weight keeping offset 0 on every plane is exactly the thing a per-plane loop gets wrong first, and plane_count is ne[2]*ne[3] with ggml's absent dims as 1, so a plain 2-D matmul takes the same path with plane_count == 1 rather than needing a separate branch. common_plane_offset decomposes the plane as ne[2]-major with nb[2]/nb[3] strides, which matches ggml's row-major batch enumeration.
The new test-backend-ops cases are the right instrument — the batched shapes are where this is either right or obviously wrong.
Not verified by me: the device. Running the matrix needs an engine build against this pin on the HRX route, and the GPU queue has been 8–9 deep all night; PR ggml-org#1104 (the bump that would carry this into an engine build) is still open. If you want the row, the moment is right after ggml-org#1104 merges with a hw-gate label.
hrx: run a batched dense MUL_MAT as one dispatch per plane (engine ggml-org#991)
The gap
A
MUL_MATwhose operands carry batch planes —ne[2] * ne[3] > 1— is the same 2-D contraction repeated per plane, andit is how the SSM/Mamba ports write their small projections (
Zamba v1'sx_projisw bf16 ne=[3712,264,2,1]againstin f32 ne=[3712,512,2,1]). Every format-agnostic matcher refused it incommon_match_mul_mat_any_formatoncommon_is_2d(*input), before any size rule, so those nodes went to the CPU and took theirCONTconsumers with them.This is not a kernel gap:
ggml_mul_mat_f32_f32_wmma(prefill) andggml_mul_mat_f32_f32_decode_wave64(token 1) alreadycompute a 2-D tile from a buffer base, so the planes can simply be bound one dispatch at a time — the mechanism
dispatch-gated-delta-net.cppalready uses per token ({ id, t * nb[2], byte_count - t * nb[2] }).What changes
common_match_mul_mat_any_formattakesallow_batched: the activation and output may carry planes when the output'splanes match the activation's. ggml pairs the operands' planes when both carry them and broadcasts when only the
activation does, so the weight may be one matrix shared by every plane or one matrix per plane — and in the
paired case the factorisation must match too (
ne[2]/ne[3]equal), because pairing follows that index order, not theplane product.
common_plane_offsetmaps a plane index to its byte offset in a contiguous value.build_batched_mul_mat_dispatchemits one dispatch per plane: activation, weight and output bindings offset to thatplane, the token count is the per-plane one. The weight offset is zero unless the weight is per-plane.
common.mul_mat.f32_f32_wmma_batched(82) andcommon.mul_mat.f32_f32_decode_batched(62) — oneabove each plain dense registration they mirror.
The K contract is deliberately unchanged: still the plain element-wise predicate, so
K % 32 != 0stays on the CPU (see"what this does not do" below).
Evidence
One binary (
1f2768431), two arms. The "off" arm setsGGML_HRX_DISABLE_DISPATCH=batched, which matches exactly the tworegistrations added here, so before/after differ in the dispatch decision only.
test-backend-ops -b HRX0 -o MUL_MAT—OKmeans HRX matched the CPU reference:The five new real-shape cases (the sixth is the boundary below):
f32/f32 m=264 n=8 k=3712 nr=[2,1](shared weight)f32/f32 m=464 n=8 k=256 nr=[16,1](shared weight, d_state planes)f32/f32 m=264 n=1 k=3712 nr=[2,1](token 1 → decode registration)bf16/f32 m=264 n=8 k=3712 bs=[2,1](one matrix per plane, the model's shape)bf16/f32 m=264 n=1 k=3712 bs=[2,1](per-plane weight, decode)bf16/f32 m=3712 n=2 k=232 bs=[2,1](unaligned contraction)The rest of the +153 are existing upstream batched cases (
nr>1, andbs>1with an F32 activation), so this admits theclass generally rather than only the shapes I added.
Zamba-7B-v1 (BF16), placement and speed,
gpu-hold,llama-bench -v -p 512 -n 64 -r 5:MUL_MATthat moved are exactly theK=3712 → o=264family, and all 76CONTnodes were their consumers (CONT 76 → 0).GGML_HRX_LOG_DISPATCH=1lists both new registrations as matched.remaining 76
MUL_MAT+ 13FLASH_ATTN_EXT, so "fewer splits" is not a benefit of this change.(on: 20.0715, 20.0718; off: 20.0688, 20.0688), i.e. a real but tiny f32-vs-device accumulation shift. The per-shape
evidence is the
test-backend-opstable, not the PPL.What this does not do
The remaining 76 nodes are
dt_proj,w ne=[232,3712,2,1], i.e.K % 32 == 8: they are refused by the plain K predicatethat this PR intentionally leaves alone. That is the padded-tile item (
K % 32 != 0/K < 256), and until it landsGGML_HRX_STRICT=1still aborts on this model — so no strict-clean claim is made here.Notes for the reviewer
nr>1withbs=1. The modelpairs the planes (
bs>1), so the first version passedtest-backend-opsand moved nothing on the model (241 → 241).A test that proves a shape nothing emits is not a test; both shapes are covered now, which is why the table above has a
shared-weight and a per-plane block.
test-backend-opsrows above are the correctness evidence.