Pull requests / #3455

#3455 [Engine] GP100 (sm_60): 4-bit slots for resident IQ2_S / IQ3_XXS / IQ3_S gate/up experts, decode +9.7% (opt-in, STRATA_EXPERT_NIBBLE=1)

open · @ruibeikaa · 0 コメント · GitHub で見る

BenchmarksSetup & installMulti-GPUAMD / HIPNVIDIA / CUDAModels & quantsDocumentationWindows

本文

## Problem and result

On a GP100 die (Tesla P100, PH402: compute capability 6.0, no `__dp4a`, no tensor cores) the decode gate/up kernels for the i-quant expert formats are nowhere near the memory wall: measured in isolation on one die at T = 1, IQ2_S reads its 1.05 MB expert in 10.2 us (~103 GB/s of the 732 GB/s HBM2), IQ3_XXS 1.25 MB in 10.0 us, IQ3_S 1.41 MB in 12.5 us. The time is the per-block serial chain (unpack the codebook indices from the packed grid, the emulated dp4a, the 16-lane reduction), not the bytes: the same kernel with every dot product removed still takes about two thirds of its time.

With `STRATA_EXPERT_NIBBLE=1` the engine, after the cache is filled and the residency is final, transcodes the gate/up blocks of every fully resident IQ2_S / IQ3_XXS / IQ3_S layer into a slot layout of 4-bit codes (one codebook index per nibble, the scales as they are; the down projections untouched) and the verify window's gate/up step reads those slots. The arithmetic order is the unchanged kernels' order, so the outputs are the same bits. On four GP100 dies decode goes 47.3 -> 51.9 tok/s (+9.7%, token-weighted over eight prompts, each +9.5..+10.9%), prompt reading is 2.3..3.0% slower, and every reply is bit for bit the unchanged engine's. Unset (the default) nothing changes: the 410 kernels the unchanged engine has in `iq_kernels.cu` have the same SASS with and without the change, in the sm_60 and the sm_86 builds, and the switch-off arm's replies and state hashes equal the unchanged engine's.

## Related discussion

Scope issue: #3454 (opened together with this PR; maintainer agreement pending). The GP100 kernels this builds on: #1424. The fusions in `fuse_decode.hpp` (#1097) show the gating used here.

## Approach

- `include/strata/core/expert_nibble.hpp`: the slot contract (per-format code layout, the per-layer on/off table, the slot byte sizes).
- `src/kernels/cuda/iq_kernels.cu`: the load-time transcoder (one kernel per format), the slot hash, the GGUF rebuild kernel, and the 4-bit gate/up kernels (R4S for IQ3_XXS, R4P for IQ2_S, R4SV / R4PX for IQ3_S by group size). `native_expert_grouped` only swaps its gate/up step for a converted layer; SwiGLU, Hadamard and the down projection stay on the engine's default path (mode 8 on GP100). No signature changes, so the SYCL and Metal definitions are untouched.
- `src/core/verify.cpp`: the converted layers' layout is picked for the window graph (`nib_layout(L)`); the S26, fused, GU_SWIGLU, LDS and AMD branches are skipped for those layers.
- `src/prefill/`: the prompt path is unchanged. Its MMQ gather rebuilds the GGUF blocks from a 4-bit slot on the fly (`nib_gguf_kernel`), so the prompt kernels read exactly the bytes they read today. At load every converted slot is rebuilt once and compared byte for byte with the staged GGUF blob before the slot is sealed.
- `src/program/generate.cpp`: the transcode after the residency is final; the VRAM accounting (the converted slots are sized into `sized_slots`); a start-up refusal, with a message, of every option that would read or write a converted slot as GGUF bytes: `STRATA_MMQ_DIRECT`, partial residency, the adaptive and PCIe tiers, `--mmap-experts` / `STRATA_ARENA_MMAP`, `STRATA_RAM_ADAPT`, `STRATA_HC_Q8` uploads, `--peer-device`, a prompt chunk below the auto ceiling or a loaned slot. `STRATA_EXPERT_NIBBLE_SKIP=<layers>` and `STRATA_EXPERT_NIBBLE_FMTS=<types>` narrow the conversion; `STRATA_EXPERT_NIBBLE_RECHECK=1` (debug) hashes every slot again after each request.
- `src/kernels/iq_nibble_parity.cpp`, `native_expert_bench --nibble`: the parity and timing tools.
- `docs/OLDER_GPUS.md`: the section under the GP100 kernels.
- Gating: the call sites and the kernels are under `STRATA_EXPERIMENTAL_SM60` (CUDA only, not HIP, not Metal); a build without it carries no 4-bit kernel.
- Cost: the slots take about 1.7x the GGUF bytes of those layers, +12.7 GiB over four dies for Swift 1.5 IQ3_S (47 of 48 layers); a card without that room keeps the GGUF slots.

## Validation

