1use std::collections::HashMap;
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
14pub enum ResourceType {
15 Flops,
17 MemoryBytes,
19 TimeMs,
21}
22
23#[derive(Clone, Debug)]
27pub struct Budget {
28 pub resource: ResourceType,
30 pub limit: u64,
32 pub used: u64,
34}
35
36impl Budget {
37 pub fn new(resource: ResourceType, limit: u64) -> Self {
39 Self {
40 resource,
41 limit,
42 used: 0,
43 }
44 }
45
46 pub fn remaining(&self) -> u64 {
48 self.limit.saturating_sub(self.used)
49 }
50
51 pub fn utilization(&self) -> f64 {
55 self.used as f64 / self.limit.max(1) as f64
56 }
57
58 pub fn is_exhausted(&self) -> bool {
60 self.used >= self.limit
61 }
62}
63
64#[derive(Clone, Debug)]
68pub struct SessionBudget {
69 pub session_id: u64,
71 pub budgets: HashMap<ResourceType, Budget>,
73}
74
75impl SessionBudget {
76 pub fn new(session_id: u64, limits: Vec<(ResourceType, u64)>) -> Self {
81 let mut budgets = HashMap::new();
82 for (resource, limit) in limits {
83 budgets.insert(resource, Budget::new(resource, limit));
84 }
85 Self {
86 session_id,
87 budgets,
88 }
89 }
90
91 pub fn consume(&mut self, resource: ResourceType, amount: u64) -> Result<(), String> {
101 let budget = self
102 .budgets
103 .entry(resource)
104 .or_insert_with(|| Budget::new(resource, u64::MAX));
105
106 if budget.used.saturating_add(amount) > budget.limit {
108 return Err("budget exceeded".to_string());
109 }
110 budget.used = budget.used.saturating_add(amount);
111 Ok(())
112 }
113
114 pub fn remaining(&self, resource: ResourceType) -> u64 {
119 self.budgets
120 .get(&resource)
121 .map(|b| b.remaining())
122 .unwrap_or(u64::MAX)
123 }
124
125 pub fn is_any_exhausted(&self) -> bool {
127 self.budgets.values().any(|b| b.is_exhausted())
128 }
129}
130
131#[derive(Clone, Debug)]
135pub struct BudgetManagerStats {
136 pub total_sessions: usize,
138 pub exhausted_sessions: usize,
140}
141
142impl BudgetManagerStats {
143 pub fn exhaustion_rate(&self) -> f64 {
147 if self.total_sessions == 0 {
148 return 0.0;
149 }
150 self.exhausted_sessions as f64 / self.total_sessions as f64
151 }
152}
153
154#[derive(Debug, Default)]
174pub struct TensorBudgetManager {
175 pub sessions: HashMap<u64, SessionBudget>,
177 pub next_session_id: u64,
179}
180
181impl TensorBudgetManager {
182 pub fn new() -> Self {
184 Self::default()
185 }
186
187 pub fn open_session(&mut self, limits: Vec<(ResourceType, u64)>) -> u64 {
191 let id = self.next_session_id;
192 self.next_session_id = self.next_session_id.saturating_add(1);
193 self.sessions.insert(id, SessionBudget::new(id, limits));
194 id
195 }
196
197 pub fn consume(
204 &mut self,
205 session_id: u64,
206 resource: ResourceType,
207 amount: u64,
208 ) -> Result<(), String> {
209 self.sessions
210 .get_mut(&session_id)
211 .ok_or_else(|| "session not found".to_string())?
212 .consume(resource, amount)
213 }
214
215 pub fn close_session(&mut self, session_id: u64) -> bool {
220 self.sessions.remove(&session_id).is_some()
221 }
222
223 pub fn stats(&self) -> BudgetManagerStats {
225 let total_sessions = self.sessions.len();
226 let exhausted_sessions = self
227 .sessions
228 .values()
229 .filter(|s| s.is_any_exhausted())
230 .count();
231 BudgetManagerStats {
232 total_sessions,
233 exhausted_sessions,
234 }
235 }
236
237 pub fn session_utilization(&self, session_id: u64) -> Option<Vec<(ResourceType, f64)>> {
241 let session = self.sessions.get(&session_id)?;
242 let utilizations = session
243 .budgets
244 .iter()
245 .map(|(&resource, budget)| (resource, budget.utilization()))
246 .collect();
247 Some(utilizations)
248 }
249}
250
251#[cfg(test)]
254mod tests {
255 use super::*;
256
257 fn make_manager_with_session() -> (TensorBudgetManager, u64) {
259 let mut mgr = TensorBudgetManager::new();
260 let sid = mgr.open_session(vec![
261 (ResourceType::Flops, 1_000),
262 (ResourceType::MemoryBytes, 4_096),
263 (ResourceType::TimeMs, 500),
264 ]);
265 (mgr, sid)
266 }
267
268 #[test]
270 fn test_open_session_creates_budgets() {
271 let (mgr, sid) = make_manager_with_session();
272 assert!(mgr.sessions.contains_key(&sid));
273 let session = &mgr.sessions[&sid];
274 assert_eq!(session.session_id, sid);
275 assert_eq!(session.budgets[&ResourceType::Flops].limit, 1_000);
276 assert_eq!(session.budgets[&ResourceType::MemoryBytes].limit, 4_096);
277 assert_eq!(session.budgets[&ResourceType::TimeMs].limit, 500);
278 }
279
280 #[test]
282 fn test_open_session_distinct_ids() {
283 let mut mgr = TensorBudgetManager::new();
284 let a = mgr.open_session(vec![]);
285 let b = mgr.open_session(vec![]);
286 let c = mgr.open_session(vec![]);
287 assert_ne!(a, b);
288 assert_ne!(b, c);
289 assert_ne!(a, c);
290 }
291
292 #[test]
294 fn test_consume_within_budget_ok() {
295 let (mut mgr, sid) = make_manager_with_session();
296 assert!(mgr.consume(sid, ResourceType::Flops, 500).is_ok());
297 assert_eq!(mgr.sessions[&sid].budgets[&ResourceType::Flops].used, 500);
298 }
299
300 #[test]
302 fn test_consume_at_limit_ok() {
303 let (mut mgr, sid) = make_manager_with_session();
304 assert!(mgr.consume(sid, ResourceType::Flops, 1_000).is_ok());
305 assert!(mgr.sessions[&sid].budgets[&ResourceType::Flops].is_exhausted());
306 }
307
308 #[test]
310 fn test_consume_exceeds_budget_err() {
311 let (mut mgr, sid) = make_manager_with_session();
312 let result = mgr.consume(sid, ResourceType::Flops, 1_001);
313 assert!(result.is_err());
314 assert_eq!(result.unwrap_err(), "budget exceeded");
315 }
316
317 #[test]
319 fn test_consume_budget_unchanged_on_err() {
320 let (mut mgr, sid) = make_manager_with_session();
321 let _ = mgr.consume(sid, ResourceType::Flops, 999);
322 let _ = mgr.consume(sid, ResourceType::Flops, 999); assert_eq!(mgr.sessions[&sid].budgets[&ResourceType::Flops].used, 999);
324 }
325
326 #[test]
328 fn test_auto_create_unlimited_budget() {
329 let mut mgr = TensorBudgetManager::new();
330 let sid = mgr.open_session(vec![(ResourceType::MemoryBytes, 1_024)]);
332 assert!(mgr.consume(sid, ResourceType::Flops, u64::MAX / 2).is_ok());
334 assert_eq!(
335 mgr.sessions[&sid].budgets[&ResourceType::Flops].limit,
336 u64::MAX
337 );
338 }
339
340 #[test]
342 fn test_remaining_after_consume() {
343 let (mut mgr, sid) = make_manager_with_session();
344 mgr.consume(sid, ResourceType::TimeMs, 200)
345 .expect("test: should succeed");
346 let session = &mgr.sessions[&sid];
347 assert_eq!(session.remaining(ResourceType::TimeMs), 300);
348 }
349
350 #[test]
352 fn test_remaining_unregistered_resource() {
353 let mut mgr = TensorBudgetManager::new();
354 let sid = mgr.open_session(vec![]); let session = &mgr.sessions[&sid];
356 assert_eq!(session.remaining(ResourceType::Flops), u64::MAX);
357 }
358
359 #[test]
361 fn test_budget_is_exhausted() {
362 let mut b = Budget::new(ResourceType::Flops, 100);
363 assert!(!b.is_exhausted());
364 b.used = 100;
365 assert!(b.is_exhausted());
366 b.used = 101;
367 assert!(b.is_exhausted());
368 }
369
370 #[test]
372 fn test_budget_utilization() {
373 let mut b = Budget::new(ResourceType::MemoryBytes, 200);
374 assert!((b.utilization() - 0.0).abs() < f64::EPSILON);
375 b.used = 100;
376 assert!((b.utilization() - 0.5).abs() < f64::EPSILON);
377 b.used = 200;
378 assert!((b.utilization() - 1.0).abs() < f64::EPSILON);
379 }
380
381 #[test]
383 fn test_budget_utilization_zero_limit() {
384 let b = Budget::new(ResourceType::Flops, 0);
385 assert!((b.utilization() - 0.0).abs() < f64::EPSILON);
387 }
388
389 #[test]
391 fn test_session_is_any_exhausted() {
392 let mut mgr = TensorBudgetManager::new();
393 let sid = mgr.open_session(vec![
394 (ResourceType::Flops, 10),
395 (ResourceType::MemoryBytes, 1_000),
396 ]);
397 assert!(!mgr.sessions[&sid].is_any_exhausted());
398 mgr.consume(sid, ResourceType::Flops, 10)
399 .expect("test: should succeed");
400 assert!(mgr.sessions[&sid].is_any_exhausted());
401 }
402
403 #[test]
405 fn test_close_session_removes() {
406 let (mut mgr, sid) = make_manager_with_session();
407 assert!(mgr.close_session(sid));
408 assert!(!mgr.sessions.contains_key(&sid));
409 }
410
411 #[test]
413 fn test_close_session_unknown_returns_false() {
414 let mut mgr = TensorBudgetManager::new();
415 assert!(!mgr.close_session(9999));
416 }
417
418 #[test]
420 fn test_stats_exhausted_sessions() {
421 let mut mgr = TensorBudgetManager::new();
422 let s1 = mgr.open_session(vec![(ResourceType::Flops, 10)]);
423 let s2 = mgr.open_session(vec![(ResourceType::Flops, 10)]);
424 let _s3 = mgr.open_session(vec![(ResourceType::Flops, 10)]);
425
426 mgr.consume(s1, ResourceType::Flops, 10)
428 .expect("test: should succeed");
429 mgr.consume(s2, ResourceType::Flops, 10)
430 .expect("test: should succeed");
431
432 let stats = mgr.stats();
433 assert_eq!(stats.total_sessions, 3);
434 assert_eq!(stats.exhausted_sessions, 2);
435 }
436
437 #[test]
439 fn test_exhaustion_rate() {
440 let mut mgr = TensorBudgetManager::new();
441 let s1 = mgr.open_session(vec![(ResourceType::Flops, 10)]);
442 let _s2 = mgr.open_session(vec![(ResourceType::Flops, 10)]);
443 mgr.consume(s1, ResourceType::Flops, 10)
444 .expect("test: should succeed");
445
446 let stats = mgr.stats();
447 assert!((stats.exhaustion_rate() - 0.5).abs() < f64::EPSILON);
448 }
449
450 #[test]
452 fn test_exhaustion_rate_no_sessions() {
453 let mgr = TensorBudgetManager::new();
454 let stats = mgr.stats();
455 assert_eq!(stats.total_sessions, 0);
456 assert!((stats.exhaustion_rate() - 0.0).abs() < f64::EPSILON);
457 }
458
459 #[test]
461 fn test_session_utilization() {
462 let mut mgr = TensorBudgetManager::new();
463 let sid = mgr.open_session(vec![
464 (ResourceType::Flops, 1_000),
465 (ResourceType::MemoryBytes, 2_000),
466 ]);
467 mgr.consume(sid, ResourceType::Flops, 250)
468 .expect("test: should succeed");
469 mgr.consume(sid, ResourceType::MemoryBytes, 1_000)
470 .expect("test: should succeed");
471
472 let utils = mgr.session_utilization(sid).expect("session must exist");
473 let mut map: HashMap<ResourceType, f64> = utils.into_iter().collect();
474 let flops_util = map.remove(&ResourceType::Flops).expect("Flops missing");
475 let mem_util = map
476 .remove(&ResourceType::MemoryBytes)
477 .expect("MemoryBytes missing");
478
479 assert!((flops_util - 0.25).abs() < 1e-9);
480 assert!((mem_util - 0.5).abs() < 1e-9);
481 }
482
483 #[test]
485 fn test_session_utilization_unknown() {
486 let mgr = TensorBudgetManager::new();
487 assert!(mgr.session_utilization(42).is_none());
488 }
489
490 #[test]
492 fn test_consume_unknown_session_err() {
493 let mut mgr = TensorBudgetManager::new();
494 let result = mgr.consume(9999, ResourceType::Flops, 1);
495 assert!(result.is_err());
496 assert_eq!(result.unwrap_err(), "session not found");
497 }
498
499 #[test]
501 fn test_multiple_resources_independent() {
502 let (mut mgr, sid) = make_manager_with_session();
503 mgr.consume(sid, ResourceType::Flops, 900)
504 .expect("test: should succeed");
505 mgr.consume(sid, ResourceType::MemoryBytes, 100)
506 .expect("test: should succeed");
507
508 let session = &mgr.sessions[&sid];
509 assert_eq!(session.budgets[&ResourceType::Flops].used, 900);
510 assert_eq!(session.budgets[&ResourceType::MemoryBytes].used, 100);
511 assert_eq!(session.budgets[&ResourceType::TimeMs].used, 0);
512 }
513}