Exp 2026 07 05 Int8 SiLU Output Quant - oRKLLM/ork-driver GitHub Wiki

int8 fused-SiLU output quantization โ€” the FFN-chain PPL gap, and how to (partly) close it

Question: the on-NPU int8 SwiGLU chain is coherent + faster at small batch but loses PPL (hybrid ~16, all-int8 ~22 vs baseline ~9/11.5). Where does the gap live, and can it be closed on-NPU?

The gap is the int8 quantization of the silu OUTPUT (ablation, ORK_GATE_ABLATE)

Isolated the gate's three error sources. Decisive: same int8 gate matmul + same LUT-index path, but fp32 silu OUTPUT โ†’ PPL 8.94 โ‰ˆ baseline; flip only the silu output to int8 โ†’ ~16. So the ~7-PPL gap is entirely the per-tensor int8 quantization of silu(g) (stored int8 to feed the ewmul), NOT the LUT-input resolution (the LUT input is the int32 accumulator โ†’ ~1024-entry int16 LUT, already fine). The output CVT (0x4010) is int8-only on the matmul-fused program (int16/fp16 there wedges โ€” see the fp16 exp log).

Why per-tensor int8 output is lossy: outlier-driven scale

s_silu = 1.15ยทmax|silu|. A few large-gate outlier neurons blow up the max, so the bulk small silu values get coarse int8. Classic LLM activation-outlier problem โ€” but on the OUTPUT scale, not the input.

What works: per-layer precision sprinkle (ORK_FFN_SILU_CPU_GMAX, fork a9d3e069b)

int8 fused silu on most layers (fast) + exact CPU fp32 silu on the few high-gmax outlier layers (their int32 gate acc โ†’ per-channel host dequant โ†’ fp32 silu = ablation mode-0 quality). gmax (max|gate|) is computed free in prep; a layer above threshold uses the exact path. Sweep (Qwen3-1.7B-Q8_0, 16 chunks):

config exact layers PPL
baseline (per-op) โ€“ 11.50
int8 chain (fused all) 0 21.93
gmax>30 7 / 28 12.31
gmax>20 13 11.91
gmax>12 19 11.77

Knee at ~7/28 layers = 92% of the gap recovered. Portability: gmax scale is per-model โ†’ ship as top-K / median+cยทMAD, auto-calibrated per model at load, NOT a fixed threshold.

What DOESN'T work: clipping the output outliers (ORK_FFN_SILU_PCT, fork d7e1b56aa) โ€” REFUTED

Tested "clip silu-output outliers so the per-tensor scale stays small" (a plausible on-NPU fix). Sweep (16ch, no-clip=21.93): [email protected]=86.7, @99.5=155, @99.0=274, @98.0=584 โ€” monotonically WORSE. Clipping just 0.1% of silu values quadrupled PPL. The large silu outliers carry critical signal (they dominate the down-projection sum) โ€” the LLM.int8() lesson: preserve outlier RANGE, don't clip. This confirms the sprinkle's premise: give outliers precision (per-channel/exact), don't remove them.

Option map for the int8 output gap (external-advice audit)

  • Clip outliers โ†’ REFUTED (above), catastrophic.
  • Asymmetric zero-point (reclaim wasted [-128,0] codes via 0x4080/R) โ†’ preserves range, ~1 bit, safe but modest; won't reach the sprinkle. Not measured (banked as marginal).
  • Per-channel output scale โ†’ the right diagnosis, but the fused stage carries only a scalar R (per-channel requant is host-side on this HW, as RKNN does). = exactly what the CPU sprinkle does on the outlier layers.
  • Un-fuse (int32 matmul โ†’ standalone SDP fp16/int16 silu โ†’ separate ewmul) โ†’ recovers the full gap but surrenders fusion + a SRAM roundtrip = architecturally rkllm/baseline. The guaranteed fallback.

Verdict

Per-tensor int8 on the fused silu output has a hard ~21.9 ceiling that no in-stage trick escapes (one global scale can't serve both the critical outliers and the bulk). The CPU-fp32 sprinkle (per-channel/exact on the ~7 outlier layers, 12.31 PPL) is the practical on-device recovery; un-fuse is the full-recovery fallback.