Exp 2026 07 13 Linear Attention Scans on NPU - oRKLLM/ork-driver GitHub Wiki

Exp 2026-07-13 โ€” Linear-Attention Scans on the NPU (Mamba-2 SSD โœ… at scale, Gated-DeltaNet โŒ)

Porting the two big linear-attention scan families to the RK3588 NPU as chunked matmul + CPU-elementwise: Mamba-2 / SSD (GGML_OP_SSM_SCAN) and Gated DeltaNet (GGML_OP_GATED_DELTA_NET; Qwen3-Next / Qwen3.5 / Ornith-1.0-9B). Both were built end-to-end, wired into the ggml-ork backend, and validated coherent on silicon. SSM wins ~2ร— at scale; GDN loses โ€” and the why is a clean architectural result.

Knobs: ORK_SSM_NPU (size-gated default-on), ORK_SSM_KEEPWARM (default on), ORK_SSM_CHAIN (default on), ORK_SSM_CS (default 128), ORK_GDN_NPU (opt-in, off), ORK_SSM_PROF / ORK_GDN_PROF (per-stage). ork-driver main @ 6ecb168 (perf defaults); GDN parked on feat/gdn @ 3ce735e.


1. Design โ€” chunked scan as matmul + CPU elementwise

Both scans use the standard chunked (CS=64) reformulation so the heavy contractions become NPU matmuls, with the sequential inter-chunk carry + gate/decay elementwise on CPU. The kernels mirror each other:

  • SSM (ork_ssm_scan_f32): 4 matmul stages/chunk (pS gram CยทBแต€, pD intra-chunk output, pC chunk-state, pO inter-chunk) on a pooled fused-multicore fp16 stream; softplus/cumsum/decay on CPU.
  • GDN (ork_gdn_scan_f32): 6 matmul stages/chunk (Sk/Sq state-reads, KK/KQ grams, O_intra, Sdelta) plus a CPU forward-substitution UT-transform โ€” the delta rule's (I+A)โปยน, the one non-matmul piece.

Both are validated by pure-CPU references (test_ssd_chunk, test_gdn_chunk) that prove the chunked form is bit-exact to the definitional sequential recurrence (~1e-11) at the real d=128/CS=64, then NPU variants (test_ssd_chunk_npu, test_gdn_chunk_npu) confirm on-hardware coherence within fp16 tolerance.


2. The general unlock โ€” keep-warm (int8โ†”fp16 no-thrash) โœ… DEFAULT ON

Both scans interleave fp16 scan matmuls between the int8 projection matmuls, and the NPU regcmd mode is stateful: each transition was doing an ACT_RESET + re-warm + scratch-realloc per layer. That churn dominated (28 "soft resets" / prefill; matmul cost tripled). The fix โ€” extend the existing ORK_MIXED_NOTHRASH keep-warm to the int8/fp16 family (ORK_KW_DT = I8_LIVE || F16) at every transition site โ€” is safe because the regcmd is re-synth'd every call regardless, and the reset was a conservative software choice, not a HW law (NVDLA/rocket reset only at init).

Result: on-NPU SSM prefill 20.7 โ†’ 103.5 tok/s (5ร—), soft-resets 28 โ†’ 1, byte-coherent. This is a general win for any fp16 op interleaved with int8 matmuls (scan, softmax, chained attention). Now default-on (ORK_SSM_KEEPWARM=0 to disable).


3. Mamba-2 / SSD โ€” โœ… crossover is REAL, ~2ร— at Q8 2.7B

Earlier 130M-scale work concluded "no crossover, param count isn't the lever." That was a small-scale + Q5_K dilution artifact. The clean picture, NPU-scan vs CPU-scan (projections int8-on-NPU both sides), warm:

model quant NPU-scan CPU-scan ratio
mamba2-130m f16 103.5 147.6 0.70ร— (loses)
mamba2-2.7b Q5_K_M 9.06 10.05 0.90ร— (diluted โ€” Q5_K projections dequant-bound both sides)
mamba2-2.7b Q8_0 9.72 4.37 2.22ร— (WINS) โ€” P=128; 2.02ร— at P=512

Warm P=128 (Q8 2.7B): 29.65 vs 6.51 tok/s = 4.55ร— (removing the one-time weight-pack un-dilutes the ratio).

