1use crate::region::wrap_anonymous;
9use crate::{
10 plan_matmul_kernel, F32MatmulMode, MatmulFallbackReason, MatmulKernelCapabilities,
11 MatmulKernelPath, MatmulKernelPlan, MatrixShape,
12};
13use vyre::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
14use vyre_spec::{QuantizationScale, QuantizationZeroPoint};
15
16const INT4_LINEAR_WORKGROUP_SIZE: [u32; 3] = [256, 1, 1];
17const AFFINE_GROUPED_WORKGROUP_SIZE: [u32; 3] = [256, 1, 1];
18const AFFINE_GROUPED_LANES_PER_OUTPUT: u32 = 32;
19const AFFINE_GROUPED_OUTPUTS_PER_WARP: u32 = 1;
20const AFFINE_GROUPED_WARPS_PER_WORKGROUP: u32 =
21 AFFINE_GROUPED_WORKGROUP_SIZE[0] / AFFINE_GROUPED_LANES_PER_OUTPUT;
22const AFFINE_GROUPED_OUTPUTS_PER_WORKGROUP: u32 =
23 AFFINE_GROUPED_WARPS_PER_WORKGROUP * AFFINE_GROUPED_OUTPUTS_PER_WARP;
24const AFFINE_GROUPED_OP_ID: &str = "vyre-libs::nn::linear_4bit_affine_grouped";
25
26pub const LINEAR_4BIT_AFFINE_GROUPED_OUTPUT_DRIFT_ABS_TOLERANCE: f32 = 1.0e-4;
28
29#[derive(Debug, Clone, PartialEq)]
31pub struct QuantizedLinear4BitPlannerEvidence {
32 pub in_dim: u32,
34 pub out_dim: u32,
36 pub group_size: u32,
38 pub group_count: u32,
40 pub packed_weight_bytes: u64,
42 pub dequantized_weight_bytes: u64,
44 pub sidecar_bytes: u64,
46 pub bias_bytes: u64,
48 pub output_bytes: u64,
50 pub dequant_bytes_elided: u64,
52 pub matmul_m: u32,
54 pub matmul_k: u32,
56 pub matmul_n: u32,
58 pub matmul_tile: u32,
60 pub matmul_selected_path: &'static str,
62 pub matmul_candidate_path: Option<&'static str>,
64 pub matmul_fallback_reason: Option<&'static str>,
66 pub tensor_core_eligible: bool,
68 pub output_drift_abs_tolerance: f32,
70}
71
72#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct QuantizedLinear4BitSpec {
80 pub in_dim: u32,
82 pub out_dim: u32,
84 pub weight_type: DataType,
86}
87
88impl QuantizedLinear4BitSpec {
89 #[must_use]
91 pub fn affine_grouped(in_dim: u32, out_dim: u32, group_size: u32) -> Self {
92 Self {
93 in_dim,
94 out_dim,
95 weight_type: DataType::Quantized {
96 storage: Box::new(DataType::I4),
97 scale: QuantizationScale::PerGroup { group_size },
98 zero_point: QuantizationZeroPoint::PerGroup { group_size },
99 },
100 }
101 }
102
103 fn affine_group_size(&self) -> Result<u32, String> {
104 match &self.weight_type {
105 DataType::Quantized {
106 storage,
107 scale: QuantizationScale::PerGroup { group_size },
108 zero_point:
109 QuantizationZeroPoint::PerGroup {
110 group_size: zp_group_size,
111 },
112 } => {
113 if storage.as_ref() != &DataType::I4 {
114 return Err(format!(
115 "Fix: grouped INT4 linear requires DataType::Quantized storage I4, got {storage}."
116 ));
117 }
118 if group_size != zp_group_size {
119 return Err(format!(
120 "Fix: grouped INT4 linear requires scale and zero-point group sizes to match, got scale={group_size}, zero_point={zp_group_size}."
121 ));
122 }
123 if *group_size == 0 {
124 return Err(
125 "Fix: grouped INT4 linear requires quantized group_size > 0.".to_string()
126 );
127 }
128 Ok(*group_size)
129 }
130 other => Err(format!(
131 "Fix: grouped INT4 linear requires DataType::Quantized<I4; PerGroup scale; PerGroup zero-point>, got {other}."
132 )),
133 }
134 }
135}
136
137pub fn linear_4bit_affine_grouped_planner_evidence(
142 spec: &QuantizedLinear4BitSpec,
143) -> Result<QuantizedLinear4BitPlannerEvidence, String> {
144 let group_size = spec.affine_group_size()?;
145 quantized_linear_4bit_planner_evidence(spec.in_dim, spec.out_dim, group_size)
146}
147
148fn quantized_linear_4bit_planner_evidence(
149 in_dim: u32,
150 out_dim: u32,
151 group_size: u32,
152) -> Result<QuantizedLinear4BitPlannerEvidence, String> {
153 if in_dim == 0 {
154 return Err(
155 "Fix: linear_4bit_affine_grouped planner evidence requires in_dim > 0.".to_string(),
156 );
157 }
158 if out_dim == 0 {
159 return Err(
160 "Fix: linear_4bit_affine_grouped planner evidence requires out_dim > 0.".to_string(),
161 );
162 }
163 if group_size == 0 {
164 return Err(
165 "Fix: linear_4bit_affine_grouped planner evidence requires group_size > 0.".to_string(),
166 );
167 }
168 if in_dim % 8 != 0 {
169 return Err(format!(
170 "Fix: linear_4bit_affine_grouped planner evidence in_dim={in_dim} is not divisible by 8."
171 ));
172 }
173
174 let packed_words = (in_dim / 8).checked_mul(out_dim).ok_or_else(|| {
175 "Fix: linear_4bit_affine_grouped planner evidence packed weights overflow u32.".to_string()
176 })?;
177 let group_count = in_dim.div_ceil(group_size);
178 let sidecar_values = group_count.checked_mul(out_dim).ok_or_else(|| {
179 "Fix: linear_4bit_affine_grouped planner evidence sidecars overflow u32.".to_string()
180 })?;
181 let matmul_shape = MatrixShape {
182 m: out_dim,
183 k: in_dim,
184 n: 1,
185 };
186 let matmul_tile = AFFINE_GROUPED_LANES_PER_OUTPUT;
187 let matmul_plan = plan_matmul_kernel(
188 &DataType::F32,
189 matmul_shape,
190 matmul_tile,
191 1,
192 F32MatmulMode::StrictF32,
193 MatmulKernelCapabilities::current_codegen(),
194 );
195 let dequantized_weight_bytes = u64::from(in_dim)
196 .saturating_mul(u64::from(out_dim))
197 .saturating_mul(core::mem::size_of::<f32>() as u64);
198 let packed_weight_bytes = u64::from(packed_words) * core::mem::size_of::<u32>() as u64;
199 let sidecar_bytes = u64::from(sidecar_values)
200 .saturating_mul((core::mem::size_of::<f32>() + core::mem::size_of::<u32>()) as u64);
201 let output_bytes = u64::from(out_dim) * core::mem::size_of::<f32>() as u64;
202
203 Ok(QuantizedLinear4BitPlannerEvidence {
204 in_dim,
205 out_dim,
206 group_size,
207 group_count,
208 packed_weight_bytes,
209 dequantized_weight_bytes,
210 sidecar_bytes,
211 bias_bytes: output_bytes,
212 output_bytes,
213 dequant_bytes_elided: dequantized_weight_bytes,
214 matmul_m: matmul_shape.m,
215 matmul_k: matmul_shape.k,
216 matmul_n: matmul_shape.n,
217 matmul_tile,
218 matmul_selected_path: matmul_path_label(matmul_plan.selected_path),
219 matmul_candidate_path: matmul_plan.candidate_path.map(matmul_path_label),
220 matmul_fallback_reason: matmul_fallback_label(&matmul_plan),
221 tensor_core_eligible: matmul_plan.selected_path != MatmulKernelPath::Cooperative,
222 output_drift_abs_tolerance: LINEAR_4BIT_AFFINE_GROUPED_OUTPUT_DRIFT_ABS_TOLERANCE,
223 })
224}
225
226fn matmul_path_label(path: MatmulKernelPath) -> &'static str {
227 match path {
228 MatmulKernelPath::Cooperative => "cooperative",
229 MatmulKernelPath::TensorCoreF16M16N8K16 => "tensor_core_f16_m16n8k16",
230 MatmulKernelPath::TensorCoreBf16M16N8K16 => "tensor_core_bf16_m16n8k16",
231 MatmulKernelPath::TensorCoreTf32M16N8K4 => "tensor_core_tf32_m16n8k4",
232 }
233}
234
235fn matmul_fallback_label(plan: &MatmulKernelPlan) -> Option<&'static str> {
236 match plan.fallback_reason {
237 Some(MatmulFallbackReason::StrictF32Requested) => Some("strict_f32_requested"),
238 Some(MatmulFallbackReason::UnsupportedDtype) => Some("unsupported_dtype"),
239 Some(MatmulFallbackReason::TileSizeMismatch { .. }) => Some("tile_size_mismatch"),
240 Some(MatmulFallbackReason::RaggedTileUnsupported) => Some("ragged_tile_unsupported"),
241 Some(MatmulFallbackReason::SplitKUnsupported) => Some("split_k_unsupported"),
242 Some(MatmulFallbackReason::TensorCoreDtypeUnsupported) => {
243 Some("tensor_core_dtype_unsupported")
244 }
245 None => None,
246 }
247}
248
249pub fn linear_4bit(
257 x: &str,
258 w_packed: &str,
259 b: &str,
260 out: &str,
261 in_dim: u32,
262 out_dim: u32,
263) -> Result<Program, String> {
264 if in_dim == 0 {
265 return Err("Fix: linear_4bit in_dim=0 is invalid: empty reduction".to_string());
266 }
267 if out_dim == 0 {
268 return Err("Fix: linear_4bit out_dim=0 is invalid: empty output".to_string());
269 }
270 if in_dim % 8 != 0 {
271 return Err(format!(
272 "Fix: linear_4bit in_dim={in_dim} is not divisible by 8; pad weights to a multiple of 8."
273 ));
274 }
275 let u32s_per_col = in_dim / 8;
276 let total_u32s = u32s_per_col.checked_mul(out_dim).ok_or_else(|| {
277 "Fix: linear_4bit in_dim/8 * out_dim overflows u32; reduce dimensions.".to_string()
278 })?;
279
280 let i = Expr::var("i");
281 let k = Expr::var("k");
282
283 let packed_idx = Expr::add(
285 Expr::mul(Expr::div(k.clone(), Expr::u32(8)), Expr::u32(out_dim)),
286 i.clone(),
287 );
288 let shift = Expr::mul(Expr::rem(k.clone(), Expr::u32(8)), Expr::u32(4));
290 let nibble = Expr::bitand(
292 Expr::shr(Expr::load(w_packed, packed_idx), shift),
293 Expr::u32(0xF),
294 );
295 let weight_f32 = Expr::cast(DataType::F32, nibble);
297
298 let body = vec![
299 Node::let_bind("i", Expr::InvocationId { axis: 0 }),
300 Node::if_then(
301 Expr::lt(i.clone(), Expr::u32(out_dim)),
302 vec![
303 Node::let_bind("acc", Expr::load(b, i.clone())),
304 Node::loop_for(
305 "k",
306 Expr::u32(0),
307 Expr::u32(in_dim),
308 vec![Node::assign(
309 "acc",
310 Expr::add(
311 Expr::var("acc"),
312 Expr::mul(Expr::load(x, k.clone()), weight_f32.clone()),
313 ),
314 )],
315 ),
316 Node::Store {
317 buffer: out.into(),
318 index: i,
319 value: Expr::var("acc"),
320 },
321 ],
322 ),
323 ];
324
325 Ok(Program::wrapped(
326 vec![
327 BufferDecl::storage(x, 0, BufferAccess::ReadOnly, DataType::F32).with_count(in_dim),
328 BufferDecl::storage(w_packed, 1, BufferAccess::ReadOnly, DataType::U32)
329 .with_count(total_u32s),
330 BufferDecl::storage(b, 2, BufferAccess::ReadOnly, DataType::F32).with_count(out_dim),
331 BufferDecl::output(out, 3, DataType::F32).with_count(out_dim),
332 ],
333 INT4_LINEAR_WORKGROUP_SIZE,
334 vec![wrap_anonymous("vyre-libs::nn::linear_4bit", body)],
335 ))
336}
337
338pub fn linear_4bit_affine_grouped(
359 x: &str,
360 w_packed: &str,
361 scale: &str,
362 zero_point: &str,
363 b: &str,
364 out: &str,
365 in_dim: u32,
366 out_dim: u32,
367 group_size: u32,
368) -> Result<Program, String> {
369 if in_dim == 0 {
370 return Err(
371 "Fix: linear_4bit_affine_grouped in_dim=0 is invalid: empty reduction".to_string(),
372 );
373 }
374 if out_dim == 0 {
375 return Err(
376 "Fix: linear_4bit_affine_grouped out_dim=0 is invalid: empty output".to_string(),
377 );
378 }
379 if group_size == 0 {
380 return Err(
381 "Fix: linear_4bit_affine_grouped group_size=0 is invalid: group size must be > 0"
382 .to_string(),
383 );
384 }
385 if in_dim % 8 != 0 {
386 return Err(format!(
387 "Fix: linear_4bit_affine_grouped in_dim={in_dim} is not divisible by 8; pad weights to a multiple of 8."
388 ));
389 }
390 let u32s_per_col = in_dim / 8;
391 let total_u32s = u32s_per_col.checked_mul(out_dim).ok_or_else(|| {
392 "Fix: linear_4bit_affine_grouped in_dim/8 * out_dim overflows u32; reduce dimensions."
393 .to_string()
394 })?;
395 let group_count = in_dim.div_ceil(group_size);
396 let sidecar_count = group_count.checked_mul(out_dim).ok_or_else(|| {
397 "Fix: linear_4bit_affine_grouped group_count*out_dim overflows u32; reduce dimensions."
398 .to_string()
399 })?;
400
401 let tile = AFFINE_GROUPED_LANES_PER_OUTPUT;
402 let chunks = in_dim.div_ceil(tile);
403 let out_idx = Expr::var("out_idx");
404 let local = Expr::var("local");
405 let lane = Expr::var("lane");
406 let k = Expr::var("k");
407 let lane_in_word = Expr::var("lane_in_word");
408 let word_leader_lane = Expr::var("word_leader_lane");
409 let word_leader_k = Expr::var("word_leader_k");
410 let packed_idx = Expr::add(
411 Expr::mul(
412 Expr::div(word_leader_k.clone(), Expr::u32(8)),
413 Expr::u32(out_dim),
414 ),
415 out_idx.clone(),
416 );
417 let shift = Expr::mul(lane_in_word.clone(), Expr::u32(4));
418 let nibble = Expr::bitand(Expr::shr(Expr::var("packed_word"), shift), Expr::u32(0xF));
419 let group = Expr::div(k.clone(), Expr::u32(group_size));
420 let chunk_sidecar_idx = Expr::add(Expr::mul(group, Expr::u32(out_dim)), out_idx.clone());
421 let weight_f32 = Expr::fma(
422 Expr::cast(DataType::F32, nibble),
423 Expr::var("group_scale"),
424 Expr::var("group_zero_offset"),
425 );
426
427 let mut per_output = vec![Node::let_bind("local_acc", Expr::f32(0.0))];
428 if group_size > tile && group_size % tile == 0 {
429 let group_chunks = group_size.div_ceil(tile);
430 per_output.push(Node::loop_for(
431 "group_idx",
432 Expr::u32(0),
433 Expr::u32(group_count),
434 vec![
435 Node::let_bind(
436 "group_base",
437 Expr::mul(Expr::var("group_idx"), Expr::u32(group_size)),
438 ),
439 Node::let_bind(
440 "sidecar_idx",
441 Expr::add(
442 Expr::mul(Expr::var("group_idx"), Expr::u32(out_dim)),
443 out_idx.clone(),
444 ),
445 ),
446 Node::let_bind(
447 "scale_lane",
448 Expr::select(
449 Expr::eq(lane.clone(), Expr::u32(0)),
450 Expr::load(scale, Expr::var("sidecar_idx")),
451 Expr::f32(0.0),
452 ),
453 ),
454 Node::let_bind(
455 "zero_point_lane",
456 Expr::select(
457 Expr::eq(lane.clone(), Expr::u32(0)),
458 Expr::load(zero_point, Expr::var("sidecar_idx")),
459 Expr::u32(0),
460 ),
461 ),
462 Node::let_bind(
463 "group_scale",
464 Expr::subgroup_shuffle(Expr::var("scale_lane"), Expr::u32(0)),
465 ),
466 Node::let_bind(
467 "group_zero_point",
468 Expr::subgroup_shuffle(Expr::var("zero_point_lane"), Expr::u32(0)),
469 ),
470 Node::let_bind(
471 "group_zero_offset",
472 Expr::mul(
473 Expr::f32(-1.0),
474 Expr::mul(
475 Expr::cast(DataType::F32, Expr::var("group_zero_point")),
476 Expr::var("group_scale"),
477 ),
478 ),
479 ),
480 Node::loop_for(
481 "group_chunk",
482 Expr::u32(0),
483 Expr::u32(group_chunks),
484 vec![
485 Node::let_bind(
486 "k",
487 Expr::add(
488 Expr::var("group_base"),
489 Expr::add(
490 Expr::mul(Expr::var("group_chunk"), Expr::u32(tile)),
491 lane.clone(),
492 ),
493 ),
494 ),
495 Node::let_bind("lane_in_word", Expr::bitand(lane.clone(), Expr::u32(7))),
496 Node::let_bind(
497 "word_leader_lane",
498 Expr::bitand(lane.clone(), Expr::u32(0xffff_fff8)),
499 ),
500 Node::let_bind(
501 "word_leader_k",
502 Expr::add(
503 Expr::var("group_base"),
504 Expr::add(
505 Expr::mul(Expr::var("group_chunk"), Expr::u32(tile)),
506 word_leader_lane.clone(),
507 ),
508 ),
509 ),
510 Node::let_bind(
511 "packed_word_lane",
512 Expr::select(
513 Expr::and(
514 Expr::eq(lane_in_word.clone(), Expr::u32(0)),
515 Expr::lt(word_leader_k.clone(), Expr::u32(in_dim)),
516 ),
517 Expr::load(w_packed, packed_idx.clone()),
518 Expr::u32(0),
519 ),
520 ),
521 Node::let_bind(
522 "packed_word",
523 Expr::subgroup_shuffle(Expr::var("packed_word_lane"), word_leader_lane),
524 ),
525 Node::if_then(
526 Expr::lt(k.clone(), Expr::u32(in_dim)),
527 vec![Node::assign(
528 "local_acc",
529 Expr::fma(
530 Expr::load(x, k.clone()),
531 weight_f32.clone(),
532 Expr::var("local_acc"),
533 ),
534 )],
535 ),
536 ],
537 ),
538 ],
539 ));
540 } else {
541 per_output.push(Node::loop_for(
542 "chunk",
543 Expr::u32(0),
544 Expr::u32(chunks),
545 vec![
546 Node::let_bind(
547 "k",
548 Expr::add(Expr::mul(Expr::var("chunk"), Expr::u32(tile)), lane.clone()),
549 ),
550 Node::let_bind("lane_in_word", Expr::bitand(lane.clone(), Expr::u32(7))),
551 Node::let_bind(
552 "word_leader_lane",
553 Expr::bitand(lane.clone(), Expr::u32(0xffff_fff8)),
554 ),
555 Node::let_bind(
556 "word_leader_k",
557 Expr::add(
558 Expr::mul(Expr::var("chunk"), Expr::u32(tile)),
559 word_leader_lane.clone(),
560 ),
561 ),
562 Node::let_bind(
563 "packed_word_lane",
564 Expr::select(
565 Expr::and(
566 Expr::eq(lane_in_word.clone(), Expr::u32(0)),
567 Expr::lt(word_leader_k.clone(), Expr::u32(in_dim)),
568 ),
569 Expr::load(w_packed, packed_idx),
570 Expr::u32(0),
571 ),
572 ),
573 Node::let_bind(
574 "packed_word",
575 Expr::subgroup_shuffle(Expr::var("packed_word_lane"), word_leader_lane),
576 ),
577 Node::let_bind("sidecar_idx", chunk_sidecar_idx),
578 Node::let_bind("group_scale", Expr::load(scale, Expr::var("sidecar_idx"))),
579 Node::let_bind(
580 "group_zero_point",
581 Expr::load(zero_point, Expr::var("sidecar_idx")),
582 ),
583 Node::let_bind(
584 "group_zero_offset",
585 Expr::mul(
586 Expr::f32(-1.0),
587 Expr::mul(
588 Expr::cast(DataType::F32, Expr::var("group_zero_point")),
589 Expr::var("group_scale"),
590 ),
591 ),
592 ),
593 Node::if_then(
594 Expr::lt(k.clone(), Expr::u32(in_dim)),
595 vec![Node::assign(
596 "local_acc",
597 Expr::fma(Expr::load(x, k.clone()), weight_f32, Expr::var("local_acc")),
598 )],
599 ),
600 ],
601 ));
602 }
603 per_output.push(Node::let_bind(
604 "warp_sum",
605 Expr::subgroup_add(Expr::var("local_acc")),
606 ));
607 per_output.push(Node::if_then(
608 Expr::eq(lane.clone(), Expr::u32(0)),
609 vec![Node::Store {
610 buffer: out.into(),
611 index: out_idx.clone(),
612 value: Expr::add(Expr::load(b, out_idx.clone()), Expr::var("warp_sum")),
613 }],
614 ));
615
616 let body = vec![
617 Node::let_bind("local", Expr::LocalId { axis: 0 }),
618 Node::let_bind(
619 "warp",
620 Expr::div(local.clone(), Expr::u32(AFFINE_GROUPED_LANES_PER_OUTPUT)),
621 ),
622 Node::let_bind(
623 "lane",
624 Expr::rem(local.clone(), Expr::u32(AFFINE_GROUPED_LANES_PER_OUTPUT)),
625 ),
626 Node::loop_for(
627 "warp_output",
628 Expr::u32(0),
629 Expr::u32(AFFINE_GROUPED_OUTPUTS_PER_WARP),
630 vec![
631 Node::let_bind(
632 "out_idx",
633 Expr::add(
634 Expr::add(
635 Expr::mul(
636 Expr::WorkgroupId { axis: 0 },
637 Expr::u32(AFFINE_GROUPED_OUTPUTS_PER_WORKGROUP),
638 ),
639 Expr::mul(
640 Expr::var("warp_output"),
641 Expr::u32(AFFINE_GROUPED_WARPS_PER_WORKGROUP),
642 ),
643 ),
644 Expr::var("warp"),
645 ),
646 ),
647 Node::if_then(Expr::lt(out_idx.clone(), Expr::u32(out_dim)), per_output),
648 ],
649 ),
650 ];
651 let output_workgroups = out_dim.div_ceil(AFFINE_GROUPED_OUTPUTS_PER_WORKGROUP);
652 let padded_output_count = output_workgroups
653 .checked_mul(AFFINE_GROUPED_WORKGROUP_SIZE[0])
654 .ok_or_else(|| {
655 "Fix: linear_4bit_affine_grouped output workgroups overflow u32; reduce dimensions."
656 .to_string()
657 })?;
658 let output_byte_len = (out_dim as usize)
659 .checked_mul(core::mem::size_of::<f32>())
660 .ok_or_else(|| {
661 "Fix: linear_4bit_affine_grouped output byte length overflows usize; reduce dimensions."
662 .to_string()
663 })?;
664
665 Ok(Program::wrapped(
666 vec![
667 BufferDecl::storage(x, 0, BufferAccess::ReadOnly, DataType::F32).with_count(in_dim),
668 BufferDecl::storage(w_packed, 1, BufferAccess::ReadOnly, DataType::U32)
669 .with_count(total_u32s),
670 BufferDecl::storage(scale, 2, BufferAccess::ReadOnly, DataType::F32)
671 .with_count(sidecar_count),
672 BufferDecl::storage(zero_point, 3, BufferAccess::ReadOnly, DataType::U32)
673 .with_count(sidecar_count),
674 BufferDecl::storage(b, 4, BufferAccess::ReadOnly, DataType::F32).with_count(out_dim),
675 BufferDecl::output(out, 5, DataType::F32)
676 .with_count(padded_output_count)
677 .with_output_byte_range(0..output_byte_len),
678 ],
679 AFFINE_GROUPED_WORKGROUP_SIZE,
680 vec![wrap_anonymous(AFFINE_GROUPED_OP_ID, body)],
681 ))
682}
683
684pub fn linear_4bit_affine_grouped_typed(
690 spec: &QuantizedLinear4BitSpec,
691 x: &str,
692 w_packed: &str,
693 scale: &str,
694 zero_point: &str,
695 b: &str,
696 out: &str,
697) -> Result<Program, String> {
698 let group_size = spec.affine_group_size()?;
699 linear_4bit_affine_grouped(
700 x,
701 w_packed,
702 scale,
703 zero_point,
704 b,
705 out,
706 spec.in_dim,
707 spec.out_dim,
708 group_size,
709 )
710}
711
712#[cfg(test)]
713mod tests {
714 use super::*;
715 use crate::test_support::byte_pack::f32_bytes;
716 use crate::test_support::byte_pack::u32_bytes;
717 use vyre_reference::value::Value;
718
719 fn expr_contains_subgroup_shuffle(expr: &Expr) -> bool {
720 match expr {
721 Expr::Load { index, .. }
722 | Expr::Cast { value: index, .. }
723 | Expr::SubgroupReduce { value: index, .. }
724 | Expr::SubgroupBallot { cond: index }
725 | Expr::UnOp { operand: index, .. } => expr_contains_subgroup_shuffle(index),
726 Expr::BinOp { left, right, .. }
727 | Expr::SubgroupShuffle {
728 value: left,
729 lane: right,
730 } => {
731 matches!(expr, Expr::SubgroupShuffle { .. })
732 || expr_contains_subgroup_shuffle(left)
733 || expr_contains_subgroup_shuffle(right)
734 }
735 Expr::Select {
736 cond,
737 true_val,
738 false_val,
739 } => {
740 expr_contains_subgroup_shuffle(cond)
741 || expr_contains_subgroup_shuffle(true_val)
742 || expr_contains_subgroup_shuffle(false_val)
743 }
744 Expr::Fma { a, b, c } => {
745 expr_contains_subgroup_shuffle(a)
746 || expr_contains_subgroup_shuffle(b)
747 || expr_contains_subgroup_shuffle(c)
748 }
749 Expr::Atomic {
750 index,
751 expected,
752 value,
753 ..
754 } => {
755 expr_contains_subgroup_shuffle(index)
756 || expected
757 .as_deref()
758 .is_some_and(expr_contains_subgroup_shuffle)
759 || expr_contains_subgroup_shuffle(value)
760 }
761 Expr::Call { args, .. } => args.iter().any(expr_contains_subgroup_shuffle),
762 Expr::LitU32(_)
763 | Expr::LitI32(_)
764 | Expr::LitF32(_)
765 | Expr::LitBool(_)
766 | Expr::Var(_)
767 | Expr::BufLen { .. }
768 | Expr::InvocationId { .. }
769 | Expr::WorkgroupId { .. }
770 | Expr::LocalId { .. }
771 | Expr::SubgroupLocalId
772 | Expr::SubgroupSize
773 | Expr::Opaque(_) => false,
774 _ => false,
775 }
776 }
777
778 fn nodes_contain_subgroup_shuffle(nodes: &[Node]) -> bool {
779 nodes.iter().any(|node| match node {
780 Node::Let { value, .. } | Node::Assign { value, .. } => {
781 expr_contains_subgroup_shuffle(value)
782 }
783 Node::Store { index, value, .. } => {
784 expr_contains_subgroup_shuffle(index) || expr_contains_subgroup_shuffle(value)
785 }
786 Node::If {
787 cond,
788 then,
789 otherwise,
790 } => {
791 expr_contains_subgroup_shuffle(cond)
792 || nodes_contain_subgroup_shuffle(then)
793 || nodes_contain_subgroup_shuffle(otherwise)
794 }
795 Node::Loop { from, to, body, .. } => {
796 expr_contains_subgroup_shuffle(from)
797 || expr_contains_subgroup_shuffle(to)
798 || nodes_contain_subgroup_shuffle(body)
799 }
800 Node::AsyncLoad { offset, size, .. } | Node::AsyncStore { offset, size, .. } => {
801 expr_contains_subgroup_shuffle(offset) || expr_contains_subgroup_shuffle(size)
802 }
803 Node::Trap { address, .. } => expr_contains_subgroup_shuffle(address),
804 Node::Block(body) => nodes_contain_subgroup_shuffle(body),
805 Node::Region { body, .. } => nodes_contain_subgroup_shuffle(body),
806 Node::IndirectDispatch { .. }
807 | Node::AsyncWait { .. }
808 | Node::AllReduce { .. }
809 | Node::AllGather { .. }
810 | Node::ReduceScatter { .. }
811 | Node::Broadcast { .. }
812 | Node::Return
813 | Node::Barrier { .. }
814 | Node::Resume { .. }
815 | Node::Opaque(_) => false,
816 _ => false,
817 })
818 }
819
820 fn collect_loop_vars(nodes: &[Node], vars: &mut Vec<String>) {
821 for node in nodes {
822 match node {
823 Node::If {
824 then, otherwise, ..
825 } => {
826 collect_loop_vars(then, vars);
827 collect_loop_vars(otherwise, vars);
828 }
829 Node::Loop { var, body, .. } => {
830 vars.push(var.to_string());
831 collect_loop_vars(body, vars);
832 }
833 Node::Block(body) => collect_loop_vars(body, vars),
834 Node::Region { body, .. } => collect_loop_vars(body, vars),
835 _ => {}
836 }
837 }
838 }
839
840 fn affine_cpu_reference(
841 x: &[f32],
842 packed: &[u32],
843 scale: &[f32],
844 zero_point: &[u32],
845 bias: &[f32],
846 in_dim: u32,
847 out_dim: u32,
848 group_size: u32,
849 ) -> Vec<f32> {
850 (0..out_dim as usize)
851 .map(|out| {
852 let mut acc = bias[out];
853 for k in 0..in_dim as usize {
854 let word = packed[(k / 8) * out_dim as usize + out];
855 let nibble = ((word >> ((k % 8) * 4)) & 0xF) as f32;
856 let sidecar_idx = (k / group_size as usize) * out_dim as usize + out;
857 acc += x[k] * (nibble - zero_point[sidecar_idx] as f32) * scale[sidecar_idx];
858 }
859 acc
860 })
861 .collect()
862 }
863
864 #[test]
865 fn linear_4bit_matches_unpack_then_linear() {
866 let x = f32_bytes(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]);
869 let col0 = 0x8765_4321u32;
875 let col1 = 0x0000_0000u32;
877 let w = u32_bytes(&[col0, col1]);
878 let b = f32_bytes(&[0.0, 0.0]);
880 let out_size = 2usize * 4;
881
882 let program = linear_4bit("x", "w", "b", "out", 8, 2).unwrap();
883 let outputs = vyre_reference::reference_eval(
884 &program,
885 &[
886 Value::from(x),
887 Value::from(w),
888 Value::from(b),
889 Value::from(vec![0u8; out_size]),
890 ],
891 )
892 .expect("Fix: reference eval must succeed");
893
894 let out_vals: Vec<f32> =
895 vyre_primitives::wire::decode_f32_le_bytes_all(&outputs[0].to_bytes());
896
897 assert!(
899 (out_vals[0] - 204.0).abs() < 1e-4,
900 "expected 204.0, got {}",
901 out_vals[0]
902 );
903 assert!(
905 (out_vals[1] - 0.0).abs() < 1e-4,
906 "expected 0.0, got {}",
907 out_vals[1]
908 );
909 }
910
911 #[test]
912 fn linear_4bit_rejects_indivisible_in_dim() {
913 let err = linear_4bit("x", "w", "b", "out", 7, 4).unwrap_err();
914 assert!(
915 err.contains("divisible by 8"),
916 "error must mention divisibility: {err}"
917 );
918 }
919
920 #[test]
921 fn linear_4bit_affine_grouped_applies_scale_and_zero_point_in_loop() {
922 let x = f32_bytes(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]);
923 let w = u32_bytes(&[0x8765_4321u32, 0x0000_0000u32]);
924 let scale = f32_bytes(&[0.5, 1.0, 2.0, 1.0]);
925 let zero_point = u32_bytes(&[1, 0, 4, 0]);
926 let b = f32_bytes(&[0.0, 3.0]);
927
928 let program = linear_4bit_affine_grouped("x", "w", "scale", "zp", "b", "out", 8, 2, 4)
929 .expect("Fix: affine grouped int4 linear fixture must build");
930 assert_eq!(
931 program.workgroup_size(),
932 AFFINE_GROUPED_WORKGROUP_SIZE,
933 "Fix: grouped INT4 linear must keep the CUDA-measured cooperative release launch shape."
934 );
935 let outputs = vyre_reference::reference_eval(
936 &program,
937 &[
938 Value::from(x),
939 Value::from(w),
940 Value::from(scale),
941 Value::from(zero_point),
942 Value::from(b),
943 Value::from(vec![0u8; 8]),
944 ],
945 )
946 .expect("Fix: affine grouped int4 linear must execute");
947
948 let out_vals = vyre_primitives::wire::decode_f32_le_bytes_all(&outputs[0].to_bytes());
949
950 assert!(
951 (out_vals[0] - 150.0).abs() < 1e-4,
952 "expected fused affine dequantized dot product 150.0, got {}",
953 out_vals[0]
954 );
955 let evidence = linear_4bit_affine_grouped_planner_evidence(
956 &QuantizedLinear4BitSpec::affine_grouped(8, 2, 4),
957 )
958 .expect("Fix: planner evidence fixture must build");
959 assert!(
960 (out_vals[0] - 150.0).abs() <= evidence.output_drift_abs_tolerance,
961 "Fix: runtime output drift must stay within planner evidence tolerance."
962 );
963 assert!(
964 (out_vals[1] - 3.0).abs() < 1e-4,
965 "expected bias-only second output 3.0, got {}",
966 out_vals[1]
967 );
968 }
969
970 #[test]
971 fn linear_4bit_affine_grouped_broadcasts_packed_weight_words() {
972 let program =
973 linear_4bit_affine_grouped("x", "w", "scale", "zp", "b", "out", 256, 4096, 64)
974 .expect("Fix: grouped INT4 affine release fixture must build");
975
976 assert!(
977 nodes_contain_subgroup_shuffle(program.entry()),
978 "Fix: grouped INT4 release kernel must broadcast each packed u32 weight word across its 8 nibble lanes instead of reloading it per MAC."
979 );
980 }
981
982 #[test]
983 fn linear_4bit_affine_grouped_hoists_sidecars_for_aligned_release_groups() {
984 let aligned =
985 linear_4bit_affine_grouped("x", "w", "scale", "zp", "b", "out", 256, 4096, 64)
986 .expect("Fix: aligned grouped INT4 release fixture must build");
987 let mut aligned_loops = Vec::new();
988 collect_loop_vars(aligned.entry(), &mut aligned_loops);
989 assert!(
990 aligned_loops.iter().any(|var| var == "group_idx")
991 && aligned_loops.iter().any(|var| var == "group_chunk"),
992 "Fix: release-aligned grouped INT4 must load and broadcast sidecars once per quantization group, then scan that group's chunks: {aligned_loops:?}"
993 );
994 assert!(
995 !aligned_loops.iter().any(|var| var == "chunk"),
996 "Fix: release-aligned grouped INT4 must not use the per-chunk sidecar broadcast path: {aligned_loops:?}"
997 );
998
999 let single_tile =
1000 linear_4bit_affine_grouped("x", "w", "scale", "zp", "b", "out", 32, 8, 32)
1001 .expect("Fix: single-tile grouped INT4 fixture must build");
1002 let mut single_tile_loops = Vec::new();
1003 collect_loop_vars(single_tile.entry(), &mut single_tile_loops);
1004 assert!(
1005 single_tile_loops.iter().any(|var| var == "chunk")
1006 && !single_tile_loops.iter().any(|var| var == "group_idx"),
1007 "Fix: single-tile and non-tile-aligned quantization groups must retain chunk-indexed sidecar selection for correctness: {single_tile_loops:?}"
1008 );
1009 }
1010
1011 #[test]
1012 fn linear_4bit_affine_grouped_rejects_zero_group_size() {
1013 let err =
1014 linear_4bit_affine_grouped("x", "w", "scale", "zp", "b", "out", 8, 4, 0).unwrap_err();
1015 assert!(
1016 err.contains("group_size=0"),
1017 "error must identify invalid group size: {err}"
1018 );
1019 }
1020
1021 #[test]
1022 fn typed_affine_grouped_builder_uses_quantized_metadata() {
1023 let spec = QuantizedLinear4BitSpec::affine_grouped(32, 7, 8);
1024 let program = linear_4bit_affine_grouped_typed(&spec, "x", "w", "scale", "zp", "b", "out")
1025 .expect("Fix: valid typed grouped INT4 spec must build");
1026
1027 assert_eq!(program.buffers()[1].name(), "w");
1028 assert_eq!(program.buffers()[1].element(), DataType::U32);
1029 assert_eq!(program.buffers()[1].count(), 28);
1030 assert!(matches!(
1031 spec.weight_type,
1032 DataType::Quantized {
1033 scale: QuantizationScale::PerGroup { group_size: 8 },
1034 zero_point: QuantizationZeroPoint::PerGroup { group_size: 8 },
1035 ..
1036 }
1037 ));
1038 }
1039
1040 #[test]
1041 fn typed_affine_grouped_planner_evidence_records_matmul_and_dequant_contract() {
1042 let spec = QuantizedLinear4BitSpec::affine_grouped(256, 4096, 64);
1043 let evidence = linear_4bit_affine_grouped_planner_evidence(&spec)
1044 .expect("Fix: release grouped INT4 evidence must build");
1045
1046 assert_eq!(evidence.in_dim, 256);
1047 assert_eq!(evidence.out_dim, 4096);
1048 assert_eq!(evidence.group_size, 64);
1049 assert_eq!(evidence.group_count, 4);
1050 assert_eq!(evidence.packed_weight_bytes, 524_288);
1051 assert_eq!(evidence.dequantized_weight_bytes, 4_194_304);
1052 assert_eq!(
1053 evidence.dequant_bytes_elided,
1054 evidence.dequantized_weight_bytes
1055 );
1056 assert_eq!(evidence.sidecar_bytes, 131_072);
1057 assert_eq!(evidence.bias_bytes, 16_384);
1058 assert_eq!(evidence.output_bytes, 16_384);
1059 assert_eq!(evidence.matmul_m, 4096);
1060 assert_eq!(evidence.matmul_k, 256);
1061 assert_eq!(evidence.matmul_n, 1);
1062 assert_eq!(evidence.matmul_tile, AFFINE_GROUPED_LANES_PER_OUTPUT);
1063 assert_eq!(evidence.matmul_selected_path, "cooperative");
1064 assert_eq!(evidence.matmul_candidate_path, None);
1065 assert_eq!(
1066 evidence.matmul_fallback_reason,
1067 Some("strict_f32_requested")
1068 );
1069 assert!(!evidence.tensor_core_eligible);
1070 assert_eq!(
1071 evidence.output_drift_abs_tolerance,
1072 LINEAR_4BIT_AFFINE_GROUPED_OUTPUT_DRIFT_ABS_TOLERANCE
1073 );
1074 }
1075
1076 #[test]
1077 fn typed_affine_grouped_builder_rejects_mismatched_quantized_metadata() {
1078 let bad_storage = QuantizedLinear4BitSpec {
1079 in_dim: 32,
1080 out_dim: 4,
1081 weight_type: DataType::Quantized {
1082 storage: Box::new(DataType::I8),
1083 scale: QuantizationScale::PerGroup { group_size: 8 },
1084 zero_point: QuantizationZeroPoint::PerGroup { group_size: 8 },
1085 },
1086 };
1087 let error =
1088 linear_4bit_affine_grouped_typed(&bad_storage, "x", "w", "scale", "zp", "b", "out")
1089 .unwrap_err();
1090 assert!(
1091 error.contains("storage I4"),
1092 "Fix: storage mismatch should be explicit: {error}"
1093 );
1094
1095 let bad_sidecar = QuantizedLinear4BitSpec {
1096 in_dim: 32,
1097 out_dim: 4,
1098 weight_type: DataType::Quantized {
1099 storage: Box::new(DataType::I4),
1100 scale: QuantizationScale::PerGroup { group_size: 8 },
1101 zero_point: QuantizationZeroPoint::PerGroup { group_size: 16 },
1102 },
1103 };
1104 let error =
1105 linear_4bit_affine_grouped_typed(&bad_sidecar, "x", "w", "scale", "zp", "b", "out")
1106 .unwrap_err();
1107 assert!(
1108 error.contains("group sizes to match"),
1109 "Fix: sidecar mismatch should be explicit: {error}"
1110 );
1111 }
1112
1113 #[test]
1114 fn generated_typed_affine_grouped_specs_build_or_reject_by_metadata_contract() {
1115 let mut accepted = 0usize;
1116 let mut rejected = 0usize;
1117 for in_dim in [8u32, 10, 16, 18, 24, 32, 64, 128] {
1118 for out_dim in [1u32, 2, 3, 7, 16, 31] {
1119 for group_size in [1u32, 2, 4, 8, 16, 32] {
1120 let spec = QuantizedLinear4BitSpec::affine_grouped(in_dim, out_dim, group_size);
1121 let result = linear_4bit_affine_grouped_typed(
1122 &spec, "x", "w", "scale", "zp", "b", "out",
1123 );
1124 if in_dim % 8 == 0 {
1125 let program = result.expect("Fix: generated valid typed spec must build");
1126 let output = &program.buffers()[5];
1127 assert!(
1128 output.count() >= out_dim,
1129 "Fix: grouped INT4 output storage must cover the logical outputs after launch padding."
1130 );
1131 assert_eq!(
1132 output.output_byte_range(),
1133 Some(0..(out_dim as usize * core::mem::size_of::<f32>())),
1134 "Fix: grouped INT4 output byte range must trim padded launch storage to the logical tensor."
1135 );
1136 accepted += 1;
1137 } else {
1138 let error = result.expect_err(
1139 "Fix: generated indivisible typed spec must reject before dispatch",
1140 );
1141 assert!(error.contains("divisible by 8"));
1142 rejected += 1;
1143 }
1144 }
1145 }
1146 }
1147
1148 assert!(
1149 accepted + rejected >= 216,
1150 "Fix: generated typed quantized specs should cover hundreds of layouts"
1151 );
1152 }
1153
1154 #[test]
1155 fn generated_affine_grouped_vectors_match_cpu_oracle() {
1156 let mut checked = 0usize;
1157 for out_dim in [1u32, 2, 3, 5, 8, 13, 21, 32] {
1158 for group_size in [1u32, 2, 4, 8, 16, 32] {
1159 for seed in 0..48u32 {
1160 let in_dim = 32u32;
1161 let group_count = in_dim.div_ceil(group_size);
1162 let x = (0..in_dim)
1163 .map(|k| ((k.wrapping_mul(3).wrapping_add(seed)) % 19) as f32)
1164 .collect::<Vec<_>>();
1165 let mut packed = vec![0u32; (in_dim / 8 * out_dim) as usize];
1166 for block in 0..(in_dim / 8) {
1167 for out in 0..out_dim {
1168 let mut word = 0u32;
1169 for lane in 0..8 {
1170 let k = block * 8 + lane;
1171 let nibble = k
1172 .wrapping_mul(7)
1173 .wrapping_add(out.wrapping_mul(11))
1174 .wrapping_add(seed)
1175 & 0xF;
1176 word |= nibble << (lane * 4);
1177 }
1178 packed[(block * out_dim + out) as usize] = word;
1179 }
1180 }
1181 let mut scale = vec![0.0f32; (group_count * out_dim) as usize];
1182 let mut zero_point = vec![0u32; (group_count * out_dim) as usize];
1183 for group in 0..group_count {
1184 for out in 0..out_dim {
1185 let idx = (group * out_dim + out) as usize;
1186 scale[idx] = match (group + out + seed) & 3 {
1187 0 => 0.25,
1188 1 => 0.5,
1189 2 => 1.0,
1190 _ => 2.0,
1191 };
1192 zero_point[idx] =
1193 group.wrapping_mul(5).wrapping_add(out).wrapping_add(seed) & 0xF;
1194 }
1195 }
1196 let bias = (0..out_dim)
1197 .map(|out| ((out + seed) & 7) as f32)
1198 .collect::<Vec<_>>();
1199
1200 let program = linear_4bit_affine_grouped(
1201 "x", "w", "scale", "zp", "b", "out", in_dim, out_dim, group_size,
1202 )
1203 .expect("Fix: generated affine grouped fixture must build");
1204 let outputs = vyre_reference::reference_eval(
1205 &program,
1206 &[
1207 Value::from(f32_bytes(&x)),
1208 Value::from(u32_bytes(&packed)),
1209 Value::from(f32_bytes(&scale)),
1210 Value::from(u32_bytes(&zero_point)),
1211 Value::from(f32_bytes(&bias)),
1212 Value::from(vec![0u8; out_dim as usize * 4]),
1213 ],
1214 )
1215 .unwrap_or_else(|error| {
1216 panic!(
1217 "Fix: generated affine grouped fixture must execute for out_dim={out_dim}, group_size={group_size}, seed={seed}: {error}"
1218 )
1219 });
1220 let actual =
1221 vyre_primitives::wire::decode_f32_le_bytes_all(&outputs[0].to_bytes());
1222 let expected = affine_cpu_reference(
1223 &x,
1224 &packed,
1225 &scale,
1226 &zero_point,
1227 &bias,
1228 in_dim,
1229 out_dim,
1230 group_size,
1231 );
1232
1233 assert_eq!(
1234 actual, expected,
1235 "generated affine grouped vector mismatch for out_dim={out_dim}, group_size={group_size}, seed={seed}"
1236 );
1237 checked += out_dim as usize;
1238 }
1239 }
1240 }
1241
1242 assert!(
1243 checked >= 24_000,
1244 "Fix: generated affine grouped coverage should exercise tens of thousands of output vectors, got {checked}"
1245 );
1246 }
1247}
1248
1249inventory::submit! {
1250 crate::harness::OpEntry {
1251 id: "vyre-libs::nn::linear_4bit",
1252 build: || {
1253 linear_4bit("x", "w", "b", "out", 8, 4).unwrap_or_else(|error| {
1254 crate::builder::invalid_output_program(
1255 "vyre-libs::nn::linear_4bit",
1256 "out",
1257 DataType::F32,
1258 error,
1259 )
1260 })
1261 },
1262 test_inputs: Some(|| {
1263 let x: Vec<f32> = (0..8).map(|i| i as f32).collect();
1264 let w: Vec<u32> = vec![0x7654_3210, 0xFEDC_BA98, 0x1111_1111, 0x0000_0000];
1265 let b: Vec<f32> = vec![0.0; 4];
1266 vec![vec![
1267 vyre_primitives::wire::pack_f32_slice(&x),
1268 vyre_primitives::wire::pack_u32_slice(&w),
1269 vyre_primitives::wire::pack_f32_slice(&b),
1270 ]]
1271 }),
1272 expected_output: Some(|| {
1273 let out = [140.0f32, 364.0, 28.0, 0.0];
1274 vec![vec![vyre_primitives::wire::pack_f32_slice(&out)]]
1275 }),
1276 category: Some("nn"),
1277 }
1278}
1279
1280inventory::submit! {
1281 crate::harness::OpEntry {
1282 id: "vyre-libs::nn::linear_4bit_affine_grouped",
1283 build: || {
1284 linear_4bit_affine_grouped("x", "w", "scale", "zp", "b", "out", 8, 2, 4)
1285 .unwrap_or_else(|error| {
1286 crate::builder::invalid_output_program(
1287 "vyre-libs::nn::linear_4bit_affine_grouped",
1288 "out",
1289 DataType::F32,
1290 error,
1291 )
1292 })
1293 },
1294 test_inputs: Some(|| {
1295 let x = [1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
1296 let w = [0x8765_4321u32, 0x0000_0000u32];
1297 let scale = [0.5f32, 1.0, 2.0, 1.0];
1298 let zp = [1u32, 0, 4, 0];
1299 let b = [0.0f32, 3.0];
1300 vec![vec![
1301 vyre_primitives::wire::pack_f32_slice(&x),
1302 vyre_primitives::wire::pack_u32_slice(&w),
1303 vyre_primitives::wire::pack_f32_slice(&scale),
1304 vyre_primitives::wire::pack_u32_slice(&zp),
1305 vyre_primitives::wire::pack_f32_slice(&b),
1306 ]]
1307 }),
1308 expected_output: Some(|| {
1309 let out = [150.0f32, 3.0];
1310 vec![vec![vyre_primitives::wire::pack_f32_slice(&out)]]
1311 }),
1312 category: Some("nn"),
1313 }
1314}