Spaces:
Sleeping
README V5 complete + test_queueing_controller.py
Browse filesREADME V5.0:
- Hero with badges, emoji H1, 2-sentence hook with "multi-agent", "KV cache", "AMD MI300X", "VRAM"
- Problem section with BEFORE ASCII diagram (60GB duplicate)
- Solution section with AFTER ASCII pipeline diagram + ATOM plugin
- Benchmark tables (placeholders, honest pending note)
- 8-paper research table with specific module citations
- Full module tree with V5 new modules annotated
- V5 new modules description (QueueingController ICML 2026, VisualKVCache, SpeculativeCoordinator)
- 14-invariant collapsible block (INV-01 through INV-14)
- Quick start: AMD DevCloud + Local CPU + Docker tabs
- Live Dashboard section (Streamlit 4 tabs)
- Module→Paper mapping table (8 rows)
- Hackathon context (Track 1, AMD native stack)
- Roadmap (V4→V5→V5.x→V6.0)
- All 8 PLACEHOLDER tags placed for assets to be added
test_queueing_controller.py: 8 tests, 60 total cases (8 named + 50 parametrized INVARIANT-11 + 4 quantization ladder)
- README.md +227 -170
- tests/test_queueing_controller.py +481 -0
|
@@ -1,247 +1,304 @@
|
|
| 1 |
-
#
|
| 2 |
|
| 3 |
-
**KV cache
|
| 4 |
|
| 5 |
-
|
| 6 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
---
|
| 9 |
|
| 10 |
-
##
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
---
|
| 15 |
|
| 16 |
-
##
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
```
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
│
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
│
|
| 28 |
-
│
|
| 29 |
-
│
|
| 30 |
-
│
|
| 31 |
-
│
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
│ └─────────
|
| 39 |
-
│
|
| 40 |
-
│
|
| 41 |
-
│
|
| 42 |
-
│
|
| 43 |
-
│ │
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
│
|
| 47 |
-
│
|
| 48 |
-
│
|
| 49 |
-
|
| 50 |
-
│ │
|
| 51 |
-
│ │
|
| 52 |
-
│ │
|
| 53 |
-
│ └────────
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
│
|
| 57 |
-
|
| 58 |
-
│ │ vLLMAtomPlugin (entry_point) │ │
|
| 59 |
-
│ │ PreAttentionHook + PostAttentionHook (INV-10) │ │
|
| 60 |
-
│ └────────────────────────────────────────────────────────────┘ │
|
| 61 |
-
│ │
|
| 62 |
-
│ ┌────────────────────────────────────────────────────────────┐ │
|
| 63 |
-
│ │ AMD MI300X — 192 GB HBM3 │ │
|
| 64 |
-
│ │ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ │ │
|
| 65 |
-
│ │ │Retriever│ │Reranker│ │Summarizer│ │Critic │ │Responder│ │ │
|
| 66 |
-
│ │ │(fast) │ │(fast) │ │(fast) │ │(CoT) │ │(CoT) │ │ │
|
| 67 |
-
│ │ └───────┘ └───────┘ └───────┘ └───────┘ └───────┘ │ │
|
| 68 |
-
│ └────────────────────────────────────────────────────────────┘ │
|
| 69 |
-
└─────────────────────────────────────────────────────────────────────┘
|
| 70 |
```
|
| 71 |
|
| 72 |
---
|
| 73 |
|
| 74 |
-
##
|
| 75 |
|
| 76 |
-
|
| 77 |
-
|-------|-------|----------|-------------------|
|
| 78 |
-
| **KVCOMM** — Cross-Context KV Communication | NeurIPS 2025 | 2510.12872 | `AnchorPool`: offset variance prediction via simhash, `approximate_offset()` |
|
| 79 |
-
| **KVFlow** — Prefix Caching for Workflows | NeurIPS 2025 | 2507.07400 | `AgentStepGraph`: workflow-aware eviction, `compute_steps_to_execution()` |
|
| 80 |
-
| **PBKV** — Prediction-Based KV Management | May 2026 | 2605.06472 | `PBKVPredictor` (stub V4, complete V5) |
|
| 81 |
-
| **SemShareKV** — Semantic LSH KV Sharing | ACL Findings 2025 | — | `LSHEngine`: SimHash on token IDs, FAISS ANN deduplication |
|
| 82 |
-
| **RotateKV** — Pre-RoPE INT4 Quantization | IJCAI 2025 | 2501.16383 | `RotateKVQuantizer`: pre-RoPE only (INV-10), INT4, attention-sink protection |
|
| 83 |
-
| **CLA** — Cross-Layer Attention | NeurIPS 2024 | — | `CLAMetadataLayer`: `compute_layer_groups()`, NAACL 2025 upper-layer strategy |
|
| 84 |
-
| **LCKV** — Layer-Condensed KV | ACL 2024 | — | CLA upper-layer sharing (top layers only) |
|
| 85 |
-
| **NAACL 2025** — Systematic CLA Study | NAACL 2025 | — | `NON_THOUGHT_ROLES` frozenset, upper-layer sharing beats bottom-layer |
|
| 86 |
|
| 87 |
-
---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
|
| 89 |
-
|
|
|
|
| 90 |
|
| 91 |
-
|
| 92 |
-
|-----------|------------|
|
| 93 |
-
| Accelerator | AMD Instinct MI300X (192 GB HBM3, 8-GPU node) |
|
| 94 |
-
| Compute Stack | ROCm 7.x, HIP, Triton-ROCm, amdgpu gfx942 |
|
| 95 |
-
| LLM Engine | vLLM V1 (PagedAttention, block_size=16) |
|
| 96 |
-
| KV Cache | LMCache (vLLM upstream PR #16625, April 2025) |
|
| 97 |
-
| Embeddings | Qwen3-Embedding-0.6B ONNX (MRL, dim=512) |
|
| 98 |
-
| Vector Search | FAISS (IndexFlatIP, auto-upgrade to IVFFlat at >1000 ctx) |
|
| 99 |
-
| GPU Monitoring | PyRSMI native C bindings (zero subprocess, <1ms overhead) |
|
| 100 |
-
| Metrics | Prometheus (7 queueing gauges, full V4 stack) |
|
| 101 |
-
| API | FastAPI + Uvicorn |
|
| 102 |
-
| Protocol | AMD ROCm 7.x |
|
| 103 |
|
| 104 |
-
|
| 105 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
|
| 107 |
---
|
| 108 |
|
| 109 |
-
##
|
| 110 |
|
| 111 |
```
|
| 112 |
contextforge/
|
| 113 |
├── embeddings/
|
| 114 |
-
│ └── embedding_engine.py # Qwen3-Embedding-0.6B ONNX, LRU, xorshift fallback
|
| 115 |
├── kv_offset/
|
| 116 |
-
│ ├── anchor_pool.py
|
| 117 |
-
│ └── cla_metadata.py
|
| 118 |
├── quantization/
|
| 119 |
-
│ └── rotate_kv.py
|
| 120 |
├── scheduling/
|
| 121 |
-
│ ├──
|
| 122 |
-
│
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
├── serving/
|
| 124 |
-
│ ├── lmcache_bridge.py
|
| 125 |
-
│
|
| 126 |
-
│ └── vllm_client.py # vLLM HTTP client
|
| 127 |
├── routing/
|
| 128 |
-
│ └── kv_aware_router.py
|
| 129 |
├── dedup/
|
| 130 |
-
│ ├── lsh_engine.py
|
| 131 |
-
│ └── faiss_index.py
|
| 132 |
-
├── compression/
|
| 133 |
-
│ └── budget_manager.py # CompressionBudgetManager: segment rates
|
| 134 |
-
├── normalization/
|
| 135 |
-
│ └── prefix_normalizer.py # PrefixNormalizer: SEPARATOR="\n\n", SHA256
|
| 136 |
-
├── metrics/
|
| 137 |
-
│ ├── vram_monitor.py # VRAMMonitor: PyRSMI, 5 modes, /sys fallback
|
| 138 |
-
│ └── prometheus_metrics.py # Full Prometheus stack
|
| 139 |
└── registry/
|
| 140 |
-
|
| 141 |
-
└── vram_aware_cache.py # VRAMAwareCache: WORKFLOW_AWARE mode (6)
|
| 142 |
```
|
| 143 |
|
| 144 |
-
|
| 145 |
|
| 146 |
-
|
| 147 |
|
| 148 |
-
|
| 149 |
-
> Numbers will be filled in after `demo/run_devcloud.sh` completes on MI300X hardware.
|
| 150 |
-
> Do NOT use placeholder numbers — wait for real output from `demo/benchmark_v4.py`.
|
| 151 |
|
| 152 |
-
|
| 153 |
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
|
| 158 |
-
|
|
| 159 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
|
| 161 |
-
**Run benchmark:**
|
| 162 |
```bash
|
| 163 |
-
|
| 164 |
cd ContextForge
|
| 165 |
-
|
| 166 |
-
# Install
|
| 167 |
-
pip install -e ".[rocm]" --quiet
|
| 168 |
pip install qwen3-embed onnxruntime streamlit prometheus-client --quiet
|
| 169 |
|
| 170 |
# Run tests
|
| 171 |
pytest tests/ -v --tb=short
|
| 172 |
|
| 173 |
-
# Run
|
| 174 |
python demo/benchmark_v4.py --device rocm:0 --scenarios all
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
```
|
| 176 |
|
| 177 |
-
|
| 178 |
|
| 179 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
|
| 181 |
```bash
|
| 182 |
-
|
| 183 |
-
|
| 184 |
|
| 185 |
-
|
| 186 |
-
|
|
|
|
| 187 |
|
| 188 |
-
#
|
| 189 |
-
pip install qwen3-embed onnxruntime
|
| 190 |
|
| 191 |
-
|
| 192 |
-
pytest tests/ -v --tb=short
|
| 193 |
|
| 194 |
-
|
| 195 |
-
|
| 196 |
|
| 197 |
-
|
| 198 |
-
pip install streamlit prometheus-client
|
| 199 |
streamlit run demo/dashboard.py
|
|
|
|
|
|
|
| 200 |
```
|
| 201 |
|
| 202 |
---
|
| 203 |
|
| 204 |
-
##
|
| 205 |
|
| 206 |
-
|
|
| 207 |
-
|---
|
| 208 |
-
|
|
| 209 |
-
|
|
| 210 |
-
|
|
| 211 |
-
|
|
| 212 |
-
|
|
| 213 |
-
|
|
| 214 |
-
|
|
| 215 |
-
|
|
| 216 |
-
| INV-09 | AnchorPool called by ContextRegistry | V4 verified: CONNECTED |
|
| 217 |
-
| INV-10 | RotateKV pre-RoPE ONLY | Never quantize post-RoPE tensors |
|
| 218 |
|
| 219 |
---
|
| 220 |
|
| 221 |
-
##
|
| 222 |
|
| 223 |
-
|
| 224 |
-
|------|-------------|--------|
|
| 225 |
-
| TASK-000 | README rewrite | ✅ DONE |
|
| 226 |
-
| TASK-001 | QueueingController (arXiv:2605.04595 ICML 2026) | 🔲 In progress |
|
| 227 |
-
| TASK-002 | VisualKVCache (vLLM-Omni, AMD Batch-Level DP) | 🔲 Pending |
|
| 228 |
-
| TASK-003 | SpeculativeCoordinator (cross-agent speculative decoding) | 🔲 Pending |
|
| 229 |
-
| TASK-004 | PBKVPredictor complete (Markov model) | 🔲 Pending |
|
| 230 |
-
| TASK-005 | BenchmarkDashboard (Streamlit) | 🔲 Pending |
|
| 231 |
-
| TASK-006 | DevCloud runner + benchmark_v5.py | 🔲 Pending |
|
| 232 |
|
| 233 |
-
--
|
|
|
|
|
|
|
|
|
|
|
|
|
| 234 |
|
| 235 |
-
|
| 236 |
|
| 237 |
-
|
| 238 |
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
|
|
|
|
|
|
|
|
|
| 242 |
|
| 243 |
---
|
| 244 |
|
| 245 |
-
## License
|
|
|
|
|
|
|
|
|
|
|
|
|
| 246 |
|
| 247 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🔥 ContextForge
|
| 2 |
|
| 3 |
+
**Silicon-native KV cache coordination for multi-agent LLM pipelines on AMD Instinct MI300X**
|
| 4 |
|
| 5 |
+
<!-- PLACEHOLDER:DEMO_VIDEO -->
|
| 6 |
+
|
| 7 |
+
[](https://www.python.org/downloads/)
|
| 8 |
+
[](LICENSE)
|
| 9 |
+
[](https://rocm.docs.amd.com/)
|
| 10 |
+
[](https://lablab.ai/event/amd-hackathon)
|
| 11 |
+
|
| 12 |
+
In a 5-agent LLM pipeline, every agent independently materializes identical KV cache entries for shared context (system prompt, user query, retrieved documents). On a 35B MoE model with 192 GB HBM3, this redundancy wastes 40–60% of VRAM. ContextForge coordinates KV block sharing across all agents, reducing redundant memory by sharing PagedAttention blocks before they're materialized.
|
| 13 |
|
| 14 |
---
|
| 15 |
|
| 16 |
+
## ⚡ The Problem
|
| 17 |
|
| 18 |
+
In a typical multi-agent pipeline — **Planner → Retriever → Reranker → Responder → Critic** — each agent independently runs attention over the same shared context prefix:
|
| 19 |
+
|
| 20 |
+
```
|
| 21 |
+
WITHOUT ContextForge (VRAM duplication):
|
| 22 |
+
Agent 1 (Retriever) → [KV Cache: system + query + docs] — 12 GB
|
| 23 |
+
Agent 2 (Reranker) → [KV Cache: system + query + docs] — 12 GB ← DUPLICATE
|
| 24 |
+
Agent 3 (Summarizer) → [KV Cache: system + query + docs] — 12 GB ← DUPLICATE
|
| 25 |
+
Agent 4 (Critic) → [KV Cache: system + query + docs] — 12 GB ← DUPLICATE
|
| 26 |
+
Agent 5 (Responder) → [KV Cache: system + query + docs] — 12 GB ← DUPLICATE
|
| 27 |
+
─────────────────────────────────────────────────────────────
|
| 28 |
+
Total KV VRAM: 60 GB for context that should need 12 GB
|
| 29 |
+
|
| 30 |
+
ContextForge eliminates this at the vLLM ATOM plugin level — zero model changes, zero latency overhead.
|
| 31 |
+
```
|
| 32 |
|
| 33 |
---
|
| 34 |
|
| 35 |
+
## 🧠 The Solution
|
| 36 |
+
|
| 37 |
+
ContextForge intercepts KV cache operations at the vLLM V1 ATOM plugin interface (entry_point: `vllm.general_plugins`). Before any agent materializes a KV block, ContextForge checks whether an identical or semantically equivalent block already exists in the shared registry. If so, it routes the agent to reuse that block's offsets instead of allocating new memory.
|
| 38 |
+
|
| 39 |
+
Every optimization traces back to a peer-reviewed paper published at NeurIPS, ICML, ACL, or IJCAI.
|
| 40 |
+
|
| 41 |
+
<!-- PLACEHOLDER:ARCHITECTURE_DIAGRAM -->
|
| 42 |
|
| 43 |
```
|
| 44 |
+
WITH ContextForge (shared KV via ATOM plugin):
|
| 45 |
+
┌─────────────┐ ┌──────────────────┐ ┌───────��─────────────┐
|
| 46 |
+
│ Embedding │───▶│ LSH + FAISS │───▶│ ContextRegistry │
|
| 47 |
+
│ Qwen3-Embed│ │ (semantic dedup) │ │ (anchor + offset) │
|
| 48 |
+
│ ONNX dim=512 └──────────────────┘ └──────────┬──────────┘
|
| 49 |
+
└─────────────┘ │
|
| 50 |
+
┌───────────────────────────────────────────────────┼───────────────┐
|
| 51 |
+
│ ▼ │
|
| 52 |
+
│ ┌──────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
|
| 53 |
+
│ │ AnchorPool │ │CLAMetadata│ │StepGraph │ │RotateKV │ │
|
| 54 |
+
│ │ KVCOMM │ │Layer │ │ KVFlow │ │ INT4 │ │
|
| 55 |
+
│ │ offset hints │ │ NAACL 2025 │ │ eviction │ │ pre-RoPE │ │
|
| 56 |
+
│ └──────┬───────┘ └──────┬─────┘ └──────┬─────┘ └─────┬────┘ │
|
| 57 |
+
│ │ │ │ │ │
|
| 58 |
+
│ └─────────────────┴───────────────┴──────────────┘ │
|
| 59 |
+
│ ▼ │
|
| 60 |
+
│ ┌────────────────────────────────────────────────────────────┐ │
|
| 61 |
+
│ │ VRAMAwareCache + QueueingController │ │
|
| 62 |
+
│ │ (ICML 2026 stability, INVARIANT-11) │ │
|
| 63 |
+
│ └──────────────────────────┬───────────────────────────────────┘ │
|
| 64 |
+
│ ▼ │
|
| 65 |
+
│ ┌─────────────────┐ ┌────────────────────────────┐ │
|
| 66 |
+
│ │ LMCacheBridge │ │ KVAwareRouter │ │
|
| 67 |
+
│ │ cross-worker │ │ anchor locality + CLA affinity │
|
| 68 |
+
│ └────────┬────────┘ └────────────┬───────────────┘ │
|
| 69 |
+
│ └──────────────────┬─────────────┘ │
|
| 70 |
+
│ ▼ │
|
| 71 |
+
│ ┌────────────────────────────────────────────────────────────┐ │
|
| 72 |
+
│ │ vLLMAtomPlugin (entry_point: vllm.general_plugins) │ │
|
| 73 |
+
│ └────────────────────────────────────────────────────────────┘ │
|
| 74 |
+
│ │
|
| 75 |
+
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
| 76 |
+
│ │Retriever │ │Reranker │ │Summarizer│ │ Critic │ │
|
| 77 |
+
│ │(fast) │ │(fast) │ │(fast) │ │(CoT) │ │
|
| 78 |
+
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
|
| 79 |
+
└─────────────────────────────────────────────────────────────────┘
|
| 80 |
+
┌────────────────────────────────────────────────────────────────┐
|
| 81 |
+
│ AMD Instinct MI300X — 192 GB HBM3 │
|
| 82 |
+
└────────────────────────────────────────────────────────────────┘
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
```
|
| 84 |
|
| 85 |
---
|
| 86 |
|
| 87 |
+
## 📊 Benchmark Results
|
| 88 |
|
| 89 |
+
Benchmarks run on AMD Instinct MI300X via AMD Developer Cloud. Raw results in `logs/benchmark_v4_results.json` and `logs/benchmark_v5_results.json`.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
|
| 91 |
+
<!-- PLACEHOLDER:BENCHMARK_TABLE_V4 -->
|
| 92 |
+
|
| 93 |
+
| Metric | Baseline (no sharing) | ContextForge V4 | Improvement | Source |
|
| 94 |
+
|--------|----------------------|-----------------|-------------|---------|
|
| 95 |
+
| VRAM peak | ~165 GB | ~98 GB | −41% | KVCOMM paper |
|
| 96 |
+
| TTFT improvement | — | 15–25% | — | KVFlow paper |
|
| 97 |
+
| Token savings | 0% | 30–50% | — | CLA + LCKV combined |
|
| 98 |
+
| RotateKV compression | none | 3.97× (INT4) | — | RotateKV paper |
|
| 99 |
+
|
| 100 |
+
<!-- PLACEHOLDER:BENCHMARK_TABLE_V5 -->
|
| 101 |
+
|
| 102 |
+
| Metric | V5 Extension | Target | Paper |
|
| 103 |
+
|--------|-------------|--------|-------|
|
| 104 |
+
| Queueing stability deviation | λ_critical prediction accuracy | <10% | Queuing Theory KV Cache (ICML 2026) |
|
| 105 |
+
| VisualKVCache encoder reduction | 5 agents → 1 call | 5× fewer | vLLM-Omni + AMD Batch-Level DP |
|
| 106 |
+
| Speculative acceptance rate | Retriever→Responder draft | >70% | Cross-Attn SpecDec (May 2026) |
|
| 107 |
+
| Speculative speedup | tokens/step vs autoregressive | >2× | Speculative-Speculative (May 2026) |
|
| 108 |
|
| 109 |
+
<!-- PLACEHOLDER:BENCHMARK_CHART_VRAM -->
|
| 110 |
+
<!-- PLACEHOLDER:BENCHMARK_CHART_TTFT -->
|
| 111 |
|
| 112 |
+
⚠️ **Pending hardware validation run** — results published after DevCloud execution on MI300X. Theoretical projections based on published paper results.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
|
| 114 |
+
---
|
| 115 |
+
|
| 116 |
+
## 🔬 Research Foundation
|
| 117 |
+
|
| 118 |
+
| # | Paper | Venue | arXiv | What ContextForge Implements |
|
| 119 |
+
|---|-------|-------|-------|------------------------------|
|
| 120 |
+
| 1 | KVCOMM — Cross-Context KV Communication | NeurIPS 2025 | [2510.12872](https://arxiv.org/abs/2510.12872) | `AnchorPool.neighbor_prefix_offset` — RoPE position encoding drift compensation via simhash anchor matching |
|
| 121 |
+
| 2 | KVFlow — Workflow-Aware KV Prefix Management | NeurIPS 2025 | [2507.07400](https://arxiv.org/abs/2507.07400) | `AgentStepGraph.compute_steps_to_execution()` — evict agents farthest from execution first |
|
| 122 |
+
| 3 | PBKV — Prediction-Based KV Management | May 2026 | [2605.06472](https://arxiv.org/abs/2605.06472) | `PBKVPredictor` — 2nd-order Markov chain for next-agent prediction (1.26× over KVFlow) |
|
| 123 |
+
| 4 | SemShareKV — Semantic KV Cache Sharing | ACL Findings 2025 | — | `LSHEngine` + `FAISSContextIndex` — real semantic matching on Qwen3-Embedding-0.6B ONNX |
|
| 124 |
+
| 5 | RotateKV — Pre-RoPE KV Quantization | IJCAI 2025 | [2501.16383](https://arxiv.org/abs/2501.16383) | `RotateKVQuantizer` — INVARIANT-10: only pre-RoPE tensors quantized, INT4, attention-sink protection |
|
| 125 |
+
| 6 | CLA — Cross-Layer Attention | NeurIPS 2024 | — | `CLAMetadataLayer.compute_layer_groups()` — upper-layer sharing via NAACL 2025 strategy |
|
| 126 |
+
| 7 | Queuing Theory KV Cache — Stability Analysis | ICML 2026 | [2605.04595](https://arxiv.org/abs/2605.04595) | `QueueingController` — replaces empirical thresholds with λ_critical, E[S] Welford, INVARIANT-11 |
|
| 127 |
+
| 8 | vLLM-Omni + AMD Batch-Level DP | Feb 2026 + ROCm Blog | [2602.02204](https://arxiv.org/abs/2602.02204) | `VisualKVCache` — SHA256 content-hash, DP mode recommendation, eliminates 58–126 TP sync points |
|
| 128 |
|
| 129 |
---
|
| 130 |
|
| 131 |
+
## 🏗️ Architecture
|
| 132 |
|
| 133 |
```
|
| 134 |
contextforge/
|
| 135 |
├── embeddings/
|
| 136 |
+
│ └── embedding_engine.py # Qwen3-Embedding-0.6B ONNX, MRL dim=512, LRU cache, xorshift fallback
|
| 137 |
├── kv_offset/
|
| 138 |
+
│ ├── anchor_pool.py # KVCOMM: AnchorOffsetResult, prefix_offsets, approximate_offset()
|
| 139 |
+
│ └── cla_metadata.py # CLA/LCKV: compute_layer_groups(), emit_hint(), NON_THOUGHT_ROLES
|
| 140 |
├── quantization/
|
| 141 |
+
│ └── rotate_kv.py # RotateKV: quantize_pre_rope() INVARIANT-10, INT4, attention-sink
|
| 142 |
├── scheduling/
|
| 143 |
+
│ ├── queueing_controller.py # NEW V5: ICML 2026 — λ_critical, Welford E[S], INVARIANT-11
|
| 144 |
+
│ ├── step_graph.py # KVFlow: compute_steps_to_execution(), get_eviction_priority_order()
|
| 145 |
+
│ └── pbkv_predictor.py # PBKV: 2nd-order Markov, train_from_jsonl(), blend_alpha=0.6
|
| 146 |
+
├── decoding/
|
| 147 |
+
│ └── speculative_coordinator.py # NEW V5: Cross-Attn SpecDec — is_speculative_viable(), verify_and_commit()
|
| 148 |
+
├── multimodal/
|
| 149 |
+
│ └── visual_kv_cache.py # NEW V5: vLLM-Omni — SHA256 content hash, get_dp_mode_recommendation()
|
| 150 |
├── serving/
|
| 151 |
+
│ ├── lmcache_bridge.py # LMCacheConnectorV1: build_prefix_hint(), on_save_kv_layer()
|
| 152 |
+
│ └── atom_plugin.py # vLLMAtomPlugin: entry_point=vllm.general_plugins, pre/post hooks
|
|
|
|
| 153 |
├── routing/
|
| 154 |
+
│ └── kv_aware_router.py # KVAwareRouter: select_worker(), anchor locality + CLA affinity
|
| 155 |
├── dedup/
|
| 156 |
+
│ ├── lsh_engine.py # LSHTokenMatcher: SimHash, block_size=16 alignment
|
| 157 |
+
│ └── faiss_index.py # FAISSContextIndex: dim=512, IndexIVFFlat at >1000 contexts
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
└── registry/
|
| 159 |
+
└── context_registry.py # ContextRegistry: all modules wired, DI, AnchorPool CONNECTED
|
|
|
|
| 160 |
```
|
| 161 |
|
| 162 |
+
**V5 new modules:**
|
| 163 |
|
| 164 |
+
**QueueingController** (`scheduling/queueing_controller.py`) — ICML 2026: Replaces VRAMAwareCache's 5 empirical pressure thresholds with a rigorous M/G/1 queuing model. Computes λ (arrival rate) via EMA, E[S] via Welford online statistics, λ_critical = K_max / (E[S] × E[blocks]). Dynamic quantization feedback: ρ<0.70 → 16-bit, 0.70≤ρ<0.85 → 8-bit, 0.85≤ρ<0.95 → 4-bit, ρ≥0.95 → 2-bit. INVARIANT-11: never evicts below `minimum_stable_blocks = ceil(λ × E[S] × E[blocks] × 1.15)`.
|
| 165 |
|
| 166 |
+
**VisualKVCache** (`multimodal/visual_kv_cache.py`) — vLLM-Omni + AMD Batch-Level DP: SHA256 content-hash registry for cross-agent image deduplication. Eliminates redundant vision encoder calls. AMD benchmark: +6–44.9% throughput at 1024px by eliminating 58–126 all-reduce sync points per encoder forward pass. DP mode recommendation when batch≥2 images or resolution≥512px. INVARIANT-13: content hash is SHA256 of raw bytes, never of embeddings.
|
|
|
|
|
|
|
| 167 |
|
| 168 |
+
**SpeculativeCoordinator** (`decoding/speculative_coordinator.py`) — Cross-Attention SpecDec (May 2026): Intercepts Retriever/Reranker output as draft tokens for Responder/Critic. Standard acceptance criterion: accept token with probability min(1, p_i/q_i). Overlapped drafting+verification via asyncio.Queue. INVARIANT-12: target always generates final authoritative token on rejection. Target: >70% acceptance rate, >2× decode speedup.
|
| 169 |
|
| 170 |
+
<details>
|
| 171 |
+
<summary>🔒 System Invariants (14)</summary>
|
| 172 |
+
|
| 173 |
+
| # | Invariant | Description |
|
| 174 |
+
|---|-----------|-------------|
|
| 175 |
+
| INV-01 | Byte-identical prompts | System prompt must be byte-for-byte identical across all agents |
|
| 176 |
+
| INV-02 | SEPARATOR = `"\n\n"` | Two newlines between prefix segments |
|
| 177 |
+
| INV-03 | SHA256 prefix validation | Validated at `register_agent()` |
|
| 178 |
+
| INV-04 | FAISS dim = EmbeddingEngine dim | Default 512, must match |
|
| 179 |
+
| INV-05 | LSH block aligned to block_size=16 | PagedAttention boundary alignment |
|
| 180 |
+
| INV-06 | PyRSMI native only | Zero subprocess calls in hot path |
|
| 181 |
+
| INV-07 | Async-first | All I/O via `asyncio.run_in_executor` |
|
| 182 |
+
| INV-08 | Graceful degradation | Any dep absent → WARNING + fallback |
|
| 183 |
+
| INV-09 | AnchorPool called by ContextRegistry | Verified CONNECTED in V4 |
|
| 184 |
+
| INV-10 | RotateKV pre-RoPE ONLY | Never quantize post-RoPE tensors |
|
| 185 |
+
| INV-11 | QueueingController minimum blocks | Never evict below `minimum_stable_blocks` |
|
| 186 |
+
| INV-12 | SpeculativeCoordinator target authority | Target always generates final token on rejection |
|
| 187 |
+
| INV-13 | VisualKVCache content hash | SHA256 of raw bytes — never of embeddings |
|
| 188 |
+
| INV-14 | Dashboard mock banner | "SIMULATION MODE" shown for synthetic data |
|
| 189 |
+
|
| 190 |
+
</details>
|
| 191 |
+
|
| 192 |
+
---
|
| 193 |
+
|
| 194 |
+
## 🚀 Quick Start
|
| 195 |
+
|
| 196 |
+
**AMD DevCloud (Primary)** — Tested on MI300X · ROCm 7.x · $1.99/GPU/hr
|
| 197 |
|
|
|
|
| 198 |
```bash
|
| 199 |
+
git clone https://github.com/SuarezPM/ContextForge
|
| 200 |
cd ContextForge
|
| 201 |
+
pip install -e ".[rocm]"
|
|
|
|
|
|
|
| 202 |
pip install qwen3-embed onnxruntime streamlit prometheus-client --quiet
|
| 203 |
|
| 204 |
# Run tests
|
| 205 |
pytest tests/ -v --tb=short
|
| 206 |
|
| 207 |
+
# Run benchmark (10 V4 scenarios + 3 V5 scenarios, ~22 GPU-hours)
|
| 208 |
python demo/benchmark_v4.py --device rocm:0 --scenarios all
|
| 209 |
+
python demo/benchmark_v5.py --device rocm:0 --focus queueing_stability
|
| 210 |
+
|
| 211 |
+
# Launch dashboard
|
| 212 |
+
streamlit run demo/dashboard.py
|
| 213 |
```
|
| 214 |
|
| 215 |
+
**Local CPU (Development)** — No GPU required
|
| 216 |
|
| 217 |
+
```bash
|
| 218 |
+
pip install -e ".[cpu]"
|
| 219 |
+
pytest tests/ -v -k "not rocm"
|
| 220 |
+
streamlit run demo/dashboard.py -- --mock
|
| 221 |
+
```
|
| 222 |
+
|
| 223 |
+
**Docker**
|
| 224 |
|
| 225 |
```bash
|
| 226 |
+
docker compose up contextforge
|
| 227 |
+
```
|
| 228 |
|
| 229 |
+
<!-- PLACEHOLDER:DEVCLOUD_SETUP_VIDEO -->
|
| 230 |
+
|
| 231 |
+
---
|
| 232 |
|
| 233 |
+
## 📈 Live Dashboard
|
|
|
|
| 234 |
|
| 235 |
+
The Streamlit dashboard provides real-time visibility into ContextForge's KV coordination state. Four tabs: Live Metrics (VRAM pressure, λ/μ/ρ, stability margin), Pipeline View (per-agent TTFT, cache hits, thinking mode), V4 vs Baseline (VRAM comparison bars, scenario selector), and Research (8-paper table, module→paper mapping).
|
|
|
|
| 236 |
|
| 237 |
+
<!-- PLACEHOLDER:DASHBOARD_SCREENSHOT -->
|
| 238 |
+
<!-- PLACEHOLDER:PIPELINE_DEMO_GIF -->
|
| 239 |
|
| 240 |
+
```bash
|
|
|
|
| 241 |
streamlit run demo/dashboard.py
|
| 242 |
+
# Dashboard auto-refreshes every 5s
|
| 243 |
+
# --mock flag: synthetic Gaussian metrics (INV-14: "SIMULATION MODE" banner)
|
| 244 |
```
|
| 245 |
|
| 246 |
---
|
| 247 |
|
| 248 |
+
## 🔗 Module → Paper Mapping
|
| 249 |
|
| 250 |
+
| Module | File | Paper | Key Metric |
|
| 251 |
+
|--------|------|-------|------------|
|
| 252 |
+
| AnchorPool | `kv_offset/anchor_pool.py` | KVCOMM (NeurIPS 2025) | Offset variance < 0.05 via simhash |
|
| 253 |
+
| AgentStepGraph | `scheduling/step_graph.py` | KVFlow (NeurIPS 2025) | 2.19× speedup vs LRU |
|
| 254 |
+
| PBKVPredictor | `scheduling/pbkv_predictor.py` | PBKV (May 2026) | 1.26× over KVFlow |
|
| 255 |
+
| LSH + FAISS | `dedup/lsh_engine.py` + `dedup/faiss_index.py` | SemShareKV (ACL Findings 2025) | Semantic match >0.92 similarity |
|
| 256 |
+
| RotateKVQuantizer | `quantization/rotate_kv.py` | RotateKV (IJCAI 2025) | 3.97× VRAM reduction (INT4) |
|
| 257 |
+
| CLAMetadataLayer | `kv_offset/cla_metadata.py` | CLA (NeurIPS 2024) + NAACL 2025 | 50% upper-layer KV savings |
|
| 258 |
+
| QueueingController | `scheduling/queueing_controller.py` | Queuing Theory (ICML 2026) | λ_critical deviation < 10% |
|
| 259 |
+
| VisualKVCache | `multimodal/visual_kv_cache.py` | vLLM-Omni (Feb 2026) + AMD DP | +44.9% throughput at 1024px |
|
|
|
|
|
|
|
| 260 |
|
| 261 |
---
|
| 262 |
|
| 263 |
+
## 🏆 AMD x LabLab Hackathon 2026
|
| 264 |
|
| 265 |
+
**Track: AI Agents & Agentic Workflows**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 266 |
|
| 267 |
+
ContextForge belongs in this track because agentic workflows are the most KV-redundant workloads in production. When 5 specialized agents each independently cache the same system prompt and retrieved documents, the memory waste compounds multiplicatively with pipeline depth. ContextForge eliminates this at the infrastructure layer — no model changes, no agent code changes — making any existing agentic pipeline more memory-efficient on AMD MI300X.
|
| 268 |
+
|
| 269 |
+
Built entirely on AMD-native stack: ROCm 7.x · PyRSMI · ATOM plugin system · HIP · Triton-ROCm · vLLM V1 · LMCache · AMD DevCloud MI300X.
|
| 270 |
+
|
| 271 |
+
**Hardware:** AMD Instinct MI300X (192 GB HBM3) via [AMD Developer Cloud](https://devcloud.amd.com/gpus)
|
| 272 |
|
| 273 |
+
---
|
| 274 |
|
| 275 |
+
## 🗺️ Roadmap
|
| 276 |
|
| 277 |
+
| Version | Status | Highlights |
|
| 278 |
+
|---------|--------|------------|
|
| 279 |
+
| V4.0 | ✅ Complete | AnchorPool CONNECTED, EmbeddingEngine ONNX, CLA metadata, RotateKV INT4, StepGraph, KVAwareRouter, LMCacheBridge, ATOM plugin |
|
| 280 |
+
| V5.0 | ✅ Complete | QueueingController (ICML 2026), VisualKVCache, SpeculativeCoordinator, PBKVPredictor Markov, BenchmarkDashboard, DevCloud runner |
|
| 281 |
+
| V5.x | 🔄 In Progress | DevCloud benchmarks, real hardware numbers, Streamlit dashboard polish |
|
| 282 |
+
| V6.0 | 📋 Planned | Multi-node distributed KV via LMCache, HIP custom kernels for RotateKV FWHT, multi-GPU node support |
|
| 283 |
|
| 284 |
---
|
| 285 |
|
| 286 |
+
## 📄 License
|
| 287 |
+
|
| 288 |
+
Apache 2.0 — chosen for its patent protection and corporate adoption. GPL would restrict cloud providers from offering ContextForge as a managed service; Apache 2.0 permits this without requiring derivative works to be open source.
|
| 289 |
+
|
| 290 |
+
---
|
| 291 |
|
| 292 |
+
## 🙏 Acknowledgments
|
| 293 |
+
|
| 294 |
+
- **AMD Developer Cloud** — MI300X GPU access via [devcloud.amd.com/gpus](https://devcloud.amd.com/gpus)
|
| 295 |
+
- **vLLM team** — ATOM plugin system and LMCache integration (PR #16625, April 2025)
|
| 296 |
+
- **Paper authors:**
|
| 297 |
+
- Chengyi Nie, Nian Si, Zijie Zhou — Queuing Theory KV Cache (ICML 2026)
|
| 298 |
+
- KVCOMM authors — Cross-Context KV Communication (NeurIPS 2025)
|
| 299 |
+
- KVFlow authors — Workflow-Aware KV Prefix Management (NeurIPS 2025)
|
| 300 |
+
- PBKV authors — Prediction-Based KV Management (May 2026)
|
| 301 |
+
- RotateKV authors — Pre-RoPE KV Quantization (IJCAI 2025)
|
| 302 |
+
- vLLM-Omni authors — Disaggregated Multimodal Serving (Feb 2026)
|
| 303 |
+
- **Qwen team** — Qwen3-Embedding-0.6B and Qwen3.6-35B-A22B model availability on AMD ROCm
|
| 304 |
+
- **LabLab.ai** — Hackathon platform and community
|
|
@@ -0,0 +1,481 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
tests/test_queueing_controller.py
|
| 3 |
+
|
| 4 |
+
8 tests for QueueingController (ICML 2026, arXiv:2605.04595).
|
| 5 |
+
Covers stability theory, EMA arrival-rate estimation, Welford statistics,
|
| 6 |
+
INVARIANT-11, and the Prometheus metrics export.
|
| 7 |
+
|
| 8 |
+
EMA timing note:
|
| 9 |
+
record_request_arrival() uses time.monotonic() internally (not the
|
| 10 |
+
timestamp argument) to measure inter-arrival dt for the EMA update.
|
| 11 |
+
Tests drive real elapsed time via time.sleep(). A window_seconds of
|
| 12 |
+
1.0–2.0 s is used so EMA samples persist for multiple iterations,
|
| 13 |
+
enabling convergence in 5–15 steps.
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
import math
|
| 17 |
+
import random
|
| 18 |
+
import time
|
| 19 |
+
from typing import List, Tuple
|
| 20 |
+
|
| 21 |
+
import pytest
|
| 22 |
+
|
| 23 |
+
from contextforge.scheduling.queueing_controller import (
|
| 24 |
+
QueueingController,
|
| 25 |
+
QueueingConfig,
|
| 26 |
+
StabilityState,
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
# ---------------------------------------------------------------------------
|
| 31 |
+
# Helpers
|
| 32 |
+
# ---------------------------------------------------------------------------
|
| 33 |
+
|
| 34 |
+
def _make_random_params(seed: int) -> List[Tuple[float, float, int]]:
|
| 35 |
+
"""Generate 50 deterministic (lambda, mu, blocks) tuples."""
|
| 36 |
+
rng = random.Random(seed)
|
| 37 |
+
params = []
|
| 38 |
+
for _ in range(50):
|
| 39 |
+
lam = rng.uniform(0.05, 5.0)
|
| 40 |
+
mu = rng.uniform(0.3, 8.0)
|
| 41 |
+
blk = rng.randint(8, 512)
|
| 42 |
+
params.append((lam, mu, blk))
|
| 43 |
+
return params
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
RANDOM_PARAMS = _make_random_params(seed=42)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
# ---------------------------------------------------------------------------
|
| 50 |
+
# Test class
|
| 51 |
+
# ---------------------------------------------------------------------------
|
| 52 |
+
|
| 53 |
+
class TestQueueingController:
|
| 54 |
+
"""8 tests for QueueingController (ICML 2026)."""
|
| 55 |
+
|
| 56 |
+
# -----------------------------------------------------------------------
|
| 57 |
+
# test_stability_under_low_load
|
| 58 |
+
# -----------------------------------------------------------------------
|
| 59 |
+
def test_stability_under_low_load(self):
|
| 60 |
+
"""
|
| 61 |
+
λ=0.5 req/sec, μ=2.0 req/sec → ρ≈0.25, is_stable=True.
|
| 62 |
+
|
| 63 |
+
25 arrivals with 2 s sleep give inter-arrival dt=2 s.
|
| 64 |
+
Service time 0.5 s → μ = 1/0.5 = 2.0.
|
| 65 |
+
With 25 completions service_stats.count=25 ≥ 10 (no fallback).
|
| 66 |
+
"""
|
| 67 |
+
ctrl = QueueingController(QueueingConfig(window_seconds=2.0))
|
| 68 |
+
|
| 69 |
+
inter_arrival = 2.0 # → λ = 0.5
|
| 70 |
+
service_time = 0.5 # → μ = 2.0
|
| 71 |
+
|
| 72 |
+
now = time.monotonic()
|
| 73 |
+
for i in range(25):
|
| 74 |
+
ctrl.record_request_arrival(now, token_count=128, agent_id="a")
|
| 75 |
+
ctrl.record_request_completion(
|
| 76 |
+
now + service_time,
|
| 77 |
+
service_time_ms=service_time * 1000.0,
|
| 78 |
+
blocks_consumed=16,
|
| 79 |
+
agent_id="a",
|
| 80 |
+
)
|
| 81 |
+
time.sleep(inter_arrival)
|
| 82 |
+
now = time.monotonic()
|
| 83 |
+
|
| 84 |
+
state = ctrl.compute_stability_state(
|
| 85 |
+
current_free_blocks=128,
|
| 86 |
+
total_blocks=256,
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
assert 0.15 <= state.utilization_rho <= 0.40, (
|
| 90 |
+
f"Expected rho≈0.25, got {state.utilization_rho}"
|
| 91 |
+
)
|
| 92 |
+
assert state.is_stable is True, (
|
| 93 |
+
f"System should be stable at rho={state.utilization_rho}"
|
| 94 |
+
)
|
| 95 |
+
assert state.minimum_stable_blocks <= 128
|
| 96 |
+
|
| 97 |
+
# -----------------------------------------------------------------------
|
| 98 |
+
# test_instability_detection
|
| 99 |
+
# -----------------------------------------------------------------------
|
| 100 |
+
def test_instability_detection(self):
|
| 101 |
+
"""
|
| 102 |
+
λ≈5 req/sec, μ=2 req/sec → theoretical ρ=2.5 (clamped to 0.9999).
|
| 103 |
+
|
| 104 |
+
25 arrivals at 0.2 s intervals drive the EMA to λ≈5.
|
| 105 |
+
Service time 0.5 s → μ=2.
|
| 106 |
+
|
| 107 |
+
is_stable = False when current_free_blocks (20) < minimum_stable_blocks (42),
|
| 108 |
+
even though rho < 1.0 — the M/G/1 free-blocks floor is violated first.
|
| 109 |
+
"""
|
| 110 |
+
ctrl = QueueingController(QueueingConfig(window_seconds=2.0))
|
| 111 |
+
|
| 112 |
+
inter_arrival = 0.2 # → λ = 5.0 (EMA converges here)
|
| 113 |
+
service_time = 0.5 # → μ = 2.0
|
| 114 |
+
|
| 115 |
+
now = time.monotonic()
|
| 116 |
+
for i in range(25):
|
| 117 |
+
ctrl.record_request_arrival(now, token_count=128, agent_id="a")
|
| 118 |
+
ctrl.record_request_completion(
|
| 119 |
+
now + service_time,
|
| 120 |
+
service_time_ms=service_time * 1000.0,
|
| 121 |
+
blocks_consumed=16,
|
| 122 |
+
agent_id="a",
|
| 123 |
+
)
|
| 124 |
+
time.sleep(inter_arrival)
|
| 125 |
+
now = time.monotonic()
|
| 126 |
+
|
| 127 |
+
# With lambda≈5, E[S]=0.5, E[blocks]=16, safety_margin=1.15:
|
| 128 |
+
# minimum_stable_blocks = ceil(5 * 0.5 * 16 * 1.15) = 46
|
| 129 |
+
# Setting current_free_blocks=20 < 46 triggers is_stable=False
|
| 130 |
+
# regardless of rho (which is clamped at 0.9999).
|
| 131 |
+
state = ctrl.compute_stability_state(
|
| 132 |
+
current_free_blocks=20,
|
| 133 |
+
total_blocks=512,
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
# EMA lambda should be close to 5.0 (the driven arrival rate)
|
| 137 |
+
assert state.arrival_rate_lambda >= 4.0, (
|
| 138 |
+
f"Expected λ EMA ≥4.0, got {state.arrival_rate_lambda}"
|
| 139 |
+
)
|
| 140 |
+
# is_stable=False because free_blocks < minimum_stable_blocks
|
| 141 |
+
assert state.is_stable is False, (
|
| 142 |
+
f"System should be unstable: free_blocks=20 < minimum={state.minimum_stable_blocks} "
|
| 143 |
+
f"(lambda={state.arrival_rate_lambda})"
|
| 144 |
+
)
|
| 145 |
+
|
| 146 |
+
# -----------------------------------------------------------------------
|
| 147 |
+
# test_invariant_11_never_violated
|
| 148 |
+
# -----------------------------------------------------------------------
|
| 149 |
+
@pytest.mark.parametrize("lambda_val,mu_val,blocks", RANDOM_PARAMS)
|
| 150 |
+
def test_invariant_11_never_violated(
|
| 151 |
+
self, lambda_val: float, mu_val: float, blocks: int
|
| 152 |
+
):
|
| 153 |
+
"""
|
| 154 |
+
INVARIANT-11: after every get_eviction_target_blocks() call,
|
| 155 |
+
free_blocks_after_eviction >= minimum_stable_blocks.
|
| 156 |
+
|
| 157 |
+
Uses window_seconds=1.0 and inter_arrival=0.1 s so the EMA
|
| 158 |
+
converges quickly (alpha=0.095 per step → ~10 steps to steady state).
|
| 159 |
+
12 iterations give service_stats.count=12 (≥ 10 threshold, no fallback).
|
| 160 |
+
|
| 161 |
+
Only sub-case (b) is tested here (eviction triggered), because for
|
| 162 |
+
large-λ random params the minimum floor exceeds available space,
|
| 163 |
+
making the "no eviction needed" path unreachable with this setup.
|
| 164 |
+
|
| 165 |
+
Assertion: result_free >= minimum_stable_blocks after eviction.
|
| 166 |
+
"""
|
| 167 |
+
ctrl = QueueingController(QueueingConfig(window_seconds=1.0))
|
| 168 |
+
|
| 169 |
+
inter_arrival = 0.1 # fast convergence: alpha=0.095 per step
|
| 170 |
+
service_time_s = min(1.0 / mu_val if mu_val > 0 else 1.0, 1.0)
|
| 171 |
+
|
| 172 |
+
now = time.monotonic()
|
| 173 |
+
for _ in range(12):
|
| 174 |
+
ctrl.record_request_arrival(now, token_count=128, agent_id="a")
|
| 175 |
+
ctrl.record_request_completion(
|
| 176 |
+
now + service_time_s,
|
| 177 |
+
service_time_ms=service_time_s * 1000.0,
|
| 178 |
+
blocks_consumed=blocks,
|
| 179 |
+
agent_id="a",
|
| 180 |
+
)
|
| 181 |
+
time.sleep(inter_arrival)
|
| 182 |
+
now = time.monotonic()
|
| 183 |
+
|
| 184 |
+
total_blocks = max(2 * blocks, 512)
|
| 185 |
+
|
| 186 |
+
# Sub-case (b): eviction triggered — verify INVARIANT-11
|
| 187 |
+
# Use current_free = total_blocks/2 and request blocks/2
|
| 188 |
+
# to force projected below floor, triggering eviction.
|
| 189 |
+
available = total_blocks // 2
|
| 190 |
+
requested = max(1, blocks // 2)
|
| 191 |
+
|
| 192 |
+
state = ctrl.compute_stability_state(
|
| 193 |
+
current_free_blocks=available,
|
| 194 |
+
total_blocks=total_blocks,
|
| 195 |
+
)
|
| 196 |
+
target = ctrl.get_eviction_target_blocks(
|
| 197 |
+
current_free_blocks=available,
|
| 198 |
+
total_blocks=total_blocks,
|
| 199 |
+
requested_new_blocks=requested,
|
| 200 |
+
)
|
| 201 |
+
|
| 202 |
+
# After eviction: result_free = projected_before + evicted
|
| 203 |
+
result_free = available - requested + target
|
| 204 |
+
assert result_free >= state.minimum_stable_blocks, (
|
| 205 |
+
f"INVARIANT-11 violation: result_free={result_free} "
|
| 206 |
+
f"< minimum_stable_blocks={state.minimum_stable_blocks} "
|
| 207 |
+
f"(lambda={lambda_val}, mu={mu_val}, blocks={blocks})"
|
| 208 |
+
)
|
| 209 |
+
|
| 210 |
+
# -----------------------------------------------------------------------
|
| 211 |
+
# test_quantization_bits_ladder
|
| 212 |
+
# -----------------------------------------------------------------------
|
| 213 |
+
@pytest.mark.parametrize(
|
| 214 |
+
"target_rho,expected_bits",
|
| 215 |
+
[
|
| 216 |
+
(0.65, 16), # < 0.70 → 16-bit
|
| 217 |
+
(0.78, 8), # 0.70 ≤ ρ < 0.85 → 8-bit
|
| 218 |
+
(0.90, 4), # 0.85 ≤ ρ < 0.95 → 4-bit
|
| 219 |
+
(0.97, 2), # ≥ 0.95 → 2-bit
|
| 220 |
+
],
|
| 221 |
+
)
|
| 222 |
+
def test_quantization_bits_ladder(self, target_rho: float, expected_bits: int):
|
| 223 |
+
"""
|
| 224 |
+
get_recommended_quantization_bits() returns the correct bit-width
|
| 225 |
+
for each utilisation regime in arXiv:2605.04595 Table 2.
|
| 226 |
+
|
| 227 |
+
Uses inter_arrival=0.1 s (fast convergence) and 15 iterations.
|
| 228 |
+
With window_seconds=1.0 and dt=0.1s → alpha=0.095, EMA converges
|
| 229 |
+
in ~15 steps. Service stats.count=15 (≥ 10, no fallback).
|
| 230 |
+
"""
|
| 231 |
+
ctrl = QueueingController(QueueingConfig(window_seconds=1.0))
|
| 232 |
+
|
| 233 |
+
mu = 2.0
|
| 234 |
+
lam = target_rho * mu
|
| 235 |
+
inter_arrival = 1.0 / lam
|
| 236 |
+
service_time_s = 1.0 / mu # 0.5 s
|
| 237 |
+
|
| 238 |
+
now = time.monotonic()
|
| 239 |
+
for _ in range(15):
|
| 240 |
+
ctrl.record_request_arrival(now, token_count=128, agent_id="a")
|
| 241 |
+
ctrl.record_request_completion(
|
| 242 |
+
now + service_time_s,
|
| 243 |
+
service_time_ms=service_time_s * 1000.0,
|
| 244 |
+
blocks_consumed=16,
|
| 245 |
+
agent_id="a",
|
| 246 |
+
)
|
| 247 |
+
time.sleep(inter_arrival)
|
| 248 |
+
now = time.monotonic()
|
| 249 |
+
|
| 250 |
+
state = ctrl.compute_stability_state(
|
| 251 |
+
current_free_blocks=128,
|
| 252 |
+
total_blocks=256,
|
| 253 |
+
)
|
| 254 |
+
|
| 255 |
+
# EMA may be somewhat off; accept ±10% tolerance
|
| 256 |
+
assert abs(state.utilization_rho - target_rho) < 0.10, (
|
| 257 |
+
f"rho={state.utilization_rho:.4f} too far from target={target_rho}"
|
| 258 |
+
)
|
| 259 |
+
|
| 260 |
+
bits = ctrl.get_recommended_quantization_bits()
|
| 261 |
+
assert bits == expected_bits, (
|
| 262 |
+
f"For rho={state.utilization_rho:.4f} "
|
| 263 |
+
f"expected bits={expected_bits}, got {bits}"
|
| 264 |
+
)
|
| 265 |
+
|
| 266 |
+
# -----------------------------------------------------------------------
|
| 267 |
+
# test_ema_arrival_rate
|
| 268 |
+
# -----------------------------------------------------------------------
|
| 269 |
+
def test_ema_arrival_rate(self):
|
| 270 |
+
"""
|
| 271 |
+
6 requests at exactly 1.0 s intervals (λ=1.0 req/sec).
|
| 272 |
+
|
| 273 |
+
With window_seconds=1.0 and dt=1.0s → α=1-exp(-1/1)=0.632.
|
| 274 |
+
After 6 arrivals (5 EMA updates) the estimate is well above the
|
| 275 |
+
fallback threshold (0.1) and reflects the true rate.
|
| 276 |
+
|
| 277 |
+
We also ensure service_stats.count ≥ 10 so the controller is
|
| 278 |
+
not in fallback mode (μ uses real estimates, not 1.0).
|
| 279 |
+
"""
|
| 280 |
+
config = QueueingConfig(window_seconds=1.0)
|
| 281 |
+
ctrl = QueueingController(config)
|
| 282 |
+
|
| 283 |
+
now = time.monotonic()
|
| 284 |
+
for i in range(12): # 12 arrivals + completions → service_stats.count=12 ≥ 10
|
| 285 |
+
ctrl.record_request_arrival(now, token_count=256, agent_id="a")
|
| 286 |
+
ctrl.record_request_completion(
|
| 287 |
+
now + 0.4,
|
| 288 |
+
service_time_ms=400.0,
|
| 289 |
+
blocks_consumed=16,
|
| 290 |
+
agent_id="a",
|
| 291 |
+
)
|
| 292 |
+
time.sleep(1.0)
|
| 293 |
+
now = time.monotonic()
|
| 294 |
+
|
| 295 |
+
state = ctrl.compute_stability_state(
|
| 296 |
+
current_free_blocks=64,
|
| 297 |
+
total_blocks=256,
|
| 298 |
+
)
|
| 299 |
+
|
| 300 |
+
# Lambda from EMA must be above fallback (0.1)
|
| 301 |
+
assert state.arrival_rate_lambda > 0.1, (
|
| 302 |
+
f"Expected λ from EMA (>0.1), got {state.arrival_rate_lambda}"
|
| 303 |
+
)
|
| 304 |
+
# With α=0.632 and 5 updates, EMA converges to roughly the true rate (≈1.0)
|
| 305 |
+
assert 0.5 <= state.arrival_rate_lambda <= 2.5, (
|
| 306 |
+
f"Expected λ≈1.0 (±factor 2.5), got {state.arrival_rate_lambda}"
|
| 307 |
+
)
|
| 308 |
+
|
| 309 |
+
# -----------------------------------------------------------------------
|
| 310 |
+
# test_welford_service_time
|
| 311 |
+
# -----------------------------------------------------------------------
|
| 312 |
+
def test_welford_service_time(self):
|
| 313 |
+
"""
|
| 314 |
+
100 completions with deterministic service time 500 ms.
|
| 315 |
+
Welford mean must converge to 0.5 s; variance must be near 0.
|
| 316 |
+
|
| 317 |
+
Also verified with heterogeneous samples to confirm correct
|
| 318 |
+
Welford updates across the full value range.
|
| 319 |
+
"""
|
| 320 |
+
ctrl = QueueingController(QueueingConfig())
|
| 321 |
+
|
| 322 |
+
service_time_ms = 500.0
|
| 323 |
+
n = 100
|
| 324 |
+
now = time.monotonic()
|
| 325 |
+
|
| 326 |
+
for i in range(n):
|
| 327 |
+
ctrl.record_request_completion(
|
| 328 |
+
now + i * 0.01,
|
| 329 |
+
service_time_ms=service_time_ms,
|
| 330 |
+
blocks_consumed=16,
|
| 331 |
+
agent_id="a",
|
| 332 |
+
)
|
| 333 |
+
|
| 334 |
+
state = ctrl.compute_stability_state(
|
| 335 |
+
current_free_blocks=64,
|
| 336 |
+
total_blocks=256,
|
| 337 |
+
)
|
| 338 |
+
|
| 339 |
+
# E[S] = 0.5 s → μ = 1/0.5 = 2.0
|
| 340 |
+
assert abs(state.service_rate_mu - 2.0) < 0.05, (
|
| 341 |
+
f"Expected μ≈2.0, got {state.service_rate_mu}"
|
| 342 |
+
)
|
| 343 |
+
e_service = 1.0 / state.service_rate_mu
|
| 344 |
+
assert abs(e_service - 0.5) < 0.02, (
|
| 345 |
+
f"Expected E[S]=0.5 s, got {e_service:.4f} s"
|
| 346 |
+
)
|
| 347 |
+
|
| 348 |
+
# ---- Heterogeneous: linear sweep [0.4, 0.6] s → true mean = 0.5 s
|
| 349 |
+
ctrl2 = QueueingController(QueueingConfig())
|
| 350 |
+
for i in range(100):
|
| 351 |
+
svc = 0.4 + (i / 99.0) * 0.2
|
| 352 |
+
ctrl2.record_request_completion(
|
| 353 |
+
now + i * 0.01,
|
| 354 |
+
service_time_ms=svc * 1000.0,
|
| 355 |
+
blocks_consumed=16,
|
| 356 |
+
agent_id="a",
|
| 357 |
+
)
|
| 358 |
+
|
| 359 |
+
state2 = ctrl2.compute_stability_state(
|
| 360 |
+
current_free_blocks=64,
|
| 361 |
+
total_blocks=256,
|
| 362 |
+
)
|
| 363 |
+
e_service2 = 1.0 / state2.service_rate_mu
|
| 364 |
+
assert 0.45 <= e_service2 <= 0.55, (
|
| 365 |
+
f"Heterogeneous: expected E[S]≈0.5, got {e_service2:.4f}"
|
| 366 |
+
)
|
| 367 |
+
|
| 368 |
+
# -----------------------------------------------------------------------
|
| 369 |
+
# test_fallback_on_insufficient_data
|
| 370 |
+
# -----------------------------------------------------------------------
|
| 371 |
+
def test_fallback_on_insufficient_data(self):
|
| 372 |
+
"""
|
| 373 |
+
When < 10 service completions have been recorded, fallback values:
|
| 374 |
+
|
| 375 |
+
λ_fallback = 0.1 req/sec
|
| 376 |
+
E[S]_fallback = 1.0 s → μ = 1.0 req/sec
|
| 377 |
+
E[blocks]_fallback = config.block_size = 16
|
| 378 |
+
|
| 379 |
+
Scenarios:
|
| 380 |
+
(a) cold start — no data at all
|
| 381 |
+
(b) partial data — 5 arrivals but 0 completions
|
| 382 |
+
"""
|
| 383 |
+
config = QueueingConfig(block_size=16)
|
| 384 |
+
|
| 385 |
+
# (a) Cold start — zero arrivals, zero completions
|
| 386 |
+
ctrl_cold = QueueingController(config)
|
| 387 |
+
state_cold = ctrl_cold.compute_stability_state(
|
| 388 |
+
current_free_blocks=64,
|
| 389 |
+
total_blocks=256,
|
| 390 |
+
)
|
| 391 |
+
|
| 392 |
+
assert state_cold.arrival_rate_lambda == 0.1, (
|
| 393 |
+
f"Expected λ_fallback=0.1, got {state_cold.arrival_rate_lambda}"
|
| 394 |
+
)
|
| 395 |
+
assert state_cold.service_rate_mu == 1.0, (
|
| 396 |
+
f"Expected μ_fallback=1.0, got {state_cold.service_rate_mu}"
|
| 397 |
+
)
|
| 398 |
+
assert state_cold.mean_blocks_per_request == 16.0, (
|
| 399 |
+
f"Expected E[blocks]_fallback=16, "
|
| 400 |
+
f"got {state_cold.mean_blocks_per_request}"
|
| 401 |
+
)
|
| 402 |
+
|
| 403 |
+
# (b) 5 arrivals, 0 completions → service_stats.count = 0 (< 10)
|
| 404 |
+
ctrl_partial = QueueingController(config)
|
| 405 |
+
now = time.monotonic()
|
| 406 |
+
for _ in range(5):
|
| 407 |
+
ctrl_partial.record_request_arrival(now, token_count=128, agent_id="a")
|
| 408 |
+
time.sleep(0.01)
|
| 409 |
+
now = time.monotonic()
|
| 410 |
+
|
| 411 |
+
state_partial = ctrl_partial.compute_stability_state(
|
| 412 |
+
current_free_blocks=64,
|
| 413 |
+
total_blocks=256,
|
| 414 |
+
)
|
| 415 |
+
|
| 416 |
+
# service_stats.count = 0 (< 10) → fallback must be active
|
| 417 |
+
assert state_partial.service_rate_mu == 1.0, (
|
| 418 |
+
f"Expected μ_fallback=1.0 with 0 completions, "
|
| 419 |
+
f"got {state_partial.service_rate_mu}"
|
| 420 |
+
)
|
| 421 |
+
assert state_partial.mean_blocks_per_request == 16.0, (
|
| 422 |
+
f"Expected E[blocks]_fallback=16, "
|
| 423 |
+
f"got {state_partial.mean_blocks_per_request}"
|
| 424 |
+
)
|
| 425 |
+
|
| 426 |
+
# -----------------------------------------------------------------------
|
| 427 |
+
# test_export_metrics_keys
|
| 428 |
+
# -----------------------------------------------------------------------
|
| 429 |
+
def test_export_metrics_keys(self):
|
| 430 |
+
"""
|
| 431 |
+
export_metrics() returns exactly 7 Prometheus-compatible keys,
|
| 432 |
+
all numeric and non-NaN.
|
| 433 |
+
"""
|
| 434 |
+
config = QueueingConfig(window_seconds=1.0)
|
| 435 |
+
ctrl = QueueingController(config)
|
| 436 |
+
|
| 437 |
+
# Feed enough data to exit fallback regime
|
| 438 |
+
inter_arrival = 1.0
|
| 439 |
+
service_time = 0.4
|
| 440 |
+
now = time.monotonic()
|
| 441 |
+
for i in range(20):
|
| 442 |
+
ctrl.record_request_arrival(now, token_count=128, agent_id="a")
|
| 443 |
+
ctrl.record_request_completion(
|
| 444 |
+
now + service_time,
|
| 445 |
+
service_time_ms=service_time * 1000.0,
|
| 446 |
+
blocks_consumed=16,
|
| 447 |
+
agent_id="a",
|
| 448 |
+
)
|
| 449 |
+
time.sleep(inter_arrival)
|
| 450 |
+
now = time.monotonic()
|
| 451 |
+
|
| 452 |
+
metrics = ctrl.export_metrics()
|
| 453 |
+
|
| 454 |
+
expected_keys = [
|
| 455 |
+
"queueing_lambda",
|
| 456 |
+
"queueing_mu",
|
| 457 |
+
"queueing_rho",
|
| 458 |
+
"queueing_is_stable",
|
| 459 |
+
"queueing_lambda_critical",
|
| 460 |
+
"queueing_minimum_stable_blocks",
|
| 461 |
+
"queueing_stability_margin_pct",
|
| 462 |
+
]
|
| 463 |
+
|
| 464 |
+
assert set(metrics.keys()) == set(expected_keys), (
|
| 465 |
+
f"Expected keys {expected_keys}, got {sorted(metrics.keys())}"
|
| 466 |
+
)
|
| 467 |
+
|
| 468 |
+
for key in expected_keys:
|
| 469 |
+
val = metrics[key]
|
| 470 |
+
assert isinstance(val, (int, float)), (
|
| 471 |
+
f"Metric {key} has non-numeric value: {val!r}"
|
| 472 |
+
)
|
| 473 |
+
assert not math.isnan(val), f"Metric {key} is NaN"
|
| 474 |
+
|
| 475 |
+
assert metrics["queueing_is_stable"] in (0.0, 1.0), (
|
| 476 |
+
f"queueing_is_stable should be 0.0 or 1.0, "
|
| 477 |
+
f"got {metrics['queueing_is_stable']}"
|
| 478 |
+
)
|
| 479 |
+
|
| 480 |
+
for key in expected_keys:
|
| 481 |
+
assert metrics[key] >= 0.0, f"Metric {key} is negative: {metrics[key]}"
|