code wiki / _hdl_build / nx_quality_balance.nx
nx_quality_balance.nx source
↩ module page · 88 lines · 4501 B
1// nx_quality_balance.nx -- BALANCE quality against speed (the operator's law: always balance
2// quality and quantity, with QUALITY SLIGHTLY MORE VALUABLE than raw speed). Cutting bytes
3// (lower quantization) buys speed in the memory-bound regime but costs QUALITY (perplexity rises).
4// "Just go Q2 for 1.8x" is wrong -- Q2_K is +8.82% perplexity. This module picks the operating
5// point that maximizes a quality-weighted score under a hard QUALITY FLOOR (rule: never strip
6// quality for speed). Data-driven: the quality + size numbers are measured config, not magic.
7//
8// RACI: the COUNCIL governs the POLICY here (the quality floor + the quality:speed weighting --
9// the value judgment). The ENGINEER supplies the measured numbers (nx_engineer_profile). The
10// BUILDER consumes qb_select to choose the operating point (nx_builder_quant). One organ per job.
11//
12// Real llama.cpp data (Llama-3.1-8B class), perplexity delta to fp16 as PERMIL (pct*10), and bits
13// per weight *10: fp16(0,160) Q8_0(1,85) Q6_K(2,66) Q5_K_M(4,55) Q4_K_M(12,45) Q3_K_M(47,34)
14// Q2_K(88,26). Refs: arxiv 2601.14277 unified quant eval; llama.cpp perplexity discussion #406.
15// license_tier: ORIGINAL
16
17import "nx_syscalls.nx"
18
19// ---- COUNCIL-GOVERNED POLICY (the value judgment: quality slightly > speed, and a hard floor) ----
20const QB_W_QUALITY: i64 = 11 // quality weight (slightly higher than speed)
21const QB_W_SPEED: i64 = 9 // speed weight
22const QB_QUALITY_FLOOR: i64 = 20 // max acceptable perplexity loss, PERMIL (2.0%); below = reject
23const QB_BITS_FP16: i64 = 160 // bits*10 of the fp16 reference (the size baseline)
24const QB_BITS_MIN: i64 = 26 // bits*10 of the smallest option (Q2_K) -- speed normalisation span
25
26// quality score 0..1000 (1000 = lossless fp16). Higher = better quality.
27func qb_quality_score(loss_permil: i64) -> i64 {
28 if loss_permil < 0 { return 0 }
29 if loss_permil > 1000 { return 0 }
30 return 1000 - loss_permil
31}
32
33// speed score 0..1000 in the MEMORY-BOUND regime (throughput ~ 1/bytes ~ 1/bits): linear in bits
34// SAVED vs fp16. Smallest option -> 1000, fp16 -> 0. Defensive: clamp bits to the valid span.
35func qb_speed_score(bits10: i64) -> i64 {
36 var b: i64 = bits10
37 if b < QB_BITS_MIN { b = QB_BITS_MIN }
38 if b > QB_BITS_FP16 { b = QB_BITS_FP16 }
39 let span: i64 = QB_BITS_FP16 - QB_BITS_MIN
40 if span <= 0 { return 0 }
41 return (1000 * (QB_BITS_FP16 - b)) / span
42}
43
44// combined score: quality weighted slightly more than speed. 0..1000.
45func qb_combined(loss_permil: i64, bits10: i64) -> i64 {
46 let q: i64 = qb_quality_score(loss_permil)
47 let s: i64 = qb_speed_score(bits10)
48 let wsum: i64 = QB_W_QUALITY + QB_W_SPEED
49 if wsum <= 0 { return 0 }
50 return (QB_W_QUALITY * q + QB_W_SPEED * s) / wsum
51}
52
53// COUNCIL gate: is this operating point ADMISSIBLE on quality? (the "never strip quality" floor).
54func council_quality_admit(loss_permil: i64, floor: i64) -> i64 {
55 if loss_permil <= floor { return 1 }
56 return 0
57}
58
59// SELECT the best operating point among n options (loss[], bits10[]), honoring the Council floor:
60// maximize the quality-weighted combined score over options that PASS the floor. If NONE pass
61// (every option too lossy), degrade GRACEFULLY to the highest-quality option (never ship below
62// the floor by going lower) -- returns its index. Defensive: n<=0 -> -1.
63func qb_select(n: i64, loss: *i64, bits10: *i64, floor: i64) -> i64 {
64 if n <= 0 { return 0 - 1 }
65 var best: i64 = 0 - 1; var bestscore: i64 = 0 - 1
66 var i: i64 = 0
67 while i < n {
68 if council_quality_admit(loss[i], floor) == 1 {
69 let sc: i64 = qb_combined(loss[i], bits10[i])
70 if sc > bestscore { bestscore = sc; best = i }
71 }
72 i = i + 1
73 }
74 if best >= 0 { return best }
75 // graceful degradation: nothing met the floor -> pick the lowest-loss (highest-quality) option.
76 var hq: i64 = 0; var lo: i64 = loss[0]; var j: i64 = 1
77 while j < n { if loss[j] < lo { lo = loss[j]; hq = j } j = j + 1 }
78 return hq
79}
80
81// the pure-SPEED selector (ignores quality) -- kept ONLY to contrast: it would pick the smallest
82// option (e.g. Q2_K) and wreck quality. Proof that the balanced selector is doing real work.
83func qb_select_speed_only(n: i64, bits10: *i64) -> i64 {
84 if n <= 0 { return 0 - 1 }
85 var best: i64 = 0; var lo: i64 = bits10[0]; var i: i64 = 1
86 while i < n { if bits10[i] < lo { lo = bits10[i]; best = i } i = i + 1 }
87 return best
88}