Skip to main content

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}