use crate::model::HostExps;
use memra_gguf::GgmlType;
fn qtype_block(qtype: i32) -> Option<u64> {
let ty = match qtype {
q if q == crate::QT_Q8_0 => GgmlType::Q8_0,
q if q == crate::QT_Q2_K => GgmlType::Q2_K,
q if q == crate::QT_Q3_K => GgmlType::Q3_K,
q if q == crate::QT_Q4_K => GgmlType::Q4_K,
q if q == crate::QT_Q5_K => GgmlType::Q5_K,
q if q == crate::QT_Q6_K => GgmlType::Q6_K,
q if q == crate::QT_IQ4_XS => GgmlType::IQ4_XS,
q if q == crate::QT_IQ3_S => GgmlType::IQ3_S,
q if q == crate::QT_NVFP4 => GgmlType::NVFP4,
q if q == crate::QT_F32 => GgmlType::F32,
q if q == crate::QT_BF16 => GgmlType::BF16,
_ => return None,
};
Some(ty.block_and_type_size().0)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExpertShard {
pub bytes: Vec<u8>,
pub in_f: usize,
pub out_f: usize,
pub row_bytes: usize,
pub expert_stride: usize,
}
fn refuse_unsplittable(h: &HostExps, what: &str) -> Result<(), Box<dyn std::error::Error>> {
if h.tiers.is_some() {
return Err(format!(
"tp expert split: {what} is per-expert tiered (spilling plan); the split needs one \
contiguous bank"
)
.into());
}
if h.layouts.is_some() {
return Err(format!(
"tp expert split: {what} carries a per-expert layouts table, so experts do not share \
one row layout and a byte prefix is not a column prefix"
)
.into());
}
if h.fp8_blk.is_some() {
return Err(format!(
"tp expert split: {what} carries a native block-E4M3 scale plane, which is indexed \
[expert, output_block, input_block] and does not split with the code bytes"
)
.into());
}
Ok(())
}
fn for_each_expert_slab<F>(bytes: &mut [u8], stride: usize, f: F)
where
F: Fn(usize, &mut [u8]) + Sync,
{
if stride == 0 || bytes.is_empty() {
return;
}
let n_expert = bytes.len() / stride;
let workers = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1)
.clamp(1, n_expert.max(1));
let per = n_expert.div_ceil(workers).max(1);
if workers == 1 {
for (ex, dst) in bytes.chunks_mut(stride).enumerate() {
f(ex, dst);
}
return;
}
let f = &f;
std::thread::scope(|s| {
for (w, slab) in bytes.chunks_mut(per * stride).enumerate() {
s.spawn(move || {
for (k, dst) in slab.chunks_mut(stride).enumerate() {
f(w * per + k, dst);
}
});
}
});
}
pub fn split_rows(
h: &HostExps,
ranks: usize,
rank: usize,
) -> Result<ExpertShard, Box<dyn std::error::Error>> {
refuse_unsplittable(h, "a row-split projection")?;
if ranks == 0 || rank >= ranks {
return Err(format!("tp expert split: rank {rank} of {ranks}").into());
}
if !h.out_f.is_multiple_of(ranks) {
return Err(format!(
"tp expert split: out_f {} does not divide across {ranks} ranks",
h.out_f
)
.into());
}
let half = h.out_f / ranks;
let src = h.bytes.as_bytes();
let stride = half * h.row_bytes;
let mut bytes = vec![0u8; h.n_expert * stride];
for_each_expert_slab(&mut bytes, stride, |ex, dst| {
let base = ex * h.expert_stride + rank * stride;
dst.copy_from_slice(&src[base..base + stride]);
});
Ok(ExpertShard {
bytes,
in_f: h.in_f,
out_f: half,
row_bytes: h.row_bytes,
expert_stride: stride,
})
}
pub fn split_cols(
h: &HostExps,
ranks: usize,
rank: usize,
) -> Result<ExpertShard, Box<dyn std::error::Error>> {
refuse_unsplittable(h, "a column-split projection")?;
if ranks == 0 || rank >= ranks {
return Err(format!("tp expert split: rank {rank} of {ranks}").into());
}
if !h.in_f.is_multiple_of(ranks) {
return Err(format!(
"tp expert split: in_f {} does not divide across {ranks} ranks",
h.in_f
)
.into());
}
let half = h.in_f / ranks;
let Some(block) = qtype_block(h.qtype) else {
return Err(format!(
"tp expert split: qtype {} has no known block size, so a column split cannot be \
proven to land on a block boundary",
h.qtype
)
.into());
};
if !(half as u64).is_multiple_of(block) {
return Err(format!(
"tp expert split: column half {half} of {} does not land on a block boundary \
(qtype {} block {block})",
h.in_f, h.qtype
)
.into());
}
debug_assert!(
(half * h.row_bytes).is_multiple_of(h.in_f),
"a block-aligned half must also divide the row bytes"
);
let keep = half * h.row_bytes / h.in_f;
let src = h.bytes.as_bytes();
let stride = h.out_f * keep;
let mut bytes = vec![0u8; h.n_expert * stride];
for_each_expert_slab(&mut bytes, stride, |ex, dst| {
let ebase = ex * h.expert_stride;
for (row, d) in dst.chunks_exact_mut(keep).enumerate() {
let sb = ebase + row * h.row_bytes + rank * keep;
d.copy_from_slice(&src[sb..sb + keep]);
}
});
Ok(ExpertShard {
bytes,
in_f: half,
out_f: h.out_f,
row_bytes: keep,
expert_stride: stride,
})
}
pub fn ep_busier_rank_experts(n_used: usize) -> (f64, f64) {
fn comb(n: usize, k: usize) -> f64 {
let mut v = 1.0;
for i in 0..k {
v = v * (n - i) as f64 / (i + 1) as f64;
}
v
}
let total: f64 = (0..=n_used).map(|k| comb(n_used, k)).sum();
let e_max: f64 = (0..=n_used)
.map(|k| comb(n_used, k) * k.max(n_used - k) as f64)
.sum::<f64>()
/ total;
(e_max, n_used as f64 / e_max)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::HostBuf;
fn bank(in_f: usize, out_f: usize, n_expert: usize, row_bytes: usize) -> HostExps {
let stride = out_f * row_bytes;
let mut v = vec![0u8; n_expert * stride];
for (i, b) in v.iter_mut().enumerate() {
*b = (i % 251) as u8;
}
HostExps {
bytes: HostBuf::Paged(v),
tiers: None,
qtype: crate::QT_Q8_0,
in_f,
out_f,
n_expert,
row_bytes,
expert_stride: stride,
layouts: None,
macros: None,
fp8_blk: None,
}
}
#[test]
fn row_split_halves_reassemble_the_bank() {
let h = bank(4096, 2048, 3, 34);
let a = split_rows(&h, 2, 0).unwrap();
let b = split_rows(&h, 2, 1).unwrap();
assert_eq!((a.out_f, a.in_f, a.row_bytes), (1024, 4096, 34));
assert_eq!(a.expert_stride, 1024 * 34);
let src = h.bytes.as_bytes();
for ex in 0..h.n_expert {
let want = &src[ex * h.expert_stride..(ex + 1) * h.expert_stride];
let mut got = Vec::new();
got.extend_from_slice(&a.bytes[ex * a.expert_stride..(ex + 1) * a.expert_stride]);
got.extend_from_slice(&b.bytes[ex * b.expert_stride..(ex + 1) * b.expert_stride]);
assert_eq!(got, want, "expert {ex} row halves do not reassemble");
}
}
#[test]
fn column_split_halves_reassemble_every_row() {
let h = bank(2048, 4096, 3, 68);
let a = split_cols(&h, 2, 0).unwrap();
let b = split_cols(&h, 2, 1).unwrap();
assert_eq!((a.in_f, a.out_f, a.row_bytes), (1024, 4096, 34));
assert_eq!(a.expert_stride, 4096 * 34);
let src = h.bytes.as_bytes();
for ex in 0..h.n_expert {
for row in 0..h.out_f {
let base = ex * h.expert_stride + row * h.row_bytes;
let want = &src[base..base + h.row_bytes];
let ab = ex * a.expert_stride + row * a.row_bytes;
let bb = ex * b.expert_stride + row * b.row_bytes;
let mut got = Vec::new();
got.extend_from_slice(&a.bytes[ab..ab + a.row_bytes]);
got.extend_from_slice(&b.bytes[bb..bb + b.row_bytes]);
assert_eq!(
got, want,
"expert {ex} row {row} column halves do not reassemble"
);
}
}
}
#[test]
fn column_split_refuses_a_half_that_lands_mid_block() {
let bad = bank(96, 8, 1, 102);
assert_eq!(
(48 * 102) % 96,
0,
"the byte-divisibility test would have passed this"
);
let err = split_cols(&bad, 2, 0).expect_err("a mid-block half must refuse");
assert!(err.to_string().contains("block boundary"), "{err}");
let good = bank(64, 8, 1, 68);
assert_eq!(split_cols(&good, 2, 0).unwrap().row_bytes, 34);
}
#[test]
fn split_refuses_layouts_a_prefix_cannot_reach() {
let mut h = bank(2048, 4096, 2, 68);
h.fp8_blk = None;
h.layouts = Some(Vec::new());
let err = split_cols(&h, 2, 0).expect_err("a per-expert layouts table must refuse");
assert!(err.to_string().contains("layouts table"), "{err}");
let err = split_rows(&h, 2, 0).expect_err("a per-expert layouts table must refuse");
assert!(err.to_string().contains("layouts table"), "{err}");
}
#[test]
fn whole_expert_ownership_pays_the_busier_rank() {
let (e_max, speedup) = ep_busier_rank_experts(8);
assert!((e_max - 5.09375).abs() < 1e-9, "E[max] = {e_max}");
assert!((speedup - 1.5709).abs() < 1e-3, "speedup = {speedup}");
assert!(
speedup < 2.0,
"EP cannot reach the split's deterministic 2x"
);
}
}