Exp 2026 07 05 fp16 Fused SiLU Calibration - oRKLLM/ork-driver GitHub Wiki
fp16 Fused Gate+SiLU — ✅ CALIBRATED (mean|err| ~0.08 / ~1%), gated OFF (speed)
Status: ✅ CRACKED — the fp16 matmul + fused SiLU produces correct silu on-NPU (mean|err| ≈ 0.08, max
0.75 over silu[−8,8], ~1%, on par with the int8 fused SiLU), no wedge. Kept gated OFF: the fp16
matmul is ~3.3× slower than int8, so it's a measured net-loss today (tools/f16_gate_bench). Built out as
the "end-goal" higher-precision on-NPU gate for a future pipeline (batched-verify / int8-win regimes) where
precise activation with no int8 quantization pays off. Commits: ork-driver 27e7cf1 (primitive), 8cbceab
(calibration crack). Probe: tools/silu_f16_calib.c. Primitive: ork_mm_run_f16_silu / set_f16_silu.
Why this matters
The int8 fused SiLU quantizes the silu output to int8 — the ablation (ORK_GATE_ABLATE) showed that is
the entire PPL gap of the on-NPU FFN chain (fp32 silu = baseline PPL; int8 silu = the ~16-vs-9 loss). A
fused gate that emits silu at fp16 precision removes that quantization while keeping silu free-on-NPU. The
open question was whether the flying-mode matmul SDP can drive the LUT with a non-int8 output at all.
Hypothesis (supported)
That an all-fp16 fused op (fp16 matmul + SiLU LUT, keeping REGCMD's native fp16→fp32 output CVT) would
run on-NPU where int8-in→fp16-out wedges. Earlier attempts to force non-int8 output on the int8 matmul
program (0x4010 PREC=1/2, int32 bit-31) all soft-reset — a proc-precision mismatch (int8 processing
vs a float output CVT). The all-fp16 graft has no mismatch. This held: it runs, no wedge.
The two-part structure (answers "model or board?")
idx(acc)→ LUT-index mapping = BOARD (the SDP's fixed index hardware; measure once per config).- LUT curve values: int8 = per-layer (each layer's quant scale changes the acc→gate mapping);
fp16 = UNIVERSAL — the accumulator is the real gate value, so
silu(gate)is one curve for every layer and model. Calibrate once, never per-layer. (A genuine fp16 advantage.)
The blocker and the crack
Ramp-measuring idx(acc) (tools/silu_f16_calib) exposed a hardware asymmetry:
| acc | idx behavior |
|---|---|
acc = 0 |
idx = 514 (= C0 from idx_off=0xffffc000) |
acc < 0 |
idx decreases toward 1 — spreads across the LUT |
acc > 0 |
clamps at 514 — one index, curve unrepresentable |
So positive gates (silu's important half, silu≈gate) all mapped to one LUT entry. Register probing:
| register | role (fp16 program) | effect on index |
|---|---|---|
0x4084/0x4088 |
output gain R (out = R·LUT[idx]) |
none (output-only) |
0x4044 (za, BS_ALU_OPERAND) |
pre-LUT bias in int8 | none here |
0x4064 |
fp16 index-scale param | default = usable spread; 0xffff7dc8 (standalone silu's value) collapses it |
idx_off 0x4110 |
index center C0 | shifts center, doesn't fix the positive clamp |
The crack: since only negative acc spreads, negate the gate — pack the gate weight as −S·W so a
positive real gate becomes a negative accumulator (spreads), and negative real gates become positive acc →
clamp to ~0, which is exactly silu(neg)≈0. Scale S≈24 maps [−8,8] onto the ~513-entry negative half.
Then the standard 2-pass build: measure idx(acc) with a ramp LUT, fill LUT[idx] = silu(−acc/S)/(R·out_scale).
Result (tools/silu_f16_calib, RK3588, K=512 N=64)
S sweep (negated gate, idx_off=0xffffc000, cfg4068=0x56391100, default 0x4064):
S=24 : mean|err|=0.0775 max=0.7530
S=30 : mean|err|=0.2298 max=2.2690
S=38 : mean|err|=0.4109 max=3.2961 (larger S over-spreads -> clamps)
per-point (positive gates correct, negative lobe clamps to ~0):
true=+2.0 npu=1.757 ref=1.762
true=+4.0 npu=3.920 ref=3.928
true=-2.0 npu=0.000 ref=-0.238 (silu neg lobe, clamped ~0)
S≈24 → mean|err| 0.078, max 0.75 over silu[−8,8] (~1%). silu's shallow negative dip (min −0.28) is the
only real loss (clamped to 0); the dominant positive lobe is accurate.
Speed (why it's gated OFF) — tools/f16_gate_bench, gate M=128 K=2048 N=6144
int8 matmul : 4640 us
fp16 matmul : 15477 us (3.34x int8)
CPU silu (M*N) : 6158 us
BASELINE gate (int8 mm + CPU silu) : 10798 us
FUSED fp16 gate (~fp16 mm) : 15477 us -> 1.43x SLOWER
Correct, but a net-loss today: the fp16 matmul penalty (+10.8ms) exceeds the CPU-silu it removes (6.2ms).
Register roles (fp16 fused SiLU, RK3588) — reference
0x4010 = 0xa8000002— fp16 output CVT (kept from REGCMD; do not override to int80x44e0).0x40c0 = 0x40(2-byte elem),0x4050 = 0x36e(fp16 row stride) — from REGCMD.0x4084= output gainR;0x4088= shift. Index-independent.0x4064= index scale — keep REGCMD default (0xffff7dc8collapses index).0x4110 = idx_off= C0 center;0x4068 = cfg4068(0x56391100).0x4044(za) inert.- Submit: LUT-load prologue
enable=0x18 regcfg=1097, then matmul+siluenable=0x1d regcfg=REGCMD_N. - fp16 weight lives in
w->Bb(single tile:Sk==1K≤2048,Sn==1N≤nmax);Bfis int8-only. - Env knobs on
set_f16_silu:ORK_F16_C4064 / ORK_F16_ZA / ORK_F16_R84 / ORK_F16_R88.
Upper LUT bank — CONFIRMED unreachable for fp16 (2026-07-05, tools/f16_bank_probe)
The RK LUT is 2 banks (neg 0..511, pos 512..1023); the int8 fused path uses BOTH (idx=in+512). The
fp16 path only ever reaches the lower bank — the negate hack exists because positive acc hard-clamps at
idx ≈ 514. Question: is the upper bank reachable under some never-checked register config? Answer: no.
Measured index vs acc (default): idx = 512 + 3.07·min(acc,0) — negative acc spreads 1..512, positive
ReLU-clamps at 514. Swept every accessible register — none lift the clamp:
| register(s) | result |
|---|---|
0x4044 za / 0x4064 idxscale / 0x4068 cfg / 0x4108/0x410c LUTcfg |
no effect on the +clamp (some collapse the spread entirely) |
0x4110 idx_off |
controls the negative-spread slope, not a center offset; +clamp stays at 514 |
0x4070/0x4004/0x411c/0x4128/0x412c |
no unlock (0x4004 bit0 wedges; kernel soft-resets & recovers) |
0x4010 output CVT (WEDGE-PRONE) |
either wedges, breaks output (R~0), or no index effect — as expected (it's post-LUT) |
Conclusion: the one-sidedness is intrinsic to the fp16 flying-mode accumulator→index datapath, not
register-configurable. fp16 fused SiLU gets ~512 entries (lower bank), which is already 2× int8's 256 — so
the fp16 error (~0.055) is not resolution-limited (it's fp16 weight/acc rounding + the single-scale
compromise). The upper bank's only real gain would be silu's negative lobe (currently clamped to 0, max ~0.28
in a low-weight region). Not worth the CVT wedge risk. The negate hack is the correct, complete approach.
RE knobs added for the record: ORK_F16_C4004/C4010/C4060/C4064/C4070/C4108/C410C/C411C/C4128/C412C + ZA.
In-model integration + bounded-Gmax fix (2026-07-05) — coherent, still gated OFF
Built ork_mm_build_f16_silu_lut() (bakes the negate/scale, returns the measured-idx LUT + S + out_scale) and
wired ORK_FFN_GATE_F16 into the ggml-ork hybrid. First in-model run = PPL 25k (garbage). Root cause was
NOT a wiring bug: S = 296/Gmax, so wide-range layers (measured gmax up to 130 on Qwen3-1.7B) drove S tiny
(~2.3), crushing the bulk of the gate distribution into a few LUT-index entries near the center → those layers
emit near-garbage silu, and one bad FFN layer nukes PPL.
Fix (commit 0de5e28): bound the effective Gmax + tune the acc target. ORK_F16_GCAP (default 40) caps
Gmax so S stays in the validated spread band (gates above the cap clamp to silu(cap), ~linear so cheap).
ORK_F16_ATGT (default 150, replaces the hardcoded 296) sets S = ATGT/Gmax = the acc range; 296 was
over-spreading small-Gmax layers (gates near Gmax hit the +clamp edge). tools/f16_gmax_sweep result:
| ATGT | bulk|err| @Gmax8 | @Gmax20 | @Gmax40 | @Gmax130 |
|---|---|---|---|---|
| 296 | 0.41 (over-spread) | 0.057 | 0.055 | 0.036 |
| 150 | 0.055 | 0.061 | 0.065 | 0.034 |
ATGT=150 gives uniform bulk|err|~0.055 across the whole gmax range (8→130) — the primitive is now COHERENT in-model. Still gated OFF: fp16 matmul 3.3× int8 (speed-dominated by the CPU-fp32 silu sprinkle, which is exact AND ~2.3× not 3.3×). This makes the fp16 gate a ready primitive for a future fully-on-NPU pipeline (batched verify), not an enabled path.
TODO to enable (future)
build_f16_silu_lut() exists (see above). Remaining: static-const the universal LUT (fixed S, RK3588-specific,
like REGCMD_SILU_LUT) and enable only where the fp16-gate cost is hidden (batched verify) or the precise gate
unlocks a downstream win. The upper-bank crack (above) is the way to make it accurate (not just coherent) —
but it's walled off, so single-region + bounded-Gmax is the ceiling.