1pub fn net_cache_budget_bytes(
42 memory_ratio: f64,
43 baseline_free_bytes: u64,
44 weights_bytes: u64,
45 fixed_cache_bytes: u64,
46) -> i64 {
47 (memory_ratio * baseline_free_bytes as f64) as i64
48 - weights_bytes as i64
49 - fixed_cache_bytes as i64
50}
51
52pub fn required_bytes(
54 moe_cache_slots: u64,
55 kv_pages: u64,
56 bytes_per_expert: u64,
57 bytes_per_page: u64,
58) -> i64 {
59 (moe_cache_slots * bytes_per_expert + kv_pages * bytes_per_page) as i64
60}
61
62pub fn startup_kv_budget(memory_ratio: f64, init_free_bytes: u64, new_free_bytes: u64) -> i64 {
69 (memory_ratio * init_free_bytes as f64) as i64
70 - (init_free_bytes as i64 - new_free_bytes as i64)
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub struct PoolSizes {
76 pub moe_cache_slots: u64,
78 pub kv_pages: u64,
80 pub prefill_overlap: bool,
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub struct BudgetTooSmall {
89 pub needed_bytes: i64,
90 pub budget_bytes: i64,
91 pub sizes: PoolSizes,
93}
94
95impl std::fmt::Display for BudgetTooSmall {
96 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97 write!(
98 f,
99 "requested cache (moe={} slots, kv={} pages) needs {} bytes but the budget is {}; \
100 the old cache is kept and still serving",
101 self.sizes.moe_cache_slots, self.sizes.kv_pages, self.needed_bytes, self.budget_bytes
102 )
103 }
104}
105
106impl std::error::Error for BudgetTooSmall {}
107
108pub fn expert_bytes_per_slot(bank_row_bytes: &[u64]) -> u64 {
116 bank_row_bytes.iter().sum()
117}
118
119#[allow(clippy::too_many_arguments)]
125pub fn plan_cache_budget(
126 budget_bytes: i64,
127 bytes_per_expert: u64,
128 bytes_per_page: u64,
129 num_experts: u64,
130 total_experts: u64,
131 prefill_overlap: bool,
132 kv_reserve_pages: u64,
133 max_slots: u64,
134) -> Result<PoolSizes, BudgetTooSmall> {
135 assert!(
136 bytes_per_expert > 0 && bytes_per_page > 0,
137 "an unpriced pool cannot be sized"
138 );
139 let hi = total_experts.min(max_slots);
140 let mut overlap = prefill_overlap && hi >= 2 * num_experts;
143 let lo = if overlap {
144 2 * num_experts
145 } else {
146 num_experts
147 };
148 assert!(
149 hi >= lo,
150 "the expert-cache ceiling of {hi} slots cannot hold the {lo} slots a layer needs"
151 );
152
153 let spare = budget_bytes - (kv_reserve_pages * bytes_per_page) as i64;
155 let raw = if spare <= 0 {
156 0
157 } else {
158 (spare as u64) / bytes_per_expert
159 };
160 let moe_cache_slots = raw.min(hi).max(lo);
161 overlap = overlap && moe_cache_slots >= 2 * num_experts;
162
163 let remaining = budget_bytes - (moe_cache_slots * bytes_per_expert) as i64;
164 let kv_pages = if remaining <= 0 {
165 kv_reserve_pages
166 } else {
167 ((remaining as u64) / bytes_per_page).max(kv_reserve_pages)
168 };
169
170 let sizes = PoolSizes {
171 moe_cache_slots,
172 kv_pages,
173 prefill_overlap: overlap,
174 };
175 let needed = required_bytes(moe_cache_slots, kv_pages, bytes_per_expert, bytes_per_page);
176 if needed > budget_bytes || kv_pages <= 1 {
177 return Err(BudgetTooSmall {
178 needed_bytes: needed,
179 budget_bytes,
180 sizes,
181 });
182 }
183 Ok(sizes)
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 const GIB: u64 = 1 << 30;
191 const MIB: u64 = 1 << 20;
192
193 #[test]
194 fn the_budget_is_free_vram_less_the_weights_and_the_headroom() {
195 assert_eq!(
196 net_cache_budget_bytes(0.9, 10 * GIB, 4 * GIB, 0),
197 (0.9 * (10 * GIB) as f64) as i64 - (4 * GIB) as i64
198 );
199 assert!(net_cache_budget_bytes(0.5, GIB, 4 * GIB, 0) < 0);
202 }
203
204 #[test]
205 fn the_startup_budget_prices_what_loading_actually_consumed() {
206 assert_eq!(startup_kv_budget(0.9, 1000, 400), 900 - 600);
207 assert_eq!(startup_kv_budget(1.0, 1000, 1000), 1000);
208 }
209
210 #[test]
213 fn the_expert_cache_is_filled_before_kv_gets_the_remainder() {
214 let sizes = plan_cache_budget(
215 (8 * GIB) as i64,
216 16 * MIB, MIB, 32, 256, false,
221 64,
222 u64::MAX,
223 )
224 .expect("fits");
225 assert_eq!(sizes.moe_cache_slots, 256, "full residency");
226 let spent = 256 * 16 * MIB;
227 assert_eq!(sizes.kv_pages, (8 * GIB - spent) / MIB);
228 }
229
230 #[test]
233 fn the_expert_cache_is_capped_at_full_residency() {
234 let sizes = plan_cache_budget((64 * GIB) as i64, MIB, MIB, 8, 64, false, 16, u64::MAX)
235 .expect("fits");
236 assert_eq!(sizes.moe_cache_slots, 64);
237 assert!(sizes.kv_pages > 60_000);
238 }
239
240 #[test]
243 fn a_backend_slot_ceiling_gives_its_bytes_to_kv() {
244 let capped =
245 plan_cache_budget((4 * GIB) as i64, MIB, MIB, 8, 4096, false, 16, 992).expect("fits");
246 assert_eq!(capped.moe_cache_slots, 992);
247 let uncapped = plan_cache_budget((4 * GIB) as i64, MIB, MIB, 8, 4096, false, 16, u64::MAX)
248 .expect("fits");
249 assert!(uncapped.moe_cache_slots > capped.moe_cache_slots);
250 assert!(capped.kv_pages > uncapped.kv_pages);
251 }
252
253 #[test]
256 fn the_kv_reserve_is_taken_out_before_the_cache_is_sized() {
257 let reserve = 1024;
258 let sizes = plan_cache_budget(
259 (2 * GIB) as i64,
260 MIB,
261 MIB,
262 8,
263 4096,
264 false,
265 reserve,
266 u64::MAX,
267 )
268 .expect("fits");
269 assert!(sizes.kv_pages >= reserve);
270 assert!(sizes.moe_cache_slots <= 2048 - reserve);
271 }
272
273 #[test]
276 fn a_tight_budget_drops_the_prefill_double_buffer() {
277 let roomy =
278 plan_cache_budget((4 * GIB) as i64, MIB, MIB, 8, 64, true, 16, u64::MAX).unwrap();
279 assert!(roomy.prefill_overlap);
280
281 let cramped = plan_cache_budget(
283 (4 * GIB) as i64,
284 MIB,
285 MIB,
286 8,
287 64,
288 true,
289 16,
290 12, )
292 .unwrap();
293 assert!(!cramped.prefill_overlap);
294 }
295
296 #[test]
297 fn a_budget_that_cannot_hold_one_layer_is_refused() {
298 let err = plan_cache_budget(MIB as i64, MIB, MIB, 8, 64, false, 16, u64::MAX).unwrap_err();
299 assert!(err.needed_bytes > err.budget_bytes);
300 assert_eq!(err.sizes.moe_cache_slots, 8, "one layer is the minimum");
301 }
302
303 #[test]
304 fn expert_slot_cost_is_the_sum_over_banks() {
305 assert_eq!(expert_bytes_per_slot(&[512, 256]), 768);
306 assert_eq!(expert_bytes_per_slot(&[]), 0);
307 }
308}