ggml-cpu: ternary I2_S matmul kernel (VibeASR, 3/4) - #445
Closed
XsquirrelC wants to merge 8 commits into
Closed
Conversation
This was referenced Sep 4, 2026
Contributor
Author
|
Closing in favour of #447. You asked for two PRs in microsoft/VibeASR.cpp#10 — one for the additive ggml changes, one for the model integration — so this four-PR stack has been reorganized into exactly that: #447 (ggml) and #448 (model). Same code, no functional change; sorry for the churn. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #440 (which is itself stacked on #438). This branch contains both of those plus one commit; only
ggml-cpu: ternary I2_S matmul kernel(69de525) is new here.Adds the CPU matmul for
GGML_TYPE_I2_S, the BitNet-style ternary weight type that VibeASR.cpp runs its Qwen2 decoder on. #438 registered the type, its packing, and its (de)quantizers; this is the kernel that makes a ternary weight usable in a graph. The decoder graph, loader, and session are the next PR — this one is numeric only.Interface
Plain
ggml_mul_matwith an I2_Ssrc0and an F32src1, F32 out. No new op, no new graph-building call, nothing for a model to opt into.ggml_compute_forward_mul_matbranches to a dedicated function whensrc0->type == GGML_TYPE_I2_S. It cannot use thevec_dot_typepath: that contract quantizessrc1one row at a time into a fixedrow_sizeand hands the kernel two row pointers, while I2_S needs the per-row activation scale and the per-row int8 sum to survive into the epilogue. Those live in a sidecar after the quantized rows inparams->wdata.The branch is 8 lines in
ggml_compute_forward_mul_matand 10 inggml_graph_plan'sGGML_OP_MUL_MATcase (work size); everything else is new code inops.cpp/vec.cpp. For reference, upstream's own fork does this by patching the body of the genericggml_compute_forward_mul_matinline — including asrc1_col_de = wdata + i11*nb11/4pointer reinterpretation — which is what this avoids.Arithmetic
I2_S stores the ternary values as the codes
{0, 1, 2}, not as{-1, 0, +1}. That is deliberate: unsigned codes let the integer multiply-add instructions (vpmaddubsw,vdot) be used directly on the packed nibbles with no sign extraction. The cost is a bias, which comes out algebraically:so the kernel returns
sum(code*q)and the epilogue subtracts the row's int8 sum. Both scales are multipliers (amax/127for activations, the weight absmax for I2_S), so they combine once at the end:Everything up to that last multiply is int32, so the unit test compares bit-exactly against a plain-loop reference rather than with a tolerance — a wrong packing that happens to be numerically close would otherwise pass.
Kernels
ggml_vec_dot_i2_i8invec.cpp, alongsideggml_vec_dot_i8_i8from #438. The packing (128 values per 32-byte group; bytegpholds group-relative positionsgp,32+gp,64+gp,96+gpin bit pairs 6/4/2/0) is chosen so the four code lanes of a group line up with four consecutive 32-value slices of the activation row — no shuffling on either side.vpmaddubswagainst four 32-byte activation loads. Accumulation widens to int32 every eight groups, not at the end of the row: a lane holds a sum of twocode*int8products, at most2*2*127 = 508, and eight groups contribute 32 of them,32*508 = 16256— inside int16 with room, but 16 groups would not be. Upstream accumulates 32 groups in int16, which can overflow on activations near full scale;test_accumulator_headroomdrives exactly that case (every code 2, every activation +127).vdotq_s32when__ARM_FEATURE_DOTPRODis available, otherwisevmull_s8+vpadalq_s16. Half a group per iteration since the register is 16 bytes wide.Threading: activation rows are quantized striped across threads (a row-wide absmax cannot be split), one barrier, then the output features are split. Sweeping the whole batch inside the output-feature loop means a weight row is streamed once and reused across every column, which matters because this op is bandwidth-bound on the weights, not on the activations.
Tests
tests/unittests/test_i2_s_mul_mat.cpp, registered in ctest asi2_s_mul_mat_test. Every case runs atnth=1andnth=4, since a missing barrier or an overlapping output split shows up as a thread-count-dependent result.test_pack_layout— decodes the packing by hand (not viaggml_i2_s_to_float) so the layout the kernel reads is pinned independently of the dequantizer written alongside it, and checksnb[1] == K/4.test_mul_mat—K= 128 (one group), 1024 (exactly the eight groups the AVX2 body accumulates in int16), 1152 (that plus a shorter second flush);N= 1 (decode-timelm_headshape), 64 (one output-channel chunk), 100 (two), 70 (unaligned), 2 (fewer output features than threads, so tail threads get an empty range).test_mul_mat_batched— 3Dsrc1, so the row indexing walksnb12and the output walksnb2.test_accumulator_headroom— the int16 worst case described above.test_degenerate_scales— all-zero weights (scale 0) and a zeroed activation row, neither of which may produce a NaN. This is the guard on the multiplier convention: a reciprocal convention divides by zero in both cases.Build
Release, gcc, x86-64 AVX2, 24-core: full
ctest69/69 pass, 0 failures (3 skipped for missing model assets, as before).i2_s_mul_mat_testpasses at both thread counts. No behaviour change for any existing type — the new branch is reached only whensrc0->type == GGML_TYPE_I2_S, and the work-size addition is inside the sameif.