Skip to content

hexagon: keep more work on the IQ-10 NPU and run six more archs - #71

Closed
thanhdq1o2 wants to merge 17 commits into
VinRobotics:mainfrom
thanhdq1o2:hexagon-b11223-ops
Closed

thanhdq1o2 wants to merge 17 commits into
VinRobotics:mainfrom
thanhdq1o2:hexagon-b11223-ops

Conversation

@thanhdq1o2

@thanhdq1o2 thanhdq1o2 commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Profiling the IQ-10 (GGML_HEXAGON_PROFILE=1, VLA_FALLBACK_STATS=1) showed matmuls the matrix unit (HMX) does not take and ops the NPU sends to the CPU. The HMX takes a matmul only when K and N are multiples of 32, and with a batch only an F16 src0. The NPU runs a concat only as a dim-1 row copy. This PR keeps that work on the NPU in Hexagon builds, and sends two ops the NPU claims but cannot run correctly to the CPU, which brings π0, VLA-JEPA, VLA-Adapter, OpenVLA-OFT, Octo-Small and TurboVLA onto the board. Other backends are unchanged.

Changes

Commit What Before
b33ead8 WeightLoader::pad() zero-pads SigLIP-so400m's MLP from 4304 to 4320 (fc1, its bias, fc2). The output is unchanged. 440 ms of N1.5's 565 ms vision tower on the vector units
b85e174 π0.5's SigLIP takes plain flash attention on Hexagon, as GR00T's tower does explicit F32 × F32 attention, 266 ms
74e145d Evo-1's LM attention takes F16 K/V (fa_kv()). Scores accumulate in F32, softmax stays F32. about 1 s on the vector units
bdacd9e docs for the three commits above
c10cb80 π0.5's VLM prefix and action expert take plain flash attention too (use_flash_attention()) single-KV-head scores sent to the CPU by the batched MUL_MAT rule
0b78eb9 concat_dim2(): the experts' dim-2 KV concats run as a dim-1 concat of the [d0*d1, n] views (same bytes) concats on the CPU every layer and step
26ad84b hexagon.md, CHANGELOG, IQ-10 benchmark re-measured at 0b78eb9
1ca8633 A CPY that reads a weight runs on the CPU. The NPU claims it, answers NO_SUPPORT, and the host aborts. VLA-Adapter (F16 action queries cast to F32) aborts
1cab1c3 A REPEAT of a computed tensor runs on the CPU. The NPU reads the source through the data cache, invalidated only between op batches, so a source written earlier in the same batch can be read stale. TurboVLA 0.19 off the reference, different on every run
6aec070 hexagon.md, CHANGELOG, IQ-10 benchmark for 12 archs at 1cab1c3
9d49a36 An MLP whose weights are quantized in the GGUF is not padded, so a file packed with scripts/quantize_gguf.py --vision loads as it does on main. Float checkpoints give bit-identical actions. declare() refused to pad the packed fc1 and the load failed
7dc3571 The status note in hexagon.md and the IQ-10 row of docs/benchmark/README.md list the twelve archs at 1cab1c3 they still said six, with the 0b78eb9 numbers

The two new rules check for a Hexagon session. On the board they fire only for VLA-Adapter (the CPY), TurboVLA (its ViT prefix repeated over two views) and Octo-Small (its task-token repeat); the other nine archs' graphs are unchanged by them.

IQ-10, vla-bench under TEMPLATE.md, min ms

