nx_q5_0_to_f32.nx source
↩ module page · 95 lines · 3783 B
1// nx_q5_0_to_f32.nx -- bits-up Q5_0 dequantization to IEEE 754 binary32.
2//
3// Q5_0 block layout (per ggml v3 spec; clean-room from public docs):
4// 18 bytes per block of 32 values:
5// bytes [0..2] d (f16 scale)
6// bytes [2..6] qh (32-bit packed high bits, LE)
7// bytes [6..22] qs (32 4-bit values, 2 per byte)
8//
9// Dequant per ggml v3 spec (block_q5_0 in ggml-common.h):
10// For j in 0..15:
11// qs_byte = qs[j]
12// qs_low = qs_byte & 0x0F
13// qs_high = (qs_byte >> 4) & 0x0F
14// qh_bit_lo = (qh >> j) & 1 -- pairs with qs_low
15// qh_bit_hi = (qh >> (j + 16)) & 1 -- pairs with qs_high
16// q5_lo = qs_low | (qh_bit_lo << 4) // 0..31
17// q5_hi = qs_high | (qh_bit_hi << 4) // 0..31
18// value[j] = (q5_lo - 16) * d_f32
19// value[j+16] = (q5_hi - 16) * d_f32
20//
21// CRITICAL: positions 0..15 use qs[j].low+qh_bit[j], positions 16..31
22// use qs[j].high+qh_bit[j+16]. The two halves of qs are interleaved
23// against DIFFERENT bit ranges of qh (not bits [i] for all i).
24// (Bug fix 2026-05-21 after live-fire on real Qwen2.5 surfaced
25// embed-magnitude blow-up due to wrong interleaving.)
26//
27// genealogy_id: ggml_q5_0_canon
28// lineage_id: substrate_q5_0_to_f32_v1
29
30import "nx_syscalls.nx"
31import "nx_tier.nx"
32import "nx_le.nx"
33import "nx_f32.nx"
34import "nx_f32_cvt.nx"
35
36const NX_Q5_0_OK: nx_int = 0
37const NX_Q5_0_ERR_BAD_DIM: nx_int = 1
38const NX_Q5_0_VPB: i64 = 32 // values per block (QK5_0)
39const NX_Q5_0_BPB: i64 = 22 // bytes per block (d:2 + qh:4 + qs:16)
40
41func _nx_q5_0_block_to_f32(buf: *u8, base_off: i64, take: i64,
42 out_f32: *i64) -> nx_int {
43 let d_f16: i64 = nx_le_read_u16(buf, base_off + 0)
44 let d_f32: i64 = nx_f16_to_f32(d_f16)
45 let qh: i64 = nx_le_read_u32(buf, base_off + 2)
46 let qs_off: i64 = base_off + 6
47
48 // Per ggml: walk j=0..15, emit value[j] (low nibble) and value[j+16]
49 // (high nibble) with paired qh bits at positions j and j+16.
50 var j: i64 = 0
51 while j < 16 {
52 let qs_byte: i64 = nx_le_read_u8(buf, qs_off + j)
53 let qs_lo: i64 = qs_byte & 0x0F
54 let qs_hi: i64 = (qs_byte >> 4) & 0x0F
55 let qh_bit_lo: i64 = (qh >> j) & 1
56 let qh_bit_hi: i64 = (qh >> (j + 16)) & 1
57 let q5_lo: i64 = qs_lo | (qh_bit_lo << 4)
58 let q5_hi: i64 = qs_hi | (qh_bit_hi << 4)
59 let s_lo: i64 = q5_lo - 16
60 let s_hi: i64 = q5_hi - 16
61
62 // HARDWARE convert+mul (2026-07-08): __f32_from_i64=cvtsi2ss (1 instr)
63 // replaces the ~40-op software nx_i32_to_f32; __f32_mul=mulss replaces
64 // software nx_f32_mul. Bit-exact for these normal finite products
65 // (proven: nx_q5_0_threaded_gate F32POOL==Q5FUSEDPOOL). Speeds the
66 // model-load dequant of the 388M Q5_0 values (79% of this model).
67 if j < take {
68 out_f32[j] = __f32_mul(d_f32, __f32_from_i64(s_lo))
69 }
70 let j_hi: i64 = j + 16
71 if j_hi < take {
72 out_f32[j_hi] = __f32_mul(d_f32, __f32_from_i64(s_hi))
73 }
74 j = j + 1
75 }
76 return NX_Q5_0_OK
77}
78
79// Multi-block dequant.
80
81func nx_q5_0_to_f32(buf: *u8, base_off: i64, n_values: i64,
82 out_f32: *i64) -> nx_int {
83 let n_blocks: i64 = (n_values + NX_Q5_0_VPB - 1) / NX_Q5_0_VPB
84 var b: i64 = 0
85 while b < n_blocks {
86 let block_off: i64 = base_off + b * NX_Q5_0_BPB
87 let block_n: i64 = n_values - b * NX_Q5_0_VPB
88 var take: i64 = block_n
89 if take > NX_Q5_0_VPB { take = NX_Q5_0_VPB }
90 _nx_q5_0_block_to_f32(buf, block_off, take,
91 ((out_f32 as i64) + b * NX_Q5_0_VPB * 8) as *i64)
92 b = b + 1
93 }
94 return NX_Q5_0_OK
95}