pub struct QuantizedMatrix {
pub data: Vec<i8>,
pub scales: Vec<f32>,
pub n: usize,
pub k: usize,
}Expand description
A weight matrix quantized with per-output-channel symmetric Q8 scales.
Layout is the nn.Linear layout the checkpoint stores: data is [n, k] row-major with one
f32 scale per output row. Quantized once at hydration; the borrowed f32 tensor is untouched.
Fields§
§data: Vec<i8>Q8 bytes, [n, k] row-major, each value in [-127, 127].
scales: Vec<f32>One symmetric scale per output row, [n].
n: usizeOutput rows.
k: usizeReduction length of one output element.
Implementations§
Source§impl QuantizedMatrix
impl QuantizedMatrix
Sourcepub fn concat_rows(parts: &[&Self]) -> Self
pub fn concat_rows(parts: &[&Self]) -> Self
Stacks matrices with a shared reduction length into one taller matrix.
Row bytes and scales are byte-identical to quantizing each part separately — this exists so fused projections (QKV, gate‖up) can run as ONE kernel dispatch while every output row keeps exactly the per-channel quantization it would have had alone.
§Panics
Panics if the parts disagree on k or the list is empty.
Sourcepub fn quantize(weight: &[f32], n: usize, k: usize) -> Self
pub fn quantize(weight: &[f32], n: usize, k: usize) -> Self
Quantizes an [n, k] f32 weight matrix one output channel at a time.
§Panics
Panics if weight.len() != n * k or any value is non-finite.
Examples found in repository?
62fn main() {
63 let tiers: Vec<Int8Tier> = Int8Tier::available();
64 println!("int8 shape bench — tiers available: {:?}", {
65 tiers.iter().map(|t| t.as_str()).collect::<Vec<_>>()
66 });
67 println!(
68 "interleaved rounds={ROUNDS} (+{WARMUP_ROUNDS} warmup); per-sample = mean over calls in one round; cv% over rounds"
69 );
70
71 for &m in &[1_usize, 16] {
72 println!(
73 "\n== m = {m} {} ==",
74 if m == 1 {
75 "(decode GEMV)"
76 } else {
77 "(seq-16 verify GEMM)"
78 }
79 );
80 for &(label, n, k) in SHAPES {
81 let calls: usize = (32 / m).max(2);
82 let weight = pseudo_random_f32(n * k, 0xbe0_0001 ^ (n as u64) << 20 ^ k as u64);
83 let x = pseudo_random_f32(m * k, 0xbe0_0002 ^ (m as u64) << 32 ^ k as u64);
84 let quantized = QuantizedMatrix::quantize(&weight, n, k);
85 let mut out = vec![0.0_f32; m * n];
86 let mut x_q = vec![0_i8; m * k];
87 let mut x_scales = vec![0.0_f32; m];
88
89 // One arm per route, all interleaved inside every round.
90 let mut f32_samples = Vec::with_capacity(ROUNDS);
91 let mut tier_samples: Vec<Vec<f64>> =
92 tiers.iter().map(|_| Vec::with_capacity(ROUNDS)).collect();
93
94 for round in 0..ROUNDS + WARMUP_ROUNDS {
95 // f32 arm
96 let start = Instant::now();
97 for _ in 0..calls {
98 f32ref::linear(
99 black_box(&x),
100 black_box(&weight),
101 None,
102 m,
103 k,
104 n,
105 black_box(&mut out),
106 );
107 }
108 let f32_us = start.elapsed().as_secs_f64() * 1e6 / calls as f64;
109
110 // W8A8 arms, including dynamic activation quantization each call.
111 let mut this_round = Vec::with_capacity(tiers.len());
112 for &tier in &tiers {
113 let start = Instant::now();
114 for _ in 0..calls {
115 for ((x_row, q_row), scale) in x
116 .chunks_exact(k)
117 .zip(x_q.chunks_exact_mut(k))
118 .zip(x_scales.iter_mut())
119 {
120 *scale = quantize_row_q8(black_box(x_row), q_row);
121 }
122 linear_q8(
123 black_box(&x_q),
124 black_box(&x_scales),
125 black_box(&quantized),
126 None,
127 m,
128 black_box(&mut out),
129 tier,
130 );
131 }
132 this_round.push(start.elapsed().as_secs_f64() * 1e6 / calls as f64);
133 }
134
135 if round >= WARMUP_ROUNDS {
136 f32_samples.push(f32_us);
137 for (samples, sample) in tier_samples.iter_mut().zip(&this_round) {
138 samples.push(*sample);
139 }
140 }
141 }
142
143 let f32_stats = stats(&f32_samples);
144 // The f32 reference loops rows outermost, so it streams the weight matrix once per
145 // activation row (m times per call); the q8 kernel is weight-stationary and streams
146 // it exactly once per call. The column reports actual weight bytes moved per second.
147 let f32_bytes = (n * k * 4 * m) as f64;
148 println!(
149 "{label} f32 {:9.1} us cv {:4.1}% ({:5.1} GB/s weight-stream)",
150 f32_stats.mean_us,
151 f32_stats.cv_percent,
152 f32_bytes / (f32_stats.mean_us * 1e-6) / 1e9,
153 );
154 for (tier, samples) in tiers.iter().zip(&tier_samples) {
155 let tier_stats = stats(samples);
156 let q8_bytes = (n * k) as f64;
157 let verdict = if tier_stats.cv_percent > 5.0 || f32_stats.cv_percent > 5.0 {
158 "REFUSED (cv>5%)"
159 } else {
160 ""
161 };
162 println!(
163 "{label} q8 {:9} {:9.1} us cv {:4.1}% ({:5.1} GB/s weight-stream) x{:.2} vs f32 {verdict}",
164 tier.as_str(),
165 tier_stats.mean_us,
166 tier_stats.cv_percent,
167 q8_bytes / (tier_stats.mean_us * 1e-6) / 1e9,
168 f32_stats.mean_us / tier_stats.mean_us,
169 );
170 }
171 }
172 }
173 println!(
174 "\nNOTE: ratios above compare routes inside this tree (self-comparison = maintenance),\nnever a pinned incumbent. cv%>5 rows are refused, not averaged."
175 );
176}More examples
45fn main() {
46 // (label, n, k) for one microdecoder layer, at decode geometry m = 1.
47 const HIDDEN: usize = 1024;
48 const INTERMEDIATE: usize = 3072;
49 const Q_WIDTH: usize = 16 * 128;
50 const KV_WIDTH: usize = 8 * 128;
51 let projections: &[(&str, usize, usize)] = &[
52 ("q_proj", Q_WIDTH, HIDDEN),
53 ("k_proj", KV_WIDTH, HIDDEN),
54 ("v_proj", KV_WIDTH, HIDDEN),
55 ("o_proj", HIDDEN, Q_WIDTH),
56 ("gate_up", INTERMEDIATE * 2, HIDDEN),
57 ("down_proj", HIDDEN, INTERMEDIATE),
58 ];
59
60 // Fifteen depths x five layers is how often this body is walked per frame.
61 const ROUNDS: usize = 15 * 5;
62
63 let dispatched = Int8Tier::dispatch();
64 println!("dispatched int8 route: {}", dispatched.as_str());
65 println!("shapes: microdecoder layer at m=1, {ROUNDS} rounds (15 depths x 5 layers)\n");
66 // The ratio columns are SPEED ratios (q4's throughput relative to the q8 variant):
67 // t_q8 / t_q4, so 1.0 means parity and 0.05 means q4 runs at 5% of q8's speed.
68 println!(
69 "{:<10} {:>6} {:>6} {:>10} {:>10} {:>10} {:>9} {:>9}",
70 "proj", "n", "k", "q8-scalar", "q4-scalar", "q8-route", "spd/q8scl", "spd/route"
71 );
72
73 let mut total_q8_scalar = 0.0_f64;
74 let mut total_q4 = 0.0_f64;
75 let mut total_q8_route = 0.0_f64;
76
77 for &(label, n, k) in projections {
78 let weight = deterministic(n * k, 0x51ED_0000 + n as u64);
79 let activation = deterministic(k, 0xA0C7_0000 + k as u64);
80 let mut x_q = vec![0_i8; k];
81 let scale = quantize_row_q8(&activation, &mut x_q);
82
83 let q8 = QuantizedMatrix::quantize(&weight, n, k);
84 let q4 = QuantizedMatrixQ4::quantize(&weight, n, k);
85 let mut out = vec![0.0_f32; n];
86
87 // Warm the caches for whichever side runs first, so the ordering does not decide it.
88 linear_q8(&x_q, &[scale], &q8, None, 1, &mut out, Int8Tier::Scalar);
89 linear_q4(&x_q, &[scale], &q4, None, 1, &mut out);
90
91 // INTERLEAVED repeats, reporting the minimum of each variant.
92 //
93 // A first attempt timed the three variants in sequential blocks and produced 98.60 ms for
94 // k_proj against 5.14 ms for v_proj — identical 1024x1024 shapes, 19x apart. That is
95 // scheduler and thermal noise, and it is precisely what doctrine #8's same-thermal-window
96 // rule exists to prevent. Interleaving puts all three variants in the same window on every
97 // repeat, and the minimum is the least noise-contaminated estimator of a deterministic
98 // kernel: noise only ever adds time.
99 const REPEATS: usize = 7;
100 let mut q8_scalar = f64::MAX;
101 let mut q4_ms = f64::MAX;
102 let mut q8_route = f64::MAX;
103 for _ in 0..REPEATS {
104 let started = Instant::now();
105 for _ in 0..ROUNDS {
106 linear_q8(&x_q, &[scale], &q8, None, 1, &mut out, Int8Tier::Scalar);
107 }
108 q8_scalar = q8_scalar.min(started.elapsed().as_secs_f64() * 1000.0);
109
110 let started = Instant::now();
111 for _ in 0..ROUNDS {
112 linear_q4(&x_q, &[scale], &q4, None, 1, &mut out);
113 }
114 q4_ms = q4_ms.min(started.elapsed().as_secs_f64() * 1000.0);
115
116 let started = Instant::now();
117 for _ in 0..ROUNDS {
118 linear_q8(&x_q, &[scale], &q8, None, 1, &mut out, dispatched);
119 }
120 q8_route = q8_route.min(started.elapsed().as_secs_f64() * 1000.0);
121 }
122
123 total_q8_scalar += q8_scalar;
124 total_q4 += q4_ms;
125 total_q8_route += q8_route;
126
127 println!(
128 "{label:<10} {n:>6} {k:>6} {q8_scalar:>9.2}m {q4_ms:>9.2}m {q8_route:>9.2}m {:>8.2}x {:>8.2}x",
129 q8_scalar / q4_ms,
130 q8_route / q4_ms
131 );
132 }
133
134 println!(
135 "\ntotals (one layer, {ROUNDS} rounds): q8-scalar {total_q8_scalar:.1} ms, \
136 q4-scalar {total_q4:.1} ms, q8-route {total_q8_route:.1} ms"
137 );
138 println!(
139 "int4 vs scalar int8 : {:.2}x ({})",
140 total_q8_scalar / total_q4,
141 if total_q4 < total_q8_scalar {
142 "FASTER - the halved-bytes thesis holds at equal implementation quality"
143 } else {
144 "SLOWER - unpack cost exceeds the traffic saving even against scalar"
145 }
146 );
147 println!(
148 "int4 vs shipping int8: {:.2}x ({})",
149 total_q8_route / total_q4,
150 if total_q4 < total_q8_route {
151 "FASTER - gate (a) PASSES; the listening gate is now worth running"
152 } else {
153 "SLOWER - gate (a) FAILS as built; int4 needs an in-register SIMD unpack to compete"
154 }
155 );
156
157 // Bytes are the thesis; report them so the ratio can be read against the traffic it saves.
158 let q8_bytes: usize = projections.iter().map(|(_, n, k)| n * k).sum();
159 println!(
160 "\nweight bytes per layer: q8 {:.1} MB, q4 {:.1} MB",
161 q8_bytes as f64 / 1e6,
162 q8_bytes as f64 / 2e6
163 );
164}Trait Implementations§
Source§impl Clone for QuantizedMatrix
impl Clone for QuantizedMatrix
Source§fn clone(&self) -> QuantizedMatrix
fn clone(&self) -> QuantizedMatrix
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read more