Model Before (#70) After max|Δ| vs CPU F32
GR00T N1.5 1020.9 613.7 (−40%) 2.4e-3
GR00T N1.6 1110.3 705.8 (−36%) 1.4e-3
GR00T N1.7 747.5 747.0 2.0e-3
SmolVLA 810.3 631.0 (−22%) 7.2e-4
π0.5 2914.6 1117.4 (−62%) 4.8e-4
Evo-1 3519.4 2579.1 (−27%) 5.5e-4
Model min ms max|Δ| fixed input max|Δ| served
Octo-Small 121.6 4.1e-4 9.3e-5
TurboVLA 428.1 3.1e-4 2.2e-4
VLA-JEPA 440.2 2.8e-3 6.7e-3
π0 1124.4 1.2e-3 5.8e-4
VLA-Adapter 1733.6 4.2e-4 4.5e-4
OpenVLA-OFT 4097.7 3.5e-4 5.0e-4

The reference is the same build on the CPU with --weight-dtype f32 --flash-attn 0; the bar is 2.9e-3. Fixed inputs use the views the LIBERO client sends, and the benchmark uses its own inputs (both listed in the docs). BitVLA is not run: its only published GGUF is int2-packed and loads only in CUDA builds.

Verified

  • All 12 archs are within 2.9e-3 on fixed inputs (vla_predict_check), and a repeated run gives the same output.
  • On a served LIBERO observation (vla-server and eval/client), each arch returns a finite chunk, the same chunk for a repeated request, an error reply to a malformed and to an invalid request, and keeps serving. GR00T N1.5 (1.1e-2), N1.6 (1.3e-2) and VLA-JEPA (6.7e-3) are above the bar on it; so is the CPU at F16 (4.3e-2, 1.1e-2, 2.1e-2).
  • The KV-concat commit leaves π0.5's and SmolVLA's outputs bit-identical.
  • An x86 CPU-only build compiles and ctest passes (10/10).

Tried and left out

vla-bench under TEMPLATE.md's protocol at e8dfa34, for the six models whose
GGUFs are on the board. The flag screen keeps F16 weights and varies only
--flash-attn and --mm-prec: the NPU has no BF16 kernels, and F16 is the
default. Memory adds the drop in system MemAvailable, since the weights sit in
FastRPC buffers that peak RSS leaves out. The CPU section and the archs
without GGUFs on the board are left for a later round.
Minimum latency at the defaults from qualcomm-iq10.md, measured at e8dfa34.
Models whose GGUFs were not on the board are marked n/r; BitVLA is N/A off CUDA.
ggml-hexagon takes an F16 weight on the matrix unit only when both dims are
multiples of 32. SigLIP-so400m's MLP is 4304 wide, so fc1 and fc2 ran on the
vector units: 440 ms of GR00T N1.5's 565 ms vision tower on the IQ-10.

WeightLoader::pad() declares a float weight larger than in the file, with the
extra elements zero, and EncStack pads fc1, its bias and fc2 to gemm_width(),
which rounds up to 32 in Hexagon builds and is the identity elsewhere. The
padded units are zero through GELU and fc2, so the block's output is the same.

On the IQ-10 against the unpadded CPU F32 reference: pi0.5 2926 -> 2122 ms
(3.1e-4), GR00T N1.5 1024 -> 610 ms (2.4e-3), N1.6 1117 -> 709 ms (1.4e-3).
SmolVLA, Evo-1 and N1.7 do not use this MLP and are unchanged.
pi0.5 ties SigLIP's flash attention to a view-taking kernel, which only SYCL
has, so on Hexagon its tower ran the explicit scores: an F32 x F32 matmul at
head dim 72 that the HMX does not take, 266 ms on the vector units. Where
flash attention is on by default (Hexagon), SigLIP now takes the plain flash
attention that GR00T's identical tower already uses. Other backends are
unchanged.

IQ-10: 2118 -> 1833 ms, 2.8e-4 from the CPU F32 reference.
The LM's explicit attention multiplies F32 K and V against a batch of 14 query
heads. ggml-hexagon's HMX takes no batched F32 src0, so both matmuls ran on the
vector units, about 1 s per chunk on the IQ-10. On Hexagon K and V now go to
F16 through fa_kv(); the scores still accumulate in F32 and the softmax stays
F32. Other backends keep F32 K/V.

IQ-10: 3261 -> 2336 ms, 4.3e-4 from the CPU F32 reference (5.2e-4 before).
The InternViT flash attention stays on the CPU: F16 K/V there measured 3.9e-2.
hexagon.md: which matmuls the HMX takes, the three that now move onto it, what
is left on the vector units, and why the GELU_ERF kernel was left out. The
fidelity and serving tables are re-measured at this commit.

The IQ-10 benchmark page and its README row are re-measured under TEMPLATE.md
at 74e145d: GR00T N1.5 613.6 ms, N1.6 706.9, pi0.5 1789.4, Evo-1 2589.2;
SmolVLA and N1.7 unchanged. The defaults stay the fastest configuration.
VinRobotics#70 was squash-merged into main; its two commits are already on this branch,
and the IQ-10 benchmark page and README row keep this branch's re-measured
numbers.
The VLM's and the expert's attention took flash attention only on a
view-taking kernel (SYCL). On Hexagon they ran explicit scores against a
single KV head, which the fallback wrapper sends to the CPU. Where flash
attention is on by default (Hexagon), all three sites now take the plain
flash attention; the view-only paths keep use_fused_attention().

IQ-10: 1773 -> 1320 ms, 4.8e-4 from the CPU F32 reference, 3.5e-4 on a
served LIBERO observation. Other models unchanged.
The NPU runs a concat only as a dim-1 copy of whole rows, so the expert's
dim-2 KV concats (π0, π0.5, SmolVLA) went to the CPU fallback every step.
concat_dim2() in backend.h does them on Hexagon as a dim-1 concat of the
[d0*d1, n] views, which writes the same bytes; other backends keep
ggml_concat(a, b, 2).

IQ-10: π0.5 1379 -> 1133 ms, SmolVLA 810 -> 638 ms, both with outputs
identical to before on fixed inputs and on a served observation.
…rk at 0b78eb9

hexagon.md: π0.5's VLM and action expert take flash attention on Hexagon,
and the experts' KV concats run as row copies that stay on the NPU (π0
shares the code but has not been run on the board). π0.5's fidelity is
re-measured: 4.8e-4 on fixed inputs, 3.5e-4 on a served observation.

