use serde::Serialize;
use trueno_gpu::driver::CudaContext;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Q4kVariant {
Legacy,
Wide,
Vectorized,
MwvDp4a,
HwDp4a,
Mwv,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Q6kVariant {
Legacy,
Mwv,
Dp4a,
HwDp4a,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "snake_case")]
pub struct GpuProfile {
pub q4k: Q4kVariant,
pub q6k: Q6kVariant,
pub mwv_warps: u32,
pub prefill_path: PrefillPathChoice,
pub hgemm_decode: bool,
pub fused_gate_up: bool,
pub fp8_prefill: bool,
pub fp8_decode: bool,
pub w4a16_interleaved: bool,
pub sm_target: String,
pub cc: u32,
}
impl GpuProfile {
pub fn detect(context: &CudaContext) -> Self {
contract_pre_target_parity!();
let (major, minor) = context.compute_capability().unwrap_or((7, 0));
let (ptx_major, ptx_minor) = if major > 9 || (major == 9 && minor > 0) {
(9, 0) } else {
(major, minor)
};
let sm_target = format!("sm_{ptx_major}{ptx_minor}");
let has_dp4a = major > 7 || (major == 7 && minor >= 5);
let num_sms = context.multiprocessor_count().unwrap_or(8) as u32;
let cc = major as u32 * 10 + minor as u32;
let q4k = Self::detect_q4k(has_dp4a, cc);
let q6k = Self::detect_q6k(has_dp4a);
let mwv_warps = Self::detect_mwv_warps();
let prefill_path =
select_prefill_path(cc, std::env::var("BATCHED_PREFILL").ok().as_deref());
let hgemm_decode = Self::detect_hgemm_decode(has_dp4a, num_sms);
let fused_gate_up =
Self::detect_fused_gate_up(&q4k, std::env::var("FUSED_GATE_UP").ok().as_deref());
let fp8_prefill = Self::detect_fp8_prefill(cc);
let fp8_decode = Self::detect_fp8_decode(fp8_prefill, cc);
let w4a16_interleaved = Self::detect_w4a16_interleaved(cc);
Self {
q4k,
q6k,
mwv_warps,
prefill_path,
hgemm_decode,
fused_gate_up,
fp8_prefill,
fp8_decode,
w4a16_interleaved,
sm_target,
cc,
}
}
fn detect_q4k(has_dp4a: bool, cc: u32) -> Q4kVariant {
if std::env::var("WIDE_Q4K_DISABLE").is_ok() {
return Q4kVariant::Legacy;
}
if std::env::var("WIDE_Q4K").is_ok() {
return Q4kVariant::Wide;
}
if std::env::var("VECTORIZED_Q4K").is_ok() {
return Q4kVariant::Vectorized;
}
if std::env::var("HW_DP4A_Q4K").is_ok() {
return Q4kVariant::HwDp4a;
}
if std::env::var("DP4A_Q4K").is_ok() {
return Q4kVariant::MwvDp4a;
}
if std::env::var("MWV_Q4K").is_ok() {
return Q4kVariant::Mwv;
}
Self::auto_q4k(has_dp4a, cc)
}
#[must_use]
pub(crate) fn auto_q4k(has_dp4a: bool, cc: u32) -> Q4kVariant {
let _ = (has_dp4a, cc);
Q4kVariant::Mwv
}
fn detect_q6k(has_dp4a: bool) -> Q6kVariant {
if std::env::var("HW_DP4A_Q6K").is_ok() {
return Q6kVariant::HwDp4a;
}
if std::env::var("DP4A_Q6K").is_ok() {
return Q6kVariant::Dp4a;
}
if std::env::var("MWV_Q6K").is_ok() {
return Q6kVariant::Mwv;
}
if has_dp4a {
Q6kVariant::HwDp4a
} else {
Q6kVariant::Mwv
}
}
fn detect_mwv_warps() -> u32 {
std::env::var("MWV_WARPS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(3)
}
#[must_use]
pub(crate) fn detect_fused_gate_up(q4k: &Q4kVariant, env: Option<&str>) -> bool {
if let Some(v) = env {
if v == "0" {
return false;
}
if *q4k != Q4kVariant::HwDp4a {
eprintln!(
"[GpuProfile] FUSED_GATE_UP={v} refused: the fused gate+up+SwiGLU \
kernel is HwDp4a-only (q4k={q4k:?}, its PTX module is preloaded \
only on that path); running unfused"
);
return false;
}
return true;
}
*q4k == Q4kVariant::HwDp4a
}
fn detect_fp8_prefill(cc: u32) -> bool {
contract_pre_fp8_architecture_guard!();
match std::env::var("FP8_PREFILL").as_deref() {
Ok("0") => false,
Ok("1") => true,
_ => cc >= 89,
}
}
fn detect_fp8_decode(fp8_prefill: bool, _cc: u32) -> bool {
match std::env::var("FP8_DECODE").as_deref() {
Ok("0") => false,
Ok("1") => true,
_ => fp8_prefill,
}
}
fn detect_w4a16_interleaved(cc: u32) -> bool {
match std::env::var("W4A16_INTERLEAVED").as_deref() {
Ok("0") => false,
Ok("1") => cc >= 70,
_ => false, }
}
fn detect_hgemm_decode(_has_dp4a: bool, _num_sms: u32) -> bool {
if let Ok(v) = std::env::var("HGEMM_DECODE") {
return v == "1";
}
false
}
}
#[cfg(test)]
mod pmat806_q4k_variant_tests {
use super::{GpuProfile, Q4kVariant};
#[test]
fn blackwell_defaults_to_fp32_mwv() {
assert_eq!(
GpuProfile::auto_q4k(true, 121),
Q4kVariant::Mwv,
"GB10 sm_121"
);
assert_eq!(
GpuProfile::auto_q4k(true, 120),
Q4kVariant::Mwv,
"cc==120 boundary"
);
}
#[test]
fn discrete_dp4a_gpus_default_to_mwv_not_hwdp4a() {
for (cc, name) in [
(89u32, "RTX 4090 sm_89"),
(80, "A100 sm_80"),
(75, "Turing sm_75"),
] {
assert_eq!(
GpuProfile::auto_q4k(true, cc),
Q4kVariant::Mwv,
"FALSIFY-Q4K-ADA-PARITY-001: {name} must default to fp32 MWV. HwDp4a \
measured cosine 0.9186 vs CPU on sm_89 (F2 floor 0.95), so the gate \
rejects it and decode silently degrades to CPU."
);
}
}
#[test]
fn no_compute_capability_defaults_to_hwdp4a() {
for cc in [0u32, 60, 70, 75, 80, 86, 89, 90, 100, 119, 120, 121, 130] {
for has_dp4a in [false, true] {
assert_ne!(
GpuProfile::auto_q4k(has_dp4a, cc),
Q4kVariant::HwDp4a,
"cc={cc} has_dp4a={has_dp4a} must not DEFAULT to the degraded \
HwDp4a path (opt-in via HW_DP4A_Q4K only)"
);
}
}
}
#[test]
fn non_dp4a_gpus_use_mwv() {
assert_eq!(GpuProfile::auto_q4k(false, 70), Q4kVariant::Mwv);
assert_eq!(GpuProfile::auto_q4k(false, 60), Q4kVariant::Mwv);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PrefillPath {
Serial,
Batched,
}
impl PrefillPath {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Serial => "serial",
Self::Batched => "batched",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub struct PrefillPathChoice {
pub path: PrefillPath,
pub reason: &'static str,
pub cc: u32,
}
#[must_use]
pub fn select_prefill_path(cc: u32, batched_prefill_env: Option<&str>) -> PrefillPathChoice {
let (path, reason) = match batched_prefill_env {
Some("0") => (PrefillPath::Serial, "env=0"),
Some(_) => (PrefillPath::Batched, "env forced"),
None if cc >= SM12X_MIN_CC => (PrefillPath::Serial, "sm12x default"),
None => (PrefillPath::Batched, "default"),
};
PrefillPathChoice { path, reason, cc }
}
pub const SM12X_MIN_CC: u32 = 120;
impl GpuProfile {
#[must_use]
pub fn prefill_path(&self) -> PrefillPathChoice {
self.prefill_path
}
#[must_use]
pub fn multi_prompt_prefill_allowed(&self) -> bool {
self.prefill_path.path == PrefillPath::Batched
}
}
#[derive(Debug, Clone, Serialize)]
pub struct GraphConfig {
pub cuda_graph_enable: bool,
pub graph_dispatch: bool,
pub prefill_graph: bool,
pub decode_graph_captured: bool,
pub batched_graph_sizes: Vec<usize>,
pub batched_graph_batch_size: usize,
pub prefill_graph_sizes: Vec<usize>,
}
#[derive(Debug, Clone, Copy, Serialize)]
pub struct MaxBatchSizing {
pub free_vram_bytes_at_sizing: usize,
pub total_vram_bytes: usize,
pub vram_query_ok: bool,
pub kv_per_slot_bytes: usize,
pub reserve_bytes: usize,
pub computed: usize,
pub clamp_min: usize,
pub clamp_max: usize,
pub resolved: usize,
pub source: &'static str,
}
pub const MAX_BATCH_SOURCE_ENV: &str = "env";
pub const MAX_BATCH_SOURCE_COMPUTED: &str = "computed";
#[derive(Debug, Clone, Serialize)]
pub struct VramReport {
pub device_name: String,
pub total_bytes: usize,
pub free_at_load_bytes: usize,
pub free_after_preload_bytes: Option<usize>,
pub preload_delta_bytes: Option<usize>,
pub free_now_bytes: Option<usize>,
pub used_now_bytes: Option<usize>,
pub used_peak_bytes: Option<usize>,
pub recorded_alloc_peak_bytes: usize,
pub kv_single_seq_bytes: usize,
pub kv_per_slot_bytes: usize,
pub kv_slots_allocated: usize,
pub kv_slots_max: usize,
pub kv_bytes_reserved: usize,
pub kv_blocks_total: Option<usize>,
pub kv_layout: &'static str,
}
pub const KV_LAYOUT: &str = "contiguous_per_slot";
#[cfg(test)]
mod pmat810_prefill_path_tests {
use super::{select_prefill_path, PrefillPath};
#[test]
fn select_prefill_path_table() {
let cases: [(u32, Option<&str>, PrefillPath, &str); 6] = [
(89, None, PrefillPath::Batched, "default"),
(121, None, PrefillPath::Serial, "sm12x default"),
(89, Some("0"), PrefillPath::Serial, "env=0"),
(121, Some("1"), PrefillPath::Batched, "env forced"),
(120, None, PrefillPath::Serial, "sm12x default"),
(75, Some("anything"), PrefillPath::Batched, "env forced"),
];
for (cc, env, expected_path, expected_reason) in cases {
let choice = select_prefill_path(cc, env);
assert_eq!(
choice.path, expected_path,
"cc={cc} BATCHED_PREFILL={env:?} must select {expected_path:?}"
);
assert_eq!(choice.reason, expected_reason, "cc={cc} env={env:?}");
assert_eq!(choice.cc, cc, "the choice must carry the cc it was made on");
}
}
#[test]
fn sm12x_boundary_is_inclusive_at_120_and_numeric() {
for cc in [90u32, 100, 103, 110] {
assert_eq!(
select_prefill_path(cc, None).path,
PrefillPath::Batched,
"cc={cc} is below the sm_12x line and keeps the batched prefill"
);
}
assert_eq!(select_prefill_path(120, None).path, PrefillPath::Serial);
assert_eq!(select_prefill_path(121, None).path, PrefillPath::Serial);
}
#[test]
fn multi_prompt_allowance_follows_the_path() {
for (cc, env) in [
(89u32, None),
(121, None),
(121, Some("1")),
(89, Some("0")),
] {
let choice = select_prefill_path(cc, env);
assert_eq!(
choice.path == PrefillPath::Batched,
matches!(choice.path, PrefillPath::Batched),
"cc={cc} env={env:?}"
);
}
assert!(select_prefill_path(89, None).path == PrefillPath::Batched);
assert!(select_prefill_path(121, None).path == PrefillPath::Serial);
}
}
#[cfg(test)]
mod pmat034_fused_gate_up_tests {
use super::{GpuProfile, Q4kVariant};
#[test]
fn refused_without_hwdp4a() {
for variant in [
Q4kVariant::Mwv,
Q4kVariant::MwvDp4a,
Q4kVariant::Wide,
Q4kVariant::Vectorized,
Q4kVariant::Legacy,
] {
assert!(
!GpuProfile::detect_fused_gate_up(&variant, Some("1")),
"FUSED_GATE_UP=1 must be refused with q4k={variant:?}"
);
}
}
#[test]
fn allowed_on_hwdp4a() {
assert!(GpuProfile::detect_fused_gate_up(
&Q4kVariant::HwDp4a,
Some("1")
));
assert!(GpuProfile::detect_fused_gate_up(
&Q4kVariant::HwDp4a,
Some("yes")
));
}
#[test]
fn env_0_disables() {
assert!(!GpuProfile::detect_fused_gate_up(
&Q4kVariant::HwDp4a,
Some("0")
));
assert!(!GpuProfile::detect_fused_gate_up(
&Q4kVariant::Mwv,
Some("0")
));
}
#[test]
fn default_follows_q4k() {
assert!(GpuProfile::detect_fused_gate_up(&Q4kVariant::HwDp4a, None));
assert!(!GpuProfile::detect_fused_gate_up(&Q4kVariant::Mwv, None));
assert!(!GpuProfile::detect_fused_gate_up(
&Q4kVariant::MwvDp4a,
None
));
}
}
#[cfg(test)]
mod pp_llama_report_serialisation_tests {
use super::{
select_prefill_path, GpuProfile, GraphConfig, MaxBatchSizing, Q4kVariant, Q6kVariant,
VramReport, KV_LAYOUT, MAX_BATCH_SOURCE_COMPUTED,
};
fn profile() -> GpuProfile {
GpuProfile {
q4k: Q4kVariant::HwDp4a,
q6k: Q6kVariant::Dp4a,
mwv_warps: 3,
prefill_path: select_prefill_path(89, None),
hgemm_decode: false,
fused_gate_up: true,
fp8_prefill: true,
fp8_decode: true,
w4a16_interleaved: false,
sm_target: "sm_89".to_string(),
cc: 89,
}
}
#[test]
fn gpu_profile_serialises_every_field_snake_case() {
let json = serde_json::to_value(profile()).expect("serialize");
let object = json.as_object().expect("object");
for key in [
"q4k",
"q6k",
"mwv_warps",
"prefill_path",
"hgemm_decode",
"fused_gate_up",
"fp8_prefill",
"fp8_decode",
"w4a16_interleaved",
"sm_target",
"cc",
] {
assert!(object.contains_key(key), "missing `{key}` in {json}");
}
assert_eq!(object.len(), 11, "field count changed: {json}");
assert_eq!(object["q4k"].as_str(), Some("hw_dp4a"));
assert_eq!(object["q6k"].as_str(), Some("dp4a"));
assert_eq!(object["cc"].as_u64(), Some(89));
assert_eq!(object["prefill_path"]["path"].as_str(), Some("batched"));
assert_eq!(object["prefill_path"]["reason"].as_str(), Some("default"));
assert_eq!(object["prefill_path"]["cc"].as_u64(), Some(89));
}
#[test]
fn a_blackwell_profile_reports_serial_prefill() {
let mut p = profile();
p.cc = 121;
p.prefill_path = select_prefill_path(121, None);
let json = serde_json::to_value(p).expect("serialize");
assert_eq!(json["prefill_path"]["path"].as_str(), Some("serial"));
assert_eq!(
json["prefill_path"]["reason"].as_str(),
Some("sm12x default")
);
}
#[test]
fn max_batch_sizing_carries_every_input() {
let sizing = MaxBatchSizing {
free_vram_bytes_at_sizing: 8_900_000_000,
total_vram_bytes: 25_757_220_864,
vram_query_ok: true,
kv_per_slot_bytes: 469_762_048,
reserve_bytes: 3_500_000_000,
computed: 11,
clamp_min: 1,
clamp_max: 32,
resolved: 11,
source: MAX_BATCH_SOURCE_COMPUTED,
};
let json = serde_json::to_value(sizing).expect("serialize");
let object = json.as_object().expect("object");
for key in [
"free_vram_bytes_at_sizing",
"total_vram_bytes",
"vram_query_ok",
"kv_per_slot_bytes",
"reserve_bytes",
"computed",
"clamp_min",
"clamp_max",
"resolved",
"source",
] {
assert!(object.contains_key(key), "missing `{key}` in {json}");
}
let free = object["free_vram_bytes_at_sizing"].as_u64().expect("free");
let reserve = object["reserve_bytes"].as_u64().expect("reserve");
let per_slot = object["kv_per_slot_bytes"].as_u64().expect("per slot");
assert_eq!(
(free - reserve) / per_slot,
object["computed"].as_u64().expect("computed"),
"the reported inputs must reconstruct the reported quotient: {json}"
);
}
#[test]
fn graph_config_distinguishes_disabled_from_uncaptured() {
let off = GraphConfig {
cuda_graph_enable: false,
graph_dispatch: false,
prefill_graph: false,
decode_graph_captured: false,
batched_graph_sizes: Vec::new(),
batched_graph_batch_size: 0,
prefill_graph_sizes: Vec::new(),
};
let on_uncaptured = GraphConfig {
cuda_graph_enable: true,
graph_dispatch: true,
prefill_graph: true,
decode_graph_captured: false,
batched_graph_sizes: Vec::new(),
batched_graph_batch_size: 0,
prefill_graph_sizes: Vec::new(),
};
let off_json = serde_json::to_value(off).expect("serialize");
let on_json = serde_json::to_value(on_uncaptured).expect("serialize");
assert_ne!(
off_json, on_json,
"a disabled graph and an enabled-but-uncaptured one must not read the same"
);
assert_eq!(off_json.as_object().expect("object").len(), 7);
}
#[test]
fn graph_config_reports_the_decode_graph_opt_in_separately() {
let base = || GraphConfig {
cuda_graph_enable: false,
graph_dispatch: true,
prefill_graph: false,
decode_graph_captured: false,
batched_graph_sizes: Vec::new(),
batched_graph_batch_size: 0,
prefill_graph_sizes: Vec::new(),
};
let eager = serde_json::to_value(base()).expect("serialize");
let graphed = serde_json::to_value(GraphConfig {
cuda_graph_enable: true,
..base()
})
.expect("serialize");
assert_eq!(eager["cuda_graph_enable"].as_bool(), Some(false));
assert_eq!(graphed["cuda_graph_enable"].as_bool(), Some(true));
assert_ne!(
eager, graphed,
"an eager-decode run and a graph-replay run must not serialize alike"
);
assert_eq!(
eager["graph_dispatch"], graphed["graph_dispatch"],
"`graph_dispatch` is a different switch and must not move with it"
);
}
#[test]
fn vram_report_names_the_recorded_peak_honestly() {
let report = VramReport {
device_name: "NVIDIA GeForce RTX 4090".to_string(),
total_bytes: 25_757_220_864,
free_at_load_bytes: 24_000_000_000,
free_after_preload_bytes: Some(14_500_000_000),
preload_delta_bytes: Some(9_500_000_000),
free_now_bytes: Some(14_000_000_000),
used_now_bytes: Some(11_757_220_864),
used_peak_bytes: Some(20_471_000_000),
recorded_alloc_peak_bytes: 7_000_000_000,
kv_single_seq_bytes: 469_762_048,
kv_per_slot_bytes: 469_762_048,
kv_slots_allocated: 4,
kv_slots_max: 32,
kv_bytes_reserved: 2_348_810_240,
kv_blocks_total: None,
kv_layout: KV_LAYOUT,
};
let json = serde_json::to_value(report).expect("serialize");
let object = json.as_object().expect("object");
assert!(
!object.contains_key("vram_peak"),
"the recorded allocation peak must not be published as `vram_peak`: {json}"
);
assert!(object.contains_key("recorded_alloc_peak_bytes"));
assert!(object.contains_key("used_peak_bytes"));
assert!(
json["recorded_alloc_peak_bytes"].as_u64() < json["used_peak_bytes"].as_u64(),
"the fixture must show the two are different quantities: {json}"
);
assert_eq!(
json["free_at_load_bytes"].as_u64().expect("at load")
- json["free_after_preload_bytes"].as_u64().expect("after"),
json["preload_delta_bytes"].as_u64().expect("delta")
);
assert!(json["kv_blocks_total"].is_null());
assert_eq!(json["kv_layout"].as_str(), Some("contiguous_per_slot"));
assert_eq!(
json["kv_bytes_reserved"].as_u64(),
Some(469_762_048 + 469_762_048 * 4)
);
}
}