1use anyhow::{Result, bail, ensure};
21use metrale_gpu_runtime::gpu::{DevicePtr, GpuBackend, KernelHandle};
22use metrale_gpu_runtime::kernel_args::KernelLaunch;
23
24use crate::layers::try_kernel;
25use crate::weight_map::{Fp8Weight, WeightQuantFormat};
26
27pub const W8A8_MAX_ROWS: usize = 256;
30pub const W8A8_LAUNCH_ROWS: usize = 128;
32pub const W8A8_K_UNIT: u32 = 128;
34const GEMV_MODULE: &str = "w8a8_gemv";
35const QUANT_MODULE: &str = "w8a8_act_quant";
36const QUANT_THREADS: u32 = 256;
37const GEMV_THREADS: u32 = 256;
39const GEMV_ROWS: u32 = 16;
41const ENTRIES: [&str; 5] = ["mb1_ku8", "mb2", "mb4", "mb8", "mb16"];
43
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
48pub enum W8a8Scale {
49 PerRow,
50 Block128,
51}
52
53impl W8a8Scale {
54 pub fn of(format: WeightQuantFormat) -> Option<Self> {
57 match format {
58 WeightQuantFormat::Fp8PerRow => Some(Self::PerRow),
59 WeightQuantFormat::Fp8BlockScaled => Some(Self::Block128),
60 _ => None,
61 }
62 }
63
64 fn segment_align(self) -> u32 {
67 match self {
68 Self::PerRow => 16,
69 Self::Block128 => 128,
70 }
71 }
72
73 pub fn act_scales_per_row(self, k: u32) -> usize {
75 match self {
76 Self::PerRow => 1,
77 Self::Block128 => (k / W8A8_K_UNIT) as usize,
78 }
79 }
80}
81
82#[derive(Clone, Copy, Debug)]
86pub struct W8a8Weight {
87 segs: [Fp8Weight; 3],
88 count: usize,
89 n: u32,
90 k: u32,
91 scale: W8a8Scale,
92}
93
94impl W8a8Weight {
95 pub fn new(segs: &[Fp8Weight]) -> Result<Self> {
99 ensure!(
100 (1..=3).contains(&segs.len()),
101 "W8a8Weight: {} segments, want 1..=3",
102 segs.len()
103 );
104 let Some(scale) = W8a8Scale::of(segs[0].scale_format) else {
105 bail!(
106 "W8a8Weight: scale format {:?} is not per-row or 128x128 block",
107 segs[0].scale_format
108 );
109 };
110 let k = segs[0].k;
111 ensure!(
112 k > 0 && k.is_multiple_of(W8A8_K_UNIT),
113 "W8a8Weight: K={k} is not a positive multiple of {W8A8_K_UNIT}"
114 );
115 for (i, s) in segs.iter().enumerate() {
116 ensure!(
117 s.k == k && s.scale_format == segs[0].scale_format,
118 "W8a8Weight: segment {i} is [{}, {}] {:?}, segment 0 is [.., {k}] {:?}",
119 s.n,
120 s.k,
121 s.scale_format,
122 segs[0].scale_format
123 );
124 ensure!(
125 s.n > 0 && s.weight != DevicePtr::NULL && s.row_scale != DevicePtr::NULL,
126 "W8a8Weight: segment {i} is empty"
127 );
128 if i + 1 < segs.len() {
129 ensure!(
130 s.n.is_multiple_of(scale.segment_align()),
131 "W8a8Weight: segment {i} has {} rows, not a multiple of {}",
132 s.n,
133 scale.segment_align()
134 );
135 }
136 }
137 let mut all = [segs[0]; 3];
138 all[..segs.len()].copy_from_slice(segs);
139 Ok(Self {
140 segs: all,
141 count: segs.len(),
142 n: segs.iter().map(|s| s.n).sum(),
143 k,
144 scale,
145 })
146 }
147
148 pub fn n(&self) -> u32 {
150 self.n
151 }
152 pub fn k(&self) -> u32 {
154 self.k
155 }
156 pub fn scale(&self) -> W8a8Scale {
157 self.scale
158 }
159
160 fn boundaries(&self) -> (u32, u32) {
163 let n0 = self.segs[0].n;
164 match self.count {
165 1 => (self.n, self.n),
166 2 => (n0, self.n),
167 _ => (n0, n0 + self.segs[1].n),
168 }
169 }
170}
171
172#[derive(Clone, Copy, Debug)]
175pub struct W8a8Kernels {
176 quant_row: KernelHandle,
177 quant_g128: KernelHandle,
178 quant_silu_row: KernelHandle,
179 quant_silu_g128: KernelHandle,
180 rowscale: [KernelHandle; 5],
181 blk128: [KernelHandle; 5],
182}
183
184impl W8a8Kernels {
185 pub fn load(gpu: &dyn GpuBackend) -> Self {
186 let tiles = |layout: &str| {
187 ENTRIES.map(|e| try_kernel(gpu, GEMV_MODULE, &format!("w8a8_gemv_{layout}_{e}")))
188 };
189 Self {
190 quant_row: try_kernel(gpu, QUANT_MODULE, "w8a8_act_quant_row"),
191 quant_g128: try_kernel(gpu, QUANT_MODULE, "w8a8_act_quant_g128"),
192 quant_silu_row: try_kernel(gpu, QUANT_MODULE, "w8a8_act_quant_silu_row"),
193 quant_silu_g128: try_kernel(gpu, QUANT_MODULE, "w8a8_act_quant_silu_g128"),
194 rowscale: tiles("rowscale"),
195 blk128: tiles("blk128"),
196 }
197 }
198
199 pub fn none() -> Self {
201 let z = KernelHandle(0);
202 Self {
203 quant_row: z,
204 quant_g128: z,
205 quant_silu_row: z,
206 quant_silu_g128: z,
207 rowscale: [z; 5],
208 blk128: [z; 5],
209 }
210 }
211
212 pub fn resolved(&self, scale: W8a8Scale) -> bool {
215 let (q, qs, g) = match scale {
216 W8a8Scale::PerRow => (self.quant_row, self.quant_silu_row, &self.rowscale),
217 W8a8Scale::Block128 => (self.quant_g128, self.quant_silu_g128, &self.blk128),
218 };
219 q.0 != 0 && qs.0 != 0 && g.iter().all(|h| h.0 != 0)
220 }
221}
222
223#[derive(Clone, Copy, Debug)]
227pub struct W8a8Scratch {
228 pub q: DevicePtr,
229 pub q_bytes: usize,
230 pub scale: DevicePtr,
231 pub scale_bytes: usize,
232}
233
234impl W8a8Scratch {
235 pub fn alloc(gpu: &dyn GpuBackend, max_k: u32) -> Result<Self> {
238 let rows = W8A8_MAX_ROWS;
239 let q_bytes = rows * max_k as usize;
240 let scale_bytes = rows * (max_k.div_ceil(W8A8_K_UNIT) as usize).max(1) * 4;
241 Ok(Self {
242 q: gpu.alloc(q_bytes)?,
243 q_bytes,
244 scale: gpu.alloc(scale_bytes)?,
245 scale_bytes,
246 })
247 }
248
249 fn fits(&self, rows: usize, k: u32, scale: W8a8Scale) -> bool {
250 self.q != DevicePtr::NULL
251 && self.q_bytes >= rows * k as usize
252 && self.scale_bytes >= rows * scale.act_scales_per_row(k) * 4
253 }
254}
255
256fn entry_index(rows: usize) -> usize {
260 match rows {
261 0..=8 => 0,
262 9..=16 => 1,
263 17..=32 => 2,
264 33..=64 => 3,
265 _ => 4,
266 }
267}
268
269pub fn w8a8_decode_available(
274 kernels: &W8a8Kernels,
275 w: &W8a8Weight,
276 rows: usize,
277 scratch: &W8a8Scratch,
278) -> bool {
279 (1..=W8A8_MAX_ROWS).contains(&rows)
280 && kernels.resolved(w.scale)
281 && scratch.fits(rows, w.k, w.scale)
282}
283
284#[allow(clippy::too_many_arguments)]
287pub fn w8a8_act_quant(
288 gpu: &dyn GpuBackend,
289 kernels: &W8a8Kernels,
290 scale: W8a8Scale,
291 x: DevicePtr,
292 ldx: u32,
293 rows: usize,
294 k: u32,
295 scratch: &W8a8Scratch,
296 stream: u64,
297) -> Result<()> {
298 ensure!(
299 (1..=W8A8_MAX_ROWS).contains(&rows) && scratch.fits(rows, k, scale),
300 "w8a8_act_quant: {rows} rows of K={k} do not fit the scratch"
301 );
302 ensure!(
303 k.is_multiple_of(W8A8_K_UNIT) && ldx >= k && ldx.is_multiple_of(8),
304 "w8a8_act_quant: K={k}, ldx={ldx} (want K % 128 == 0, ldx >= K, ldx % 8 == 0)"
305 );
306 let kernel = match scale {
307 W8a8Scale::PerRow => kernels.quant_row,
308 W8a8Scale::Block128 => kernels.quant_g128,
309 };
310 ensure!(
311 kernel.0 != 0,
312 "w8a8_act_quant: {scale:?} quantizer not compiled"
313 );
314 KernelLaunch::new(gpu, kernel)
315 .grid([rows as u32, 1, 1])
316 .block([QUANT_THREADS, 1, 1])
317 .arg_ptr(x)
318 .arg_ptr(scratch.q)
319 .arg_ptr(scratch.scale)
320 .arg_u32(k)
321 .arg_u32(ldx)
322 .arg_u32(k)
323 .launch(stream)
324}
325
326#[allow(clippy::too_many_arguments)]
330pub fn w8a8_act_quant_silu(
331 gpu: &dyn GpuBackend,
332 kernels: &W8a8Kernels,
333 scale: W8a8Scale,
334 gate: DevicePtr,
335 up: DevicePtr,
336 ld: u32,
337 rows: usize,
338 k: u32,
339 scratch: &W8a8Scratch,
340 stream: u64,
341) -> Result<()> {
342 ensure!(
343 (1..=W8A8_MAX_ROWS).contains(&rows) && scratch.fits(rows, k, scale),
344 "w8a8_act_quant_silu: {rows} rows of K={k} do not fit the scratch"
345 );
346 ensure!(
347 k.is_multiple_of(W8A8_K_UNIT) && ld >= k && ld.is_multiple_of(8),
348 "w8a8_act_quant_silu: K={k}, ld={ld} (want K % 128 == 0, ld >= K, ld % 8 == 0)"
349 );
350 let kernel = match scale {
351 W8a8Scale::PerRow => kernels.quant_silu_row,
352 W8a8Scale::Block128 => kernels.quant_silu_g128,
353 };
354 ensure!(
355 kernel.0 != 0,
356 "w8a8_act_quant_silu: {scale:?} quantizer not compiled"
357 );
358 KernelLaunch::new(gpu, kernel)
359 .grid([rows as u32, 1, 1])
360 .block([QUANT_THREADS, 1, 1])
361 .arg_ptr(gate)
362 .arg_ptr(up)
363 .arg_ptr(scratch.q)
364 .arg_ptr(scratch.scale)
365 .arg_u32(k)
366 .arg_u32(ld)
367 .arg_u32(ld)
368 .arg_u32(k)
369 .launch(stream)
370}
371
372pub fn w8a8_gemv(
375 gpu: &dyn GpuBackend,
376 kernels: &W8a8Kernels,
377 w: &W8a8Weight,
378 scratch: &W8a8Scratch,
379 rows: usize,
380 out: DevicePtr,
381 ldc: u32,
382 stream: u64,
383) -> Result<()> {
384 ensure!(
385 w8a8_decode_available(kernels, w, rows, scratch),
386 "w8a8_gemv: not available for {rows} rows of [{}, {}] {:?}",
387 w.n,
388 w.k,
389 w.scale
390 );
391 ensure!(ldc >= w.n, "w8a8_gemv: ldc={ldc} < N={}", w.n);
392 let (n1, n2) = w.boundaries();
393 let s = &w.segs;
394 let a_scales = w.scale.act_scales_per_row(w.k) * 4;
395 let mut done = 0;
397 while done < rows {
398 let m = (rows - done).min(W8A8_LAUNCH_ROWS);
399 let e = entry_index(m);
400 let kernel = match w.scale {
401 W8a8Scale::PerRow => kernels.rowscale[e],
402 W8a8Scale::Block128 => kernels.blk128[e],
403 };
404 KernelLaunch::new(gpu, kernel)
405 .grid([w.n.div_ceil(GEMV_ROWS), 1, 1])
406 .block([GEMV_THREADS, 1, 1])
407 .arg_ptr(scratch.q.offset(done * w.k as usize))
408 .arg_ptr(scratch.scale.offset(done * a_scales))
409 .arg_ptr(s[0].weight)
410 .arg_ptr(s[0].row_scale)
411 .arg_ptr(s[1].weight)
412 .arg_ptr(s[1].row_scale)
413 .arg_ptr(s[2].weight)
414 .arg_ptr(s[2].row_scale)
415 .arg_ptr(out.offset(done * ldc as usize * 2))
416 .arg_u32(m as u32)
417 .arg_u32(w.n)
418 .arg_u32(w.k)
419 .arg_u32(w.k)
420 .arg_u32(ldc)
421 .arg_u32(n1)
422 .arg_u32(n2)
423 .launch(stream)?;
424 done += m;
425 }
426 Ok(())
427}
428
429#[allow(clippy::too_many_arguments)]
432pub fn w8a8_proj(
433 gpu: &dyn GpuBackend,
434 kernels: &W8a8Kernels,
435 w: &W8a8Weight,
436 x: DevicePtr,
437 ldx: u32,
438 rows: usize,
439 out: DevicePtr,
440 ldc: u32,
441 scratch: &W8a8Scratch,
442 stream: u64,
443) -> Result<()> {
444 ensure!(
445 w8a8_decode_available(kernels, w, rows, scratch),
446 "w8a8_proj: not available for {rows} rows of [{}, {}] {:?}",
447 w.n,
448 w.k,
449 w.scale
450 );
451 w8a8_act_quant(gpu, kernels, w.scale, x, ldx, rows, w.k, scratch, stream)?;
452 w8a8_gemv(gpu, kernels, w, scratch, rows, out, ldc, stream)
453}
454
455#[path = "w8a8_decode/parts.rs"]
456mod parts;
457
458#[cfg(test)]
459#[path = "w8a8_decode_tests.rs"]
460mod tests;