The IQ-10 benchmark page and its README row are re-measured under
TEMPLATE.md at 0b78eb9: π0.5 1117.2 ms, SmolVLA 633.0 ms; the other
models are unchanged. The defaults stay the fastest configuration.
@thanhdq1o2 thanhdq1o2 changed the title hexagon: put SigLIP's MLP and Evo-1's attention on the HMX hexagon: keep more of π0.5, SmolVLA, Evo-1 and GR00T on the NPU Nov 3, 2026
ggml-hexagon claims a CPY whose source is a weight, then answers the batch
with NO_SUPPORT and the host aborts. VLA-Adapter casts its F16 action
queries to F32 in the graph and aborted on every predict on the Dragonwing
IQ-10. With the cast on the CPU it is within 4.2e-4 of the CPU F32
reference, 4.5e-4 when served.
The NPU's REPEAT reads its source through the data cache, which is
invalidated only between op batches, while ops write their output by DMA.
When the source is written earlier in the same batch, REPEAT can read stale
lines. TurboVLA's ViT prefix, repeated over two views, came out 0.19 off
the CPU F32 reference and different on every run; with the repeat on the
CPU it is 3.1e-4 and repeatable, 2.2e-4 when served.
@thanhdq1o2 thanhdq1o2 changed the title hexagon: keep more of π0.5, SmolVLA, Evo-1 and GR00T on the NPU hexagon: keep more work on the IQ-10 NPU and run six more archs Nov 4, 2026
…file

pad_mlp() checked only the loader's GEMM type. scripts/quantize_gguf.py
--vision packs SigLIP's fc1 [1152, 4304] to Q8_0 while fc2, 4304 wide,
stays float; the loader keeps fc1 packed and declare() refused to pad it,
so such a GGUF failed to load in Hexagon builds. An MLP with either weight
quantized in the file now loads unpadded, as it did before the padding.
Float checkpoints are unchanged: π0, π0.5, GR00T N1.5/N1.6/N1.7,
VLA-Adapter and OpenVLA-OFT give bit-identical actions on the Dragonwing
IQ-10.
… summary

The previous docs commit added six archs to the IQ-10 pages but left two
summaries behind: the status note in docs/backend/hexagon.md still said six
checkpoints run on the IQ-10, and the IQ-10 row of docs/benchmark/README.md
still carried the 0b78eb9 numbers with the six new archs marked not run. Both
now match docs/benchmark/qualcomm-iq10.md at 1cab1c3.
@khanhnd61-vr

Copy link
Copy Markdown
Collaborator

Closing in favour of #76, which contains every commit on this branch (this head, 43683b8, is an ancestor of #76's head), so merging #76 lands this work as well. Please carry this PR's change table and IQ-10 numbers into #76's description so they are not lost with this one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants