use anyhow::Result;
pub use crate::backend::cpu::RopeType;
use crate::gguf::GgufFile;
use crate::model::ModelConfig;
use crate::model::transformer::WeightRef;
pub trait GpuWeightSource {
fn config(&self) -> &ModelConfig;
fn gguf(&self) -> &GgufFile;
fn output_norm_weight(&self) -> &[f32];
fn attn_norm_weight(&self, layer: usize) -> &[f32];
fn ffn_norm_weight(&self, layer: usize) -> &[f32];
fn attn_q_norm_weight(&self, layer: usize) -> Option<&[f32]>;
fn attn_k_norm_weight(&self, layer: usize) -> Option<&[f32]>;
fn conv_weight(&self, layer: usize) -> Option<&[f32]>;
fn attn_q_bias(&self, layer: usize) -> Option<&[f32]>;
fn attn_k_bias(&self, layer: usize) -> Option<&[f32]>;
fn attn_v_bias(&self, layer: usize) -> Option<&[f32]>;
fn rope_freqs(&self) -> Option<&[f32]>;
fn embedding_tensor(&self) -> Result<crate::tensor::Tensor> {
self.gguf().get_tensor("token_embd.weight")
}
fn embedding_tensor_data(&self) -> Result<std::borrow::Cow<'_, [u8]>> {
self.gguf()
.tensor_data("token_embd.weight")
.map(std::borrow::Cow::Borrowed)
}
#[cfg_attr(not(feature = "gpu"), allow(dead_code))]
fn weight_bytes(&self, wref: &WeightRef) -> std::borrow::Cow<'_, [u8]>;
#[cfg_attr(not(feature = "gpu"), allow(dead_code))]
fn dequantize_weight(&self, wref: &WeightRef) -> Vec<f32>;
fn output_ref(&self) -> Option<&WeightRef>;
fn ffn_gate_ref(&self, layer: usize) -> Result<&WeightRef>;
fn ffn_up_ref(&self, layer: usize) -> Result<&WeightRef>;
fn ffn_down_ref(&self, layer: usize) -> Result<&WeightRef>;
fn moe_refs(&self, _layer: usize) -> Option<&crate::model::lfm2::MoeFfnRefs> {
None
}
fn conv_in_proj_ref(&self, layer: usize) -> Option<&WeightRef>;
fn conv_out_proj_ref(&self, layer: usize) -> Option<&WeightRef>;
fn attn_q_ref(&self, layer: usize) -> Option<&WeightRef>;
fn attn_k_ref(&self, layer: usize) -> Option<&WeightRef>;
fn attn_v_ref(&self, layer: usize) -> Option<&WeightRef>;
fn attn_output_ref(&self, layer: usize) -> Option<&WeightRef>;
fn rope_type(&self) -> RopeType;
#[cfg_attr(not(feature = "gpu"), allow(dead_code))]
fn supports_batched_prefill(&self) -> bool;
}
pub(crate) const MOE_MAX_EXPERTS: u32 = 256;
pub(crate) const MOE_MAX_EXPERT_USED: u32 = 16;
pub(crate) struct StackedExperts {
#[cfg_attr(not(feature = "gpu"), allow(dead_code))]
pub rows: usize,
#[cfg_attr(not(feature = "gpu"), allow(dead_code))]
pub inner: usize,
pub expert_stride: u32,
#[cfg_attr(not(feature = "gpu"), allow(dead_code))]
pub total_bytes: u32,
}
pub(crate) fn stacked_expert_layout(
refs: &[WeightRef],
layer: usize,
what: &str,
backend: &str,
) -> Result<StackedExperts> {
use anyhow::Context;
let first = refs
.first()
.with_context(|| format!("layer {layer}: {what} has no experts"))?;
anyhow::ensure!(
first.dtype == crate::tensor::DType::Q4_0,
"layer {layer}: the {backend} expert GEMV kernel is Q4_0-only, {what} is {:?}",
first.dtype,
);
anyhow::ensure!(
first.k.is_multiple_of(first.dtype.block_size()),
"layer {layer}: {what} inner dim k={} is not divisible by the {:?} block size {}",
first.k,
first.dtype,
first.dtype.block_size(),
);
let stride = first
.m
.checked_mul(first.k / first.dtype.block_size())
.and_then(|blocks| blocks.checked_mul(first.dtype.block_bytes()))
.and_then(|bytes| u32::try_from(bytes).ok())
.with_context(|| format!("layer {layer}: {what} expert stride overflows"))?;
let n = refs.len();
let total_bytes = u32::try_from(n)
.ok()
.and_then(|experts| experts.checked_mul(stride))
.with_context(|| {
format!(
"layer {layer}: {what} stacked across {n} experts at a {stride}-byte stride \
overflows the u32 byte offset the expert GEMV addresses them with"
)
})?;
refs.iter().enumerate().try_for_each(|(e, r)| {
anyhow::ensure!(
r.dtype == first.dtype && r.m == first.m && r.k == first.k,
"layer {layer}: {what} expert {e} has shape {}x{} {:?}, expert 0 has {}x{} {:?}",
r.m,
r.k,
r.dtype,
first.m,
first.k,
first.dtype,
);
anyhow::ensure!(
r.start == first.start + (e as u64) * (stride as u64),
"layer {layer}: {what} expert {e} starts at byte {} but a {stride}-byte stride from \
expert 0 puts it at {}; the experts are not evenly stacked",
r.start,
first.start + (e as u64) * (stride as u64),
);
Ok(())
})?;
Ok(StackedExperts {
rows: first.m,
inner: first.k,
expert_stride: stride,
total_bytes,
})
}
#[cfg(test)]
mod moe_bound_tests {
use super::{MOE_MAX_EXPERT_USED, MOE_MAX_EXPERTS};
#[test]
fn loader_bounds_match_the_routing_kernel() {
const SRC: &str = include_str!("../backend/shaders/slang/moe_route.slang");
let decl = |name: &str| -> u32 {
SRC.lines()
.filter_map(|l| l.trim().strip_prefix("static const uint "))
.find_map(|rest| {
rest.strip_prefix(name)?
.trim_start()
.strip_prefix("= ")?
.trim_end_matches(';')
.trim_end_matches('u')
.parse()
.ok()
})
.unwrap_or_else(|| {
panic!(
"moe_route.slang declares no `static const uint {name}`; it was renamed \
or removed, and this test is the only thing pinning the loaders' bound \
to it"
)
})
};
assert_eq!(
decl("MAX_EXPERTS"),
MOE_MAX_EXPERTS,
"the routing kernel's groupshared probability array and the loaders' expert-count \
bound disagree"
);
assert_eq!(
decl("MAX_USED"),
MOE_MAX_EXPERT_USED,
"the routing kernel's per-thread winners array and the loaders' active-expert bound \
disagree"
);
}
}