Skip to content

npu: serve GR00T N1.7 on Ascend 910B4 (396.67 → 52.40 ms, cosine 0.9999570) - #200

Merged
LiangSu8899 merged 1 commit into
mainfrom
feat/npu-groot-n17-native
Sep 13, 2026
Merged

LiangSu8899 merged 1 commit into
mainfrom
feat/npu-groot-n17-native

Conversation

@LiangSu8899

@LiangSu8899 LiangSu8899 commented Sep 12, 2026

Copy link
Copy Markdown
Member

Adds a native Ascend 910B4 path for GR00T N1.7 (Qwen3-VL ViT + truncated
Cosmos-Reason2 LLM + VL adapter + 32-layer DiT action head), routed as
("groot_n17", "torch", "npu"). The whole model is one captured graph with three
inputs — camera patches, robot state, noise draw — plus three native kernels: the
DiT's attention, a fused add-and-normalise, and the evaluation image transform's
resize.

Results

Full frame, end to end: one observation in, denormalized robot-space actions out.
Ascend 910B4, CANN 8.5.2, PyTorch 2.7.1 + torch_npu 2.7.1.post2.

PyTorch eager, official policy 396.67 ms
this path 52.40 ms (p90 52.61) — 7.6×
combined denormalized-action cosine 0.9999570
device memory 6.09 GB

torch.compile was not measured on this backend, so the judged baseline is eager.
Per modality: end-effector 0.9998915, joint 0.9999829, gripper 0.9956549 (nearly
constant over a trajectory, not a useful gate). 300 replays of the captured chain
give one distinct answer, bit-identical to the same graph run eagerly.

Build

Default is unchanged — the four shared objects Pi0.5 loads, ABI still 1. This
model's units are opt-in:

FLASHRT_ENABLE_NPU_GROOT_N17=ON bash scripts/npu/build.sh

Documentation

  • docs/deployment_npu_groot_n17.md — usage, the prompt bundle's keys, what each
    kernel does and why, precision, tests
  • docs/deployment_npu.md — the shared Ascend build
  • csrc/npu/README.md — the three entry points, their envelopes and refusals

Tests

python -m pytest tests/test_npu_*.py -q → 146 passed, 52 skipped without a
device. tests/test_npu_groot_n17_device.py holds the kernels' numerics and needs
the part.


Draft: the figures above were measured before this branch's last revisions
(shared-object names, operand checks, build selection, the frontend) and are being
re-taken on this head.

@LiangSu8899
LiangSu8899 marked this pull request as draft September 12, 2026 18:14
A native Ascend path for GR00T N1.7 -- 3 B of Qwen3-VL ViT, a truncated
Cosmos-Reason2 language model, a VL adapter and a 32-layer DiT action head --
routed as ("groot_n17", "torch", "npu"). The whole model is one captured graph
with three inputs, so a frame refills the camera patches, the robot state and
the noise draw and replays.

396.67 ms of eager reference becomes 52.40 ms, 7.6x, at a combined
denormalized-action cosine of 0.9999570 against the original FP32 reference with
the initial noise pinned. Of that, 48.02 ms is the graph and the rest is host.
The 1-D gripper channel reads 0.9956549 and is not a useful gate, being nearly
constant over a trajectory; end-effector and joint targets are 0.9998915 and
0.9999829. Three hundred replays of the captured action chain give one distinct
answer, and the replay is bit-identical to the same graph run eagerly.
torch.compile was not measured on this backend, so the judged baseline is eager;
on CDNA4 the same model compiles to 77.9 ms against eager's 67.9.

Three native units, in csrc/npu/kernels/groot_n17/ because each serves one
model's fixed geometries. Each exists because the vendor operator at those
shapes is dominated by fixed cost rather than by work.

The DiT's attention drives Mmad directly and keeps a whole head in L0 untiled:
41 query rows against 41, 13 or 448 keys over 32 heads of 48 channels, where the
vendor's prompt flash-attention charges 42 to 49 us for every one of those and
the cost barely moves between 13 keys and 448, while the 41x41 case moves 378 KB
and does 10 MFLOP. The row softmax runs over a whole score plane with no
vector-to-scalar round trips. 14.9 us against 42.0, cosine 0.999996. The value
operand is transposed on the way from L1 to L0B rather than by a separate
permute, which is bit-exact and removes two launches a layer.