Why it crosses over: 130Mโ†’2.7B, the CPU scan degrades ~33ร— (Mamba-2's per-token recurrence is memory-bound and craters under cache pressure) while the NPU degrades only ~11ร— (it amortizes the submit floor across the growing count of fixed-size 64ร—128ร—64 matmuls via chaining). Output byte-identical NPU vs CPU. Enablers all default-on: keep-warm + fused-multicore fp16 stream (ORK_SSM_CHAIN) + CS=128 + persistent pool.

Size-gated default (ORK_SSM_NPU unset = auto): on-NPU only when the model is large enough to win โ€” n_head โ‰ฅ 64 (โ‰ˆ โ‰ฅ1.5B Mamba-2; head_dim/d_state are fixed in the family so nh is the size proxy). Small Mamba-2 (130m, nh=24) and decode stay on CPU. ORK_SSM_NPU=1/=0 forces on/off for A/B.


4. Gated DeltaNet โ€” โŒ built + coherent, but not a win

Full stack built following the SSM pattern and validated coherent end-to-end in a real Qwen3.5 model (byte-identical output vs CPU, no wedges): the ork_gdn_scan_f32 kernel, a ggml-ork marshaller (densify non-contiguous q/k/v/g/ฮฒ + state key/val transpose + GQA hv%Hk head replication), the GATED_DELTA_NET supports_op/graph_compute cases, and a weightless-op offload path in ggml-backend.cpp (GDN has no USAGE_WEIGHTS src, so the sched wouldn't route it otherwise โ€” scoped to the one op).

But the per-stage timer (ORK_GDN_PROF) shows the scan loses to ggml's CPU scan at both scales, gap widening:

model quant NPU-GDN scan CPU-GDN scan ratio
Qwen3.5-0.8B Q8 ~262 ms ~135 ms ~1.9ร— slower
Qwen3.5-9B Q4 ~748 ms ~156 ms ~4.8ร— slower

NPU-scan breakdown (both scales): NPU matmul-submit 44%, CPU UT-solve 38%, prep (fp16 cast) 14%, post 4%. (An earlier "0.8B 1.14ร— win" was single-run noise โ€” the CPU number jumped 112โ†’150 between runs; the isolated per-stage measurement is the honest read.)

Why GDN loses where SSM won (the real result):

  1. The chunked form's UT-solve is pure overhead here. ggml's CPU GDN op is a solve-free per-token recurrence โ€” it never does the (I+A)โปยน transform. The chunked reformulation (mandatory to make the scan into NPU matmuls) adds the CPU forward-substitution (38% of the scan) that the CPU path simply skips.
  2. NPU submit overhead on many small 128ร—128 matmuls ร— heads ร— layers (44%).
  3. No scale crossover. SSM won because Mamba-2's CPU recurrence craters at scale. GDN's state is a fixed 128ร—128/head that fits in cache, so ggml's vectorized recurrence stays fast at any size โ€” there is no cratering baseline for the NPU to overtake.

Verdict: not-yet-viable. Would need the UT-solve on-NPU (kill the 38%) and far cheaper submit, and even then it fights a cache-resident CPU recurrence. Parked on feat/gdn (coherent, ORK_GDN_NPU-gated, no regression). Reusable if a breakthrough or a different linear-attention variant with a crater-prone CPU baseline appears.


5. Reusable machinery (survives regardless of the GDN verdict)

  • Keep-warm (ยง2) โ€” general int8โ†”fp16 no-thrash; default on.
  • Fused-multicore fp16 stream (ork_mm_run_stream_f16_chain) โ€” PC-chaining + 3-core, amortizes the submit floor.
  • Weightless-op offload (ggml-backend.cpp) โ€” routes activation-only ops (no weight src) to the NPU.
  • densify marshaller โ€” feeds non-contiguous (permuted-view) in-graph operands to the NPU.

These three (weightless-offload + densify + fused-multicore fp16 + keep-warm) are exactly the machinery attention-on-NPU needs โ€” the top re-evaluation target now that GDN proved they work end-to-end.

6. Method note โ€” verify, don't recall

The GDN "win" I nearly reported was a false positive from a coarse end-to-end prefill number; the isolated ORK_GDN_PROF measurement overturned it. With linear-attention scans + activations now running on the NPU, prior "not viable / closed / CPU-only" verdicts predate the capability and should be re-derived on hardware, not trusted (e.g. the 130M "no SSM crossover" conclusion, which was wrong at scale).