Repository navigation
npu: LFM2's gated short convolution as host arithmetic, with known answers — first slice of serving the lfm2 packs (Refs #1786) - #2161
Conversation
…swers (Refs #1786) npu/forward/include/lfm2_shortconv.h: the block between an LFM2 conv layer's two projections (B*X, a causal depthwise convolution over the last L-1 values, gated by C), with its per-layer state, defined once for decode, batched prefill and state snapshots. Not wired into the forward yet; this is the reference the wiring will be checked against. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
|
ready for review at 7eb633c |
PR Reviewer Guide 🔍Here are some key observations to aid the review process:
|
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 7eb633c: READY
Reviewed head 7eb633c (refs #1786). This is the first verdict.
The definition matches the fork's graph. I read src/models/lfm2.cpp's build_shortconv_block at the engine's pin (204493d40) next to lfm2_shortconv.h:
bcxis viewed as B | C | X (chunk offsets 0, 1, 2), and the header readsB = bcx,C = bcx + H,X = bcx + 2H.bx = B ⊙ X.- In the causal case the state is concatenated before
bx,ggml_ssm_convruns over[state | bx](so the present token meets the last tap,k[L-1] * bx), and the new state is the lastd_convcolumns. The header's oldest-first history with the newest atL-2is the same thing. y = C ⊙ conv, beforeout_proj.- The per-channel kernel
taps[h·L + j]matches ggml's conv weight (L taps contiguous per channel).
The fork also has a non-causal branch (symmetric padding) that the header does not implement. That is fine for LFM2's causal text models, and worth one line in the header so nobody reuses it for a bidirectional encoder.
Checked
git merge-treeonto main: no conflicts.lfm2_shortconv_test, built from this head: "lfm2_shortconv: ok".- It is a build dependency of
onebit(CMakeLists.txt:1382), and CI runs it:#183: lfm2_shortconv … Passed, 231/231. CIbuild,hrx_buildand CodeQL green. - Mutants of the header, beyond the body's two, each rebuilt and run:
| mutant | result |
|---|---|
| B and C swapped | rc 1, 21 failing checks |
the newest B*X written to the oldest history slot |
rc 1, 12 failing checks |
| the state never advanced | rc 1, 17 failing checks |
I restored the file (git status clean).
Nothing in the forward changes, so #2158's load refusal stays the behaviour, as the body says.
Refs #1786 (backlog). First, device-free slice of serving the
lfm2family; the design note for the whole thing is on the issue. Nothing in the forward changes: #2158's refusal stays the behaviour until the rest lands.What it adds
npu/forward/include/lfm2_shortconv.h: the arithmetic between an LFM2 conv layer's two projections, and the state it carries.ShortConvState:(L-1) * Hfloats per conv layer, zeros at position 0.shortconv_step(): one token.shortconv_rows(): M tokens, defined as M steps in order, so a batched prefill cannot disagree with decode.The definition is the graph our own llama.cpp fork builds for this family (
src/models/lfm2.cpp,build_shortconv_block): chunk order B, C, X; the present token meets the last tap; the gate multiplies the convolution's output. No vendor code was read for it.Why a header before the forward
Three places will need this (token-by-token decode, batched prefill, and the state snapshot the prefix cache takes), and the failure it guards against is the quiet one: a prefill that advances the state differently from decode is wrong at every later position and looks like a weak model. Writing it once, with the equalities asserted, is cheaper than finding that on the device.
Tests
tests/lfm2_shortconv_test.cpp(ctestlfm2_shortconv, a build dependency ofonebitso CI builds it), no model and no device, for kernel lengths 1 to 4, H 37, 23 tokens with zeros and both signs:B*X, oldest first;Cgates the result;lfm2_shortconv: ok. Two mutants from a copy of the header: the present token on the first tap fails 15 checks; the gate dropped fails 18.tools/copyright.py --checkrc 0;ci_targets_check.py --checkbaseline 9, none new.What this does not show
That the definition matches the weights in FastFlowLM's packs. It matches our fork's graph; whether the container's
shortconv.conv.weightis[H][L]with the last tap on the present token is settled only by running the model and comparing its text with FastFlowLM's runtime on the same file, which is step (c) of the note on the issue.🤖 Generated with Claude Code