The fused add-and-normalise is a draw on time against the vendor's fused form
and is here for its arithmetic: the residual sum is rounded to BF16 once,
because that value is what the next block carries forward, and the
normalisation runs in FP32 from it rather than rounding a second time. It also
adds the bias of the projection that produced its branch, and adds it to the
branch and rounds it there, which is where the biased matmul it replaces
rounded -- that placement is worth 0.9999570 against 0.9999531 end to end.

The evaluation image transform's resize reproduces OpenCV's enlarging INTER_AREA
bit for bit: zero differing samples over twenty-eight million, for 0.30 ms of
device against 7.0 ms of host. A torch version of the same arithmetic measures
8.9 ms, because an integer right shift on this part runs at 15 GB/s where a
multiply runs at 151, and the shifts cannot become float multiplies outside a
kernel -- the vertical pass forms a 26-bit product and FP32 has 24 bits.
Shrinking is a different branch inside OpenCV and is refused rather than
approximated.

Everything else runs on torch_npu operators with the weights in Ascend
fractal-NZ layout.

The build selects per model: FLASHRT_ENABLE_NPU_PI05 (ON) and
FLASHRT_ENABLE_NPU_GROOT_N17 (OFF), so the default emits exactly the four
shared objects it did before these units existed. A deselected model's standard
artifacts are removed rather than left behind, because a selection that only
decides what to add leaves a directory disagreeing with the selection that
produced it and a loader would bind the leftovers. At least one model has to be
selected and a switch that is neither ON nor OFF, empty included, is refused by
name. The published ABI stays at 1: adding units with new symbols changes no
contract behind an already-published entry point, and bumping would refuse every
shared object already built.

Every native entry point here is reached with a raw address, so
flash_rt/npu/core/operands.py checks device, dtype, shape, row pitch and
contiguity in front of each one and names the operand that was wrong. Residency
is a separate check because "the same device as that other operand" passes a
launch whose operands are consistently on the host and still faults, and camera
frames are on the host by default.

The checkpoint description moves to flash_rt/models/groot_n17/weight_spec.py,
where the backends that already share it can reach it without one of them
importing another's frontend; the former module re-exports it, so no existing
importer changes.

The tier is BF16. 910/A2 parts have no FP8 tensor hardware, and an INT8 request
is refused rather than downgraded. INT8 on the DiT was built and measured and is
not here: the quantised GEMM wins 3.05 us a call while the kernel types it adds
to the block's per-layer loop make the launches already in that loop slower,
including ones it never touches. On this part a kernel's cost includes what else
is interleaved with it, about 4.5 us a launch.

Tests run on any host except one file: the frontend's routing and refusals, what
a build selection compiles and removes -- driven against a fake toolchain, so the
script carries no test hook of its own -- the operand checks, each unit's
identity check and the refusal an unbuilt one gives, the action decode and state
encode including every representation they refuse, the weight specification, and
the image transform's tap construction against OpenCV itself at four enlarging
geometries. 146 passed and 52 skipped with no Ascend device present;
tests/test_npu_groot_n17_device.py holds the kernels' numerics and needs the
part.
@LiangSu8899
LiangSu8899 force-pushed the feat/npu-groot-n17-native branch from 4bf088f to efacaf8 Compare September 13, 2026 10:36
@LiangSu8899 LiangSu8899 changed the title Ascend: native GR00T N1.7 compute path (396.67 → 52.40 ms, cosine 0.9999570) npu: serve GR00T N1.7 on Ascend 910B4 (396.67 → 52.40 ms, cosine 0.9999570) Sep 13, 2026
@LiangSu8899
LiangSu8899 marked this pull request as ready for review September 13, 2026 12:52
@LiangSu8899
LiangSu8899 merged commit 3a14a05 into main Sep 13, 2026
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.

1 participant