**Environment note: everything below was measured with every expert resident in VRAM** (four dies, 64 GB in all, the Swift 1.5 IQ3_S pack fits whole; no CPU share, no PCIe share, no adaptive tier). A card that holds its experts partly in RAM was not measured, and the switch refuses to convert a layer that is not fully resident, so on such a card the engine keeps the GGUF slots and nothing changes.

Rig: PH402 SKU 200 x2 (four GP100 dies, 48 SMs and 16 GB HBM2 each, application clocks 715 / 1050 MHz), Windows 11, driver 581.80 (TCC), CUDA 12.9, VS 2022; build `-DSTRATA_EXPERIMENTAL_SM60=ON -DCMAKE_CUDA_ARCHITECTURES=60`, ggml at the pinned 3cf03257f. Revision: main e88ee39d (engine 0.1.45.3) + this branch. Model: Swift 1.5 IQ3_S (abliterated GSQ-RCO GGUF), 1M context, `--layer-split 12,24,36 --trim-stage-weights --pipeline-windows 2 --spec-min-p 0.85`, MTP, every expert resident, conversation cache on, the same launcher for every arm; the unchanged engine is the same revision built the same way.

**Passed:**
- `iq_nibble_parity <shard> --layers-per-fmt 2` on one die: PASS, 6 layers + 3 synthetic formats, 0 differences (gate, up, h, hq and the combined output against the unchanged `native_expert_grouped` in mode 0 and in the card's default mode 8; the rebuilt GGUF blocks byte for byte the blobs). `--negative` (a deliberately corrupted slot): FAIL with 264,813 differences, exit 1.
- Three arms, one fresh engine each (unchanged engine P; this branch with the switch off N0; on N1), 15 requests each (five short prompts, 8K / 26K / 88K-token contexts, 90-token..30K prompt reads, a 260K-token read): every reply's hash equal across the three arms; the engine's state hashes (`STRATA_STATE_HASH`) equal for the live state (GDN, PLE, tail, pooled, KV, MTP) on every request; N1's `STRATA_EXPERT_NIBBLE_RECHECK` clean after every request. N1 converted 47 of 48 layers (IQ2_S 20, IQ3_XXS 17, IQ3_S 10), +12.70 GiB.
- Timing, order P, N0, N1, N1, N0, P, decode of eight prompts token-weighted: P 47.25 / 41.13, N0 47.27 / 47.35, N1 51.91 / 51.90 tok/s. N1 over N0 +9.71% (code 63.1 vs 56.9, SQL 54.4 vs 49.5, English code 52.5 vs 47.8, story 50.8 vs 46.4, explanation 52.3 vs 47.6 tok/s). Prompt reading N1 / N0: 90 tokens x1.030, 1K x1.026, 6.9K x1.027, 30K x1.023, a 1K append after 26K x1.030. Start-up N1 - N0 = -3.1 s. Peak VRAM per die 25.1 / 24.8 / 25.5 / 29.0 GiB (N0: 21.1 / 21.6 / 22.5 / 26.3). The last P run (41.1 tok/s) is an outlier against its own first run (47.25) and the two N0 runs; it is kept in the raw data.
- Builds: sm_60 with `STRATA_EXPERIMENTAL_SM60` (strata, iq_nibble_parity, native_expert_bench) and sm_86 without it; `cuobjdump` shows the unchanged engine's 410 kernels in `iq_kernels.cu` with identical SASS in both, and no 4-bit kernel in the sm_86 build.

**Failed:** none observed.

**Not run:** HIP (gfx906, wave32), SYCL and Metal builds (no such machine here; the new code is under `STRATA_EXPERIMENTAL_SM60` and CUDA-only guards, and `verify.cpp`'s new references are guarded for the Metal build of the CUDA tree). A single Tesla P100 (one die, partial residency: the switch refuses and the GGUF slots stay). Other packs (IQ3_XXS, IQ2_XS) on this engine. `STRATA_MMQ_TAB` builds (`STRATA_MMQ_DIRECT` is refused at start).

## Limits

- Only the three formats, only fully resident layers, only CUDA on compute capability 6.0 builds with `STRATA_EXPERIMENTAL_SM60`; the kernels compile for other archs but were measured on GP100 only, and only in the all-experts-in-VRAM setup described above (a single 16 GB P100 with experts partly in RAM gets nothing from this change today).
- VRAM: about 1.7x the GGUF bytes of the converted layers; cards without the room keep the GGUF slots (`STRATA_EXPERT_NIBBLE_SKIP` narrows the set).
- Prompt reading pays the gather's rebuild (2.3..3.0% here).
- The first-window logits dump (`STRATA_DUMP_FIRST_LOGITS`) does not write under `--pipeline-windows`, so the parity evidence is the reply hashes, the state hashes and the slot re-hash, not a logits file.

関連リンク

インストール・モデル・リリースへのサイト内リンク。