ferrox_core/qstar.rs
1//! The `q*` policy: bandwidth-adaptive CPU/GPU expert execution.
2//!
3//! On a consumer machine an MoE model's experts do not fit in VRAM. A
4//! decode step routes to a handful of them, some already resident in
5//! the GPU expert cache and some not. The misses have to come from
6//! somewhere, and there are exactly two places they can come from:
7//!
8//! - **fetch** them over PCIe into the cache and multiply on the GPU;
9//! - **compute** them on the CPU, straight out of host RAM.
10//!
11//! FreeToken's observation is that these two run *concurrently* on
12//! different hardware, so the step costs `max(fetch_time,
13//! cpu_time)`, not their sum -- and the split that minimizes that
14//! maximum depends on the machine. A desktop with a x16 link and slow
15//! DDR4 should fetch nearly everything; a laptop with a x4 link and
16//! fast DDR5 should compute nearly everything on the CPU. Neither
17//! choice is right in general, which is why the split is a *measured*
18//! parameter rather than a constant.
19//!
20//! # The fraction
21//!
22//! With PCIe bandwidth `p` and CPU-MoE bandwidth `c` over the same
23//! bytes, perfect overlap wants `fetched : cpu_computed = p : (c - p)`,
24//! i.e. fetch a `p/c` fraction of each step's misses. When the two are
25//! measured *while contending with each other* -- the honest
26//! measurement, since that is how they actually run -- the equivalent
27//! expression is `p_ov / (p_ov + c_ov)`. That is the preferred form;
28//! the ratio of standalone numbers is the fallback.
29//!
30//! # Why fixed point
31//!
32//! The fraction is carried as Q16 ([`FRACTION_ONE`]) and every split is
33//! integer arithmetic. A GPU kernel and a CPU reference implementation
34//! have to agree on the split *exactly* -- they are two halves of one
35//! step, and a one-expert disagreement means an expert computed twice
36//! or not at all. Floating point does not guarantee that across two
37//! compilers; fixed point does.
38//!
39//! Ported 1:1 from FreeToken's `moe/bench_profile.py` and the split in
40//! `moe/offload_kernels.py` (Apache-2.0); see
41//! `docs/THIRD_PARTY_NOTICES.md`.
42
43use serde::{Deserialize, Serialize};
44
45/// Q16: `FRACTION_ONE` means "fetch everything".
46pub const FRACTION_ONE: u64 = 1 << 16;
47
48/// Which MoE execution backend a machine should serve with.
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
50#[serde(rename_all = "lowercase")]
51pub enum MoeBackend {
52 /// Stream every missing expert over PCIe; the GPU does all the
53 /// multiplying.
54 Offload,
55 /// Fetch some misses, compute the rest on the CPU, overlapped.
56 Hybrid,
57 /// Ship activations to the CPU and compute every expert there.
58 Cpu,
59 /// Experts are resident in VRAM; no cache, no streaming.
60 Fused,
61}
62
63/// The default `cpu_bw > threshold * pcie_bw` factor for recommending
64/// hybrid over offload.
65pub const DEFAULT_RECOMMEND_THRESHOLD: f64 = 2.0;
66
67/// Which backend a machine's measured bandwidths call for.
68///
69/// Offload is the always-safe answer, so hybrid has to *earn* the
70/// recommendation by a real margin: a CPU only marginally faster than
71/// the link buys nothing once the CPU is also running the rest of the
72/// model, and paying for a CPU-MoE path that never wins is worse than
73/// not having one.
74pub fn recommend_backend(cpu_bw_gbs: f64, pcie_bw_gbs: f64, threshold: f64) -> MoeBackend {
75 if cpu_bw_gbs > threshold * pcie_bw_gbs {
76 MoeBackend::Hybrid
77 } else {
78 MoeBackend::Offload
79 }
80}
81
82/// The fetch fraction implied by two *standalone* bandwidths.
83///
84/// Assumes each side gets the whole machine, which is not what happens
85/// when they run together -- prefer
86/// [`fetch_fraction_from_overlap`] when the contended pair was
87/// measured.
88pub fn fetch_fraction_from_bandwidths(cpu_gbs: f64, pcie_gbs: f64) -> Option<f64> {
89 if cpu_gbs <= 0.0 || pcie_gbs <= 0.0 {
90 return None;
91 }
92 Some((pcie_gbs / cpu_gbs).min(1.0))
93}
94
95/// The fetch fraction implied by the *contended* pair: both sides
96/// measured while the other was running.
97pub fn fetch_fraction_from_overlap(cpu_overlap_gbs: f64, pcie_overlap_gbs: f64) -> Option<f64> {
98 if cpu_overlap_gbs <= 0.0 || pcie_overlap_gbs <= 0.0 {
99 return None;
100 }
101 Some((pcie_overlap_gbs / (pcie_overlap_gbs + cpu_overlap_gbs)).min(1.0))
102}
103
104/// How one step's misses are split.
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106pub struct QStarSplit {
107 /// Misses to fetch over the link and multiply on the GPU.
108 pub fetch: usize,
109 /// Misses to compute on the CPU from host RAM.
110 pub cpu: usize,
111}
112
113/// The split policy for one served model.
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub struct QStarPolicy {
116 /// Q16 fetch fraction. Zero means "no fraction configured": the
117 /// fixed cap decides instead.
118 fraction_q16: u64,
119 /// The per-layer, per-step ceiling on fetches, used when no
120 /// fraction is configured.
121 max_fetch: usize,
122}
123
124impl QStarPolicy {
125 /// A fixed cap of `max_fetch` experts fetched per layer per step.
126 ///
127 /// This is what an unbenchmarked machine gets, with `max_fetch ==
128 /// 1`: enough to keep the cache warming without betting the step on
129 /// a link whose speed nobody measured. `max_fetch == 0` never
130 /// fetches (pure CPU); a very large cap is effectively pure
131 /// offload.
132 pub fn fixed_cap(max_fetch: usize) -> Self {
133 QStarPolicy {
134 fraction_q16: 0,
135 max_fetch,
136 }
137 }
138
139 /// A measured fetch fraction. Replaces the cap entirely.
140 pub fn from_fraction(fraction: f64) -> Self {
141 let scaled = (fraction * FRACTION_ONE as f64).round();
142 let clamped = scaled.clamp(0.0, FRACTION_ONE as f64) as u64;
143 QStarPolicy {
144 fraction_q16: clamped,
145 max_fetch: usize::MAX,
146 }
147 }
148
149 pub fn fraction_q16(&self) -> u64 {
150 self.fraction_q16
151 }
152
153 /// The configured fraction as a float, for reporting.
154 pub fn fraction(&self) -> Option<f64> {
155 if self.fraction_q16 == 0 {
156 None
157 } else {
158 Some(self.fraction_q16 as f64 / FRACTION_ONE as f64)
159 }
160 }
161
162 /// Split `missing` cache misses between the link and the CPU.
163 pub fn split(&self, missing: usize) -> QStarSplit {
164 let fetch = if self.fraction_q16 > 0 {
165 balanced_fetch(missing, self.fraction_q16).min(missing)
166 } else {
167 self.max_fetch.min(missing)
168 };
169 QStarSplit {
170 fetch,
171 cpu: missing - fetch,
172 }
173 }
174}
175
176/// How many of `missing` misses to fetch, for a Q16 fraction `f`.
177///
178/// The fetch side takes time proportional to `F * (1 - f)` and the CPU
179/// side to `(M - F) * f`, in units where both bandwidths are folded
180/// into `f`. Since they overlap, the step costs the **larger** of the
181/// two, so the answer is the `F` that minimizes that maximum.
182///
183/// The exact value `f * M` is generally not an integer, and rounding it
184/// the obvious way is wrong: `ceil` over-fetches. With `M = 3` and `f =
185/// 0.415` the ideal is 1.24, and fetching 2 makes the link side ~1.6x
186/// slower than balance while the CPU sits idle -- so the rule tries
187/// both neighbours and keeps whichever has the smaller maximum, ties to
188/// the lower.
189pub fn balanced_fetch(missing: usize, fraction_q16: u64) -> usize {
190 if fraction_q16 == 0 || missing == 0 {
191 return 0;
192 }
193 let m = missing as i128;
194 let f = fraction_q16 as i128;
195 let q = FRACTION_ONE as i128;
196 let cost = |fetched: i128| -> i128 { (fetched * (q - f)).max((m - fetched) * f) };
197 let lo = (m * f) >> 16;
198 let best = if cost(lo) <= cost(lo + 1) { lo } else { lo + 1 };
199 best.clamp(0, m) as usize
200}
201
202/// One measured (format, hardware) pair from a bandwidth benchmark.
203#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
204#[serde(default)]
205pub struct KernelBandwidths {
206 /// CPU-MoE bandwidth, measured alone.
207 pub cpu_moe_gbs: Option<f64>,
208 /// PCIe expert-gather bandwidth, measured alone.
209 pub pcie_gather_gbs: Option<f64>,
210 /// CPU-MoE bandwidth measured while the gather was running.
211 pub cpu_moe_overlap_gbs: Option<f64>,
212 /// Gather bandwidth measured while CPU-MoE was running.
213 pub pcie_gather_overlap_gbs: Option<f64>,
214 pub recommended: Option<MoeBackend>,
215}
216
217impl KernelBandwidths {
218 /// The fetch fraction this entry implies, contended pair first.
219 pub fn fetch_fraction(&self) -> Option<f64> {
220 if let (Some(cpu), Some(pcie)) = (self.cpu_moe_overlap_gbs, self.pcie_gather_overlap_gbs) {
221 if let Some(fraction) = fetch_fraction_from_overlap(cpu, pcie) {
222 return Some(fraction);
223 }
224 }
225 if let (Some(cpu), Some(pcie)) = (self.cpu_moe_gbs, self.pcie_gather_gbs) {
226 return fetch_fraction_from_bandwidths(cpu, pcie);
227 }
228 None
229 }
230}
231
232/// Which GPU a profile was measured on.
233#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
234#[serde(default)]
235pub struct ProfileGpu {
236 pub index: Option<u32>,
237 pub name: Option<String>,
238 pub uuid: Option<String>,
239}
240
241/// A measured bandwidth profile for one machine.
242///
243/// These numbers are hardware facts, so a profile is keyed to the card
244/// it was taken on. A profile whose GPU name disagrees with the card in
245/// front of you is not "close enough": applying another machine's split
246/// is worse than having no split at all, so the lookups below refuse it
247/// rather than approximate.
248#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
249#[serde(default)]
250pub struct BandwidthProfile {
251 pub version: Option<u32>,
252 pub threshold: Option<f64>,
253 pub gpu: ProfileGpu,
254 /// Per-format verdicts, the authoritative source.
255 pub dtypes: std::collections::BTreeMap<String, MoeBackend>,
256 /// Per-format measured bandwidths.
257 pub dtype_kernels: std::collections::BTreeMap<String, KernelBandwidths>,
258 /// Per-model detail, consulted when the per-format entry is absent.
259 pub workloads: std::collections::BTreeMap<String, Workload>,
260}
261
262#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
263#[serde(default)]
264pub struct Workload {
265 pub kernels: std::collections::BTreeMap<String, KernelBandwidths>,
266}
267
268impl BandwidthProfile {
269 /// Whether this profile describes the card in front of us.
270 ///
271 /// A profile with no recorded GPU name is accepted (an older
272 /// benchmark), because refusing it would silently discard a
273 /// measurement the user did take. A profile with a *different*
274 /// recorded name is refused.
275 pub fn matches_gpu(&self, gpu_name: Option<&str>) -> bool {
276 match (self.gpu.name.as_deref(), gpu_name) {
277 (Some(recorded), Some(actual)) => recorded == actual,
278 _ => true,
279 }
280 }
281
282 /// The backend this profile recommends for `format`.
283 ///
284 /// The per-format verdict wins. Failing that, the per-model entries
285 /// for the same format are aggregated **conservatively**: hybrid
286 /// only if every model that was measured picked hybrid, because one
287 /// model that does not benefit is evidence the machine is on the
288 /// wrong side of the line.
289 pub fn backend_for(&self, format: &str) -> Option<MoeBackend> {
290 if let Some(verdict) = self.dtypes.get(format) {
291 return Some(*verdict);
292 }
293 let picks: Vec<MoeBackend> = self
294 .workloads
295 .values()
296 .filter_map(|w| w.kernels.get(format))
297 .filter_map(|k| k.recommended)
298 .collect();
299 if picks.is_empty() {
300 return None;
301 }
302 Some(if picks.iter().all(|p| *p == MoeBackend::Hybrid) {
303 MoeBackend::Hybrid
304 } else {
305 MoeBackend::Offload
306 })
307 }
308
309 /// The fetch fraction this profile implies for `format`.
310 pub fn fetch_fraction_for(&self, format: &str) -> Option<f64> {
311 if let Some(fraction) = self
312 .dtype_kernels
313 .get(format)
314 .and_then(KernelBandwidths::fetch_fraction)
315 {
316 return Some(fraction);
317 }
318 self.workloads
319 .values()
320 .filter_map(|w| w.kernels.get(format))
321 .find_map(KernelBandwidths::fetch_fraction)
322 }
323
324 /// The policy to serve `format` with, given this profile.
325 ///
326 /// A profile that measured nothing usable for this format yields
327 /// the unbenchmarked default: a fixed cap of one fetch per layer
328 /// per step.
329 pub fn policy_for(&self, format: &str) -> QStarPolicy {
330 match self.fetch_fraction_for(format) {
331 Some(fraction) => QStarPolicy::from_fraction(fraction),
332 None => QStarPolicy::fixed_cap(1),
333 }
334 }
335}
336
337#[cfg(test)]
338mod tests {
339 use super::*;
340
341 #[test]
342 fn hybrid_has_to_beat_the_link_by_a_real_margin() {
343 assert_eq!(
344 recommend_backend(100.0, 40.0, DEFAULT_RECOMMEND_THRESHOLD),
345 MoeBackend::Hybrid
346 );
347 // Only 1.5x: not worth a CPU-MoE path.
348 assert_eq!(
349 recommend_backend(60.0, 40.0, DEFAULT_RECOMMEND_THRESHOLD),
350 MoeBackend::Offload
351 );
352 }
353
354 #[test]
355 fn the_fraction_comes_from_the_two_bandwidths() {
356 assert_eq!(fetch_fraction_from_bandwidths(100.0, 40.0), Some(0.4));
357 assert_eq!(fetch_fraction_from_overlap(90.0, 30.0), Some(0.25));
358 // A link faster than host RAM should fetch everything, never
359 // more than everything.
360 assert_eq!(fetch_fraction_from_bandwidths(10.0, 40.0), Some(1.0));
361 assert_eq!(fetch_fraction_from_bandwidths(0.0, 40.0), None);
362 }
363
364 /// The regression the rule exists for: rounding up over-fetches and
365 /// leaves the CPU idle while the link is the bottleneck.
366 #[test]
367 fn the_split_minimizes_the_slower_side_rather_than_rounding() {
368 let policy = QStarPolicy::from_fraction(0.415);
369 assert_eq!(policy.split(3).fetch, 1, "1.24 ideal -> 1, not ceil 2");
370 assert_eq!(policy.split(4).fetch, 2, "1.66 ideal -> 2");
371 }
372
373 #[test]
374 fn the_split_tracks_the_fraction_within_one_expert() {
375 for fraction in [0.1, 0.415, 0.454, 0.7, 1.0] {
376 let policy = QStarPolicy::from_fraction(fraction);
377 for missing in 0..=64usize {
378 let split = policy.split(missing);
379 assert_eq!(split.fetch + split.cpu, missing, "every miss is assigned");
380 let ideal = fraction * missing as f64;
381 assert!(
382 (split.fetch as f64 - ideal).abs() <= 1.0,
383 "fraction={fraction} missing={missing} fetch={}",
384 split.fetch
385 );
386 }
387 }
388 }
389
390 #[test]
391 fn a_full_fraction_fetches_everything_and_a_zero_cap_fetches_nothing() {
392 assert_eq!(QStarPolicy::from_fraction(1.0).split(9).fetch, 9);
393 assert_eq!(QStarPolicy::fixed_cap(0).split(9).fetch, 0);
394 assert_eq!(QStarPolicy::fixed_cap(0).split(9).cpu, 9);
395 }
396
397 /// The unbenchmarked default: warm the cache one expert at a time,
398 /// send the rest to the CPU.
399 #[test]
400 fn the_fixed_cap_bounds_fetches_per_step() {
401 let policy = QStarPolicy::fixed_cap(1);
402 assert_eq!(policy.split(8), QStarSplit { fetch: 1, cpu: 7 });
403 assert_eq!(policy.split(0), QStarSplit { fetch: 0, cpu: 0 });
404 assert_eq!(policy.fraction(), None);
405 }
406
407 fn profile() -> BandwidthProfile {
408 let json = serde_json::json!({
409 "version": 4,
410 "gpu": {"name": "NVIDIA GeForce RTX 4090", "uuid": "GPU-abc"},
411 "dtypes": {"nvfp4": "hybrid"},
412 "dtype_kernels": {
413 "nvfp4": {"cpu_moe_gbs": 100.0, "pcie_gather_gbs": 40.0,
414 "cpu_moe_overlap_gbs": 90.0, "pcie_gather_overlap_gbs": 30.0},
415 "bf16": {"cpu_moe_gbs": 100.0, "pcie_gather_gbs": 40.0}
416 },
417 "workloads": {
418 "qwen": {"kernels": {"mxfp4_triton": {"cpu_moe_gbs": 80.0, "pcie_gather_gbs": 50.0,
419 "recommended": "hybrid"}}}
420 }
421 });
422 serde_json::from_value(json).expect("profile parses")
423 }
424
425 /// The contended pair is the honest measurement, so it wins over
426 /// the standalone ratio for the same format.
427 #[test]
428 fn the_overlapped_measurement_wins_over_the_standalone_ratio() {
429 let profile = profile();
430 assert_eq!(profile.fetch_fraction_for("nvfp4"), Some(0.25));
431 assert_eq!(profile.fetch_fraction_for("bf16"), Some(0.4));
432 }
433
434 #[test]
435 fn a_per_model_entry_fills_in_for_a_missing_per_format_one() {
436 let profile = profile();
437 assert_eq!(profile.fetch_fraction_for("mxfp4_triton"), Some(0.625));
438 assert_eq!(
439 profile.backend_for("mxfp4_triton"),
440 Some(MoeBackend::Hybrid)
441 );
442 assert_eq!(profile.backend_for("nvfp4"), Some(MoeBackend::Hybrid));
443 assert_eq!(profile.backend_for("q4_0"), None);
444 }
445
446 /// A profile from another card is refused rather than approximated:
447 /// these are hardware numbers, and the wrong ones are worse than
448 /// none.
449 #[test]
450 fn a_profile_from_another_card_is_refused() {
451 let profile = profile();
452 assert!(profile.matches_gpu(Some("NVIDIA GeForce RTX 4090")));
453 assert!(!profile.matches_gpu(Some("NVIDIA GeForce RTX 3060 Ti")));
454 assert!(
455 profile.matches_gpu(None),
456 "an unnamed card is not a mismatch"
457 );
458 }
459
460 #[test]
461 fn an_unmeasured_format_falls_back_to_the_one_fetch_default() {
462 let profile = profile();
463 assert_eq!(profile.policy_for("q4_0"), QStarPolicy::fixed_cap(1));
464 assert_eq!(
465 profile.policy_for("nvfp4"),
466 QStarPolicy::from_fraction(0.25)
467 );
468 }
469}