use crate::DType;
use crate::Shape;
use crate::probe::BackendId;
use std::collections::HashMap;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
pub const PROFILE_REPORT_VERSION: u32 = 5;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[non_exhaustive]
pub enum OpKind {
MatMul,
AddElementwise,
SubElementwise,
MulElementwise,
DivElementwise,
ReluElementwise,
NegElementwise,
SqrElementwise,
SqrtElementwise,
RecipElementwise,
AbsElementwise,
TanhElementwise,
ExpElementwise,
LogElementwise,
SinElementwise,
CosElementwise,
SigmoidElementwise,
SiluElementwise,
GeluTanhElementwise,
StepElementwise,
SumReduce,
MaxReduce,
MinReduce,
MeanReduce,
Cast,
Conv2D,
ConvTranspose2D,
ReduceSumTo,
ReduceMaxTo,
FusedLinear,
FlashAttn,
FlashAttnBackwardQ,
FlashAttnBackwardK,
FlashAttnBackwardV,
PagedAttn,
Affine,
ClampElementwise,
PowIElementwise,
PowIElementwiseBackward,
MaximumElementwise,
MinimumElementwise,
EqualElementwise,
NotEqualElementwise,
LessElementwise,
LessEqualElementwise,
GreaterElementwise,
GreaterEqualElementwise,
Where,
FloorElementwise,
CeilElementwise,
RoundElementwise,
SignElementwise,
ErfElementwise,
GeluElementwise,
PowElementwise,
RsqrtElementwise,
RemElementwise,
Flip,
Roll,
CumSum,
Pad,
Triu,
Tril,
LogSoftmaxLastDim,
LogSoftmaxLastDimBackward,
MaskedFill,
PadBackward,
Concat,
SoftmaxLastDim,
SoftmaxLastDimBackward,
RmsNormLastDim,
RmsNormLastDimBackward,
LayerNormLastDim,
LayerNormLastDimBackward,
ReduceMaxToBackward,
IndexSelect,
Gather,
Rope,
IndexAdd,
ScatterAdd,
ArgMaxDim,
ArgMinDim,
QMatMul,
WriteSlice,
WriteSliceRotating,
WriteSliceDoff,
Copy,
ReluInplace,
SiluInplace,
GeluTanhInplace,
TanhInplace,
SigmoidInplace,
NegInplace,
AbsInplace,
SqrInplace,
SqrtInplace,
RsqrtInplace,
RecipInplace,
ExpInplace,
LogInplace,
SinInplace,
CosInplace,
SignInplace,
FloorInplace,
CeilInplace,
RoundInplace,
ErfInplace,
GeluInplace,
ClampInplace,
PowIInplace,
InplaceAffine,
FusedSoftmaxCrossEntropy,
CausalConv1d,
SelectiveScan,
SsdChunkScan,
Nf4Matmul,
AwqMatmul,
RuntimeFused,
NonZeroIndices,
}
impl OpKind {
pub fn as_str(self) -> &'static str {
match self {
OpKind::MatMul => "matmul",
OpKind::AddElementwise => "add",
OpKind::SubElementwise => "sub",
OpKind::MulElementwise => "mul",
OpKind::DivElementwise => "div",
OpKind::ReluElementwise => "relu",
OpKind::NegElementwise => "neg",
OpKind::SqrElementwise => "sqr",
OpKind::SqrtElementwise => "sqrt",
OpKind::RecipElementwise => "recip",
OpKind::AbsElementwise => "abs",
OpKind::TanhElementwise => "tanh",
OpKind::ExpElementwise => "exp",
OpKind::LogElementwise => "log",
OpKind::SinElementwise => "sin",
OpKind::CosElementwise => "cos",
OpKind::SigmoidElementwise => "sigmoid",
OpKind::SiluElementwise => "silu",
OpKind::GeluTanhElementwise => "gelu_tanh",
OpKind::StepElementwise => "step",
OpKind::SumReduce => "sum_reduce",
OpKind::MaxReduce => "max_reduce",
OpKind::MinReduce => "min_reduce",
OpKind::MeanReduce => "mean_reduce",
OpKind::Cast => "cast",
OpKind::Conv2D => "conv2d",
OpKind::ConvTranspose2D => "conv_transpose2d",
OpKind::ReduceSumTo => "reduce_sum_to",
OpKind::ReduceMaxTo => "reduce_max_to",
OpKind::FusedLinear => "fused_linear",
OpKind::FlashAttn => "flash_attn",
OpKind::FlashAttnBackwardQ => "flash_attn_backward_q",
OpKind::FlashAttnBackwardK => "flash_attn_backward_k",
OpKind::FlashAttnBackwardV => "flash_attn_backward_v",
OpKind::PagedAttn => "paged_attn",
OpKind::Affine => "affine",
OpKind::ClampElementwise => "clamp",
OpKind::PowIElementwise => "powi",
OpKind::PowIElementwiseBackward => "powi_backward",
OpKind::MaximumElementwise => "maximum",
OpKind::MinimumElementwise => "minimum",
OpKind::EqualElementwise => "eq",
OpKind::NotEqualElementwise => "ne",
OpKind::LessElementwise => "lt",
OpKind::LessEqualElementwise => "le",
OpKind::GreaterElementwise => "gt",
OpKind::GreaterEqualElementwise => "ge",
OpKind::Where => "where",
OpKind::FloorElementwise => "floor",
OpKind::CeilElementwise => "ceil",
OpKind::RoundElementwise => "round",
OpKind::SignElementwise => "sign",
OpKind::ErfElementwise => "erf",
OpKind::GeluElementwise => "gelu",
OpKind::PowElementwise => "pow",
OpKind::RsqrtElementwise => "rsqrt",
OpKind::RemElementwise => "rem",
OpKind::Flip => "flip",
OpKind::Roll => "roll",
OpKind::CumSum => "cumsum",
OpKind::Pad => "pad",
OpKind::PadBackward => "pad_backward",
OpKind::Triu => "triu",
OpKind::Tril => "tril",
OpKind::LogSoftmaxLastDim => "log_softmax_last_dim",
OpKind::LogSoftmaxLastDimBackward => "log_softmax_last_dim_backward",
OpKind::MaskedFill => "masked_fill",
OpKind::Concat => "concat",
OpKind::SoftmaxLastDim => "softmax_last_dim",
OpKind::SoftmaxLastDimBackward => "softmax_last_dim_backward",
OpKind::RmsNormLastDim => "rms_norm_last_dim",
OpKind::RmsNormLastDimBackward => "rms_norm_last_dim_backward",
OpKind::LayerNormLastDim => "layer_norm_last_dim",
OpKind::LayerNormLastDimBackward => "layer_norm_last_dim_backward",
OpKind::ReduceMaxToBackward => "reduce_max_to_backward",
OpKind::IndexSelect => "index_select",
OpKind::Gather => "gather",
OpKind::Rope => "rope",
OpKind::IndexAdd => "index_add",
OpKind::ScatterAdd => "scatter_add",
OpKind::ArgMaxDim => "argmax_dim",
OpKind::ArgMinDim => "argmin_dim",
OpKind::QMatMul => "qmatmul",
OpKind::WriteSlice => "write_slice",
OpKind::WriteSliceRotating => "write_slice_rotating",
OpKind::WriteSliceDoff => "write_slice_doff",
OpKind::Copy => "copy",
OpKind::ReluInplace => "relu_inplace",
OpKind::SiluInplace => "silu_inplace",
OpKind::GeluTanhInplace => "gelu_tanh_inplace",
OpKind::TanhInplace => "tanh_inplace",
OpKind::SigmoidInplace => "sigmoid_inplace",
OpKind::NegInplace => "neg_inplace",
OpKind::AbsInplace => "abs_inplace",
OpKind::SqrInplace => "sqr_inplace",
OpKind::SqrtInplace => "sqrt_inplace",
OpKind::RsqrtInplace => "rsqrt_inplace",
OpKind::RecipInplace => "recip_inplace",
OpKind::ExpInplace => "exp_inplace",
OpKind::LogInplace => "log_inplace",
OpKind::SinInplace => "sin_inplace",
OpKind::CosInplace => "cos_inplace",
OpKind::SignInplace => "sign_inplace",
OpKind::FloorInplace => "floor_inplace",
OpKind::CeilInplace => "ceil_inplace",
OpKind::RoundInplace => "round_inplace",
OpKind::ErfInplace => "erf_inplace",
OpKind::GeluInplace => "gelu_inplace",
OpKind::ClampInplace => "clamp_inplace",
OpKind::PowIInplace => "powi_inplace",
OpKind::InplaceAffine => "inplace_affine",
OpKind::FusedSoftmaxCrossEntropy => "fused_softmax_cross_entropy",
OpKind::CausalConv1d => "causal_conv1d",
OpKind::SelectiveScan => "selective_scan",
OpKind::SsdChunkScan => "ssd_chunk_scan",
OpKind::Nf4Matmul => "nf4_matmul",
OpKind::AwqMatmul => "awq_matmul",
OpKind::RuntimeFused => "runtime_fused",
OpKind::NonZeroIndices => "nonzero_indices",
}
}
}
impl std::fmt::Display for OpKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct SizeClass(pub u32);
impl SizeClass {
pub fn from_elem_count(n: usize) -> Self {
let n = n.max(1);
let log2 = 63 - (n as u64).leading_zeros();
SizeClass(log2)
}
pub fn matmul(m: usize, n: usize, k: usize) -> Self {
let mn = Self::from_elem_count(m.saturating_mul(n)).0 & 0xff;
let ml = Self::from_elem_count(m).0 & 0xff;
let kl = Self::from_elem_count(k).0 & 0xff;
SizeClass((mn << 16) | (ml << 8) | kl)
}
pub fn attention(hq: usize, k_len: usize, d: usize) -> Self {
let work = hq.saturating_mul(k_len).saturating_mul(d);
let mag = Self::from_elem_count(work).0 & 0xff;
let kl = Self::from_elem_count(k_len).0 & 0xff;
let dl = Self::from_elem_count(d).0 & 0xff;
SizeClass((mag << 16) | (kl << 8) | dl)
}
pub fn for_op(op: OpKind, input_shapes: &[Shape]) -> Self {
if op == OpKind::MatMul
&& let (Some(lhs), Some(rhs)) = (input_shapes.first(), input_shapes.get(1))
{
let (ld, rd) = (lhs.dims(), rhs.dims());
if ld.len() >= 2 && rd.len() >= 2 {
let m = ld[ld.len() - 2];
let k = ld[ld.len() - 1];
let n = rd[rd.len() - 1];
return Self::matmul(m, n, k);
}
}
if op == OpKind::FlashAttn {
if let (Some(q), Some(k)) = (input_shapes.first(), input_shapes.get(1)) {
let (qd, kd) = (q.dims(), k.dims());
if qd.len() >= 4 && kd.len() >= 2 {
let hq = qd[1];
let d = qd[qd.len() - 1];
let k_len = kd[kd.len() - 2];
return Self::attention(hq, k_len, d);
}
}
}
match input_shapes.first() {
Some(s) => Self::from_elem_count(s.elem_count()),
None => SizeClass(0),
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct ProfileEntry {
pub op: OpKind,
pub dtype: DType,
pub size_class: SizeClass,
pub backend: BackendId,
pub device_index: u32,
pub latency_ns: u64,
pub iterations: u32,
pub max_rel_error: f32,
#[cfg_attr(feature = "serde", serde(default))]
pub kernel_source: String,
#[cfg_attr(feature = "serde", serde(default))]
pub kernel_revision_hash: u64,
#[cfg_attr(feature = "serde", serde(default = "default_device_identity"))]
pub device: crate::probe::EquivalenceKey,
}
#[cfg(feature = "serde")]
fn default_device_identity() -> crate::probe::EquivalenceKey {
crate::probe::EquivalenceKey {
backend: BackendId::Cpu,
vendor_id: 0,
device_id: 0,
compute_capability: None,
driver_version: "<unset: pre-v5 profile, rejected by the version gate>".to_string(),
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct ProfileReport {
pub version: u32,
pub entries: Vec<ProfileEntry>,
}
#[cfg(feature = "serde")]
impl ProfileReport {
pub fn save(&self, path: &std::path::Path) -> crate::Result<()> {
let json = serde_json::to_vec_pretty(self)
.map_err(|e| crate::Error::Msg(format!("judge: JSON encode failed: {e}")))?;
let tmp = path.with_extension("tmp");
std::fs::write(&tmp, &json)
.map_err(|e| crate::Error::Msg(format!("judge: write {tmp:?} failed: {e}")))?;
std::fs::rename(&tmp, path).map_err(|e| {
crate::Error::Msg(format!("judge: rename {tmp:?} → {path:?} failed: {e}"))
})?;
Ok(())
}
pub fn load(
path: &std::path::Path,
current: &[crate::probe::EquivalenceKey],
) -> crate::Result<Option<Self>> {
let bytes = match std::fs::read(path) {
Ok(b) => b,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(e) => {
return Err(crate::Error::Msg(format!(
"judge: read {path:?} failed: {e}"
)));
}
};
let report: Self = serde_json::from_slice(&bytes)
.map_err(|e| crate::Error::Msg(format!("judge: parse {path:?} failed: {e}")))?;
if report.version != PROFILE_REPORT_VERSION {
return Ok(None);
}
if report.entries.iter().any(|e| !current.contains(&e.device)) {
return Ok(None);
}
Ok(Some(report))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum Criterion {
Fastest,
MostAccurate,
Balanced,
}
impl Criterion {
pub fn as_str(self) -> &'static str {
match self {
Criterion::Fastest => "fastest",
Criterion::MostAccurate => "accurate",
Criterion::Balanced => "balanced",
}
}
}
impl std::fmt::Display for Criterion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
pub const DEFAULT_ACCURACY_PENALTY: f64 = 100.0;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct DispatchKey {
op: OpKind,
dtype: DType,
size_class: SizeClass,
criterion: Criterion,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Pick {
pub backend: BackendId,
pub device_index: u32,
pub kernel_source: &'static str,
pub kernel_revision_hash: u64,
}
pub fn kernel_source_intern(s: &str) -> &'static str {
match s {
"" => "",
"portable-cpu" => "portable-cpu",
"aocl" => "aocl",
"mkl" => "mkl",
"cublas" => "cublas",
"cutlass" => "cutlass",
"baracuda" => "baracuda",
"slang" => "slang",
other => intern_unknown_kernel_source(other),
}
}
fn intern_unknown_kernel_source(s: &str) -> &'static str {
use std::collections::HashSet;
use std::sync::{Mutex, OnceLock};
static POOL: OnceLock<Mutex<HashSet<&'static str>>> = OnceLock::new();
let pool = POOL.get_or_init(|| Mutex::new(HashSet::new()));
let mut set = pool.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
if let Some(&existing) = set.get(s) {
return existing;
}
let leaked: &'static str = Box::leak(s.to_owned().into_boxed_str());
set.insert(leaked);
leaked
}
#[derive(Debug, Clone, Copy)]
pub struct DispatchOptions {
pub accuracy_penalty: f64,
}
impl Default for DispatchOptions {
fn default() -> Self {
Self {
accuracy_penalty: DEFAULT_ACCURACY_PENALTY,
}
}
}
impl DispatchOptions {
pub fn with_balanced_penalty(mut self, k: f64) -> Self {
self.accuracy_penalty = k;
self
}
}
#[derive(Debug, Clone)]
pub struct DispatchTable {
entries: HashMap<DispatchKey, Pick>,
size_index: HashMap<(OpKind, DType), Vec<SizeClass>>,
accuracy_penalty: f64,
}
impl DispatchTable {
pub fn build(report: &ProfileReport) -> Self {
Self::build_with(report, DispatchOptions::default())
}
pub fn build_with(report: &ProfileReport, opts: DispatchOptions) -> Self {
let mut tbl = Self {
entries: HashMap::new(),
size_index: HashMap::new(),
accuracy_penalty: opts.accuracy_penalty,
};
tbl.rebuild_from(report);
tbl
}
fn rebuild_from(&mut self, report: &ProfileReport) {
self.entries.clear();
self.size_index.clear();
let mut groups: HashMap<(OpKind, DType, SizeClass), Vec<&ProfileEntry>> = HashMap::new();
for e in &report.entries {
groups
.entry((e.op, e.dtype, e.size_class))
.or_default()
.push(e);
}
for ((op, dtype, size_class), group) in &groups {
for &criterion in &[
Criterion::Fastest,
Criterion::MostAccurate,
Criterion::Balanced,
] {
if let Some(winner) = self.pick_winner(group, criterion) {
let key = DispatchKey {
op: *op,
dtype: *dtype,
size_class: *size_class,
criterion,
};
self.entries.insert(
key,
Pick {
backend: winner.backend,
device_index: winner.device_index,
kernel_source: kernel_source_intern(&winner.kernel_source),
kernel_revision_hash: winner.kernel_revision_hash,
},
);
}
}
self.size_index
.entry((*op, *dtype))
.or_default()
.push(*size_class);
}
for classes in self.size_index.values_mut() {
classes.sort_by_key(|s| s.0);
classes.dedup();
}
}
fn pick_winner<'a>(
&self,
group: &[&'a ProfileEntry],
crit: Criterion,
) -> Option<&'a ProfileEntry> {
match crit {
Criterion::Fastest => group.iter().copied().min_by_key(|e| e.latency_ns),
Criterion::MostAccurate => group.iter().copied().min_by(|a, b| {
a.max_rel_error
.total_cmp(&b.max_rel_error)
.then(a.latency_ns.cmp(&b.latency_ns))
}),
Criterion::Balanced => group.iter().copied().min_by(|a, b| {
let sa =
a.latency_ns as f64 * (1.0 + self.accuracy_penalty * a.max_rel_error as f64);
let sb =
b.latency_ns as f64 * (1.0 + self.accuracy_penalty * b.max_rel_error as f64);
sa.total_cmp(&sb)
}),
}
}
pub fn pick(
&self,
op: OpKind,
dtype: DType,
size_class: SizeClass,
criterion: Criterion,
) -> Option<Pick> {
self.entries
.get(&DispatchKey {
op,
dtype,
size_class,
criterion,
})
.copied()
}
pub fn pick_nearest(
&self,
op: OpKind,
dtype: DType,
size_class: SizeClass,
criterion: Criterion,
) -> Option<Pick> {
if let Some(p) = self.pick(op, dtype, size_class, criterion) {
return Some(p);
}
let classes = self.size_index.get(&(op, dtype))?;
if classes.is_empty() {
return None;
}
let target = size_class.0 as i32;
let nearest = classes.iter().min_by_key(|c| {
let diff = (c.0 as i32 - target).abs();
(diff, -(c.0 as i32))
})?;
self.pick(op, dtype, *nearest, criterion)
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn keys(&self) -> Vec<(OpKind, DType, SizeClass)> {
let mut seen: std::collections::HashSet<(OpKind, DType, SizeClass)> = Default::default();
for k in self.entries.keys() {
seen.insert((k.op, k.dtype, k.size_class));
}
let mut out: Vec<_> = seen.into_iter().collect();
out.sort_by(|a, b| {
a.0.as_str()
.cmp(b.0.as_str())
.then(format!("{:?}", a.1).cmp(&format!("{:?}", b.1)))
.then(a.2.0.cmp(&b.2.0))
});
out
}
}
#[cfg(test)]
mod size_class_tests {
use super::*;
#[test]
fn intern_preserves_an_arbitrary_unknown_tag() {
let unknown = "some-future-provider-xyz";
assert_eq!(
kernel_source_intern(unknown),
unknown,
"an unknown tag must be preserved, never collapsed to \"\""
);
assert_ne!(kernel_source_intern(unknown), "");
assert!(std::ptr::eq(
kernel_source_intern(unknown),
kernel_source_intern(unknown)
));
assert_eq!(kernel_source_intern("slang"), "slang");
assert_eq!(kernel_source_intern(""), "");
}
#[test]
fn matmul_key_producer_equals_consumer_non_square() {
let (m, n, k) = (1usize, 5632usize, 2048usize);
let producer = SizeClass::matmul(m, n, k);
let consumer = SizeClass::for_op(
OpKind::MatMul,
&[Shape::from_dims(&[m, k]), Shape::from_dims(&[k, n])],
);
assert_eq!(
producer, consumer,
"producer (native m,n,k) and consumer (operand shapes) must \
derive one identical key",
);
assert_ne!(
SizeClass::from_elem_count(m * n), SizeClass::from_elem_count(m * k), "the non-square shape is exactly where the old scalar keys \
diverged",
);
}
#[test]
fn matmul_key_batched_ignores_leading_dims() {
let per_head = SizeClass::for_op(
OpKind::MatMul,
&[Shape::from_dims(&[1, 64]), Shape::from_dims(&[64, 128])],
);
let batched = SizeClass::for_op(
OpKind::MatMul,
&[
Shape::from_dims(&[32, 1, 64]),
Shape::from_dims(&[32, 64, 128]),
],
);
assert_eq!(per_head, batched);
assert_eq!(per_head, SizeClass::matmul(1, 128, 64));
}
#[test]
fn gemv_and_same_output_square_are_distinct() {
let gemv = SizeClass::matmul(1, 4096, 2048);
let square = SizeClass::matmul(64, 64, 64);
#[allow(clippy::identity_op)] let gemv_elems = 1 * 4096; let square_elems = 64 * 64; assert_eq!(
SizeClass::from_elem_count(gemv_elems),
SizeClass::from_elem_count(square_elems),
"sanity: equal output size → the OLD scalar keys collided",
);
assert_ne!(
gemv, square,
"a bandwidth-bound GEMV must not share a dispatch cell with a \
compute-bound square of equal output size",
);
}
#[test]
fn non_matmul_keeps_total_elements_key() {
let sc = SizeClass::for_op(OpKind::AddElementwise, &[Shape::from_dims(&[1024])]);
assert_eq!(sc, SizeClass::from_elem_count(1024));
assert_eq!(sc.0, 10); }
#[test]
fn attention_key_producer_equals_consumer() {
let (b, hq, hkv, d, k_len) = (1usize, 32usize, 4usize, 64usize, 128usize);
let producer = SizeClass::attention(hq, k_len, d);
let consumer = SizeClass::for_op(
OpKind::FlashAttn,
&[
Shape::from_dims(&[b, hq, 1, d]),
Shape::from_dims(&[b, hkv, k_len, d]),
Shape::from_dims(&[b, hkv, k_len, d]),
],
);
assert_eq!(
producer, consumer,
"producer (native hq,k_len,d) and consumer (operand shapes) must \
derive one identical attention key",
);
}
#[test]
fn attention_decode_shapes_distinct_by_klen() {
let small = SizeClass::attention(32, 128, 64);
let cap = SizeClass::attention(32, 4096, 64);
assert_ne!(
small, cap,
"short-context and capacity-context decode attention must not \
share a dispatch cell",
);
}
#[test]
fn attention_key_ignores_batch_and_hkv() {
let base = SizeClass::for_op(
OpKind::FlashAttn,
&[
Shape::from_dims(&[1, 32, 1, 64]),
Shape::from_dims(&[1, 4, 128, 64]),
],
);
let other = SizeClass::for_op(
OpKind::FlashAttn,
&[
Shape::from_dims(&[8, 32, 1, 64]),
Shape::from_dims(&[8, 8, 128, 64]),
],
);
assert_eq!(base, other);
assert_eq!(base, SizeClass::attention(32, 128, 64));
}
}