Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- Dockerfile +43 -0
- README.md +115 -5
- ROOT_CAUSE_VISIBLE_PLAN.md +332 -0
- __init__.py +10 -0
- client.py +56 -0
- models.py +41 -0
- openenv.yaml +6 -0
- openenv_stack_doctor.egg-info/PKG-INFO +9 -0
- openenv_stack_doctor.egg-info/SOURCES.txt +16 -0
- openenv_stack_doctor.egg-info/dependency_links.txt +1 -0
- openenv_stack_doctor.egg-info/entry_points.txt +2 -0
- openenv_stack_doctor.egg-info/requires.txt +5 -0
- openenv_stack_doctor.egg-info/top_level.txt +1 -0
- pyproject.toml +26 -0
- server/__init__.py +6 -0
- server/app.py +41 -0
- server/baselines.py +203 -0
- server/requirements.txt +6 -0
- server/scenarios.py +1893 -0
- server/stack_doctor_environment.py +269 -0
- server/stack_doctor_mcp.py +393 -0
- training/Dockerfile +39 -0
- training/__init__.py +0 -0
- training/eval_stack_doctor.py +143 -0
- training/train_stack_doctor.py +311 -0
- uv.lock +0 -0
Dockerfile
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Stack Doctor — OpenEnv Environment
|
| 2 |
+
# Standard pattern from OpenEnv docs (slide 11)
|
| 3 |
+
|
| 4 |
+
ARG BASE_IMAGE=ghcr.io/meta-pytorch/openenv-base:latest
|
| 5 |
+
FROM ${BASE_IMAGE} AS builder
|
| 6 |
+
|
| 7 |
+
WORKDIR /app
|
| 8 |
+
|
| 9 |
+
# Ensure git is available for VCS dependencies
|
| 10 |
+
RUN apt-get update && \
|
| 11 |
+
apt-get install -y --no-install-recommends git && \
|
| 12 |
+
rm -rf /var/lib/apt/lists/*
|
| 13 |
+
|
| 14 |
+
COPY . /app/env
|
| 15 |
+
WORKDIR /app/env
|
| 16 |
+
|
| 17 |
+
# Ensure uv is available
|
| 18 |
+
RUN if ! command -v uv >/dev/null 2>&1; then \
|
| 19 |
+
curl -LsSf https://astral.sh/uv/install.sh | sh && \
|
| 20 |
+
mv /root/.local/bin/uv /usr/local/bin/uv && \
|
| 21 |
+
mv /root/.local/bin/uvx /usr/local/bin/uvx; \
|
| 22 |
+
fi
|
| 23 |
+
|
| 24 |
+
# Install dependencies — try frozen first, fall back to fresh resolve
|
| 25 |
+
RUN --mount=type=cache,target=/root/.cache/uv \
|
| 26 |
+
uv sync --frozen --no-editable 2>/dev/null || uv sync --no-editable
|
| 27 |
+
|
| 28 |
+
# Final runtime stage
|
| 29 |
+
FROM ${BASE_IMAGE}
|
| 30 |
+
|
| 31 |
+
WORKDIR /app
|
| 32 |
+
|
| 33 |
+
COPY --from=builder /app/env/.venv /app/.venv
|
| 34 |
+
COPY --from=builder /app/env /app/env
|
| 35 |
+
|
| 36 |
+
ENV PATH="/app/.venv/bin:$PATH"
|
| 37 |
+
ENV PYTHONPATH="/app/env:$PYTHONPATH"
|
| 38 |
+
|
| 39 |
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
| 40 |
+
CMD curl -f http://localhost:8000/health || exit 1
|
| 41 |
+
|
| 42 |
+
ENV ENABLE_WEB_INTERFACE=true
|
| 43 |
+
CMD ["sh", "-c", "cd /app/env && uvicorn server.app:app --host 0.0.0.0 --port 8000"]
|
README.md
CHANGED
|
@@ -1,10 +1,120 @@
|
|
| 1 |
---
|
| 2 |
-
title: Stack Doctor
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Stack Doctor Environment Server
|
| 3 |
+
emoji: 🩺
|
| 4 |
+
colorFrom: red
|
| 5 |
+
colorTo: blue
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
+
app_port: 8000
|
| 9 |
+
base_path: /web
|
| 10 |
+
tags:
|
| 11 |
+
- openenv
|
| 12 |
---
|
| 13 |
|
| 14 |
+
# Stack Doctor
|
| 15 |
+
|
| 16 |
+
An OpenEnv RL environment where an overseer LLM diagnoses sick inference stacks. The agent probes subsystems, reconciles conflicting specialist-agent reports (some of which are wrong), and selects the minimal correct fix — all within a 6-step budget.
|
| 17 |
+
|
| 18 |
+
Inspired by real SM12x enablement bugs across vLLM, FlashInfer, SGLang, CUTLASS, and Flash-Attention.
|
| 19 |
+
|
| 20 |
+
**Track**: Statement 3.1 — World Modeling / Professional Tasks
|
| 21 |
+
**Sub-theme**: Fleet AI — Scalable Oversight Agents ($10K)
|
| 22 |
+
|
| 23 |
+
## Quick Start
|
| 24 |
+
|
| 25 |
+
```python
|
| 26 |
+
from stack_doctor import StackDoctorEnv, StackDoctorAction
|
| 27 |
+
import json
|
| 28 |
+
|
| 29 |
+
env = StackDoctorEnv(base_url="https://bledden-stack-doctor.hf.space")
|
| 30 |
+
env.connect()
|
| 31 |
+
|
| 32 |
+
# Start a new incident
|
| 33 |
+
result = env.reset()
|
| 34 |
+
print(result.observation.incident_ticket)
|
| 35 |
+
print(result.observation.specialist_opinions)
|
| 36 |
+
|
| 37 |
+
# Investigate
|
| 38 |
+
result = env.step(StackDoctorAction(message=json.dumps(
|
| 39 |
+
{"type": "inspect", "target": "logs"}
|
| 40 |
+
)))
|
| 41 |
+
print(result.observation.output)
|
| 42 |
+
|
| 43 |
+
# Submit diagnosis
|
| 44 |
+
result = env.step(StackDoctorAction(message=json.dumps(
|
| 45 |
+
{"type": "submit", "root_cause": "arch_guard", "fix": "relax_arch_check"}
|
| 46 |
+
)))
|
| 47 |
+
print(f"Reward: {result.reward}, Done: {result.done}")
|
| 48 |
+
|
| 49 |
+
env.close()
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
## Environment Design
|
| 53 |
+
|
| 54 |
+
### Root Causes (6) and Fixes (6)
|
| 55 |
+
|
| 56 |
+
| Root Cause | Fix | Real-World Motif |
|
| 57 |
+
|-----------|-----|-----------------|
|
| 58 |
+
| `arch_guard` | `relax_arch_check` | FlashInfer SM121 capability checks |
|
| 59 |
+
| `backend_whitelist` | `add_whitelist_entry` | vLLM Marlin SM121+ whitelist gaps |
|
| 60 |
+
| `runtime_loader` | `fix_runtime_path` | SGLang CUDA 13 runtime issues |
|
| 61 |
+
| `backend_selector` | `switch_backend` | CUTLASS dispatch mistakes |
|
| 62 |
+
| `model_config` | `update_model_config` | Model config mismatches on new hardware |
|
| 63 |
+
| `weight_layout` | `fix_weight_mapping` | Weight layout problems across backends |
|
| 64 |
+
|
| 65 |
+
### Specialists (4)
|
| 66 |
+
|
| 67 |
+
`runtime`, `dispatch`, `kernel`, `loader` — at least one gives wrong advice per scenario.
|
| 68 |
+
|
| 69 |
+
### Action Space (JSON)
|
| 70 |
+
|
| 71 |
+
```json
|
| 72 |
+
{"type":"inspect","target":"logs|config|snippet|metrics"}
|
| 73 |
+
{"type":"ask_specialist","specialist":"runtime|dispatch|kernel|loader"}
|
| 74 |
+
{"type":"apply_fix","fix":"<one of 6 fixes>"}
|
| 75 |
+
{"type":"submit","root_cause":"<one of 6>","fix":"<one of 6>"}
|
| 76 |
+
```
|
| 77 |
+
|
| 78 |
+
### Reward Function
|
| 79 |
+
|
| 80 |
+
| Event | Reward |
|
| 81 |
+
|-------|--------|
|
| 82 |
+
| `inspect` or `ask_specialist` | -0.25 |
|
| 83 |
+
| Correct `apply_fix` | +3 |
|
| 84 |
+
| Wrong `apply_fix` | -2 |
|
| 85 |
+
| Correct `submit` (per field) | +8 |
|
| 86 |
+
| Wrong `submit` (per field) | -4 |
|
| 87 |
+
| Solved in ≤4 steps | +2 bonus |
|
| 88 |
+
| Invalid action | -2 |
|
| 89 |
+
|
| 90 |
+
### Baselines
|
| 91 |
+
|
| 92 |
+
| Policy | RC Accuracy | Fix Accuracy | Avg Steps | Avg Reward |
|
| 93 |
+
|--------|:-:|:-:|:-:|:-:|
|
| 94 |
+
| Oracle | 100% | 100% | 1.0 | 18.0 |
|
| 95 |
+
| Heuristic | 100% | 100% | 4.0 | 20.5 |
|
| 96 |
+
| Random | 18% | 18% | 3.2 | -4.1 |
|
| 97 |
+
|
| 98 |
+
## Fleet AI: Specialist Oversight
|
| 99 |
+
|
| 100 |
+
The core mechanic that targets Fleet AI's $10K sub-theme: the agent must act as a **scalable oversight agent** that reconciles conflicting specialist reports. Specialists have per-scenario reliability — the agent cannot learn "always trust specialist X" and must evaluate evidence on each case.
|
| 101 |
+
|
| 102 |
+
## Training
|
| 103 |
+
|
| 104 |
+
Uses Unsloth + TRL GRPO with 3 reward signals:
|
| 105 |
+
1. **Valid JSON** — can the output be parsed as an action plan?
|
| 106 |
+
2. **Environment reward** — cumulative reward from executing the plan
|
| 107 |
+
3. **Efficiency** — bonus for shorter plans that still submit correctly
|
| 108 |
+
|
| 109 |
+
## Development
|
| 110 |
+
|
| 111 |
+
```bash
|
| 112 |
+
# Local server
|
| 113 |
+
cd stack_doctor && PYTHONPATH=. uvicorn server.app:app --port 8000
|
| 114 |
+
|
| 115 |
+
# Run baselines
|
| 116 |
+
PYTHONPATH=. python3 -c "from server.baselines import *; ..."
|
| 117 |
+
|
| 118 |
+
# Deploy to HF Spaces
|
| 119 |
+
openenv push --repo-id bledden/stack-doctor
|
| 120 |
+
```
|
ROOT_CAUSE_VISIBLE_PLAN.md
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Root-Cause-Visible Stack Doctor Plan
|
| 2 |
+
|
| 3 |
+
## Summary
|
| 4 |
+
|
| 5 |
+
This proposal adds a second mode to Stack Doctor where the agent is told the true root cause at the start of the episode.
|
| 6 |
+
|
| 7 |
+
Instead of diagnosing from noisy evidence and conflicting specialists, the agent's job becomes:
|
| 8 |
+
|
| 9 |
+
1. Validate the known root cause with the minimum useful evidence.
|
| 10 |
+
2. Choose the correct and safest fix.
|
| 11 |
+
3. Apply or recommend the fix.
|
| 12 |
+
4. Submit a short operational justification.
|
| 13 |
+
|
| 14 |
+
This makes the environment easier to explain in a hackathon setting while keeping it meaningfully interactive.
|
| 15 |
+
|
| 16 |
+
## Recommendation
|
| 17 |
+
|
| 18 |
+
Do **not** replace the current Stack Doctor environment entirely.
|
| 19 |
+
|
| 20 |
+
Instead, support two modes:
|
| 21 |
+
|
| 22 |
+
- `blind_diagnosis`: current mode, where the agent must infer the root cause from imperfect evidence.
|
| 23 |
+
- `root_cause_visible`: new mode, where the root cause is given and the task becomes evidence-based remediation.
|
| 24 |
+
|
| 25 |
+
Reason:
|
| 26 |
+
|
| 27 |
+
- The current mode is stronger as an oversight benchmark.
|
| 28 |
+
- The new mode is cleaner and easier for judges to understand quickly.
|
| 29 |
+
- Having both lets us tell a better story: "same incident world, two difficulty levels."
|
| 30 |
+
|
| 31 |
+
## Why Change It
|
| 32 |
+
|
| 33 |
+
The current environment is a valid RL environment, but it can look messy to people seeing it for the first time because:
|
| 34 |
+
|
| 35 |
+
- specialist opinions can be wrong
|
| 36 |
+
- the agent has to infer latent state
|
| 37 |
+
- the reward mixes diagnosis quality with investigation efficiency
|
| 38 |
+
|
| 39 |
+
Giving the root cause up front removes the hardest-to-explain part of the setup and shifts the task toward operational decision-making:
|
| 40 |
+
|
| 41 |
+
- What evidence should I verify before acting?
|
| 42 |
+
- Which fix is safest and most minimal?
|
| 43 |
+
- How much investigation is enough?
|
| 44 |
+
- Can I justify the rollout clearly?
|
| 45 |
+
|
| 46 |
+
That is still a good agent task. It is just a different one.
|
| 47 |
+
|
| 48 |
+
## New Product Framing
|
| 49 |
+
|
| 50 |
+
Position the new mode as:
|
| 51 |
+
|
| 52 |
+
**"An incident commander agent that receives a probable root cause from upstream monitoring and must validate, remediate, and explain the fix."**
|
| 53 |
+
|
| 54 |
+
This framing is cleaner than "the model magically knows everything," because it implies:
|
| 55 |
+
|
| 56 |
+
- another system or monitor identified the likely root cause
|
| 57 |
+
- Stack Doctor is responsible for safe execution, not initial detection
|
| 58 |
+
|
| 59 |
+
## Environment Changes
|
| 60 |
+
|
| 61 |
+
### 1. Observation Schema
|
| 62 |
+
|
| 63 |
+
Add a field to the initial observation:
|
| 64 |
+
|
| 65 |
+
```json
|
| 66 |
+
{
|
| 67 |
+
"known_root_cause": "runtime_loader"
|
| 68 |
+
}
|
| 69 |
+
```
|
| 70 |
+
|
| 71 |
+
Recommended additions:
|
| 72 |
+
|
| 73 |
+
- `known_root_cause`
|
| 74 |
+
- `mode`
|
| 75 |
+
- optional `recommended_fix_family` if we want a very easy demo mode later
|
| 76 |
+
|
| 77 |
+
In `root_cause_visible` mode, the reset observation should explicitly say:
|
| 78 |
+
|
| 79 |
+
> Root cause has been pre-identified. Validate it, choose the minimal safe fix, and submit.
|
| 80 |
+
|
| 81 |
+
### 2. Action Space
|
| 82 |
+
|
| 83 |
+
Keep the action space mostly the same to minimize changes:
|
| 84 |
+
|
| 85 |
+
- `inspect`
|
| 86 |
+
- `ask_specialist`
|
| 87 |
+
- `apply_fix`
|
| 88 |
+
- `submit`
|
| 89 |
+
|
| 90 |
+
But change the meaning of `submit`.
|
| 91 |
+
|
| 92 |
+
### Current `submit`
|
| 93 |
+
|
| 94 |
+
The agent submits:
|
| 95 |
+
|
| 96 |
+
- `root_cause`
|
| 97 |
+
- `fix`
|
| 98 |
+
|
| 99 |
+
### Proposed `submit`
|
| 100 |
+
|
| 101 |
+
The agent submits:
|
| 102 |
+
|
| 103 |
+
- `fix`
|
| 104 |
+
- `evidence`
|
| 105 |
+
- `justification`
|
| 106 |
+
|
| 107 |
+
Suggested JSON:
|
| 108 |
+
|
| 109 |
+
```json
|
| 110 |
+
{
|
| 111 |
+
"type": "submit",
|
| 112 |
+
"fix": "fix_runtime_path",
|
| 113 |
+
"evidence": ["logs", "config"],
|
| 114 |
+
"justification": "CUDA 13 is installed, but LD_LIBRARY_PATH still points to cuda-12."
|
| 115 |
+
}
|
| 116 |
+
```
|
| 117 |
+
|
| 118 |
+
If backward compatibility matters, keep `root_cause` in the schema but ignore scoring for it in `root_cause_visible` mode.
|
| 119 |
+
|
| 120 |
+
### 3. Specialists
|
| 121 |
+
|
| 122 |
+
In the new mode, specialists should no longer be the center of the task.
|
| 123 |
+
|
| 124 |
+
Recommended options:
|
| 125 |
+
|
| 126 |
+
- keep specialists, but make them supportive rather than adversarial
|
| 127 |
+
- reduce emphasis on conflicting specialist opinions
|
| 128 |
+
- use specialists mainly for implementation details and risk checks
|
| 129 |
+
|
| 130 |
+
Example:
|
| 131 |
+
|
| 132 |
+
- `runtime`: confirms the path mismatch
|
| 133 |
+
- `dispatch`: says whether dispatch will recover after the fix
|
| 134 |
+
- `loader`: clarifies whether a restart is needed
|
| 135 |
+
|
| 136 |
+
This makes the environment feel less noisy without removing interactivity.
|
| 137 |
+
|
| 138 |
+
### 4. Reward Redesign
|
| 139 |
+
|
| 140 |
+
If the root cause is visible, the current reward design should change. The agent should no longer get major reward for naming the diagnosis correctly.
|
| 141 |
+
|
| 142 |
+
### Proposed reward priorities
|
| 143 |
+
|
| 144 |
+
1. Correct fix selection
|
| 145 |
+
2. Minimal useful investigation
|
| 146 |
+
3. Safe behavior
|
| 147 |
+
4. Clear justification
|
| 148 |
+
|
| 149 |
+
### Example reward table
|
| 150 |
+
|
| 151 |
+
| Event | Reward |
|
| 152 |
+
|---|---:|
|
| 153 |
+
| `inspect` or `ask_specialist` | -0.25 |
|
| 154 |
+
| relevant evidence inspected | +0.5 |
|
| 155 |
+
| irrelevant or redundant evidence | 0 |
|
| 156 |
+
| correct `apply_fix` | +4 |
|
| 157 |
+
| wrong `apply_fix` | -4 |
|
| 158 |
+
| correct `submit.fix` | +10 |
|
| 159 |
+
| wrong `submit.fix` | -6 |
|
| 160 |
+
| concise valid justification | +1 |
|
| 161 |
+
| solved in `<= 4` steps | +2 |
|
| 162 |
+
| unsafe sequence or invalid action | -2 to -4 |
|
| 163 |
+
|
| 164 |
+
Key point: in this mode, the skill is not "guess the cause." The skill is "verify enough, then act correctly."
|
| 165 |
+
|
| 166 |
+
### 5. Success Criteria
|
| 167 |
+
|
| 168 |
+
The policy should be judged on:
|
| 169 |
+
|
| 170 |
+
- fix accuracy
|
| 171 |
+
- average steps
|
| 172 |
+
- evidence efficiency
|
| 173 |
+
- justification quality
|
| 174 |
+
- avoidable bad interventions
|
| 175 |
+
|
| 176 |
+
Optional additional metric:
|
| 177 |
+
|
| 178 |
+
- `evidence_precision`: fraction of inspected items that were actually relevant
|
| 179 |
+
|
| 180 |
+
This gives a more legible evaluation story than pure diagnosis accuracy.
|
| 181 |
+
|
| 182 |
+
## Repo Changes
|
| 183 |
+
|
| 184 |
+
### 1. `models.py`
|
| 185 |
+
|
| 186 |
+
Add new observation fields:
|
| 187 |
+
|
| 188 |
+
- `known_root_cause: str = ""`
|
| 189 |
+
- `mode: str = "blind_diagnosis"`
|
| 190 |
+
|
| 191 |
+
Potentially add:
|
| 192 |
+
|
| 193 |
+
- `recommended_fix_family: str = ""`
|
| 194 |
+
|
| 195 |
+
### 2. `server/stack_doctor_environment.py`
|
| 196 |
+
|
| 197 |
+
Add a reset kwarg:
|
| 198 |
+
|
| 199 |
+
```python
|
| 200 |
+
mode = kwargs.get("mode", "blind_diagnosis")
|
| 201 |
+
```
|
| 202 |
+
|
| 203 |
+
Implementation steps:
|
| 204 |
+
|
| 205 |
+
- store the mode on episode state
|
| 206 |
+
- include `known_root_cause` in reset observation when mode is `root_cause_visible`
|
| 207 |
+
- branch reward logic inside `_handle_submit`
|
| 208 |
+
- optionally branch specialist behavior to be less misleading
|
| 209 |
+
- keep existing default behavior unchanged
|
| 210 |
+
|
| 211 |
+
### 3. `server/scenarios.py`
|
| 212 |
+
|
| 213 |
+
No structural rewrite is required.
|
| 214 |
+
|
| 215 |
+
Small recommended additions:
|
| 216 |
+
|
| 217 |
+
- tag which inspect targets are most probative for each scenario
|
| 218 |
+
- tag which specialist follow-ups are useful vs distracting
|
| 219 |
+
- optionally define a `minimal_evidence` set per scenario
|
| 220 |
+
|
| 221 |
+
This will help score validation quality in the new mode.
|
| 222 |
+
|
| 223 |
+
### 4. `training/train_stack_doctor.py`
|
| 224 |
+
|
| 225 |
+
Add a second training prompt for `root_cause_visible` mode.
|
| 226 |
+
|
| 227 |
+
The prompt should tell the model:
|
| 228 |
+
|
| 229 |
+
- the root cause is already known
|
| 230 |
+
- do not waste steps proving obvious facts
|
| 231 |
+
- verify the highest-value evidence
|
| 232 |
+
- choose the safest correct fix
|
| 233 |
+
- submit a short justification
|
| 234 |
+
|
| 235 |
+
Also update reward functions to score:
|
| 236 |
+
|
| 237 |
+
- correct fix choice
|
| 238 |
+
- evidence use
|
| 239 |
+
- step efficiency
|
| 240 |
+
- valid justification text
|
| 241 |
+
|
| 242 |
+
### 5. `training/eval_stack_doctor.py`
|
| 243 |
+
|
| 244 |
+
Add mode-aware evaluation metrics:
|
| 245 |
+
|
| 246 |
+
- `fix_accuracy`
|
| 247 |
+
- `avg_steps`
|
| 248 |
+
- `avg_reward`
|
| 249 |
+
- `evidence_precision`
|
| 250 |
+
- `justification_pass_rate`
|
| 251 |
+
|
| 252 |
+
### 6. `README.md`
|
| 253 |
+
|
| 254 |
+
Update the README to explain both modes:
|
| 255 |
+
|
| 256 |
+
- what each mode is testing
|
| 257 |
+
- why both matter
|
| 258 |
+
- which one is easiest to demo to judges
|
| 259 |
+
|
| 260 |
+
## Demo Story
|
| 261 |
+
|
| 262 |
+
Recommended demo sequence:
|
| 263 |
+
|
| 264 |
+
1. Show one `root_cause_visible` episode first.
|
| 265 |
+
2. Explain that upstream monitoring identified the likely cause.
|
| 266 |
+
3. Let Stack Doctor inspect 1-2 evidence sources, choose the fix, and justify it.
|
| 267 |
+
4. Then mention that the same environment also supports the harder `blind_diagnosis` mode.
|
| 268 |
+
|
| 269 |
+
This makes the system understandable in under a minute.
|
| 270 |
+
|
| 271 |
+
## Risks
|
| 272 |
+
|
| 273 |
+
### Risk 1: Too easy
|
| 274 |
+
|
| 275 |
+
If the root cause is visible and the only remaining task is mapping root cause to fix, the environment becomes trivial.
|
| 276 |
+
|
| 277 |
+
Mitigation:
|
| 278 |
+
|
| 279 |
+
- make evidence validation matter
|
| 280 |
+
- score fix safety and justification
|
| 281 |
+
- include cases where multiple fixes are plausible but only one is minimal
|
| 282 |
+
|
| 283 |
+
### Risk 2: Loses the best part of the current project
|
| 284 |
+
|
| 285 |
+
The current environment's most differentiated feature is conflicting specialist oversight.
|
| 286 |
+
|
| 287 |
+
Mitigation:
|
| 288 |
+
|
| 289 |
+
- keep current mode
|
| 290 |
+
- present `root_cause_visible` as a simpler companion mode, not a replacement
|
| 291 |
+
|
| 292 |
+
### Risk 3: Becomes a static classification problem again
|
| 293 |
+
|
| 294 |
+
If the model can submit immediately with no downside, the interaction disappears.
|
| 295 |
+
|
| 296 |
+
Mitigation:
|
| 297 |
+
|
| 298 |
+
- require evidence references in `submit`
|
| 299 |
+
- reward minimal but real validation
|
| 300 |
+
- penalize unsupported submissions
|
| 301 |
+
|
| 302 |
+
## MVP Scope
|
| 303 |
+
|
| 304 |
+
For a hackathon-friendly implementation, do only this:
|
| 305 |
+
|
| 306 |
+
1. Add `mode` and `known_root_cause` to the observation.
|
| 307 |
+
2. Branch scoring so `submit` is mostly about the fix in `root_cause_visible` mode.
|
| 308 |
+
3. Require a short justification string in submit.
|
| 309 |
+
4. Update the training prompt and evaluation script.
|
| 310 |
+
5. Update the README and demo flow.
|
| 311 |
+
|
| 312 |
+
This is enough to tell the story cleanly without rewriting the whole project.
|
| 313 |
+
|
| 314 |
+
## Stretch Scope
|
| 315 |
+
|
| 316 |
+
If there is extra time:
|
| 317 |
+
|
| 318 |
+
- add `minimal_evidence` scoring per scenario
|
| 319 |
+
- add safe-vs-risky fix tradeoffs
|
| 320 |
+
- generate a postmortem note at the end of the episode
|
| 321 |
+
- support multi-incident scheduling where root cause is known but resources are limited
|
| 322 |
+
|
| 323 |
+
## Final Recommendation
|
| 324 |
+
|
| 325 |
+
Proceed with a **dual-mode** design.
|
| 326 |
+
|
| 327 |
+
That gives the team two benefits:
|
| 328 |
+
|
| 329 |
+
- a cleaner, easier-to-pitch hackathon demo with `root_cause_visible`
|
| 330 |
+
- a stronger long-term benchmark with `blind_diagnosis`
|
| 331 |
+
|
| 332 |
+
If we collapse entirely to "the agent sees the true root cause," the project becomes easier to explain but materially less differentiated. The best version is to keep both and present them as two levels of the same environment.
|
__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Stack Doctor Environment."""
|
| 2 |
+
|
| 3 |
+
from .client import StackDoctorEnv
|
| 4 |
+
from .models import StackDoctorAction, StackDoctorObservation
|
| 5 |
+
|
| 6 |
+
__all__ = [
|
| 7 |
+
"StackDoctorAction",
|
| 8 |
+
"StackDoctorObservation",
|
| 9 |
+
"StackDoctorEnv",
|
| 10 |
+
]
|
client.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Stack Doctor Client."""
|
| 2 |
+
|
| 3 |
+
from typing import Dict
|
| 4 |
+
|
| 5 |
+
from openenv.core.client_types import StepResult
|
| 6 |
+
from openenv.core.env_server.types import State
|
| 7 |
+
from openenv.core import EnvClient
|
| 8 |
+
|
| 9 |
+
from .models import StackDoctorAction, StackDoctorObservation
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class StackDoctorEnv(EnvClient[StackDoctorAction, StackDoctorObservation, State]):
|
| 13 |
+
"""
|
| 14 |
+
Client for the Stack Doctor Environment.
|
| 15 |
+
|
| 16 |
+
Example:
|
| 17 |
+
>>> env = StackDoctorEnv(base_url="http://localhost:8000")
|
| 18 |
+
>>> env.connect()
|
| 19 |
+
>>> result = env.reset()
|
| 20 |
+
>>> print(result.observation.incident_ticket)
|
| 21 |
+
>>> result = env.step(StackDoctorAction(message='{"type":"inspect","target":"logs"}'))
|
| 22 |
+
>>> print(result.observation.output)
|
| 23 |
+
>>> env.close()
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
def _step_payload(self, action: StackDoctorAction) -> Dict:
|
| 27 |
+
return {"message": action.message}
|
| 28 |
+
|
| 29 |
+
def _parse_result(self, payload: Dict) -> StepResult[StackDoctorObservation]:
|
| 30 |
+
obs_data = payload.get("observation", {})
|
| 31 |
+
observation = StackDoctorObservation(
|
| 32 |
+
output=obs_data.get("output", ""),
|
| 33 |
+
incident_ticket=obs_data.get("incident_ticket", ""),
|
| 34 |
+
hardware=obs_data.get("hardware", ""),
|
| 35 |
+
model_name=obs_data.get("model_name", ""),
|
| 36 |
+
backend=obs_data.get("backend", ""),
|
| 37 |
+
log_excerpt=obs_data.get("log_excerpt", ""),
|
| 38 |
+
code_snippet=obs_data.get("code_snippet", ""),
|
| 39 |
+
specialist_opinions=obs_data.get("specialist_opinions", {}),
|
| 40 |
+
steps_remaining=obs_data.get("steps_remaining", 0),
|
| 41 |
+
fix_used=obs_data.get("fix_used", False),
|
| 42 |
+
done=payload.get("done", False),
|
| 43 |
+
reward=payload.get("reward"),
|
| 44 |
+
metadata=obs_data.get("metadata", {}),
|
| 45 |
+
)
|
| 46 |
+
return StepResult(
|
| 47 |
+
observation=observation,
|
| 48 |
+
reward=payload.get("reward"),
|
| 49 |
+
done=payload.get("done", False),
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
def _parse_state(self, payload: Dict) -> State:
|
| 53 |
+
return State(
|
| 54 |
+
episode_id=payload.get("episode_id"),
|
| 55 |
+
step_count=payload.get("step_count", 0),
|
| 56 |
+
)
|
models.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Data models for the Stack Doctor Environment.
|
| 3 |
+
|
| 4 |
+
An overseer LLM diagnoses sick inference stacks by probing subsystems,
|
| 5 |
+
reconciling conflicting specialist-agent reports, and selecting the
|
| 6 |
+
minimal correct fix.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from pydantic import Field
|
| 10 |
+
|
| 11 |
+
from openenv.core.env_server.types import Action, Observation
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class StackDoctorAction(Action):
|
| 15 |
+
"""Agent action — a JSON message selecting one of 4 action types."""
|
| 16 |
+
|
| 17 |
+
message: str = Field(
|
| 18 |
+
...,
|
| 19 |
+
description=(
|
| 20 |
+
'JSON action. One of:\n'
|
| 21 |
+
' {"type":"inspect","target":"logs|config|snippet|metrics"}\n'
|
| 22 |
+
' {"type":"ask_specialist","specialist":"runtime|dispatch|kernel|loader"}\n'
|
| 23 |
+
' {"type":"apply_fix","fix":"relax_arch_check|add_whitelist_entry|fix_runtime_path|switch_backend|update_model_config|fix_weight_mapping"}\n'
|
| 24 |
+
' {"type":"submit","root_cause":"...","fix":"...","justification":"..."}'
|
| 25 |
+
),
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class StackDoctorObservation(Observation):
|
| 30 |
+
"""What the agent sees after each action."""
|
| 31 |
+
|
| 32 |
+
output: str = Field(default="", description="Natural-language feedback")
|
| 33 |
+
incident_ticket: str = Field(default="", description="The incident description")
|
| 34 |
+
hardware: str = Field(default="", description="Hardware identifier")
|
| 35 |
+
model_name: str = Field(default="", description="Model being served")
|
| 36 |
+
backend: str = Field(default="", description="Inference backend in use")
|
| 37 |
+
log_excerpt: str = Field(default="", description="Log snippet")
|
| 38 |
+
code_snippet: str = Field(default="", description="Config or code snippet")
|
| 39 |
+
specialist_opinions: dict = Field(default_factory=dict, description="Specialist name -> {opinion, confidence}")
|
| 40 |
+
steps_remaining: int = Field(default=6, description="Steps left in episode")
|
| 41 |
+
fix_used: bool = Field(default=False, description="Whether apply_fix has been used")
|
openenv.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
spec_version: 1
|
| 2 |
+
name: stack_doctor
|
| 3 |
+
type: space
|
| 4 |
+
runtime: fastapi
|
| 5 |
+
app: server.app:app
|
| 6 |
+
port: 8000
|
openenv_stack_doctor.egg-info/PKG-INFO
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Metadata-Version: 2.4
|
| 2 |
+
Name: openenv-stack-doctor
|
| 3 |
+
Version: 0.1.0
|
| 4 |
+
Summary: Stack Doctor: an RL environment for diagnosing inference-stack incidents
|
| 5 |
+
Requires-Python: >=3.10
|
| 6 |
+
Requires-Dist: openenv-core[core]>=0.2.0
|
| 7 |
+
Provides-Extra: dev
|
| 8 |
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
| 9 |
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
openenv_stack_doctor.egg-info/SOURCES.txt
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
README.md
|
| 2 |
+
pyproject.toml
|
| 3 |
+
./__init__.py
|
| 4 |
+
./client.py
|
| 5 |
+
./models.py
|
| 6 |
+
openenv_stack_doctor.egg-info/PKG-INFO
|
| 7 |
+
openenv_stack_doctor.egg-info/SOURCES.txt
|
| 8 |
+
openenv_stack_doctor.egg-info/dependency_links.txt
|
| 9 |
+
openenv_stack_doctor.egg-info/entry_points.txt
|
| 10 |
+
openenv_stack_doctor.egg-info/requires.txt
|
| 11 |
+
openenv_stack_doctor.egg-info/top_level.txt
|
| 12 |
+
server/__init__.py
|
| 13 |
+
server/app.py
|
| 14 |
+
server/baselines.py
|
| 15 |
+
server/scenarios.py
|
| 16 |
+
server/stack_doctor_environment.py
|
openenv_stack_doctor.egg-info/dependency_links.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|
openenv_stack_doctor.egg-info/entry_points.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[console_scripts]
|
| 2 |
+
server = stack_doctor.server.app:main
|
openenv_stack_doctor.egg-info/requires.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
openenv-core[core]>=0.2.0
|
| 2 |
+
|
| 3 |
+
[dev]
|
| 4 |
+
pytest>=8.0.0
|
| 5 |
+
pytest-cov>=4.0.0
|
openenv_stack_doctor.egg-info/top_level.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
stack_doctor
|
pyproject.toml
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[build-system]
|
| 2 |
+
requires = ["setuptools>=45", "wheel"]
|
| 3 |
+
build-backend = "setuptools.build_meta"
|
| 4 |
+
|
| 5 |
+
[project]
|
| 6 |
+
name = "openenv-stack-doctor"
|
| 7 |
+
version = "0.1.0"
|
| 8 |
+
description = "Stack Doctor: an RL environment for diagnosing inference-stack incidents"
|
| 9 |
+
requires-python = ">=3.10"
|
| 10 |
+
dependencies = [
|
| 11 |
+
"openenv-core[core]>=0.2.0",
|
| 12 |
+
]
|
| 13 |
+
|
| 14 |
+
[project.optional-dependencies]
|
| 15 |
+
dev = [
|
| 16 |
+
"pytest>=8.0.0",
|
| 17 |
+
"pytest-cov>=4.0.0",
|
| 18 |
+
]
|
| 19 |
+
|
| 20 |
+
[project.scripts]
|
| 21 |
+
server = "stack_doctor.server.app:main"
|
| 22 |
+
|
| 23 |
+
[tool.setuptools]
|
| 24 |
+
include-package-data = true
|
| 25 |
+
packages = ["stack_doctor", "stack_doctor.server"]
|
| 26 |
+
package-dir = { "stack_doctor" = ".", "stack_doctor.server" = "server" }
|
server/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Stack Doctor environment server components."""
|
| 2 |
+
|
| 3 |
+
from .stack_doctor_environment import StackDoctorEnvironment
|
| 4 |
+
from .stack_doctor_mcp import StackDoctorMCPEnvironment
|
| 5 |
+
|
| 6 |
+
__all__ = ["StackDoctorEnvironment", "StackDoctorMCPEnvironment"]
|
server/app.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
FastAPI application for the Stack Doctor Environment.
|
| 3 |
+
|
| 4 |
+
Exposes both:
|
| 5 |
+
- WebSocket API (reset/step/state) for RL training
|
| 6 |
+
- MCP API (tools/list, tools/call) for agent interaction
|
| 7 |
+
|
| 8 |
+
Usage:
|
| 9 |
+
uvicorn server.app:app --reload --host 0.0.0.0 --port 8000
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
try:
|
| 13 |
+
from openenv.core.env_server.http_server import create_app
|
| 14 |
+
except Exception as e:
|
| 15 |
+
raise ImportError(
|
| 16 |
+
"openenv is required. Install with: uv sync"
|
| 17 |
+
) from e
|
| 18 |
+
|
| 19 |
+
from models import StackDoctorAction, StackDoctorObservation
|
| 20 |
+
from .stack_doctor_mcp import StackDoctorMCPEnvironment
|
| 21 |
+
|
| 22 |
+
app = create_app(
|
| 23 |
+
StackDoctorMCPEnvironment,
|
| 24 |
+
StackDoctorAction,
|
| 25 |
+
StackDoctorObservation,
|
| 26 |
+
env_name="stack_doctor",
|
| 27 |
+
max_concurrent_envs=4,
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def main(host: str = "0.0.0.0", port: int = 8000):
|
| 32 |
+
import uvicorn
|
| 33 |
+
uvicorn.run(app, host=host, port=port)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
if __name__ == "__main__":
|
| 37 |
+
import argparse
|
| 38 |
+
parser = argparse.ArgumentParser()
|
| 39 |
+
parser.add_argument("--port", type=int, default=8000)
|
| 40 |
+
args = parser.parse_args()
|
| 41 |
+
main(port=args.port)
|
server/baselines.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Oracle, heuristic, and random baselines for Stack Doctor.
|
| 3 |
+
|
| 4 |
+
Used to validate the reward function: random < heuristic < oracle must hold.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
import json
|
| 10 |
+
import random
|
| 11 |
+
|
| 12 |
+
from .scenarios import (
|
| 13 |
+
ROOT_CAUSE_TO_FIX,
|
| 14 |
+
ROOT_CAUSES,
|
| 15 |
+
FIXES,
|
| 16 |
+
SPECIALISTS,
|
| 17 |
+
Scenario,
|
| 18 |
+
SCENARIOS,
|
| 19 |
+
TRAIN_SCENARIOS,
|
| 20 |
+
EVAL_SCENARIOS,
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def oracle_policy(scenario: Scenario) -> list[dict]:
|
| 25 |
+
"""Perfect policy: submit correct answer in 1 step."""
|
| 26 |
+
return [
|
| 27 |
+
{
|
| 28 |
+
"type": "submit",
|
| 29 |
+
"root_cause": scenario.root_cause,
|
| 30 |
+
"fix": scenario.correct_fix,
|
| 31 |
+
"justification": f"Root cause is {scenario.root_cause}, applying the correct fix.",
|
| 32 |
+
}
|
| 33 |
+
]
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def heuristic_policy(scenario: Scenario) -> list[dict]:
|
| 37 |
+
"""
|
| 38 |
+
Reasonable heuristic: inspect logs, ask the highest-confidence specialist,
|
| 39 |
+
then submit based on clues.
|
| 40 |
+
|
| 41 |
+
Uses keyword matching on specialist opinions and logs to guess root cause.
|
| 42 |
+
"""
|
| 43 |
+
actions = []
|
| 44 |
+
|
| 45 |
+
# Step 1: inspect logs
|
| 46 |
+
actions.append({"type": "inspect", "target": "logs"})
|
| 47 |
+
|
| 48 |
+
# Step 2: ask the highest-confidence specialist
|
| 49 |
+
best_spec = max(
|
| 50 |
+
scenario.specialist_opinions.items(),
|
| 51 |
+
key=lambda kv: kv[1].confidence,
|
| 52 |
+
)
|
| 53 |
+
actions.append({"type": "ask_specialist", "specialist": best_spec[0]})
|
| 54 |
+
|
| 55 |
+
# Step 3: heuristic root-cause guess from keywords
|
| 56 |
+
combined_text = (
|
| 57 |
+
scenario.incident_ticket
|
| 58 |
+
+ " " + scenario.initial_log
|
| 59 |
+
+ " " + best_spec[1].opinion
|
| 60 |
+
).lower()
|
| 61 |
+
|
| 62 |
+
guess = _keyword_guess(combined_text)
|
| 63 |
+
|
| 64 |
+
# Step 4: apply fix
|
| 65 |
+
actions.append({"type": "apply_fix", "fix": ROOT_CAUSE_TO_FIX[guess]})
|
| 66 |
+
|
| 67 |
+
# Step 5: submit
|
| 68 |
+
actions.append({
|
| 69 |
+
"type": "submit",
|
| 70 |
+
"root_cause": guess,
|
| 71 |
+
"fix": ROOT_CAUSE_TO_FIX[guess],
|
| 72 |
+
})
|
| 73 |
+
|
| 74 |
+
return actions
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def random_policy(scenario: Scenario) -> list[dict]:
|
| 78 |
+
"""Random policy: random actions, random submit."""
|
| 79 |
+
actions = []
|
| 80 |
+
n_steps = random.randint(1, 5)
|
| 81 |
+
|
| 82 |
+
for _ in range(n_steps - 1):
|
| 83 |
+
choice = random.choice(["inspect", "ask_specialist"])
|
| 84 |
+
if choice == "inspect":
|
| 85 |
+
actions.append({
|
| 86 |
+
"type": "inspect",
|
| 87 |
+
"target": random.choice(["logs", "config", "snippet", "metrics"]),
|
| 88 |
+
})
|
| 89 |
+
else:
|
| 90 |
+
actions.append({
|
| 91 |
+
"type": "ask_specialist",
|
| 92 |
+
"specialist": random.choice(SPECIALISTS),
|
| 93 |
+
})
|
| 94 |
+
|
| 95 |
+
# Final: random submit
|
| 96 |
+
rc = random.choice(ROOT_CAUSES)
|
| 97 |
+
actions.append({
|
| 98 |
+
"type": "submit",
|
| 99 |
+
"root_cause": rc,
|
| 100 |
+
"fix": ROOT_CAUSE_TO_FIX[rc],
|
| 101 |
+
})
|
| 102 |
+
|
| 103 |
+
return actions
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def _keyword_guess(text: str) -> str:
|
| 107 |
+
"""Guess root cause from keyword presence in text."""
|
| 108 |
+
scores = {
|
| 109 |
+
"arch_guard": 0,
|
| 110 |
+
"backend_whitelist": 0,
|
| 111 |
+
"runtime_loader": 0,
|
| 112 |
+
"backend_selector": 0,
|
| 113 |
+
"model_config": 0,
|
| 114 |
+
"weight_layout": 0,
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
# arch_guard keywords
|
| 118 |
+
for kw in ["arch", "architecture", "sm_12", "sm_120", "sm_121", "supported_arch", "capability", "is_supported"]:
|
| 119 |
+
if kw in text:
|
| 120 |
+
scores["arch_guard"] += 1
|
| 121 |
+
|
| 122 |
+
# backend_whitelist keywords
|
| 123 |
+
for kw in ["whitelist", "supported_gpu", "not in", "marlin", "awq", "gpu name"]:
|
| 124 |
+
if kw in text:
|
| 125 |
+
scores["backend_whitelist"] += 1
|
| 126 |
+
|
| 127 |
+
# runtime_loader keywords
|
| 128 |
+
for kw in ["runtime", "libcuda", "ld_library", "cuda_home", "symlink", "shared object", "rocm_path", "hipError"]:
|
| 129 |
+
if kw in text:
|
| 130 |
+
scores["runtime_loader"] += 1
|
| 131 |
+
|
| 132 |
+
# backend_selector keywords
|
| 133 |
+
for kw in ["backend", "selector", "xformers", "flash_attn", "latency", "slow", "e4m3fn", "fp8 format"]:
|
| 134 |
+
if kw in text:
|
| 135 |
+
scores["backend_selector"] += 1
|
| 136 |
+
|
| 137 |
+
# model_config keywords
|
| 138 |
+
for kw in ["config", "num_expert", "shape mismatch", "rope", "checkpoint", "config.json"]:
|
| 139 |
+
if kw in text:
|
| 140 |
+
scores["model_config"] += 1
|
| 141 |
+
|
| 142 |
+
# weight_layout keywords
|
| 143 |
+
for kw in ["weight", "mapping", "swap", "gate_proj", "up_proj", "convert", "layout", "qkv"]:
|
| 144 |
+
if kw in text:
|
| 145 |
+
scores["weight_layout"] += 1
|
| 146 |
+
|
| 147 |
+
return max(scores, key=scores.get)
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
def evaluate_policy(policy_fn, scenarios: list[Scenario], n_runs: int = 1) -> dict:
|
| 151 |
+
"""
|
| 152 |
+
Run a policy across scenarios and compute metrics.
|
| 153 |
+
|
| 154 |
+
Returns dict with:
|
| 155 |
+
- rc_accuracy: fraction of correct root cause submissions
|
| 156 |
+
- fix_accuracy: fraction of correct fix submissions
|
| 157 |
+
- avg_steps: average steps to resolution
|
| 158 |
+
- avg_reward: average cumulative reward
|
| 159 |
+
"""
|
| 160 |
+
from .stack_doctor_environment import StackDoctorEnvironment
|
| 161 |
+
from models import StackDoctorAction
|
| 162 |
+
|
| 163 |
+
total_rc_correct = 0
|
| 164 |
+
total_fix_correct = 0
|
| 165 |
+
total_steps = 0
|
| 166 |
+
total_reward = 0.0
|
| 167 |
+
total_episodes = 0
|
| 168 |
+
|
| 169 |
+
for _ in range(n_runs):
|
| 170 |
+
for scenario in scenarios:
|
| 171 |
+
env = StackDoctorEnvironment()
|
| 172 |
+
env.reset(scenario_id=scenario.id)
|
| 173 |
+
|
| 174 |
+
actions = policy_fn(scenario)
|
| 175 |
+
cumulative = 0.0
|
| 176 |
+
steps = 0
|
| 177 |
+
|
| 178 |
+
for action_dict in actions:
|
| 179 |
+
obs = env.step(StackDoctorAction(message=json.dumps(action_dict)))
|
| 180 |
+
cumulative += obs.reward
|
| 181 |
+
steps += 1
|
| 182 |
+
if obs.done:
|
| 183 |
+
break
|
| 184 |
+
|
| 185 |
+
# Check if submit happened
|
| 186 |
+
last_action = actions[-1] if actions else {}
|
| 187 |
+
if last_action.get("type") == "submit":
|
| 188 |
+
if last_action["root_cause"] == scenario.root_cause:
|
| 189 |
+
total_rc_correct += 1
|
| 190 |
+
if last_action["fix"] == scenario.correct_fix:
|
| 191 |
+
total_fix_correct += 1
|
| 192 |
+
|
| 193 |
+
total_steps += steps
|
| 194 |
+
total_reward += cumulative
|
| 195 |
+
total_episodes += 1
|
| 196 |
+
|
| 197 |
+
return {
|
| 198 |
+
"rc_accuracy": total_rc_correct / total_episodes if total_episodes else 0,
|
| 199 |
+
"fix_accuracy": total_fix_correct / total_episodes if total_episodes else 0,
|
| 200 |
+
"avg_steps": total_steps / total_episodes if total_episodes else 0,
|
| 201 |
+
"avg_reward": total_reward / total_episodes if total_episodes else 0,
|
| 202 |
+
"n_episodes": total_episodes,
|
| 203 |
+
}
|
server/requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
openenv[core]>=0.2.0
|
| 2 |
+
fastapi>=0.115.0
|
| 3 |
+
uvicorn>=0.24.0
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
|
server/scenarios.py
ADDED
|
@@ -0,0 +1,1893 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Scenario data for the Launch-Day War Room.
|
| 3 |
+
|
| 4 |
+
Each scenario encodes a hidden root cause, the correct fix, an incident ticket,
|
| 5 |
+
hardware/model/backend context, log and code snippets, and specialist opinions
|
| 6 |
+
(some of which may be wrong).
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import random
|
| 12 |
+
from dataclasses import dataclass, field
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
ROOT_CAUSES = [
|
| 16 |
+
"arch_guard",
|
| 17 |
+
"backend_whitelist",
|
| 18 |
+
"runtime_loader",
|
| 19 |
+
"backend_selector",
|
| 20 |
+
"model_config",
|
| 21 |
+
"weight_layout",
|
| 22 |
+
]
|
| 23 |
+
|
| 24 |
+
FIXES = [
|
| 25 |
+
"relax_arch_check",
|
| 26 |
+
"add_whitelist_entry",
|
| 27 |
+
"fix_runtime_path",
|
| 28 |
+
"switch_backend",
|
| 29 |
+
"update_model_config",
|
| 30 |
+
"fix_weight_mapping",
|
| 31 |
+
]
|
| 32 |
+
|
| 33 |
+
# 1:1 mapping
|
| 34 |
+
ROOT_CAUSE_TO_FIX = dict(zip(ROOT_CAUSES, FIXES))
|
| 35 |
+
FIX_TO_ROOT_CAUSE = {v: k for k, v in ROOT_CAUSE_TO_FIX.items()}
|
| 36 |
+
|
| 37 |
+
SPECIALISTS = ["runtime", "dispatch", "kernel", "loader"]
|
| 38 |
+
|
| 39 |
+
HARDWARE_OPTIONS = [
|
| 40 |
+
"NVIDIA SM121 (DGX Spark)",
|
| 41 |
+
"NVIDIA SM120 (GeForce RTX 5090)",
|
| 42 |
+
"AMD MI300X",
|
| 43 |
+
"AMD MI355X",
|
| 44 |
+
"NVIDIA H100",
|
| 45 |
+
"NVIDIA B200",
|
| 46 |
+
]
|
| 47 |
+
|
| 48 |
+
MODEL_OPTIONS = [
|
| 49 |
+
"DeepSeek-V3-671B",
|
| 50 |
+
"Llama-4-Maverick-17Bx128E",
|
| 51 |
+
"Qwen3-235B-A22B",
|
| 52 |
+
"Mistral-Large-2",
|
| 53 |
+
"DeepSeek-R1-Distill-70B",
|
| 54 |
+
"Llama-3.3-70B-Instruct",
|
| 55 |
+
]
|
| 56 |
+
|
| 57 |
+
BACKEND_OPTIONS = [
|
| 58 |
+
"vLLM 0.8.x",
|
| 59 |
+
"SGLang 0.5.x",
|
| 60 |
+
"TensorRT-LLM 0.18",
|
| 61 |
+
"FlashInfer 0.4",
|
| 62 |
+
"Triton Inference Server",
|
| 63 |
+
]
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
@dataclass
|
| 67 |
+
class SpecialistOpinion:
|
| 68 |
+
opinion: str
|
| 69 |
+
confidence: float
|
| 70 |
+
is_correct: bool
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
@dataclass
|
| 74 |
+
class InspectResult:
|
| 75 |
+
logs: str
|
| 76 |
+
config: str
|
| 77 |
+
snippet: str
|
| 78 |
+
metrics: str
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
@dataclass
|
| 82 |
+
class Scenario:
|
| 83 |
+
id: str
|
| 84 |
+
root_cause: str
|
| 85 |
+
correct_fix: str
|
| 86 |
+
incident_ticket: str
|
| 87 |
+
hardware: str
|
| 88 |
+
model_name: str
|
| 89 |
+
backend: str
|
| 90 |
+
initial_log: str
|
| 91 |
+
initial_snippet: str
|
| 92 |
+
specialist_opinions: dict[str, SpecialistOpinion]
|
| 93 |
+
inspect_results: InspectResult
|
| 94 |
+
# For ask_specialist follow-ups
|
| 95 |
+
specialist_followups: dict[str, str] = field(default_factory=dict)
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
# ---------------------------------------------------------------------------
|
| 99 |
+
# Seed scenarios
|
| 100 |
+
# ---------------------------------------------------------------------------
|
| 101 |
+
|
| 102 |
+
def _make_scenarios() -> list[Scenario]:
|
| 103 |
+
scenarios = []
|
| 104 |
+
|
| 105 |
+
# --- arch_guard scenarios ---
|
| 106 |
+
scenarios.append(Scenario(
|
| 107 |
+
id="arch_guard_01",
|
| 108 |
+
root_cause="arch_guard",
|
| 109 |
+
correct_fix="relax_arch_check",
|
| 110 |
+
incident_ticket=(
|
| 111 |
+
"INCIDENT: FlashInfer attention kernel fails to launch on newly provisioned "
|
| 112 |
+
"DGX Spark nodes. Error: 'Unsupported GPU architecture sm_121'. "
|
| 113 |
+
"Identical model config works on H100 nodes."
|
| 114 |
+
),
|
| 115 |
+
hardware="NVIDIA SM121 (DGX Spark)",
|
| 116 |
+
model_name="DeepSeek-V3-671B",
|
| 117 |
+
backend="FlashInfer 0.4",
|
| 118 |
+
initial_log=(
|
| 119 |
+
"[FlashInfer] Checking GPU capability... sm_121 detected\n"
|
| 120 |
+
"[FlashInfer] ERROR: is_supported_arch() returned False for sm_121\n"
|
| 121 |
+
"[FlashInfer] Falling back to... no fallback available\n"
|
| 122 |
+
"RuntimeError: No compatible attention kernel for architecture sm_121"
|
| 123 |
+
),
|
| 124 |
+
initial_snippet=(
|
| 125 |
+
"# flashinfer/arch_check.py\n"
|
| 126 |
+
"SUPPORTED_ARCHS = {70, 75, 80, 86, 89, 90}\n"
|
| 127 |
+
"\n"
|
| 128 |
+
"def is_supported_arch(cc: int) -> bool:\n"
|
| 129 |
+
" return cc in SUPPORTED_ARCHS"
|
| 130 |
+
),
|
| 131 |
+
specialist_opinions={
|
| 132 |
+
"runtime": SpecialistOpinion(
|
| 133 |
+
"CUDA runtime loaded successfully. No runtime issues detected.", 0.85, False
|
| 134 |
+
),
|
| 135 |
+
"dispatch": SpecialistOpinion(
|
| 136 |
+
"Architecture check is blocking kernel dispatch. The SM121 architecture "
|
| 137 |
+
"is not in the supported set despite being SM90-compatible at the instruction level.", 0.92, True
|
| 138 |
+
),
|
| 139 |
+
"kernel": SpecialistOpinion(
|
| 140 |
+
"The HMMA m16n8k16 instructions used by the attention kernel are available on SM121. "
|
| 141 |
+
"This looks like a capability check issue, not a kernel issue.", 0.88, True
|
| 142 |
+
),
|
| 143 |
+
"loader": SpecialistOpinion(
|
| 144 |
+
"Model weights loaded correctly. Weight layout is standard.", 0.80, False
|
| 145 |
+
),
|
| 146 |
+
},
|
| 147 |
+
inspect_results=InspectResult(
|
| 148 |
+
logs=(
|
| 149 |
+
"[FlashInfer] GPU: NVIDIA GH200 (sm_121)\n"
|
| 150 |
+
"[FlashInfer] CUDA version: 13.0\n"
|
| 151 |
+
"[FlashInfer] is_supported_arch(121) = False\n"
|
| 152 |
+
"[FlashInfer] Architecture check FAILED\n"
|
| 153 |
+
"[CUDA] All CUDA operations nominal\n"
|
| 154 |
+
"[System] GPU memory: 96GB available"
|
| 155 |
+
),
|
| 156 |
+
config=(
|
| 157 |
+
"gpu_architecture: sm_121\n"
|
| 158 |
+
"cuda_version: 13.0\n"
|
| 159 |
+
"flashinfer_version: 0.4.1\n"
|
| 160 |
+
"attention_backend: flashinfer\n"
|
| 161 |
+
"supported_archs: [70, 75, 80, 86, 89, 90]"
|
| 162 |
+
),
|
| 163 |
+
snippet=(
|
| 164 |
+
"# The arch check function uses an exact match:\n"
|
| 165 |
+
"def is_supported_arch(cc):\n"
|
| 166 |
+
" return cc in SUPPORTED_ARCHS # misses sm_12x family\n\n"
|
| 167 |
+
"# SM121 supports HMMA m16n8k16 (same as SM90)\n"
|
| 168 |
+
"# but is not in the allowlist"
|
| 169 |
+
),
|
| 170 |
+
metrics=(
|
| 171 |
+
"kernel_launch_attempts: 47\n"
|
| 172 |
+
"kernel_launch_failures: 47\n"
|
| 173 |
+
"fallback_attempts: 47\n"
|
| 174 |
+
"fallback_failures: 47\n"
|
| 175 |
+
"gpu_utilization: 0%"
|
| 176 |
+
),
|
| 177 |
+
),
|
| 178 |
+
specialist_followups={
|
| 179 |
+
"runtime": "I confirmed CUDA 13.0 runtime is functional. All driver calls succeed. This isn't a runtime issue.",
|
| 180 |
+
"dispatch": "The dispatch table maps arch -> kernel. SM121 has no entry. Adding sm_12x family to the arch check should fix it.",
|
| 181 |
+
"kernel": "I inspected the PTX. The kernel only needs HMMA m16n8k16 which SM121 supports. The kernel itself is fine.",
|
| 182 |
+
"loader": "Weights are in the expected layout. No loader issues.",
|
| 183 |
+
},
|
| 184 |
+
))
|
| 185 |
+
|
| 186 |
+
scenarios.append(Scenario(
|
| 187 |
+
id="arch_guard_02",
|
| 188 |
+
root_cause="arch_guard",
|
| 189 |
+
correct_fix="relax_arch_check",
|
| 190 |
+
incident_ticket=(
|
| 191 |
+
"INCIDENT: MLA attention fails on GeForce RTX 5090. Error: "
|
| 192 |
+
"'compute capability 120 not supported'. Customer reports RTX 4090 works fine."
|
| 193 |
+
),
|
| 194 |
+
hardware="NVIDIA SM120 (GeForce RTX 5090)",
|
| 195 |
+
model_name="DeepSeek-R1-Distill-70B",
|
| 196 |
+
backend="vLLM 0.8.x",
|
| 197 |
+
initial_log=(
|
| 198 |
+
"[vLLM] Detecting GPU... GeForce RTX 5090 (sm_120)\n"
|
| 199 |
+
"[vLLM] FlashAttention: compute capability 120 not in supported list\n"
|
| 200 |
+
"[vLLM] ERROR: Cannot initialize attention backend"
|
| 201 |
+
),
|
| 202 |
+
initial_snippet=(
|
| 203 |
+
"# vllm/attention/backends/flash_attn.py\n"
|
| 204 |
+
"MIN_CC = 80\n"
|
| 205 |
+
"MAX_CC = 90\n"
|
| 206 |
+
"\n"
|
| 207 |
+
"def is_supported(cc: int) -> bool:\n"
|
| 208 |
+
" return MIN_CC <= cc <= MAX_CC"
|
| 209 |
+
),
|
| 210 |
+
specialist_opinions={
|
| 211 |
+
"runtime": SpecialistOpinion("Runtime is fine. CUDA 13 loaded.", 0.75, False),
|
| 212 |
+
"dispatch": SpecialistOpinion(
|
| 213 |
+
"The capability range check excludes SM120. Needs to include SM12x family.", 0.90, True
|
| 214 |
+
),
|
| 215 |
+
"kernel": SpecialistOpinion(
|
| 216 |
+
"Possible kernel incompatibility — SM120 lacks tcgen05 MMA.", 0.60, False
|
| 217 |
+
),
|
| 218 |
+
"loader": SpecialistOpinion("Weights look fine.", 0.70, False),
|
| 219 |
+
},
|
| 220 |
+
inspect_results=InspectResult(
|
| 221 |
+
logs="[vLLM] GPU cc=120 rejected by range [80,90]\n[vLLM] No fallback attention backend",
|
| 222 |
+
config="compute_capability: 120\nmax_supported_cc: 90\nattention_backend: flash_attn",
|
| 223 |
+
snippet="# Range check: MIN_CC(80) <= cc <= MAX_CC(90)\n# SM120 = 120 > 90, so rejected\n# Fix: add sm_12x family check",
|
| 224 |
+
metrics="attention_init_failures: 1\nmodel_load_time: 0s (blocked at init)",
|
| 225 |
+
),
|
| 226 |
+
specialist_followups={
|
| 227 |
+
"runtime": "CUDA 13.0 runtime is healthy. Driver version matches.",
|
| 228 |
+
"dispatch": "SM120 uses HMMA path (no warp specialization), same code path as SM86. Just need to update the arch range.",
|
| 229 |
+
"kernel": "On closer inspection, SM120 does support the needed HMMA instructions. My earlier concern about tcgen05 was wrong — that's only needed for Hopper-style warp specialization.",
|
| 230 |
+
"loader": "No weight issues detected.",
|
| 231 |
+
},
|
| 232 |
+
))
|
| 233 |
+
|
| 234 |
+
# --- backend_whitelist scenarios ---
|
| 235 |
+
scenarios.append(Scenario(
|
| 236 |
+
id="backend_whitelist_01",
|
| 237 |
+
root_cause="backend_whitelist",
|
| 238 |
+
correct_fix="add_whitelist_entry",
|
| 239 |
+
incident_ticket=(
|
| 240 |
+
"INCIDENT: Marlin quantized inference crashes on SM121 nodes. "
|
| 241 |
+
"Error: 'Marlin kernel not available for current GPU'. "
|
| 242 |
+
"FP16 inference works, only quantized (GPTQ/AWQ) path fails."
|
| 243 |
+
),
|
| 244 |
+
hardware="NVIDIA SM121 (DGX Spark)",
|
| 245 |
+
model_name="Llama-3.3-70B-Instruct",
|
| 246 |
+
backend="vLLM 0.8.x",
|
| 247 |
+
initial_log=(
|
| 248 |
+
"[vLLM] Loading GPTQ-quantized model...\n"
|
| 249 |
+
"[vLLM] Checking Marlin kernel availability for sm_121\n"
|
| 250 |
+
"[vLLM] WARNING: GPU sm_121 not in Marlin whitelist\n"
|
| 251 |
+
"[vLLM] ERROR: No quantization kernel available"
|
| 252 |
+
),
|
| 253 |
+
initial_snippet=(
|
| 254 |
+
"# vllm/model_executor/layers/quantization/marlin.py\n"
|
| 255 |
+
"MARLIN_SUPPORTED_GPUS = [\n"
|
| 256 |
+
" 'A100', 'A10', 'H100', 'L40', 'RTX 4090',\n"
|
| 257 |
+
"]\n"
|
| 258 |
+
),
|
| 259 |
+
specialist_opinions={
|
| 260 |
+
"runtime": SpecialistOpinion("CUDA runtime OK. Libraries loaded.", 0.80, False),
|
| 261 |
+
"dispatch": SpecialistOpinion(
|
| 262 |
+
"Marlin whitelist doesn't include SM121 GPU names. Need to add the entry.", 0.91, True
|
| 263 |
+
),
|
| 264 |
+
"kernel": SpecialistOpinion(
|
| 265 |
+
"Marlin kernels use standard HMMA ops that SM121 supports. It's just not whitelisted.", 0.85, True
|
| 266 |
+
),
|
| 267 |
+
"loader": SpecialistOpinion(
|
| 268 |
+
"Quantized weights loaded but kernel never launches. Might be a weight format issue.", 0.55, False
|
| 269 |
+
),
|
| 270 |
+
},
|
| 271 |
+
inspect_results=InspectResult(
|
| 272 |
+
logs="[Marlin] GPU name 'NVIDIA GH200' not in whitelist\n[Marlin] Whitelist: ['A100','A10','H100','L40','RTX 4090']",
|
| 273 |
+
config="quantization: gptq\nmarlin_whitelist: [A100, A10, H100, L40, RTX 4090]\ngpu_name: NVIDIA GH200",
|
| 274 |
+
snippet="# Whitelist check uses GPU product name string matching\n# GH200 / DGX Spark not in the list\n# Should use arch family check instead of name matching",
|
| 275 |
+
metrics="quantized_kernel_attempts: 1\nquantized_kernel_failures: 1\nfp16_fallback: not_attempted",
|
| 276 |
+
),
|
| 277 |
+
specialist_followups={
|
| 278 |
+
"runtime": "All good on the runtime side.",
|
| 279 |
+
"dispatch": "The whitelist is name-based, not arch-based. Adding 'GH200' or switching to family-level arch checks fixes this.",
|
| 280 |
+
"kernel": "The Marlin FP8 GEMM dispatch works with SM121's MMA units. It's purely a whitelist gap.",
|
| 281 |
+
"loader": "Actually, the weights loaded fine. I retract my earlier concern.",
|
| 282 |
+
},
|
| 283 |
+
))
|
| 284 |
+
|
| 285 |
+
scenarios.append(Scenario(
|
| 286 |
+
id="backend_whitelist_02",
|
| 287 |
+
root_cause="backend_whitelist",
|
| 288 |
+
correct_fix="add_whitelist_entry",
|
| 289 |
+
incident_ticket=(
|
| 290 |
+
"INCIDENT: AWQ quantization backend refuses to initialize on MI300X. "
|
| 291 |
+
"Error: 'GPU not supported for AWQ acceleration'. "
|
| 292 |
+
"Other backends work fine on the same hardware."
|
| 293 |
+
),
|
| 294 |
+
hardware="AMD MI300X",
|
| 295 |
+
model_name="Qwen3-235B-A22B",
|
| 296 |
+
backend="vLLM 0.8.x",
|
| 297 |
+
initial_log=(
|
| 298 |
+
"[vLLM] Initializing AWQ backend...\n"
|
| 299 |
+
"[vLLM] GPU: AMD Instinct MI300X\n"
|
| 300 |
+
"[vLLM] AWQ: GPU not in supported devices list\n"
|
| 301 |
+
"[vLLM] ERROR: AWQ acceleration unavailable"
|
| 302 |
+
),
|
| 303 |
+
initial_snippet=(
|
| 304 |
+
"# vllm/model_executor/layers/quantization/awq.py\n"
|
| 305 |
+
"AWQ_SUPPORTED = {'A100', 'H100', 'RTX 4090', 'L40S'}\n"
|
| 306 |
+
),
|
| 307 |
+
specialist_opinions={
|
| 308 |
+
"runtime": SpecialistOpinion("ROCm runtime healthy. HIP version matches.", 0.82, False),
|
| 309 |
+
"dispatch": SpecialistOpinion(
|
| 310 |
+
"AWQ whitelist is NVIDIA-only. MI300X needs to be added.", 0.93, True
|
| 311 |
+
),
|
| 312 |
+
"kernel": SpecialistOpinion(
|
| 313 |
+
"MI300X has MFMA instructions that can handle the AWQ GEMM. Not a kernel issue.", 0.87, True
|
| 314 |
+
),
|
| 315 |
+
"loader": SpecialistOpinion("Weight format might not match AMD layout expectations.", 0.50, False),
|
| 316 |
+
},
|
| 317 |
+
inspect_results=InspectResult(
|
| 318 |
+
logs="[AWQ] Device 'AMD Instinct MI300X' not in AWQ_SUPPORTED\n[AWQ] Supported: A100, H100, RTX 4090, L40S",
|
| 319 |
+
config="quantization: awq\nawq_supported: [A100, H100, RTX 4090, L40S]\ngpu: AMD Instinct MI300X",
|
| 320 |
+
snippet="# AWQ_SUPPORTED only lists NVIDIA GPUs\n# MI300X MFMA f32_32x32x8_f16 can handle AWQ ops\n# Need to add MI300X to whitelist",
|
| 321 |
+
metrics="awq_init_failures: 1\nfallback_to_fp16: pending",
|
| 322 |
+
),
|
| 323 |
+
specialist_followups={
|
| 324 |
+
"runtime": "ROCm 6.3 loaded successfully. No runtime concerns.",
|
| 325 |
+
"dispatch": "Simple whitelist gap. Adding MI300X resolves the issue.",
|
| 326 |
+
"kernel": "Confirmed: MFMA ops on MI300X handle the AWQ GEMM pattern.",
|
| 327 |
+
"loader": "I was wrong earlier — weights are fine. It's the whitelist.",
|
| 328 |
+
},
|
| 329 |
+
))
|
| 330 |
+
|
| 331 |
+
# --- runtime_loader scenarios ---
|
| 332 |
+
scenarios.append(Scenario(
|
| 333 |
+
id="runtime_loader_01",
|
| 334 |
+
root_cause="runtime_loader",
|
| 335 |
+
correct_fix="fix_runtime_path",
|
| 336 |
+
incident_ticket=(
|
| 337 |
+
"INCIDENT: SGLang server crashes on startup with CUDA 13 on DGX Spark. "
|
| 338 |
+
"Error: 'libcudart.so.13: cannot open shared object file'. "
|
| 339 |
+
"System has CUDA 13 installed but SGLang can't find it."
|
| 340 |
+
),
|
| 341 |
+
hardware="NVIDIA SM121 (DGX Spark)",
|
| 342 |
+
model_name="Llama-4-Maverick-17Bx128E",
|
| 343 |
+
backend="SGLang 0.5.x",
|
| 344 |
+
initial_log=(
|
| 345 |
+
"[SGLang] Starting server...\n"
|
| 346 |
+
"[SGLang] Loading CUDA runtime...\n"
|
| 347 |
+
"[SGLang] ERROR: libcudart.so.13: cannot open shared object file\n"
|
| 348 |
+
"[SGLang] LD_LIBRARY_PATH=/usr/local/cuda-12/lib64\n"
|
| 349 |
+
"ImportError: CUDA runtime not found"
|
| 350 |
+
),
|
| 351 |
+
initial_snippet=(
|
| 352 |
+
"# sglang/startup.py\n"
|
| 353 |
+
"CUDA_LIB_PATH = os.environ.get(\n"
|
| 354 |
+
" 'CUDA_HOME', '/usr/local/cuda'\n"
|
| 355 |
+
") + '/lib64'\n"
|
| 356 |
+
"# Hardcoded to cuda, not cuda-13\n"
|
| 357 |
+
),
|
| 358 |
+
specialist_opinions={
|
| 359 |
+
"runtime": SpecialistOpinion(
|
| 360 |
+
"CUDA 13 is installed at /usr/local/cuda-13 but LD_LIBRARY_PATH points to cuda-12. "
|
| 361 |
+
"The runtime path needs to be updated.", 0.95, True
|
| 362 |
+
),
|
| 363 |
+
"dispatch": SpecialistOpinion("Can't tell — server never gets to dispatch phase.", 0.40, False),
|
| 364 |
+
"kernel": SpecialistOpinion("No kernel issue — server crashes before kernel init.", 0.60, False),
|
| 365 |
+
"loader": SpecialistOpinion(
|
| 366 |
+
"The CUDA shared library loader can't find libcudart.so.13. Path issue.", 0.88, True
|
| 367 |
+
),
|
| 368 |
+
},
|
| 369 |
+
inspect_results=InspectResult(
|
| 370 |
+
logs=(
|
| 371 |
+
"[System] CUDA installations:\n"
|
| 372 |
+
" /usr/local/cuda-12 -> CUDA 12.4\n"
|
| 373 |
+
" /usr/local/cuda-13 -> CUDA 13.0\n"
|
| 374 |
+
" /usr/local/cuda -> symlink to cuda-12\n"
|
| 375 |
+
"[SGLang] Trying to load libcudart.so.13 from /usr/local/cuda/lib64 -> NOT FOUND"
|
| 376 |
+
),
|
| 377 |
+
config="CUDA_HOME=/usr/local/cuda\nLD_LIBRARY_PATH=/usr/local/cuda-12/lib64\ncuda_13_path=/usr/local/cuda-13",
|
| 378 |
+
snippet="# /usr/local/cuda symlinks to cuda-12\n# Need: export CUDA_HOME=/usr/local/cuda-13\n# Or: update symlink",
|
| 379 |
+
metrics="server_start_attempts: 3\nserver_start_failures: 3\nuptime: 0s",
|
| 380 |
+
),
|
| 381 |
+
specialist_followups={
|
| 382 |
+
"runtime": "Confirmed: /usr/local/cuda symlink targets cuda-12. CUDA 13 is at /usr/local/cuda-13. Fix the path.",
|
| 383 |
+
"dispatch": "Server never started, so I can't diagnose dispatch.",
|
| 384 |
+
"kernel": "Same — no kernel loaded.",
|
| 385 |
+
"loader": "The dynamic linker searches LD_LIBRARY_PATH first. It needs /usr/local/cuda-13/lib64.",
|
| 386 |
+
},
|
| 387 |
+
))
|
| 388 |
+
|
| 389 |
+
scenarios.append(Scenario(
|
| 390 |
+
id="runtime_loader_02",
|
| 391 |
+
root_cause="runtime_loader",
|
| 392 |
+
correct_fix="fix_runtime_path",
|
| 393 |
+
incident_ticket=(
|
| 394 |
+
"INCIDENT: ROCm HIP runtime fails to initialize on MI300X cluster. "
|
| 395 |
+
"Error: 'hipErrorNoDevice' despite GPUs being visible in lspci. "
|
| 396 |
+
"Worked yesterday before system update."
|
| 397 |
+
),
|
| 398 |
+
hardware="AMD MI300X",
|
| 399 |
+
model_name="DeepSeek-V3-671B",
|
| 400 |
+
backend="vLLM 0.8.x",
|
| 401 |
+
initial_log=(
|
| 402 |
+
"[HIP] Initializing runtime...\n"
|
| 403 |
+
"[HIP] ERROR: hipErrorNoDevice (code 100)\n"
|
| 404 |
+
"[System] lspci shows 8x AMD Instinct MI300X\n"
|
| 405 |
+
"[System] /opt/rocm -> /opt/rocm-6.2 (outdated symlink)"
|
| 406 |
+
),
|
| 407 |
+
initial_snippet=(
|
| 408 |
+
"# environment setup\n"
|
| 409 |
+
"ROCM_PATH=/opt/rocm # symlinks to rocm-6.2\n"
|
| 410 |
+
"# But rocm-6.3 installed at /opt/rocm-6.3\n"
|
| 411 |
+
"# Driver expects rocm-6.3 runtime\n"
|
| 412 |
+
),
|
| 413 |
+
specialist_opinions={
|
| 414 |
+
"runtime": SpecialistOpinion(
|
| 415 |
+
"ROCm path mismatch. /opt/rocm points to 6.2 but driver needs 6.3 runtime.", 0.94, True
|
| 416 |
+
),
|
| 417 |
+
"dispatch": SpecialistOpinion("Not a dispatch issue — runtime doesn't initialize.", 0.70, False),
|
| 418 |
+
"kernel": SpecialistOpinion("Might be a kernel module issue with the GPU driver.", 0.45, False),
|
| 419 |
+
"loader": SpecialistOpinion("ROCm shared libraries at wrong version.", 0.80, True),
|
| 420 |
+
},
|
| 421 |
+
inspect_results=InspectResult(
|
| 422 |
+
logs="[System] /opt/rocm -> /opt/rocm-6.2\n[System] Driver version: 6.3.0\n[HIP] Runtime version mismatch: expected 6.3, found 6.2",
|
| 423 |
+
config="ROCM_PATH=/opt/rocm\nrocm_symlink_target=/opt/rocm-6.2\ninstalled_versions: [6.2, 6.3]\ndriver_version: 6.3.0",
|
| 424 |
+
snippet="# The system was updated and ROCm 6.3 driver installed\n# But /opt/rocm symlink still points to 6.2\n# Fix: ln -sf /opt/rocm-6.3 /opt/rocm",
|
| 425 |
+
metrics="gpu_init_failures: 8\ndriver_version: 6.3.0\nruntime_version: 6.2.0",
|
| 426 |
+
),
|
| 427 |
+
specialist_followups={
|
| 428 |
+
"runtime": "Classic version mismatch after system update. Fix the symlink to point to rocm-6.3.",
|
| 429 |
+
"dispatch": "Can't assess dispatch without a working runtime.",
|
| 430 |
+
"kernel": "I was wrong — it's not a kernel module issue. The GPU driver is fine, it's the userspace runtime path.",
|
| 431 |
+
"loader": "The shared library loader finds rocm-6.2 libs but driver expects 6.3. Path fix needed.",
|
| 432 |
+
},
|
| 433 |
+
))
|
| 434 |
+
|
| 435 |
+
# --- backend_selector scenarios ---
|
| 436 |
+
scenarios.append(Scenario(
|
| 437 |
+
id="backend_selector_01",
|
| 438 |
+
root_cause="backend_selector",
|
| 439 |
+
correct_fix="switch_backend",
|
| 440 |
+
incident_ticket=(
|
| 441 |
+
"INCIDENT: Extreme latency (10x expected) on H100 serving Llama-3.3-70B. "
|
| 442 |
+
"No errors, just very slow. GPU utilization looks low. "
|
| 443 |
+
"Other models on the same node are fast."
|
| 444 |
+
),
|
| 445 |
+
hardware="NVIDIA H100",
|
| 446 |
+
model_name="Llama-3.3-70B-Instruct",
|
| 447 |
+
backend="vLLM 0.8.x",
|
| 448 |
+
initial_log=(
|
| 449 |
+
"[vLLM] Selected attention backend: xformers\n"
|
| 450 |
+
"[vLLM] WARNING: FlashAttention v2 not selected (override with VLLM_ATTENTION_BACKEND)\n"
|
| 451 |
+
"[vLLM] Serving Llama-3.3-70B-Instruct...\n"
|
| 452 |
+
"[vLLM] p99 latency: 4200ms (expected: ~400ms)"
|
| 453 |
+
),
|
| 454 |
+
initial_snippet=(
|
| 455 |
+
"# vllm/attention/selector.py\n"
|
| 456 |
+
"def get_attention_backend(model_config):\n"
|
| 457 |
+
" if model_config.head_dim not in [64, 128]:\n"
|
| 458 |
+
" return 'xformers' # fallback\n"
|
| 459 |
+
" return 'flash_attn'\n"
|
| 460 |
+
),
|
| 461 |
+
specialist_opinions={
|
| 462 |
+
"runtime": SpecialistOpinion("CUDA runtime is fine. No errors.", 0.75, False),
|
| 463 |
+
"dispatch": SpecialistOpinion(
|
| 464 |
+
"Wrong attention backend selected. xformers is much slower than FlashAttention on H100. "
|
| 465 |
+
"The backend selector has a bug in head_dim detection.", 0.94, True
|
| 466 |
+
),
|
| 467 |
+
"kernel": SpecialistOpinion(
|
| 468 |
+
"The xformers kernel is correct but suboptimal for H100. Should use flash_attn.", 0.82, True
|
| 469 |
+
),
|
| 470 |
+
"loader": SpecialistOpinion("Model loaded correctly. Not a weight issue.", 0.80, False),
|
| 471 |
+
},
|
| 472 |
+
inspect_results=InspectResult(
|
| 473 |
+
logs="[vLLM] head_dim=128, num_heads=64\n[vLLM] Backend selection: model reports head_dim=None (config missing) -> fallback to xformers",
|
| 474 |
+
config="attention_backend: xformers (auto-selected)\nmodel_head_dim: null\nactual_head_dim: 128\ngpu: H100",
|
| 475 |
+
snippet="# The model config doesn't explicitly set head_dim\n# Selector falls back to xformers when head_dim is None\n# Should infer head_dim from hidden_size / num_heads",
|
| 476 |
+
metrics="p50_latency_ms: 3100\np99_latency_ms: 4200\ngpu_utilization: 12%\nexpected_gpu_util: 85%",
|
| 477 |
+
),
|
| 478 |
+
specialist_followups={
|
| 479 |
+
"runtime": "No runtime issues. The server is running, just slow.",
|
| 480 |
+
"dispatch": "Backend selector bug: head_dim is None in model config, causing xformers fallback. Switch to flash_attn.",
|
| 481 |
+
"kernel": "xformers works but doesn't use H100 TMA/warp specialization. flash_attn v2 would be 8-10x faster.",
|
| 482 |
+
"loader": "Weights loaded correctly.",
|
| 483 |
+
},
|
| 484 |
+
))
|
| 485 |
+
|
| 486 |
+
scenarios.append(Scenario(
|
| 487 |
+
id="backend_selector_02",
|
| 488 |
+
root_cause="backend_selector",
|
| 489 |
+
correct_fix="switch_backend",
|
| 490 |
+
incident_ticket=(
|
| 491 |
+
"INCIDENT: FP8 inference on MI300X producing garbage output. "
|
| 492 |
+
"Model loads, tokens generate, but output is nonsensical. "
|
| 493 |
+
"BF16 inference on same hardware works perfectly."
|
| 494 |
+
),
|
| 495 |
+
hardware="AMD MI300X",
|
| 496 |
+
model_name="Mistral-Large-2",
|
| 497 |
+
backend="vLLM 0.8.x",
|
| 498 |
+
initial_log=(
|
| 499 |
+
"[vLLM] FP8 quantization: e4m3fn format selected\n"
|
| 500 |
+
"[vLLM] WARNING: MI300X uses e4m3fnuz format, not e4m3fn\n"
|
| 501 |
+
"[vLLM] Serving with FP8...\n"
|
| 502 |
+
"[vLLM] Output quality check: FAIL (perplexity 847.3, expected <15)"
|
| 503 |
+
),
|
| 504 |
+
initial_snippet=(
|
| 505 |
+
"# vllm/quantization/fp8.py\n"
|
| 506 |
+
"FP8_FORMAT = 'e4m3fn' # NVIDIA default\n"
|
| 507 |
+
"# AMD MI300X needs e4m3fnuz (no NaN, unsigned zero)\n"
|
| 508 |
+
),
|
| 509 |
+
specialist_opinions={
|
| 510 |
+
"runtime": SpecialistOpinion("ROCm runtime is healthy.", 0.80, False),
|
| 511 |
+
"dispatch": SpecialistOpinion(
|
| 512 |
+
"Wrong FP8 format selected. MI300X uses e4m3fnuz, not e4m3fn. "
|
| 513 |
+
"The backend selector should detect AMD and switch format.", 0.93, True
|
| 514 |
+
),
|
| 515 |
+
"kernel": SpecialistOpinion(
|
| 516 |
+
"The GEMM kernel runs but produces wrong results due to format mismatch.", 0.85, True
|
| 517 |
+
),
|
| 518 |
+
"loader": SpecialistOpinion(
|
| 519 |
+
"Weight dequantization might be wrong for AMD FP8 format.", 0.65, False
|
| 520 |
+
),
|
| 521 |
+
},
|
| 522 |
+
inspect_results=InspectResult(
|
| 523 |
+
logs="[FP8] Using e4m3fn format\n[FP8] AMD GPU detected but format not switched\n[FP8] Numerical errors in first GEMM",
|
| 524 |
+
config="fp8_format: e4m3fn\ngpu_vendor: AMD\nexpected_format: e4m3fnuz\nformat_mismatch: true",
|
| 525 |
+
snippet="# e4m3fn: 1 sign, 4 exp, 3 mantissa, has NaN encoding\n# e4m3fnuz: 1 sign, 4 exp, 3 mantissa, NO NaN, unsigned zero\n# Bit patterns interpreted differently -> garbage output",
|
| 526 |
+
metrics="output_perplexity: 847.3\nexpected_perplexity: 12.5\ngemm_numerical_errors: 100%",
|
| 527 |
+
),
|
| 528 |
+
specialist_followups={
|
| 529 |
+
"runtime": "ROCm fine. This is a numerical issue, not runtime.",
|
| 530 |
+
"dispatch": "Switch the FP8 format selector to use e4m3fnuz for AMD GPUs. Clear fix.",
|
| 531 |
+
"kernel": "The kernel math is correct for the format it's given — the problem is the format itself.",
|
| 532 |
+
"loader": "Actually, weights are fine. The issue is at the GEMM dispatch level.",
|
| 533 |
+
},
|
| 534 |
+
))
|
| 535 |
+
|
| 536 |
+
# --- model_config scenarios ---
|
| 537 |
+
scenarios.append(Scenario(
|
| 538 |
+
id="model_config_01",
|
| 539 |
+
root_cause="model_config",
|
| 540 |
+
correct_fix="update_model_config",
|
| 541 |
+
incident_ticket=(
|
| 542 |
+
"INCIDENT: DeepSeek-V3 MoE routing crashes with shape mismatch. "
|
| 543 |
+
"Error: 'Expected expert count 256, got 160'. "
|
| 544 |
+
"Model just updated to new checkpoint, was working before."
|
| 545 |
+
),
|
| 546 |
+
hardware="NVIDIA H100",
|
| 547 |
+
model_name="DeepSeek-V3-671B",
|
| 548 |
+
backend="SGLang 0.5.x",
|
| 549 |
+
initial_log=(
|
| 550 |
+
"[SGLang] Loading DeepSeek-V3-671B...\n"
|
| 551 |
+
"[SGLang] MoE config: num_experts=256 (from config.json)\n"
|
| 552 |
+
"[SGLang] Actual weight shape: experts.0-159\n"
|
| 553 |
+
"[SGLang] ERROR: Shape mismatch in MoE layer: expected 256 experts, found 160"
|
| 554 |
+
),
|
| 555 |
+
initial_snippet=(
|
| 556 |
+
"# config.json (model repo)\n"
|
| 557 |
+
'{\n'
|
| 558 |
+
' "num_local_experts": 256,\n'
|
| 559 |
+
' "num_experts_per_tok": 8,\n'
|
| 560 |
+
' "intermediate_size": 2048\n'
|
| 561 |
+
'}\n'
|
| 562 |
+
"# But actual checkpoint has 160 experts\n"
|
| 563 |
+
),
|
| 564 |
+
specialist_opinions={
|
| 565 |
+
"runtime": SpecialistOpinion("Runtime is fine. Model loading proceeds until shape error.", 0.75, False),
|
| 566 |
+
"dispatch": SpecialistOpinion("Not a dispatch bug — the model config is wrong.", 0.70, False),
|
| 567 |
+
"kernel": SpecialistOpinion(
|
| 568 |
+
"MoE kernel expects expert count from config. Config says 256 but weights have 160. "
|
| 569 |
+
"Config needs to be updated to match the new checkpoint.", 0.90, True
|
| 570 |
+
),
|
| 571 |
+
"loader": SpecialistOpinion(
|
| 572 |
+
"The model config doesn't match the checkpoint. num_local_experts should be 160.", 0.92, True
|
| 573 |
+
),
|
| 574 |
+
},
|
| 575 |
+
inspect_results=InspectResult(
|
| 576 |
+
logs="[SGLang] config.json: num_local_experts=256\n[SGLang] checkpoint expert layers: 160\n[SGLang] Mismatch detected at layer 0",
|
| 577 |
+
config="num_local_experts: 256 (config)\nactual_experts: 160 (checkpoint)\nnum_experts_per_tok: 8\ncheckpoint_version: v3.1",
|
| 578 |
+
snippet="# New checkpoint v3.1 reduced experts from 256 to 160\n# But config.json wasn't updated\n# Fix: set num_local_experts=160 in config.json",
|
| 579 |
+
metrics="model_load_progress: 15%\nlayers_loaded: 0/60\nerror_at: moe_layer_0",
|
| 580 |
+
),
|
| 581 |
+
specialist_followups={
|
| 582 |
+
"runtime": "No runtime issue. Pure config mismatch.",
|
| 583 |
+
"dispatch": "Dispatch looks fine. The error is before dispatch even runs.",
|
| 584 |
+
"kernel": "The grouped GEMM kernel allocates buffers based on config expert count. Fix the config.",
|
| 585 |
+
"loader": "Config.json says 256 experts but the v3.1 checkpoint only has 160. Update the config.",
|
| 586 |
+
},
|
| 587 |
+
))
|
| 588 |
+
|
| 589 |
+
scenarios.append(Scenario(
|
| 590 |
+
id="model_config_02",
|
| 591 |
+
root_cause="model_config",
|
| 592 |
+
correct_fix="update_model_config",
|
| 593 |
+
incident_ticket=(
|
| 594 |
+
"INCIDENT: Qwen3 MoE model gives wrong results after hardware migration. "
|
| 595 |
+
"Output is coherent but factually wrong. "
|
| 596 |
+
"Same model on old cluster was correct."
|
| 597 |
+
),
|
| 598 |
+
hardware="NVIDIA B200",
|
| 599 |
+
model_name="Qwen3-235B-A22B",
|
| 600 |
+
backend="vLLM 0.8.x",
|
| 601 |
+
initial_log=(
|
| 602 |
+
"[vLLM] Loading Qwen3-235B-A22B...\n"
|
| 603 |
+
"[vLLM] Config: rope_theta=1000000.0\n"
|
| 604 |
+
"[vLLM] WARNING: RoPE scaling config missing for extended context\n"
|
| 605 |
+
"[vLLM] Serving... output quality degraded at positions > 4096"
|
| 606 |
+
),
|
| 607 |
+
initial_snippet=(
|
| 608 |
+
"# config.json\n"
|
| 609 |
+
'{\n'
|
| 610 |
+
' "rope_theta": 1000000.0,\n'
|
| 611 |
+
' "max_position_embeddings": 32768\n'
|
| 612 |
+
' // Missing: rope_scaling config for YaRN\n'
|
| 613 |
+
'}\n'
|
| 614 |
+
),
|
| 615 |
+
specialist_opinions={
|
| 616 |
+
"runtime": SpecialistOpinion("Runtime fine. No crashes.", 0.80, False),
|
| 617 |
+
"dispatch": SpecialistOpinion("Backend selected correctly.", 0.65, False),
|
| 618 |
+
"kernel": SpecialistOpinion(
|
| 619 |
+
"RoPE computation looks standard. Config might be missing the scaling parameters.", 0.78, True
|
| 620 |
+
),
|
| 621 |
+
"loader": SpecialistOpinion(
|
| 622 |
+
"Model config is incomplete — missing rope_scaling section for YaRN. "
|
| 623 |
+
"Old cluster had a patched config.", 0.91, True
|
| 624 |
+
),
|
| 625 |
+
},
|
| 626 |
+
inspect_results=InspectResult(
|
| 627 |
+
logs="[vLLM] RoPE: theta=1e6, no scaling applied\n[vLLM] Quality degrades > 4096 tokens\n[vLLM] Old cluster config had rope_scaling: {type: yarn, factor: 4}",
|
| 628 |
+
config="rope_theta: 1000000.0\nrope_scaling: null\nmax_position_embeddings: 32768\nold_config_had: {rope_scaling: {type: yarn, factor: 4}}",
|
| 629 |
+
snippet="# Missing rope_scaling config:\n# rope_scaling: {type: 'yarn', factor: 4, ...}\n# Without it, positions > 4096 are garbage",
|
| 630 |
+
metrics="quality_0_4k: 95%\nquality_4k_8k: 43%\nquality_8k_plus: 12%",
|
| 631 |
+
),
|
| 632 |
+
specialist_followups={
|
| 633 |
+
"runtime": "No runtime issues.",
|
| 634 |
+
"dispatch": "Backend is correct. Not a dispatch issue.",
|
| 635 |
+
"kernel": "The RoPE kernel is fine — it just doesn't have the scaling config to apply YaRN.",
|
| 636 |
+
"loader": "The config.json from the model repo is missing rope_scaling. Add it back.",
|
| 637 |
+
},
|
| 638 |
+
))
|
| 639 |
+
|
| 640 |
+
# --- weight_layout scenarios ---
|
| 641 |
+
scenarios.append(Scenario(
|
| 642 |
+
id="weight_layout_01",
|
| 643 |
+
root_cause="weight_layout",
|
| 644 |
+
correct_fix="fix_weight_mapping",
|
| 645 |
+
incident_ticket=(
|
| 646 |
+
"INCIDENT: Model produces random output after converting weights from "
|
| 647 |
+
"HuggingFace format to TensorRT-LLM format. Conversion reported success "
|
| 648 |
+
"but inference output is gibberish."
|
| 649 |
+
),
|
| 650 |
+
hardware="NVIDIA H100",
|
| 651 |
+
model_name="Llama-3.3-70B-Instruct",
|
| 652 |
+
backend="TensorRT-LLM 0.18",
|
| 653 |
+
initial_log=(
|
| 654 |
+
"[TRT-LLM] Loading converted weights...\n"
|
| 655 |
+
"[TRT-LLM] Weight shapes match expected layout\n"
|
| 656 |
+
"[TRT-LLM] Running inference...\n"
|
| 657 |
+
"[TRT-LLM] Output: 'asdfjkl; the the the purple 2847...'\n"
|
| 658 |
+
"[TRT-LLM] Perplexity: 2341.7 (expected < 10)"
|
| 659 |
+
),
|
| 660 |
+
initial_snippet=(
|
| 661 |
+
"# convert_weights.py\n"
|
| 662 |
+
"# gate_proj and up_proj were swapped during conversion\n"
|
| 663 |
+
"mapping = {\n"
|
| 664 |
+
" 'gate_proj': 'linear_fc1_gate',\n"
|
| 665 |
+
" 'up_proj': 'linear_fc1_up',\n"
|
| 666 |
+
"}\n"
|
| 667 |
+
"# TRT-LLM expects opposite order\n"
|
| 668 |
+
),
|
| 669 |
+
specialist_opinions={
|
| 670 |
+
"runtime": SpecialistOpinion("Runtime and engine init successful. No errors.", 0.80, False),
|
| 671 |
+
"dispatch": SpecialistOpinion("Backend dispatch is correct. TRT engine built fine.", 0.70, False),
|
| 672 |
+
"kernel": SpecialistOpinion(
|
| 673 |
+
"Kernels execute without error. This is a data issue, not compute.", 0.75, False
|
| 674 |
+
),
|
| 675 |
+
"loader": SpecialistOpinion(
|
| 676 |
+
"Weight mapping is wrong. gate_proj and up_proj are swapped in the conversion script. "
|
| 677 |
+
"TRT-LLM expects the opposite order.", 0.94, True
|
| 678 |
+
),
|
| 679 |
+
},
|
| 680 |
+
inspect_results=InspectResult(
|
| 681 |
+
logs="[TRT-LLM] Weight conversion: gate_proj -> linear_fc1_gate, up_proj -> linear_fc1_up\n[TRT-LLM] Expected: gate_proj -> linear_fc1_up, up_proj -> linear_fc1_gate",
|
| 682 |
+
config="weight_mapping:\n gate_proj: linear_fc1_gate # WRONG\n up_proj: linear_fc1_up # WRONG\n # Should be swapped",
|
| 683 |
+
snippet="# TRT-LLM MLP layout: [up_proj; gate_proj] concatenated\n# But converter wrote [gate_proj; up_proj]\n# Result: SiLU applied to wrong half",
|
| 684 |
+
metrics="output_perplexity: 2341.7\nexpected_perplexity: 8.2\nweight_shapes: correct\nweight_values: misaligned",
|
| 685 |
+
),
|
| 686 |
+
specialist_followups={
|
| 687 |
+
"runtime": "Engine runs fine. Not a runtime issue.",
|
| 688 |
+
"dispatch": "TRT engine dispatch is correct.",
|
| 689 |
+
"kernel": "Compute is correct for the data it gets. Fix the data (weights).",
|
| 690 |
+
"loader": "Classic weight mapping bug. Swap gate_proj and up_proj in the conversion mapping.",
|
| 691 |
+
},
|
| 692 |
+
))
|
| 693 |
+
|
| 694 |
+
scenarios.append(Scenario(
|
| 695 |
+
id="weight_layout_02",
|
| 696 |
+
root_cause="weight_layout",
|
| 697 |
+
correct_fix="fix_weight_mapping",
|
| 698 |
+
incident_ticket=(
|
| 699 |
+
"INCIDENT: QKV attention weights transposed incorrectly for GQA model. "
|
| 700 |
+
"Attention scores are wrong — model generates repetitive text. "
|
| 701 |
+
"Happened after switching from MHA to GQA config."
|
| 702 |
+
),
|
| 703 |
+
hardware="AMD MI300X",
|
| 704 |
+
model_name="Llama-4-Maverick-17Bx128E",
|
| 705 |
+
backend="FlashInfer 0.4",
|
| 706 |
+
initial_log=(
|
| 707 |
+
"[FlashInfer] GQA mode: 64 query heads, 8 KV heads\n"
|
| 708 |
+
"[FlashInfer] WARNING: QKV projection weight shape unexpected\n"
|
| 709 |
+
"[FlashInfer] Expected Q:[8192,8192] K:[8192,1024] V:[8192,1024]\n"
|
| 710 |
+
"[FlashInfer] Got Q:[8192,8192] K:[8192,8192] V:[8192,1024]\n"
|
| 711 |
+
"[FlashInfer] Repetitive output detected"
|
| 712 |
+
),
|
| 713 |
+
initial_snippet=(
|
| 714 |
+
"# weight_converter.py\n"
|
| 715 |
+
"# GQA: Q has num_heads, K/V have num_kv_heads\n"
|
| 716 |
+
"q_proj = weights['q_proj'] # [8192, 8192] correct\n"
|
| 717 |
+
"k_proj = weights['q_proj'] # BUG: should be 'k_proj'\n"
|
| 718 |
+
"v_proj = weights['v_proj'] # [8192, 1024] correct\n"
|
| 719 |
+
),
|
| 720 |
+
specialist_opinions={
|
| 721 |
+
"runtime": SpecialistOpinion("ROCm runtime fine.", 0.75, False),
|
| 722 |
+
"dispatch": SpecialistOpinion("FlashInfer dispatch selected GQA path correctly.", 0.70, False),
|
| 723 |
+
"kernel": SpecialistOpinion(
|
| 724 |
+
"GQA attention kernel is correct but K weights are wrong shape. "
|
| 725 |
+
"Looks like Q weights loaded twice instead of K.", 0.88, True
|
| 726 |
+
),
|
| 727 |
+
"loader": SpecialistOpinion(
|
| 728 |
+
"Weight mapping bug: k_proj loaded from q_proj key. Copy-paste error in converter.", 0.95, True
|
| 729 |
+
),
|
| 730 |
+
},
|
| 731 |
+
inspect_results=InspectResult(
|
| 732 |
+
logs="[FlashInfer] K weight shape [8192,8192] != expected [8192,1024]\n[FlashInfer] K weights appear identical to Q weights\n[FlashInfer] This causes attention to compute Q*Q^T instead of Q*K^T",
|
| 733 |
+
config="num_query_heads: 64\nnum_kv_heads: 8\nhead_dim: 128\nq_shape: [8192,8192]\nk_shape: [8192,8192] # WRONG\nv_shape: [8192,1024]",
|
| 734 |
+
snippet="# Bug in weight_converter.py line 47:\n# k_proj = weights['q_proj'] # should be weights['k_proj']\n# Result: K = Q, so attention = softmax(Q @ Q^T) -> repetitive",
|
| 735 |
+
metrics="attention_entropy: 0.03 (expected > 2.0)\nrepetition_rate: 94%\nperplexity: 567.8",
|
| 736 |
+
),
|
| 737 |
+
specialist_followups={
|
| 738 |
+
"runtime": "No runtime problems.",
|
| 739 |
+
"dispatch": "GQA dispatch path is correct for this model.",
|
| 740 |
+
"kernel": "Attention kernel computes correctly for the data given. K weights are just wrong.",
|
| 741 |
+
"loader": "Line 47 has `weights['q_proj']` instead of `weights['k_proj']`. Classic copy-paste bug.",
|
| 742 |
+
},
|
| 743 |
+
))
|
| 744 |
+
|
| 745 |
+
# --- arch_guard additional scenarios ---
|
| 746 |
+
scenarios.append(Scenario(
|
| 747 |
+
id="arch_guard_03",
|
| 748 |
+
root_cause="arch_guard",
|
| 749 |
+
correct_fix="relax_arch_check",
|
| 750 |
+
incident_ticket=(
|
| 751 |
+
"INCIDENT: TensorRT-LLM refuses to build engine for B200 GPU. "
|
| 752 |
+
"Error: 'Unsupported compute capability 120'. "
|
| 753 |
+
"Same model builds fine targeting H100."
|
| 754 |
+
),
|
| 755 |
+
hardware="NVIDIA B200",
|
| 756 |
+
model_name="Qwen3-235B-A22B",
|
| 757 |
+
backend="TensorRT-LLM 0.18",
|
| 758 |
+
initial_log=(
|
| 759 |
+
"[TRT-LLM] Building engine for gpu_arch=sm_120...\n"
|
| 760 |
+
"[TRT-LLM] ERROR: Compute capability 120 not in supported set\n"
|
| 761 |
+
"[TRT-LLM] Supported: {70, 75, 80, 86, 89, 90}"
|
| 762 |
+
),
|
| 763 |
+
initial_snippet=(
|
| 764 |
+
"# tensorrt_llm/builder.py\n"
|
| 765 |
+
"SUPPORTED_SM = {70, 75, 80, 86, 89, 90}\n"
|
| 766 |
+
"if sm not in SUPPORTED_SM:\n"
|
| 767 |
+
" raise UnsupportedGPU(f'sm_{sm}')"
|
| 768 |
+
),
|
| 769 |
+
specialist_opinions={
|
| 770 |
+
"runtime": SpecialistOpinion("CUDA 13 runtime loaded fine.", 0.78, False),
|
| 771 |
+
"dispatch": SpecialistOpinion(
|
| 772 |
+
"Architecture guard rejects sm_120. B200 uses Blackwell arch not in the allowlist.", 0.91, True
|
| 773 |
+
),
|
| 774 |
+
"kernel": SpecialistOpinion(
|
| 775 |
+
"Try switching to a different quantization scheme for B200.", 0.45, False
|
| 776 |
+
),
|
| 777 |
+
"loader": SpecialistOpinion("No weight loading attempted yet — blocked at engine build.", 0.72, False),
|
| 778 |
+
},
|
| 779 |
+
inspect_results=InspectResult(
|
| 780 |
+
logs="[TRT-LLM] sm_120 not in {70,75,80,86,89,90}\n[TRT-LLM] Engine build aborted before weight conversion",
|
| 781 |
+
config="target_gpu: sm_120\nsupported_sm: [70,75,80,86,89,90]\nbuilder_version: 0.18.0",
|
| 782 |
+
snippet="# B200 (sm_120) supports FP8 MMA, BF16 HMMA\n# Same instruction set as H100 for inference\n# Just not in the allowlist",
|
| 783 |
+
metrics="engine_build_attempts: 1\nengine_build_failures: 1\nmodel_loaded: false",
|
| 784 |
+
),
|
| 785 |
+
specialist_followups={
|
| 786 |
+
"runtime": "Runtime is fine. Engine builder is the blocker.",
|
| 787 |
+
"dispatch": "Add sm_120 (and sm_12x family) to SUPPORTED_SM. The instructions are compatible.",
|
| 788 |
+
"kernel": "On reflection, quantization scheme isn't the issue. It's the arch check.",
|
| 789 |
+
"loader": "Can't load weights until engine builds.",
|
| 790 |
+
},
|
| 791 |
+
))
|
| 792 |
+
|
| 793 |
+
scenarios.append(Scenario(
|
| 794 |
+
id="arch_guard_04",
|
| 795 |
+
root_cause="arch_guard",
|
| 796 |
+
correct_fix="relax_arch_check",
|
| 797 |
+
incident_ticket=(
|
| 798 |
+
"INCIDENT: Flash-Attention fwd pass returns CUDA error on MI355X. "
|
| 799 |
+
"Error: 'Unsupported AMD GPU architecture'. "
|
| 800 |
+
"MI300X works fine with same code."
|
| 801 |
+
),
|
| 802 |
+
hardware="AMD MI355X",
|
| 803 |
+
model_name="Llama-3.3-70B-Instruct",
|
| 804 |
+
backend="vLLM 0.8.x",
|
| 805 |
+
initial_log=(
|
| 806 |
+
"[Flash-Attn] Checking GPU: AMD Instinct MI355X (gfx950)\n"
|
| 807 |
+
"[Flash-Attn] Supported AMD archs: [gfx90a, gfx942]\n"
|
| 808 |
+
"[Flash-Attn] ERROR: gfx950 not supported"
|
| 809 |
+
),
|
| 810 |
+
initial_snippet=(
|
| 811 |
+
"# flash_attn/amd_check.py\n"
|
| 812 |
+
"AMD_SUPPORTED = ['gfx90a', 'gfx942']\n"
|
| 813 |
+
"if gpu_arch not in AMD_SUPPORTED:\n"
|
| 814 |
+
" raise RuntimeError(f'{gpu_arch} not supported')"
|
| 815 |
+
),
|
| 816 |
+
specialist_opinions={
|
| 817 |
+
"runtime": SpecialistOpinion("ROCm 6.4 runtime operational.", 0.80, False),
|
| 818 |
+
"dispatch": SpecialistOpinion(
|
| 819 |
+
"gfx950 (MI355X/CDNA4) isn't in the AMD arch allowlist. Needs to be added.", 0.92, True
|
| 820 |
+
),
|
| 821 |
+
"kernel": SpecialistOpinion(
|
| 822 |
+
"MI355X has different MFMA tile sizes — kernel might actually be incompatible.", 0.55, False
|
| 823 |
+
),
|
| 824 |
+
"loader": SpecialistOpinion("Can't assess — kernel never launched.", 0.60, False),
|
| 825 |
+
},
|
| 826 |
+
inspect_results=InspectResult(
|
| 827 |
+
logs="[Flash-Attn] gfx950 not in [gfx90a, gfx942]\n[Flash-Attn] MI355X CDNA4 arch check failed",
|
| 828 |
+
config="gpu_arch: gfx950\namd_supported: [gfx90a, gfx942]\nrocm_version: 6.4",
|
| 829 |
+
snippet="# MI355X (gfx950/CDNA4) extends gfx942 instruction set\n# MFMA f32_32x32x16_fp8 available\n# Just missing from allowlist",
|
| 830 |
+
metrics="kernel_launch_failures: 1\ngpu_utilization: 0%",
|
| 831 |
+
),
|
| 832 |
+
specialist_followups={
|
| 833 |
+
"runtime": "ROCm works. Not a runtime issue.",
|
| 834 |
+
"dispatch": "Add gfx950 to AMD_SUPPORTED. CDNA4 is backwards-compatible with gfx942 kernels.",
|
| 835 |
+
"kernel": "I was wrong — gfx950 does support the needed MFMA instructions. It's just the allowlist.",
|
| 836 |
+
"loader": "No weight issues.",
|
| 837 |
+
},
|
| 838 |
+
))
|
| 839 |
+
|
| 840 |
+
scenarios.append(Scenario(
|
| 841 |
+
id="arch_guard_05",
|
| 842 |
+
root_cause="arch_guard",
|
| 843 |
+
correct_fix="relax_arch_check",
|
| 844 |
+
incident_ticket=(
|
| 845 |
+
"INCIDENT: Triton kernel compilation fails on RTX 5090 for custom MoE layer. "
|
| 846 |
+
"Error: 'target sm_120 not recognized'. Compiled fine for sm_90."
|
| 847 |
+
),
|
| 848 |
+
hardware="NVIDIA SM120 (GeForce RTX 5090)",
|
| 849 |
+
model_name="DeepSeek-V3-671B",
|
| 850 |
+
backend="SGLang 0.5.x",
|
| 851 |
+
initial_log=(
|
| 852 |
+
"[Triton] Compiling MoE routing kernel for sm_120...\n"
|
| 853 |
+
"[Triton] ERROR: Unknown target 'sm_120'\n"
|
| 854 |
+
"[Triton] Known targets: sm_70, sm_75, sm_80, sm_86, sm_89, sm_90"
|
| 855 |
+
),
|
| 856 |
+
initial_snippet=(
|
| 857 |
+
"# triton/compiler/target.py\n"
|
| 858 |
+
"KNOWN_TARGETS = ['sm_70','sm_75','sm_80','sm_86','sm_89','sm_90']\n"
|
| 859 |
+
),
|
| 860 |
+
specialist_opinions={
|
| 861 |
+
"runtime": SpecialistOpinion("CUDA and Triton installed correctly.", 0.78, False),
|
| 862 |
+
"dispatch": SpecialistOpinion(
|
| 863 |
+
"Triton's target list doesn't include sm_120. Need to add Blackwell family.", 0.90, True
|
| 864 |
+
),
|
| 865 |
+
"kernel": SpecialistOpinion(
|
| 866 |
+
"The MoE kernel uses standard tl.dot which works on any SM >= 70.", 0.82, True
|
| 867 |
+
),
|
| 868 |
+
"loader": SpecialistOpinion(
|
| 869 |
+
"Weights load fine. Error is at JIT compilation stage.", 0.70, False
|
| 870 |
+
),
|
| 871 |
+
},
|
| 872 |
+
inspect_results=InspectResult(
|
| 873 |
+
logs="[Triton] JIT target 'sm_120' not recognized\n[Triton] Compilation aborted before PTX generation",
|
| 874 |
+
config="triton_target: sm_120\nknown_targets: [sm_70..sm_90]\ntriton_version: 3.2",
|
| 875 |
+
snippet="# Triton target registry doesn't know sm_120\n# sm_120 can use sm_90 codegen path\n# Add sm_120 to target list or use family mapping",
|
| 876 |
+
metrics="jit_compile_failures: 1\nkernel_cache_hits: 0",
|
| 877 |
+
),
|
| 878 |
+
specialist_followups={
|
| 879 |
+
"runtime": "No runtime issue. Triton JIT compiler is the blocker.",
|
| 880 |
+
"dispatch": "Triton target registry needs sm_120. Can map to sm_90 codegen path since instruction set overlaps.",
|
| 881 |
+
"kernel": "The kernel code is fine — it's the compiler target check, not the kernel logic.",
|
| 882 |
+
"loader": "No weight involvement at this stage.",
|
| 883 |
+
},
|
| 884 |
+
))
|
| 885 |
+
|
| 886 |
+
# --- backend_whitelist additional scenarios ---
|
| 887 |
+
scenarios.append(Scenario(
|
| 888 |
+
id="backend_whitelist_03",
|
| 889 |
+
root_cause="backend_whitelist",
|
| 890 |
+
correct_fix="add_whitelist_entry",
|
| 891 |
+
incident_ticket=(
|
| 892 |
+
"INCIDENT: GPTQ quantization fails on B200 with 'GPU not whitelisted for Marlin'. "
|
| 893 |
+
"Same quantized model serves fine on H100. B200 has FP16 working."
|
| 894 |
+
),
|
| 895 |
+
hardware="NVIDIA B200",
|
| 896 |
+
model_name="Mistral-Large-2",
|
| 897 |
+
backend="vLLM 0.8.x",
|
| 898 |
+
initial_log=(
|
| 899 |
+
"[vLLM] Loading GPTQ model on B200...\n"
|
| 900 |
+
"[vLLM] Marlin check: GPU 'NVIDIA B200' not whitelisted\n"
|
| 901 |
+
"[vLLM] Available kernels for non-whitelisted: none\n"
|
| 902 |
+
"[vLLM] ERROR: Cannot serve quantized model"
|
| 903 |
+
),
|
| 904 |
+
initial_snippet=(
|
| 905 |
+
"# vllm/quantization/marlin.py\n"
|
| 906 |
+
"WHITELIST = {'A100','H100','A10G','L40S','RTX 4090'}\n"
|
| 907 |
+
"if gpu_name not in WHITELIST:\n"
|
| 908 |
+
" raise RuntimeError('GPU not whitelisted')\n"
|
| 909 |
+
),
|
| 910 |
+
specialist_opinions={
|
| 911 |
+
"runtime": SpecialistOpinion("CUDA runtime healthy on B200.", 0.80, False),
|
| 912 |
+
"dispatch": SpecialistOpinion(
|
| 913 |
+
"Whitelist check is string-based. 'B200' not in the set. Add it.", 0.93, True
|
| 914 |
+
),
|
| 915 |
+
"kernel": SpecialistOpinion(
|
| 916 |
+
"B200 FP8 is different from H100. Might need a different quantization kernel.", 0.50, False
|
| 917 |
+
),
|
| 918 |
+
"loader": SpecialistOpinion("Quantized weights loaded correctly.", 0.75, False),
|
| 919 |
+
},
|
| 920 |
+
inspect_results=InspectResult(
|
| 921 |
+
logs="[Marlin] GPU 'NVIDIA B200' not in whitelist\n[Marlin] Whitelist: {A100,H100,A10G,L40S,RTX 4090}",
|
| 922 |
+
config="gpu_name: NVIDIA B200\nmarlin_whitelist: [A100,H100,A10G,L40S,RTX 4090]\nquant_method: gptq",
|
| 923 |
+
snippet="# B200 supports all Marlin GEMM ops (INT4 deq + FP16 MMA)\n# Name-based whitelist just doesn't include it\n# Fix: add 'B200' or switch to arch-based check",
|
| 924 |
+
metrics="quant_init_failures: 1\nfp16_serving: available\nquant_serving: blocked",
|
| 925 |
+
),
|
| 926 |
+
specialist_followups={
|
| 927 |
+
"runtime": "Runtime fine.",
|
| 928 |
+
"dispatch": "Simple whitelist gap. Add 'B200' to WHITELIST set.",
|
| 929 |
+
"kernel": "I was wrong — B200 Marlin kernels use same INT4 deq + MMA path as H100. Whitelist issue only.",
|
| 930 |
+
"loader": "Weights are fine.",
|
| 931 |
+
},
|
| 932 |
+
))
|
| 933 |
+
|
| 934 |
+
scenarios.append(Scenario(
|
| 935 |
+
id="backend_whitelist_04",
|
| 936 |
+
root_cause="backend_whitelist",
|
| 937 |
+
correct_fix="add_whitelist_entry",
|
| 938 |
+
incident_ticket=(
|
| 939 |
+
"INCIDENT: FlashInfer FP8 GEMM blocked on DGX Spark. "
|
| 940 |
+
"Error: 'FP8 dispatch not available for this GPU'. "
|
| 941 |
+
"SM121 should support FP8 natively."
|
| 942 |
+
),
|
| 943 |
+
hardware="NVIDIA SM121 (DGX Spark)",
|
| 944 |
+
model_name="DeepSeek-R1-Distill-70B",
|
| 945 |
+
backend="FlashInfer 0.4",
|
| 946 |
+
initial_log=(
|
| 947 |
+
"[FlashInfer] FP8 GEMM dispatch...\n"
|
| 948 |
+
"[FlashInfer] GPU family check: sm_121\n"
|
| 949 |
+
"[FlashInfer] FP8 whitelist: [sm_89, sm_90]\n"
|
| 950 |
+
"[FlashInfer] ERROR: FP8 not available for sm_121"
|
| 951 |
+
),
|
| 952 |
+
initial_snippet=(
|
| 953 |
+
"# flashinfer/gemm/fp8_dispatch.py\n"
|
| 954 |
+
"FP8_ENABLED_SM = {89, 90} # Ada, Hopper\n"
|
| 955 |
+
"# Missing SM12x which has FP8 MMA\n"
|
| 956 |
+
),
|
| 957 |
+
specialist_opinions={
|
| 958 |
+
"runtime": SpecialistOpinion("CUDA 13 runtime fine.", 0.78, False),
|
| 959 |
+
"dispatch": SpecialistOpinion(
|
| 960 |
+
"FP8 dispatch whitelist only has Ada/Hopper. SM121 supports FP8 MMA natively but isn't listed.", 0.94, True
|
| 961 |
+
),
|
| 962 |
+
"kernel": SpecialistOpinion(
|
| 963 |
+
"SM121 FP8 might use different MMA instruction encoding.", 0.48, False
|
| 964 |
+
),
|
| 965 |
+
"loader": SpecialistOpinion("FP8 weights loaded. Dispatch is the blocker.", 0.82, True),
|
| 966 |
+
},
|
| 967 |
+
inspect_results=InspectResult(
|
| 968 |
+
logs="[FlashInfer] sm_121 not in FP8_ENABLED_SM {89, 90}\n[FlashInfer] FP8 GEMM dispatch blocked",
|
| 969 |
+
config="gpu_sm: 121\nfp8_whitelist: [89, 90]\nfp8_hw_support: true",
|
| 970 |
+
snippet="# SM121 uses m16n8k32 FP8 MMA (same encoding as SM90)\n# Just not in FP8_ENABLED_SM set\n# Add 120, 121 to enable FP8 dispatch",
|
| 971 |
+
metrics="fp8_dispatch_blocked: true\nfp8_hw_capable: true\nfallback_to_bf16: not_attempted",
|
| 972 |
+
),
|
| 973 |
+
specialist_followups={
|
| 974 |
+
"runtime": "Runtime is fine.",
|
| 975 |
+
"dispatch": "Add SM12x to FP8_ENABLED_SM. SM121 uses identical FP8 MMA to SM90.",
|
| 976 |
+
"kernel": "I checked — SM121 uses the same m16n8k32 encoding as SM90. My concern was unfounded.",
|
| 977 |
+
"loader": "FP8 weights are ready. Just need dispatch to be unblocked.",
|
| 978 |
+
},
|
| 979 |
+
))
|
| 980 |
+
|
| 981 |
+
scenarios.append(Scenario(
|
| 982 |
+
id="backend_whitelist_05",
|
| 983 |
+
root_cause="backend_whitelist",
|
| 984 |
+
correct_fix="add_whitelist_entry",
|
| 985 |
+
incident_ticket=(
|
| 986 |
+
"INCIDENT: SGLang refuses to enable speculative decoding on RTX 5090. "
|
| 987 |
+
"Error: 'Speculative decoding not supported for consumer GPUs'. "
|
| 988 |
+
"Feature works on A100."
|
| 989 |
+
),
|
| 990 |
+
hardware="NVIDIA SM120 (GeForce RTX 5090)",
|
| 991 |
+
model_name="Llama-3.3-70B-Instruct",
|
| 992 |
+
backend="SGLang 0.5.x",
|
| 993 |
+
initial_log=(
|
| 994 |
+
"[SGLang] Speculative decoding requested...\n"
|
| 995 |
+
"[SGLang] GPU: GeForce RTX 5090\n"
|
| 996 |
+
"[SGLang] Spec decode whitelist: [A100, H100, A10G]\n"
|
| 997 |
+
"[SGLang] ERROR: Consumer GPU not in spec-decode whitelist"
|
| 998 |
+
),
|
| 999 |
+
initial_snippet=(
|
| 1000 |
+
"# sglang/server/spec_decode.py\n"
|
| 1001 |
+
"SPEC_DECODE_GPUS = ['A100', 'H100', 'A10G']\n"
|
| 1002 |
+
"# Only data center GPUs whitelisted\n"
|
| 1003 |
+
),
|
| 1004 |
+
specialist_opinions={
|
| 1005 |
+
"runtime": SpecialistOpinion("Runtime fine. GPU has 24GB VRAM.", 0.78, False),
|
| 1006 |
+
"dispatch": SpecialistOpinion(
|
| 1007 |
+
"RTX 5090 not in spec-decode whitelist. Datacenter-only check is too restrictive.", 0.91, True
|
| 1008 |
+
),
|
| 1009 |
+
"kernel": SpecialistOpinion(
|
| 1010 |
+
"RTX 5090 might not have enough VRAM for speculative decoding with 70B.", 0.60, False
|
| 1011 |
+
),
|
| 1012 |
+
"loader": SpecialistOpinion("Model weights fine.", 0.72, False),
|
| 1013 |
+
},
|
| 1014 |
+
inspect_results=InspectResult(
|
| 1015 |
+
logs="[SGLang] GPU 'GeForce RTX 5090' not in SPEC_DECODE_GPUS\n[SGLang] Whitelist is datacenter-only",
|
| 1016 |
+
config="gpu_name: GeForce RTX 5090\nspec_decode_whitelist: [A100,H100,A10G]\nvram: 32GB",
|
| 1017 |
+
snippet="# RTX 5090 has 32GB VRAM, sufficient for spec decode\n# Whitelist artificially restricts to datacenter GPUs\n# Add RTX 5090 or use VRAM-based check",
|
| 1018 |
+
metrics="spec_decode_attempts: 1\nspec_decode_blocked: true\nvram_available: 32GB",
|
| 1019 |
+
),
|
| 1020 |
+
specialist_followups={
|
| 1021 |
+
"runtime": "No runtime issue.",
|
| 1022 |
+
"dispatch": "Add RTX 5090 to whitelist. 32GB VRAM is plenty for spec decode.",
|
| 1023 |
+
"kernel": "32GB is sufficient for speculative decoding with 70B quantized. VRAM isn't the issue.",
|
| 1024 |
+
"loader": "Weights loaded. Dispatch blocker only.",
|
| 1025 |
+
},
|
| 1026 |
+
))
|
| 1027 |
+
|
| 1028 |
+
# --- runtime_loader additional scenarios ---
|
| 1029 |
+
scenarios.append(Scenario(
|
| 1030 |
+
id="runtime_loader_03",
|
| 1031 |
+
root_cause="runtime_loader",
|
| 1032 |
+
correct_fix="fix_runtime_path",
|
| 1033 |
+
incident_ticket=(
|
| 1034 |
+
"INCIDENT: vLLM fails with 'libcublas.so.13 not found' on freshly provisioned node. "
|
| 1035 |
+
"nvidia-smi shows GPU. CUDA toolkit installed. Other CUDA apps work."
|
| 1036 |
+
),
|
| 1037 |
+
hardware="NVIDIA H100",
|
| 1038 |
+
model_name="Llama-4-Maverick-17Bx128E",
|
| 1039 |
+
backend="vLLM 0.8.x",
|
| 1040 |
+
initial_log=(
|
| 1041 |
+
"[vLLM] Initializing CUDA...\n"
|
| 1042 |
+
"[vLLM] ERROR: libcublas.so.13: cannot open shared object file\n"
|
| 1043 |
+
"[vLLM] LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu\n"
|
| 1044 |
+
"[vLLM] Note: /usr/local/cuda-13/lib64 not in path"
|
| 1045 |
+
),
|
| 1046 |
+
initial_snippet=(
|
| 1047 |
+
"# /etc/environment\n"
|
| 1048 |
+
"LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu\n"
|
| 1049 |
+
"# Missing: /usr/local/cuda-13/lib64\n"
|
| 1050 |
+
),
|
| 1051 |
+
specialist_opinions={
|
| 1052 |
+
"runtime": SpecialistOpinion(
|
| 1053 |
+
"CUDA 13 is installed but its lib64 directory isn't in LD_LIBRARY_PATH. Path fix needed.", 0.95, True
|
| 1054 |
+
),
|
| 1055 |
+
"dispatch": SpecialistOpinion("Server crashes before any dispatch.", 0.65, False),
|
| 1056 |
+
"kernel": SpecialistOpinion("Not a kernel issue — can't load CUDA libraries.", 0.70, False),
|
| 1057 |
+
"loader": SpecialistOpinion(
|
| 1058 |
+
"Dynamic linker can't find libcublas.so.13. Add CUDA 13 lib path.", 0.90, True
|
| 1059 |
+
),
|
| 1060 |
+
},
|
| 1061 |
+
inspect_results=InspectResult(
|
| 1062 |
+
logs="[ldconfig] libcublas.so.13 not in cache\n[System] /usr/local/cuda-13/lib64/libcublas.so.13 EXISTS but not in path",
|
| 1063 |
+
config="LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu\ncuda_13_libs=/usr/local/cuda-13/lib64\nldconfig_cache: stale",
|
| 1064 |
+
snippet="# libcublas.so.13 exists at /usr/local/cuda-13/lib64/\n# But LD_LIBRARY_PATH doesn't include it\n# Fix: add /usr/local/cuda-13/lib64 to LD_LIBRARY_PATH",
|
| 1065 |
+
metrics="import_failures: 1\ncuda_available: false (library missing)",
|
| 1066 |
+
),
|
| 1067 |
+
specialist_followups={
|
| 1068 |
+
"runtime": "Classic provisioning issue. CUDA installed but path not configured. Add to LD_LIBRARY_PATH.",
|
| 1069 |
+
"dispatch": "Nothing to dispatch — server won't start.",
|
| 1070 |
+
"kernel": "No kernel involvement.",
|
| 1071 |
+
"loader": "Add /usr/local/cuda-13/lib64 to LD_LIBRARY_PATH or run ldconfig.",
|
| 1072 |
+
},
|
| 1073 |
+
))
|
| 1074 |
+
|
| 1075 |
+
scenarios.append(Scenario(
|
| 1076 |
+
id="runtime_loader_04",
|
| 1077 |
+
root_cause="runtime_loader",
|
| 1078 |
+
correct_fix="fix_runtime_path",
|
| 1079 |
+
incident_ticket=(
|
| 1080 |
+
"INCIDENT: FlashInfer JIT compilation fails with 'nvcc not found'. "
|
| 1081 |
+
"GPU inference should work but JIT kernels can't compile. "
|
| 1082 |
+
"nvidia-smi works fine."
|
| 1083 |
+
),
|
| 1084 |
+
hardware="NVIDIA SM121 (DGX Spark)",
|
| 1085 |
+
model_name="Qwen3-235B-A22B",
|
| 1086 |
+
backend="FlashInfer 0.4",
|
| 1087 |
+
initial_log=(
|
| 1088 |
+
"[FlashInfer] JIT compiling attention kernel for sm_121...\n"
|
| 1089 |
+
"[FlashInfer] Searching for nvcc...\n"
|
| 1090 |
+
"[FlashInfer] ERROR: nvcc not found in PATH\n"
|
| 1091 |
+
"[FlashInfer] CUDA_HOME not set"
|
| 1092 |
+
),
|
| 1093 |
+
initial_snippet=(
|
| 1094 |
+
"# Container environment\n"
|
| 1095 |
+
"PATH=/usr/local/bin:/usr/bin:/bin\n"
|
| 1096 |
+
"# Missing: /usr/local/cuda-13/bin (where nvcc lives)\n"
|
| 1097 |
+
"CUDA_HOME= # not set\n"
|
| 1098 |
+
),
|
| 1099 |
+
specialist_opinions={
|
| 1100 |
+
"runtime": SpecialistOpinion(
|
| 1101 |
+
"CUDA toolkit is installed but nvcc isn't in PATH and CUDA_HOME isn't set.", 0.93, True
|
| 1102 |
+
),
|
| 1103 |
+
"dispatch": SpecialistOpinion("Dispatch can't run without JIT-compiled kernels.", 0.60, False),
|
| 1104 |
+
"kernel": SpecialistOpinion(
|
| 1105 |
+
"SM121 needs JIT compilation for attention kernels. Without nvcc, it can't compile.", 0.80, True
|
| 1106 |
+
),
|
| 1107 |
+
"loader": SpecialistOpinion("Try using pre-compiled AOT kernels instead.", 0.45, False),
|
| 1108 |
+
},
|
| 1109 |
+
inspect_results=InspectResult(
|
| 1110 |
+
logs="[System] which nvcc -> not found\n[System] ls /usr/local/cuda-13/bin/nvcc -> EXISTS\n[System] CUDA_HOME unset",
|
| 1111 |
+
config="PATH=/usr/local/bin:/usr/bin:/bin\nCUDA_HOME=(unset)\nnvcc_location=/usr/local/cuda-13/bin/nvcc",
|
| 1112 |
+
snippet="# nvcc exists at /usr/local/cuda-13/bin/ but not in PATH\n# Fix: export CUDA_HOME=/usr/local/cuda-13\n# Fix: export PATH=$CUDA_HOME/bin:$PATH",
|
| 1113 |
+
metrics="jit_compile_attempts: 3\njit_compile_failures: 3\naot_kernels_available: false",
|
| 1114 |
+
),
|
| 1115 |
+
specialist_followups={
|
| 1116 |
+
"runtime": "Set CUDA_HOME=/usr/local/cuda-13 and add its bin/ to PATH.",
|
| 1117 |
+
"dispatch": "Once nvcc is found, JIT compilation will work and dispatch proceeds normally.",
|
| 1118 |
+
"kernel": "The kernel code is ready to compile. Just need the compiler to be findable.",
|
| 1119 |
+
"loader": "AOT kernels aren't available for SM121 yet. JIT path is needed.",
|
| 1120 |
+
},
|
| 1121 |
+
))
|
| 1122 |
+
|
| 1123 |
+
scenarios.append(Scenario(
|
| 1124 |
+
id="runtime_loader_05",
|
| 1125 |
+
root_cause="runtime_loader",
|
| 1126 |
+
correct_fix="fix_runtime_path",
|
| 1127 |
+
incident_ticket=(
|
| 1128 |
+
"INCIDENT: Python can't import torch on MI300X node. "
|
| 1129 |
+
"Error: 'libtorch_hip.so: cannot open shared object'. "
|
| 1130 |
+
"PyTorch ROCm wheel installed but missing HIP libs."
|
| 1131 |
+
),
|
| 1132 |
+
hardware="AMD MI300X",
|
| 1133 |
+
model_name="Mistral-Large-2",
|
| 1134 |
+
backend="vLLM 0.8.x",
|
| 1135 |
+
initial_log=(
|
| 1136 |
+
"[Python] import torch\n"
|
| 1137 |
+
"[Python] ERROR: libtorch_hip.so: cannot open shared object file\n"
|
| 1138 |
+
"[System] ROCm installed at /opt/rocm-6.3\n"
|
| 1139 |
+
"[System] LD_LIBRARY_PATH does not include /opt/rocm-6.3/lib"
|
| 1140 |
+
),
|
| 1141 |
+
initial_snippet=(
|
| 1142 |
+
"# Container env\n"
|
| 1143 |
+
"LD_LIBRARY_PATH=/usr/local/lib\n"
|
| 1144 |
+
"# Needs: /opt/rocm-6.3/lib:/opt/rocm-6.3/hip/lib\n"
|
| 1145 |
+
),
|
| 1146 |
+
specialist_opinions={
|
| 1147 |
+
"runtime": SpecialistOpinion(
|
| 1148 |
+
"ROCm 6.3 installed but libs not in LD_LIBRARY_PATH. Classic path issue.", 0.94, True
|
| 1149 |
+
),
|
| 1150 |
+
"dispatch": SpecialistOpinion("Can't assess — Python crashes on import.", 0.50, False),
|
| 1151 |
+
"kernel": SpecialistOpinion("Maybe PyTorch ROCm wheel is for wrong ROCm version.", 0.55, False),
|
| 1152 |
+
"loader": SpecialistOpinion(
|
| 1153 |
+
"Dynamic linker needs /opt/rocm-6.3/lib in LD_LIBRARY_PATH.", 0.90, True
|
| 1154 |
+
),
|
| 1155 |
+
},
|
| 1156 |
+
inspect_results=InspectResult(
|
| 1157 |
+
logs="[System] /opt/rocm-6.3/lib/libtorch_hip.so EXISTS\n[System] ldd: libtorch_hip.so => not found\n[System] LD_LIBRARY_PATH=/usr/local/lib only",
|
| 1158 |
+
config="LD_LIBRARY_PATH=/usr/local/lib\nrocm_path=/opt/rocm-6.3\nrocm_lib=/opt/rocm-6.3/lib",
|
| 1159 |
+
snippet="# ROCm libs at /opt/rocm-6.3/lib/ and /opt/rocm-6.3/hip/lib/\n# Not in LD_LIBRARY_PATH\n# Fix: export LD_LIBRARY_PATH=/opt/rocm-6.3/lib:/opt/rocm-6.3/hip/lib:$LD_LIBRARY_PATH",
|
| 1160 |
+
metrics="import_failures: 1\ntorch_available: false",
|
| 1161 |
+
),
|
| 1162 |
+
specialist_followups={
|
| 1163 |
+
"runtime": "Add ROCm lib paths to LD_LIBRARY_PATH. Standard post-install issue.",
|
| 1164 |
+
"dispatch": "Can't run without PyTorch importing.",
|
| 1165 |
+
"kernel": "The ROCm version matches the wheel. It's just a path issue.",
|
| 1166 |
+
"loader": "Add /opt/rocm-6.3/lib to LD_LIBRARY_PATH.",
|
| 1167 |
+
},
|
| 1168 |
+
))
|
| 1169 |
+
|
| 1170 |
+
# --- backend_selector additional scenarios ---
|
| 1171 |
+
scenarios.append(Scenario(
|
| 1172 |
+
id="backend_selector_03",
|
| 1173 |
+
root_cause="backend_selector",
|
| 1174 |
+
correct_fix="switch_backend",
|
| 1175 |
+
incident_ticket=(
|
| 1176 |
+
"INCIDENT: SGLang MoE expert parallelism selecting wrong GEMM backend. "
|
| 1177 |
+
"Using generic GEMM instead of grouped GEMM for MoE layers. "
|
| 1178 |
+
"Throughput is 5x lower than expected."
|
| 1179 |
+
),
|
| 1180 |
+
hardware="NVIDIA H100",
|
| 1181 |
+
model_name="DeepSeek-V3-671B",
|
| 1182 |
+
backend="SGLang 0.5.x",
|
| 1183 |
+
initial_log=(
|
| 1184 |
+
"[SGLang] MoE layer: 256 experts, top-8 routing\n"
|
| 1185 |
+
"[SGLang] GEMM backend: generic (cublas)\n"
|
| 1186 |
+
"[SGLang] WARNING: Grouped GEMM backend not selected\n"
|
| 1187 |
+
"[SGLang] Throughput: 15 tok/s (expected: 80 tok/s)"
|
| 1188 |
+
),
|
| 1189 |
+
initial_snippet=(
|
| 1190 |
+
"# sglang/moe/dispatch.py\n"
|
| 1191 |
+
"def select_moe_backend(num_experts, gpu):\n"
|
| 1192 |
+
" if num_experts <= 64:\n"
|
| 1193 |
+
" return 'grouped_gemm'\n"
|
| 1194 |
+
" return 'generic' # Wrong fallback for large expert count\n"
|
| 1195 |
+
),
|
| 1196 |
+
specialist_opinions={
|
| 1197 |
+
"runtime": SpecialistOpinion("CUDA runtime fine. No errors.", 0.75, False),
|
| 1198 |
+
"dispatch": SpecialistOpinion(
|
| 1199 |
+
"MoE backend selector falls back to generic GEMM when experts > 64. "
|
| 1200 |
+
"Should use grouped GEMM for any expert count on H100.", 0.95, True
|
| 1201 |
+
),
|
| 1202 |
+
"kernel": SpecialistOpinion(
|
| 1203 |
+
"Generic cuBLAS GEMM launches one kernel per expert. Grouped GEMM batches them. "
|
| 1204 |
+
"Switch to grouped GEMM backend.", 0.88, True
|
| 1205 |
+
),
|
| 1206 |
+
"loader": SpecialistOpinion("Weights loaded. Not a loading issue.", 0.72, False),
|
| 1207 |
+
},
|
| 1208 |
+
inspect_results=InspectResult(
|
| 1209 |
+
logs="[SGLang] 256 experts > 64 threshold -> generic backend\n[SGLang] Each expert: separate cuBLAS call\n[SGLang] Kernel launch overhead: 256 launches/layer",
|
| 1210 |
+
config="num_experts: 256\nmoe_backend: generic\nthreshold: 64\ngpu: H100",
|
| 1211 |
+
snippet="# Backend selector has wrong threshold logic\n# Should use grouped_gemm for ALL expert counts on H100\n# Current: only grouped_gemm when experts <= 64",
|
| 1212 |
+
metrics="throughput_tok_s: 15\nexpected_throughput: 80\nkernel_launches_per_step: 256\ngpu_utilization: 18%",
|
| 1213 |
+
),
|
| 1214 |
+
specialist_followups={
|
| 1215 |
+
"runtime": "No runtime issues.",
|
| 1216 |
+
"dispatch": "Switch to grouped_gemm backend. The 64-expert threshold is a bug.",
|
| 1217 |
+
"kernel": "Grouped GEMM would batch all 256 experts into one kernel launch. 10-15x fewer launches.",
|
| 1218 |
+
"loader": "Not a weight issue.",
|
| 1219 |
+
},
|
| 1220 |
+
))
|
| 1221 |
+
|
| 1222 |
+
scenarios.append(Scenario(
|
| 1223 |
+
id="backend_selector_04",
|
| 1224 |
+
root_cause="backend_selector",
|
| 1225 |
+
correct_fix="switch_backend",
|
| 1226 |
+
incident_ticket=(
|
| 1227 |
+
"INCIDENT: Attention on B200 using FlashAttention v1 path instead of v2. "
|
| 1228 |
+
"Memory usage 3x higher than expected. OOM on large batch sizes. "
|
| 1229 |
+
"Same model fits in memory on H100."
|
| 1230 |
+
),
|
| 1231 |
+
hardware="NVIDIA B200",
|
| 1232 |
+
model_name="Llama-4-Maverick-17Bx128E",
|
| 1233 |
+
backend="vLLM 0.8.x",
|
| 1234 |
+
initial_log=(
|
| 1235 |
+
"[vLLM] Attention backend: flash_attn_v1\n"
|
| 1236 |
+
"[vLLM] WARNING: v2 backend not selected (GPU not in v2 list)\n"
|
| 1237 |
+
"[vLLM] Memory: attention uses O(n^2) instead of O(n)\n"
|
| 1238 |
+
"[vLLM] OOM at batch_size=32 (expected to fit at batch_size=128)"
|
| 1239 |
+
),
|
| 1240 |
+
initial_snippet=(
|
| 1241 |
+
"# vllm/attention/selector.py\n"
|
| 1242 |
+
"def select_flash_version(gpu_sm):\n"
|
| 1243 |
+
" if gpu_sm in {80, 86, 89, 90}:\n"
|
| 1244 |
+
" return 'v2'\n"
|
| 1245 |
+
" return 'v1' # B200 (sm_120) falls here\n"
|
| 1246 |
+
),
|
| 1247 |
+
specialist_opinions={
|
| 1248 |
+
"runtime": SpecialistOpinion("CUDA runtime OK. Memory allocation works.", 0.75, False),
|
| 1249 |
+
"dispatch": SpecialistOpinion(
|
| 1250 |
+
"Backend selector picks FA v1 for sm_120. B200 supports v2 — selector needs updating.", 0.93, True
|
| 1251 |
+
),
|
| 1252 |
+
"kernel": SpecialistOpinion(
|
| 1253 |
+
"FA v1 uses O(n^2) memory. v2 uses O(n). That explains the OOM.", 0.85, True
|
| 1254 |
+
),
|
| 1255 |
+
"loader": SpecialistOpinion(
|
| 1256 |
+
"Maybe model weights are larger than expected for this architecture.", 0.45, False
|
| 1257 |
+
),
|
| 1258 |
+
},
|
| 1259 |
+
inspect_results=InspectResult(
|
| 1260 |
+
logs="[vLLM] sm_120 not in {80,86,89,90} -> flash_attn_v1\n[vLLM] FA v1 attention memory: O(seq_len^2)\n[vLLM] OOM threshold hit at 32 batch",
|
| 1261 |
+
config="gpu_sm: 120\nflash_attn_version: v1\nv2_supported_sm: [80,86,89,90]\nmemory_profile: quadratic",
|
| 1262 |
+
snippet="# B200 (sm_120) supports FlashAttention v2\n# Selector only checks old SM list\n# Fix: add sm_120 to v2 supported set or switch to v2 backend",
|
| 1263 |
+
metrics="attention_memory_gb: 24.5\nexpected_attention_memory_gb: 2.1\nbatch_size_limit: 32\nexpected_batch_limit: 128",
|
| 1264 |
+
),
|
| 1265 |
+
specialist_followups={
|
| 1266 |
+
"runtime": "Memory system works. Problem is FA v1's quadratic memory.",
|
| 1267 |
+
"dispatch": "Add sm_120 to v2 supported set. B200 has full v2 support.",
|
| 1268 |
+
"kernel": "FA v1 materializes full attention matrix. v2 uses tiling. Fix the selector.",
|
| 1269 |
+
"loader": "Weight size is correct. It's the attention memory that's excessive.",
|
| 1270 |
+
},
|
| 1271 |
+
))
|
| 1272 |
+
|
| 1273 |
+
scenarios.append(Scenario(
|
| 1274 |
+
id="backend_selector_05",
|
| 1275 |
+
root_cause="backend_selector",
|
| 1276 |
+
correct_fix="switch_backend",
|
| 1277 |
+
incident_ticket=(
|
| 1278 |
+
"INCIDENT: MI300X inference using CK (Composable Kernel) attention but should use Triton. "
|
| 1279 |
+
"CK path has a known bug with GQA + variable-length sequences. "
|
| 1280 |
+
"Random crashes during batched inference."
|
| 1281 |
+
),
|
| 1282 |
+
hardware="AMD MI300X",
|
| 1283 |
+
model_name="Qwen3-235B-A22B",
|
| 1284 |
+
backend="vLLM 0.8.x",
|
| 1285 |
+
initial_log=(
|
| 1286 |
+
"[vLLM] AMD GPU detected -> Composable Kernel attention\n"
|
| 1287 |
+
"[vLLM] GQA + varlen: CK backend selected\n"
|
| 1288 |
+
"[vLLM] CRASH: segfault in ck_attention_varlen_gqa\n"
|
| 1289 |
+
"[vLLM] This is a known CK bug. Use Triton backend instead."
|
| 1290 |
+
),
|
| 1291 |
+
initial_snippet=(
|
| 1292 |
+
"# vllm/attention/backends/rocm.py\n"
|
| 1293 |
+
"def get_rocm_backend(config):\n"
|
| 1294 |
+
" return 'composable_kernel' # Always uses CK\n"
|
| 1295 |
+
" # Should check for known CK bugs and use Triton\n"
|
| 1296 |
+
),
|
| 1297 |
+
specialist_opinions={
|
| 1298 |
+
"runtime": SpecialistOpinion("ROCm runtime fine before the segfault.", 0.72, False),
|
| 1299 |
+
"dispatch": SpecialistOpinion(
|
| 1300 |
+
"Backend selector always picks CK on AMD. Should use Triton for GQA+varlen due to known CK bug.", 0.94, True
|
| 1301 |
+
),
|
| 1302 |
+
"kernel": SpecialistOpinion(
|
| 1303 |
+
"Known CK bug with GQA + varlen sequences. Triton attention works correctly.", 0.90, True
|
| 1304 |
+
),
|
| 1305 |
+
"loader": SpecialistOpinion("Might be a weight alignment issue for AMD.", 0.40, False),
|
| 1306 |
+
},
|
| 1307 |
+
inspect_results=InspectResult(
|
| 1308 |
+
logs="[CK] ck_attention_varlen_gqa: SIGSEGV\n[CK] Known issue: GQA + variable-length triggers OOB access\n[Triton] Triton attention works for this config",
|
| 1309 |
+
config="rocm_attention: composable_kernel\ngqa_enabled: true\nvarlen: true\nknown_ck_bugs: [gqa_varlen]",
|
| 1310 |
+
snippet="# CK has a bug in GQA + varlen attention (OOB memory access)\n# Triton backend handles this correctly\n# Fix: route GQA+varlen to Triton on AMD",
|
| 1311 |
+
metrics="crashes: 3/10 requests\nsegfaults: 3\ntriton_fallback: not_configured",
|
| 1312 |
+
),
|
| 1313 |
+
specialist_followups={
|
| 1314 |
+
"runtime": "The segfault is in CK library code, not a runtime issue.",
|
| 1315 |
+
"dispatch": "Switch to Triton attention for GQA+varlen on AMD. CK bug is known and not yet fixed upstream.",
|
| 1316 |
+
"kernel": "CK varlen GQA kernel has off-by-one in tile boundary. Triton implementation doesn't have this bug.",
|
| 1317 |
+
"loader": "Not a weight issue. The crash is in the attention computation.",
|
| 1318 |
+
},
|
| 1319 |
+
))
|
| 1320 |
+
|
| 1321 |
+
# --- model_config additional scenarios ---
|
| 1322 |
+
scenarios.append(Scenario(
|
| 1323 |
+
id="model_config_03",
|
| 1324 |
+
root_cause="model_config",
|
| 1325 |
+
correct_fix="update_model_config",
|
| 1326 |
+
incident_ticket=(
|
| 1327 |
+
"INCIDENT: DeepSeek MLA attention produces wrong KV cache size. "
|
| 1328 |
+
"OOM on sequences that should fit. Config shows standard MHA dimensions "
|
| 1329 |
+
"but model uses MLA with compressed KV."
|
| 1330 |
+
),
|
| 1331 |
+
hardware="NVIDIA SM121 (DGX Spark)",
|
| 1332 |
+
model_name="DeepSeek-V3-671B",
|
| 1333 |
+
backend="FlashInfer 0.4",
|
| 1334 |
+
initial_log=(
|
| 1335 |
+
"[FlashInfer] KV cache: allocating for 64 KV heads x 128 dim = 8192 per token\n"
|
| 1336 |
+
"[FlashInfer] Expected MLA: kv_lora_rank=512, much smaller KV cache\n"
|
| 1337 |
+
"[FlashInfer] OOM: KV cache exceeds 80GB at seq_len=4096"
|
| 1338 |
+
),
|
| 1339 |
+
initial_snippet=(
|
| 1340 |
+
"# config.json\n"
|
| 1341 |
+
'{\n'
|
| 1342 |
+
' "num_key_value_heads": 64,\n'
|
| 1343 |
+
' "head_dim": 128\n'
|
| 1344 |
+
' // Missing: kv_lora_rank, qk_rope_head_dim for MLA\n'
|
| 1345 |
+
'}\n'
|
| 1346 |
+
),
|
| 1347 |
+
specialist_opinions={
|
| 1348 |
+
"runtime": SpecialistOpinion("Memory allocation works. Just allocating too much.", 0.72, False),
|
| 1349 |
+
"dispatch": SpecialistOpinion("FlashInfer correctly reading config. Config is the problem.", 0.68, False),
|
| 1350 |
+
"kernel": SpecialistOpinion(
|
| 1351 |
+
"MLA attention needs kv_lora_rank in config to use compressed KV. "
|
| 1352 |
+
"Without it, falls back to full MHA KV cache sizing.", 0.92, True
|
| 1353 |
+
),
|
| 1354 |
+
"loader": SpecialistOpinion(
|
| 1355 |
+
"Config.json doesn't have MLA parameters. Need kv_lora_rank=512 and qk_rope_head_dim=64.", 0.93, True
|
| 1356 |
+
),
|
| 1357 |
+
},
|
| 1358 |
+
inspect_results=InspectResult(
|
| 1359 |
+
logs="[FlashInfer] No kv_lora_rank in config -> full MHA KV\n[FlashInfer] KV per token: 64*128*2=16384 (should be 512*2=1024 with MLA)\n[FlashInfer] 16x memory overhead",
|
| 1360 |
+
config="num_kv_heads: 64\nhead_dim: 128\nkv_lora_rank: (missing)\nqk_rope_head_dim: (missing)\nattention_type: inferred as MHA",
|
| 1361 |
+
snippet="# DeepSeek MLA config needs:\n# kv_lora_rank: 512\n# qk_rope_head_dim: 64\n# Without these, system allocates full MHA KV cache",
|
| 1362 |
+
metrics="kv_cache_per_token_bytes: 16384\nexpected_bytes: 1024\nmemory_overhead: 16x\noom_at_seq_len: 4096",
|
| 1363 |
+
),
|
| 1364 |
+
specialist_followups={
|
| 1365 |
+
"runtime": "No runtime issue. Memory allocation succeeds until OOM.",
|
| 1366 |
+
"dispatch": "Config drives the dispatch. Fix the config.",
|
| 1367 |
+
"kernel": "MLA kernel exists but won't activate without kv_lora_rank in config.",
|
| 1368 |
+
"loader": "Add kv_lora_rank=512 and qk_rope_head_dim=64 to config.json.",
|
| 1369 |
+
},
|
| 1370 |
+
))
|
| 1371 |
+
|
| 1372 |
+
scenarios.append(Scenario(
|
| 1373 |
+
id="model_config_04",
|
| 1374 |
+
root_cause="model_config",
|
| 1375 |
+
correct_fix="update_model_config",
|
| 1376 |
+
incident_ticket=(
|
| 1377 |
+
"INCIDENT: Llama-4 Maverick MoE model failing with 'Expected 128 experts'. "
|
| 1378 |
+
"Config lists num_local_experts=128 but actual checkpoint uses sparse layout "
|
| 1379 |
+
"with 16 active experts per token from 128 total, stored differently."
|
| 1380 |
+
),
|
| 1381 |
+
hardware="NVIDIA H100",
|
| 1382 |
+
model_name="Llama-4-Maverick-17Bx128E",
|
| 1383 |
+
backend="vLLM 0.8.x",
|
| 1384 |
+
initial_log=(
|
| 1385 |
+
"[vLLM] MoE init: 128 experts, 2 active per token\n"
|
| 1386 |
+
"[vLLM] Loading expert weights...\n"
|
| 1387 |
+
"[vLLM] WARNING: Expert weight tensor shape doesn't match config\n"
|
| 1388 |
+
"[vLLM] Expected: [128, hidden, ffn] Got: [128, ffn//4, hidden]"
|
| 1389 |
+
),
|
| 1390 |
+
initial_snippet=(
|
| 1391 |
+
"# config.json\n"
|
| 1392 |
+
'{\n'
|
| 1393 |
+
' "num_local_experts": 128,\n'
|
| 1394 |
+
' "num_experts_per_tok": 2,\n'
|
| 1395 |
+
' "expert_layout": "dense"\n'
|
| 1396 |
+
' // Should be "interleaved" for Maverick architecture\n'
|
| 1397 |
+
'}\n'
|
| 1398 |
+
),
|
| 1399 |
+
specialist_opinions={
|
| 1400 |
+
"runtime": SpecialistOpinion("Runtime OK.", 0.75, False),
|
| 1401 |
+
"dispatch": SpecialistOpinion("MoE dispatch looks correct for the config.", 0.60, False),
|
| 1402 |
+
"kernel": SpecialistOpinion(
|
| 1403 |
+
"Expert weight tensor shape is transposed vs config expectation. "
|
| 1404 |
+
"Config says dense layout but weights are in interleaved format.", 0.85, True
|
| 1405 |
+
),
|
| 1406 |
+
"loader": SpecialistOpinion(
|
| 1407 |
+
"Config expert_layout should be 'interleaved' not 'dense'. "
|
| 1408 |
+
"Maverick uses interleaved expert storage.", 0.93, True
|
| 1409 |
+
),
|
| 1410 |
+
},
|
| 1411 |
+
inspect_results=InspectResult(
|
| 1412 |
+
logs="[vLLM] Config: expert_layout=dense\n[vLLM] Actual weights: interleaved layout\n[vLLM] Shape mismatch in MoE layer 0",
|
| 1413 |
+
config="expert_layout: dense (wrong)\nactual_layout: interleaved\nnum_experts: 128\nexperts_per_token: 2",
|
| 1414 |
+
snippet="# Maverick checkpoint uses interleaved expert layout:\n# experts stored as [expert_idx, ffn_chunk, hidden]\n# Config says 'dense' which expects [expert_idx, hidden, ffn]\n# Fix: set expert_layout='interleaved'",
|
| 1415 |
+
metrics="model_load_progress: 5%\nshape_mismatches: 128\nerror_at: expert_layer_0",
|
| 1416 |
+
),
|
| 1417 |
+
specialist_followups={
|
| 1418 |
+
"runtime": "Not a runtime issue.",
|
| 1419 |
+
"dispatch": "Dispatch follows config. Fix the config first.",
|
| 1420 |
+
"kernel": "Weight shapes don't match the layout assumption. Config needs updating.",
|
| 1421 |
+
"loader": "Set expert_layout to 'interleaved' in config.json. Maverick stores experts interleaved.",
|
| 1422 |
+
},
|
| 1423 |
+
))
|
| 1424 |
+
|
| 1425 |
+
scenarios.append(Scenario(
|
| 1426 |
+
id="model_config_05",
|
| 1427 |
+
root_cause="model_config",
|
| 1428 |
+
correct_fix="update_model_config",
|
| 1429 |
+
incident_ticket=(
|
| 1430 |
+
"INCIDENT: Sliding window attention not activating for Mistral model. "
|
| 1431 |
+
"Memory usage growing linearly with sequence length. "
|
| 1432 |
+
"Should plateau after window size."
|
| 1433 |
+
),
|
| 1434 |
+
hardware="NVIDIA B200",
|
| 1435 |
+
model_name="Mistral-Large-2",
|
| 1436 |
+
backend="SGLang 0.5.x",
|
| 1437 |
+
initial_log=(
|
| 1438 |
+
"[SGLang] Attention config: full attention (no sliding window)\n"
|
| 1439 |
+
"[SGLang] KV cache growing linearly with seq_len\n"
|
| 1440 |
+
"[SGLang] Memory at 32k tokens: 40GB (expected: 12GB with sliding window)\n"
|
| 1441 |
+
"[SGLang] sliding_window not found in config.json"
|
| 1442 |
+
),
|
| 1443 |
+
initial_snippet=(
|
| 1444 |
+
"# config.json\n"
|
| 1445 |
+
'{\n'
|
| 1446 |
+
' "max_position_embeddings": 32768,\n'
|
| 1447 |
+
' "num_attention_heads": 96\n'
|
| 1448 |
+
' // Missing: "sliding_window": 4096\n'
|
| 1449 |
+
'}\n'
|
| 1450 |
+
),
|
| 1451 |
+
specialist_opinions={
|
| 1452 |
+
"runtime": SpecialistOpinion("Runtime fine. Memory growing as expected for full attention.", 0.78, False),
|
| 1453 |
+
"dispatch": SpecialistOpinion(
|
| 1454 |
+
"Backend correctly doing full attention because config doesn't specify sliding window.", 0.70, True
|
| 1455 |
+
),
|
| 1456 |
+
"kernel": SpecialistOpinion(
|
| 1457 |
+
"Kernel supports sliding window. Config just needs the parameter.", 0.82, True
|
| 1458 |
+
),
|
| 1459 |
+
"loader": SpecialistOpinion(
|
| 1460 |
+
"Config.json missing sliding_window=4096. Mistral models use 4096-token sliding window.", 0.92, True
|
| 1461 |
+
),
|
| 1462 |
+
},
|
| 1463 |
+
inspect_results=InspectResult(
|
| 1464 |
+
logs="[SGLang] No sliding_window in config -> full attention\n[SGLang] KV cache: 32k * 96 heads * 128 dim * 2 = 40GB",
|
| 1465 |
+
config="sliding_window: null\nmax_position_embeddings: 32768\nexpected_sliding_window: 4096",
|
| 1466 |
+
snippet="# Mistral-Large-2 uses 4096-token sliding window\n# Config missing: sliding_window: 4096\n# Without it, full O(n) KV cache used",
|
| 1467 |
+
metrics="kv_cache_32k_gb: 40\nexpected_kv_cache_gb: 12\nmemory_overhead: 3.3x",
|
| 1468 |
+
),
|
| 1469 |
+
specialist_followups={
|
| 1470 |
+
"runtime": "Memory growth is correct for the config given. Fix the config.",
|
| 1471 |
+
"dispatch": "Backend reads config. Add sliding_window=4096.",
|
| 1472 |
+
"kernel": "Sliding window attention kernel exists. Just needs the config parameter to activate.",
|
| 1473 |
+
"loader": "Add sliding_window: 4096 to config.json.",
|
| 1474 |
+
},
|
| 1475 |
+
))
|
| 1476 |
+
|
| 1477 |
+
# --- weight_layout additional scenarios ---
|
| 1478 |
+
scenarios.append(Scenario(
|
| 1479 |
+
id="weight_layout_03",
|
| 1480 |
+
root_cause="weight_layout",
|
| 1481 |
+
correct_fix="fix_weight_mapping",
|
| 1482 |
+
incident_ticket=(
|
| 1483 |
+
"INCIDENT: Model outputs garbage after quantization with GPTQ. "
|
| 1484 |
+
"Original FP16 model is fine. GPTQ quantization reports success "
|
| 1485 |
+
"but group indices are misaligned."
|
| 1486 |
+
),
|
| 1487 |
+
hardware="NVIDIA H100",
|
| 1488 |
+
model_name="Qwen3-235B-A22B",
|
| 1489 |
+
backend="vLLM 0.8.x",
|
| 1490 |
+
initial_log=(
|
| 1491 |
+
"[vLLM] Loading GPTQ-quantized Qwen3...\n"
|
| 1492 |
+
"[vLLM] Quantization: 4-bit, group_size=128\n"
|
| 1493 |
+
"[vLLM] WARNING: g_idx tensor shape mismatch in layer 0\n"
|
| 1494 |
+
"[vLLM] Output: incoherent (perplexity 1247)"
|
| 1495 |
+
),
|
| 1496 |
+
initial_snippet=(
|
| 1497 |
+
"# GPTQ packing\n"
|
| 1498 |
+
"# g_idx maps each weight column to its quantization group\n"
|
| 1499 |
+
"# Expected shape: [in_features]\n"
|
| 1500 |
+
"# Got shape: [in_features // group_size] (wrong!)\n"
|
| 1501 |
+
),
|
| 1502 |
+
specialist_opinions={
|
| 1503 |
+
"runtime": SpecialistOpinion("CUDA fine. Kernels launch.", 0.78, False),
|
| 1504 |
+
"dispatch": SpecialistOpinion("GPTQ backend selected correctly.", 0.65, False),
|
| 1505 |
+
"kernel": SpecialistOpinion(
|
| 1506 |
+
"Dequantization kernel gets wrong group assignments because g_idx is wrong shape.", 0.82, True
|
| 1507 |
+
),
|
| 1508 |
+
"loader": SpecialistOpinion(
|
| 1509 |
+
"GPTQ group index (g_idx) tensor has wrong shape. The quantization script packed it incorrectly. "
|
| 1510 |
+
"Needs regeneration with correct per-column group mapping.", 0.94, True
|
| 1511 |
+
),
|
| 1512 |
+
},
|
| 1513 |
+
inspect_results=InspectResult(
|
| 1514 |
+
logs="[GPTQ] g_idx shape: [128] (wrong) vs expected [16384]\n[GPTQ] Each column needs its own group index\n[GPTQ] Wrong g_idx causes random dequant scale selection",
|
| 1515 |
+
config="group_size: 128\nin_features: 16384\ng_idx_shape: [128]\nexpected_g_idx_shape: [16384]",
|
| 1516 |
+
snippet="# g_idx should be per-column: shape [in_features]\n# But quantizer produced per-group: shape [in_features//group_size]\n# This assigns wrong scales during dequantization",
|
| 1517 |
+
metrics="perplexity: 1247\nexpected_perplexity: 10.2\nlayers_affected: all\ng_idx_misaligned: true",
|
| 1518 |
+
),
|
| 1519 |
+
specialist_followups={
|
| 1520 |
+
"runtime": "No runtime issues.",
|
| 1521 |
+
"dispatch": "Backend selection is fine.",
|
| 1522 |
+
"kernel": "Kernel dequantizes correctly when given right g_idx. Fix the mapping.",
|
| 1523 |
+
"loader": "Regenerate g_idx with per-column mapping (shape [in_features], not [in_features//group_size]).",
|
| 1524 |
+
},
|
| 1525 |
+
))
|
| 1526 |
+
|
| 1527 |
+
scenarios.append(Scenario(
|
| 1528 |
+
id="weight_layout_04",
|
| 1529 |
+
root_cause="weight_layout",
|
| 1530 |
+
correct_fix="fix_weight_mapping",
|
| 1531 |
+
incident_ticket=(
|
| 1532 |
+
"INCIDENT: FP8 model on MI300X gives NaN after first layer. "
|
| 1533 |
+
"Dequantization scales appear transposed. "
|
| 1534 |
+
"Same checkpoint works on NVIDIA with e4m3fn format."
|
| 1535 |
+
),
|
| 1536 |
+
hardware="AMD MI300X",
|
| 1537 |
+
model_name="DeepSeek-R1-Distill-70B",
|
| 1538 |
+
backend="vLLM 0.8.x",
|
| 1539 |
+
initial_log=(
|
| 1540 |
+
"[vLLM] FP8 dequant: loading scales...\n"
|
| 1541 |
+
"[vLLM] Scale tensor shape: [out_features, 1] — expected [1, out_features] for AMD\n"
|
| 1542 |
+
"[vLLM] Layer 0 output: NaN (scale applied to wrong dimension)\n"
|
| 1543 |
+
"[vLLM] All subsequent layers: NaN"
|
| 1544 |
+
),
|
| 1545 |
+
initial_snippet=(
|
| 1546 |
+
"# fp8_weights.py\n"
|
| 1547 |
+
"# NVIDIA: scales are per-output-channel [out, 1]\n"
|
| 1548 |
+
"# AMD: scales are per-input-channel [1, in]\n"
|
| 1549 |
+
"# Converter didn't transpose for AMD\n"
|
| 1550 |
+
),
|
| 1551 |
+
specialist_opinions={
|
| 1552 |
+
"runtime": SpecialistOpinion("ROCm runtime fine.", 0.78, False),
|
| 1553 |
+
"dispatch": SpecialistOpinion("FP8 backend selected. Format mismatch possible.", 0.65, False),
|
| 1554 |
+
"kernel": SpecialistOpinion(
|
| 1555 |
+
"FP8 GEMM applies scale in wrong dimension due to transposed scale tensor.", 0.85, True
|
| 1556 |
+
),
|
| 1557 |
+
"loader": SpecialistOpinion(
|
| 1558 |
+
"FP8 scale tensors need transposing for AMD. NVIDIA uses [out,1], AMD uses [1,in]. "
|
| 1559 |
+
"Weight converter didn't handle this.", 0.95, True
|
| 1560 |
+
),
|
| 1561 |
+
},
|
| 1562 |
+
inspect_results=InspectResult(
|
| 1563 |
+
logs="[FP8] Scale shape [4096,1] but AMD MFMA expects [1,4096]\n[FP8] Dequant: scale broadcast on wrong axis -> NaN\n[FP8] First non-NaN result never produced",
|
| 1564 |
+
config="fp8_scale_shape: [out_features, 1]\namd_expected: [1, in_features]\nscale_transpose_needed: true",
|
| 1565 |
+
snippet="# NVIDIA layout: W_fp8 * scale[out,1] -> per-output-channel\n# AMD layout: W_fp8 * scale[1,in] -> per-input-channel\n# Converter assumed NVIDIA layout\n# Fix: transpose scales for AMD",
|
| 1566 |
+
metrics="nan_outputs: 100%\nlayers_producing_nan: all\nfirst_nan_at: layer_0",
|
| 1567 |
+
),
|
| 1568 |
+
specialist_followups={
|
| 1569 |
+
"runtime": "Not a runtime issue.",
|
| 1570 |
+
"dispatch": "FP8 selected correctly. Scale orientation is the issue.",
|
| 1571 |
+
"kernel": "GEMM kernel applies scale along wrong dimension. Transpose the scales.",
|
| 1572 |
+
"loader": "Transpose FP8 scale tensors from [out,1] to [1,in] for AMD.",
|
| 1573 |
+
},
|
| 1574 |
+
))
|
| 1575 |
+
|
| 1576 |
+
scenarios.append(Scenario(
|
| 1577 |
+
id="weight_layout_05",
|
| 1578 |
+
root_cause="weight_layout",
|
| 1579 |
+
correct_fix="fix_weight_mapping",
|
| 1580 |
+
incident_ticket=(
|
| 1581 |
+
"INCIDENT: Embedding layer produces identical vectors for all tokens. "
|
| 1582 |
+
"After checkpoint conversion, embedding weights appear row-shuffled. "
|
| 1583 |
+
"Tokenizer maps to wrong rows."
|
| 1584 |
+
),
|
| 1585 |
+
hardware="NVIDIA SM121 (DGX Spark)",
|
| 1586 |
+
model_name="Llama-4-Maverick-17Bx128E",
|
| 1587 |
+
backend="SGLang 0.5.x",
|
| 1588 |
+
initial_log=(
|
| 1589 |
+
"[SGLang] Embedding layer: 128256 tokens x 4096 dim\n"
|
| 1590 |
+
"[SGLang] Token 'Hello' -> embedding row 85432 (expected: row 9906)\n"
|
| 1591 |
+
"[SGLang] All outputs identical — embeddings mapped to wrong rows\n"
|
| 1592 |
+
"[SGLang] Suspect: tokenizer vocab offset not applied during conversion"
|
| 1593 |
+
),
|
| 1594 |
+
initial_snippet=(
|
| 1595 |
+
"# convert_checkpoint.py\n"
|
| 1596 |
+
"embed = original_weights['embed_tokens.weight'] # [128256, 4096]\n"
|
| 1597 |
+
"# BUG: added_tokens offset not applied\n"
|
| 1598 |
+
"# Tokenizer expects base_vocab at rows 0-127999\n"
|
| 1599 |
+
"# Converter put added_tokens at rows 0-255\n"
|
| 1600 |
+
),
|
| 1601 |
+
specialist_opinions={
|
| 1602 |
+
"runtime": SpecialistOpinion("Runtime fine. Model loads.", 0.75, False),
|
| 1603 |
+
"dispatch": SpecialistOpinion("Backend dispatch correct.", 0.68, False),
|
| 1604 |
+
"kernel": SpecialistOpinion(
|
| 1605 |
+
"Embedding lookup works mechanically but returns wrong vectors. Data issue.", 0.78, True
|
| 1606 |
+
),
|
| 1607 |
+
"loader": SpecialistOpinion(
|
| 1608 |
+
"Embedding weight rows are misaligned after conversion. Tokenizer indices map to wrong rows. "
|
| 1609 |
+
"Converter needs to preserve original row ordering.", 0.94, True
|
| 1610 |
+
),
|
| 1611 |
+
},
|
| 1612 |
+
inspect_results=InspectResult(
|
| 1613 |
+
logs="[SGLang] Token 'Hello' (id=9906) -> embedding from original row 85432\n[SGLang] Row mapping offset: 75526\n[SGLang] Converter applied wrong row permutation",
|
| 1614 |
+
config="vocab_size: 128256\nembed_dim: 4096\nrow_offset_error: 75526",
|
| 1615 |
+
snippet="# Converter reordered rows: put added_tokens (256) first, then base vocab\n# Tokenizer expects base vocab at row 0\n# Fix: preserve original row order in embedding conversion",
|
| 1616 |
+
metrics="embedding_cosine_sim_to_expected: 0.02\nall_outputs_identical: true\nperplexity: infinity",
|
| 1617 |
+
),
|
| 1618 |
+
specialist_followups={
|
| 1619 |
+
"runtime": "No runtime issue.",
|
| 1620 |
+
"dispatch": "Dispatch is correct.",
|
| 1621 |
+
"kernel": "Embedding lookup returns whatever is at the indexed row. The rows are just wrong.",
|
| 1622 |
+
"loader": "Converter put added_tokens at index 0. Fix: keep original row order.",
|
| 1623 |
+
},
|
| 1624 |
+
))
|
| 1625 |
+
|
| 1626 |
+
# --- Additional eval scenarios (_06 suffix) ---
|
| 1627 |
+
scenarios.append(Scenario(
|
| 1628 |
+
id="arch_guard_06",
|
| 1629 |
+
root_cause="arch_guard",
|
| 1630 |
+
correct_fix="relax_arch_check",
|
| 1631 |
+
incident_ticket=(
|
| 1632 |
+
"INCIDENT: CUTLASS GEMM kernel rejects SM121 with 'unsupported architecture'. "
|
| 1633 |
+
"is_family_of() check fails because SM121 not in family table. "
|
| 1634 |
+
"FP8 inference completely blocked."
|
| 1635 |
+
),
|
| 1636 |
+
hardware="NVIDIA SM121 (DGX Spark)",
|
| 1637 |
+
model_name="Mistral-Large-2",
|
| 1638 |
+
backend="TensorRT-LLM 0.18",
|
| 1639 |
+
initial_log=(
|
| 1640 |
+
"[CUTLASS] is_family_of(sm_121, sm_90) = false\n"
|
| 1641 |
+
"[CUTLASS] SM121 not registered in family hierarchy\n"
|
| 1642 |
+
"[CUTLASS] FP8 GEMM dispatch: BLOCKED"
|
| 1643 |
+
),
|
| 1644 |
+
initial_snippet=(
|
| 1645 |
+
"# cutlass/arch/family.py\n"
|
| 1646 |
+
"FAMILY_MAP = {90: [90], 89: [89], 86: [86], 80: [80]}\n"
|
| 1647 |
+
"# SM121 not in any family\n"
|
| 1648 |
+
),
|
| 1649 |
+
specialist_opinions={
|
| 1650 |
+
"runtime": SpecialistOpinion("CUDA 13 fine.", 0.78, False),
|
| 1651 |
+
"dispatch": SpecialistOpinion(
|
| 1652 |
+
"CUTLASS family map doesn't include SM12x. Need to register SM120/121 family.", 0.93, True
|
| 1653 |
+
),
|
| 1654 |
+
"kernel": SpecialistOpinion(
|
| 1655 |
+
"The kernel weight format might be wrong for SM121.", 0.40, False
|
| 1656 |
+
),
|
| 1657 |
+
"loader": SpecialistOpinion("Engine built. Weights loaded. GEMM dispatch blocked.", 0.70, False),
|
| 1658 |
+
},
|
| 1659 |
+
inspect_results=InspectResult(
|
| 1660 |
+
logs="[CUTLASS] FAMILY_MAP has no entry for 121\n[CUTLASS] is_family_of(121, 90) -> False\n[CUTLASS] FP8 GEMM requires family >= 90",
|
| 1661 |
+
config="gpu_sm: 121\nfamily_map: {90:[90],89:[89],...}\nsm121_family: undefined",
|
| 1662 |
+
snippet="# SM12x is its own family but shares FP8 MMA with SM90\n# Fix: add 120: [120, 121] and 121: [120, 121] to FAMILY_MAP\n# Or: register SM12x as SM90-compatible for GEMM",
|
| 1663 |
+
metrics="fp8_gemm_blocked: true\nbf16_gemm: functional",
|
| 1664 |
+
),
|
| 1665 |
+
specialist_followups={
|
| 1666 |
+
"runtime": "Runtime fine.",
|
| 1667 |
+
"dispatch": "Register SM12x family in CUTLASS. SM121 FP8 MMA is SM90-compatible.",
|
| 1668 |
+
"kernel": "Weight format is fine. It's the arch family check blocking dispatch.",
|
| 1669 |
+
"loader": "Weights loaded correctly. GEMM dispatch is the issue.",
|
| 1670 |
+
},
|
| 1671 |
+
))
|
| 1672 |
+
|
| 1673 |
+
scenarios.append(Scenario(
|
| 1674 |
+
id="backend_selector_06",
|
| 1675 |
+
root_cause="backend_selector",
|
| 1676 |
+
correct_fix="switch_backend",
|
| 1677 |
+
incident_ticket=(
|
| 1678 |
+
"INCIDENT: DGX Spark running PagedAttention v1 instead of v2. "
|
| 1679 |
+
"Prefix caching not working. Cache hit rate near 0%. "
|
| 1680 |
+
"Same prompts re-computed every request."
|
| 1681 |
+
),
|
| 1682 |
+
hardware="NVIDIA SM121 (DGX Spark)",
|
| 1683 |
+
model_name="DeepSeek-V3-671B",
|
| 1684 |
+
backend="vLLM 0.8.x",
|
| 1685 |
+
initial_log=(
|
| 1686 |
+
"[vLLM] PagedAttention version: v1\n"
|
| 1687 |
+
"[vLLM] Prefix caching: disabled (requires PA v2)\n"
|
| 1688 |
+
"[vLLM] Cache hit rate: 0.1% (expected: 60%+ with repeated prefixes)\n"
|
| 1689 |
+
"[vLLM] TTFT p99: 2100ms (expected: 400ms with caching)"
|
| 1690 |
+
),
|
| 1691 |
+
initial_snippet=(
|
| 1692 |
+
"# vllm/core/scheduler.py\n"
|
| 1693 |
+
"def select_paged_attention(gpu_sm):\n"
|
| 1694 |
+
" if gpu_sm >= 80 and gpu_sm <= 90:\n"
|
| 1695 |
+
" return 'v2' # with prefix caching\n"
|
| 1696 |
+
" return 'v1' # SM121 > 90, falls here\n"
|
| 1697 |
+
),
|
| 1698 |
+
specialist_opinions={
|
| 1699 |
+
"runtime": SpecialistOpinion("CUDA runtime fine. Server runs.", 0.75, False),
|
| 1700 |
+
"dispatch": SpecialistOpinion(
|
| 1701 |
+
"PagedAttention version selector has range bug. SM121 > 90 so gets v1 without prefix caching.", 0.94, True
|
| 1702 |
+
),
|
| 1703 |
+
"kernel": SpecialistOpinion(
|
| 1704 |
+
"PA v2 kernel works on SM121. It's the selector that's wrong.", 0.85, True
|
| 1705 |
+
),
|
| 1706 |
+
"loader": SpecialistOpinion("Model loaded fine. Not a weight issue.", 0.72, False),
|
| 1707 |
+
},
|
| 1708 |
+
inspect_results=InspectResult(
|
| 1709 |
+
logs="[vLLM] sm_121 not in range [80,90] -> PA v1\n[vLLM] PA v1 doesn't support prefix caching\n[vLLM] Every prefix re-computed from scratch",
|
| 1710 |
+
config="paged_attention: v1\nprefix_caching: disabled\ngpu_sm: 121\nv2_range: [80, 90]",
|
| 1711 |
+
snippet="# PA v2 supports prefix caching, reducing TTFT 3-5x\n# Selector range [80,90] excludes SM121\n# Fix: include SM12x in v2-eligible set",
|
| 1712 |
+
metrics="cache_hit_rate: 0.1%\nexpected_cache_hit_rate: 62%\nttft_p99_ms: 2100\nexpected_ttft_ms: 400",
|
| 1713 |
+
),
|
| 1714 |
+
specialist_followups={
|
| 1715 |
+
"runtime": "Server runs fine. Performance issue only.",
|
| 1716 |
+
"dispatch": "Fix the range check to include SM12x. PA v2 works on SM121.",
|
| 1717 |
+
"kernel": "PA v2 kernel is compatible. Just need the selector to pick it.",
|
| 1718 |
+
"loader": "Not a loading issue.",
|
| 1719 |
+
},
|
| 1720 |
+
))
|
| 1721 |
+
|
| 1722 |
+
scenarios.append(Scenario(
|
| 1723 |
+
id="runtime_loader_06",
|
| 1724 |
+
root_cause="runtime_loader",
|
| 1725 |
+
correct_fix="fix_runtime_path",
|
| 1726 |
+
incident_ticket=(
|
| 1727 |
+
"INCIDENT: Container on B200 node fails with 'CUDA driver version insufficient'. "
|
| 1728 |
+
"Host has driver 565 but container sees driver 535. "
|
| 1729 |
+
"nvidia-smi inside container shows old driver."
|
| 1730 |
+
),
|
| 1731 |
+
hardware="NVIDIA B200",
|
| 1732 |
+
model_name="Llama-3.3-70B-Instruct",
|
| 1733 |
+
backend="vLLM 0.8.x",
|
| 1734 |
+
initial_log=(
|
| 1735 |
+
"[Container] nvidia-smi: Driver Version: 535.183.01\n"
|
| 1736 |
+
"[Host] nvidia-smi: Driver Version: 565.57.01\n"
|
| 1737 |
+
"[vLLM] CUDA 13 requires driver >= 560\n"
|
| 1738 |
+
"[vLLM] ERROR: CUDA driver version insufficient for CUDA runtime"
|
| 1739 |
+
),
|
| 1740 |
+
initial_snippet=(
|
| 1741 |
+
"# Docker run command\n"
|
| 1742 |
+
"docker run --gpus all \\\n"
|
| 1743 |
+
" -e NVIDIA_DRIVER_CAPABILITIES=compute,utility \\\n"
|
| 1744 |
+
" -e NVIDIA_VISIBLE_DEVICES=all \\\n"
|
| 1745 |
+
" # Missing: --runtime=nvidia or proper CDI config\n"
|
| 1746 |
+
),
|
| 1747 |
+
specialist_opinions={
|
| 1748 |
+
"runtime": SpecialistOpinion(
|
| 1749 |
+
"Container seeing old driver. Docker GPU passthrough not configured correctly. "
|
| 1750 |
+
"Need proper nvidia-container-runtime setup.", 0.94, True
|
| 1751 |
+
),
|
| 1752 |
+
"dispatch": SpecialistOpinion("Server never starts. Can't assess dispatch.", 0.50, False),
|
| 1753 |
+
"kernel": SpecialistOpinion(
|
| 1754 |
+
"Maybe the B200 needs a newer CUDA toolkit version.", 0.45, False
|
| 1755 |
+
),
|
| 1756 |
+
"loader": SpecialistOpinion(
|
| 1757 |
+
"Container's nvidia driver libs are stale. Bind mount is pointing to wrong driver version.", 0.88, True
|
| 1758 |
+
),
|
| 1759 |
+
},
|
| 1760 |
+
inspect_results=InspectResult(
|
| 1761 |
+
logs="[Container] /usr/lib/x86_64-linux-gnu/libnvidia-ml.so -> driver 535\n[Host] /usr/lib/x86_64-linux-gnu/libnvidia-ml.so -> driver 565\n[Docker] nvidia-container-runtime not in daemon.json",
|
| 1762 |
+
config="host_driver: 565.57.01\ncontainer_driver: 535.183.01\nnvidia_runtime: not_configured",
|
| 1763 |
+
snippet="# Docker daemon.json missing nvidia runtime\n# Container bundles old driver libs instead of using host driver\n# Fix: configure nvidia-container-runtime or CDI",
|
| 1764 |
+
metrics="container_start_failures: 1\ndriver_mismatch: true\ncuda_init: failed",
|
| 1765 |
+
),
|
| 1766 |
+
specialist_followups={
|
| 1767 |
+
"runtime": "nvidia-container-toolkit needs to be configured to pass host driver into container.",
|
| 1768 |
+
"dispatch": "Can't run without CUDA init.",
|
| 1769 |
+
"kernel": "The toolkit version is fine. It's the driver passthrough that's broken.",
|
| 1770 |
+
"loader": "Container needs host's driver libs mounted. Fix Docker runtime config.",
|
| 1771 |
+
},
|
| 1772 |
+
))
|
| 1773 |
+
|
| 1774 |
+
scenarios.append(Scenario(
|
| 1775 |
+
id="model_config_06",
|
| 1776 |
+
root_cause="model_config",
|
| 1777 |
+
correct_fix="update_model_config",
|
| 1778 |
+
incident_ticket=(
|
| 1779 |
+
"INCIDENT: BF16 model serving on MI300X has 2x expected memory usage. "
|
| 1780 |
+
"Config says float16 dtype but model should use bfloat16. "
|
| 1781 |
+
"Unnecessary fp16->bf16 conversion happening at runtime."
|
| 1782 |
+
),
|
| 1783 |
+
hardware="AMD MI300X",
|
| 1784 |
+
model_name="DeepSeek-R1-Distill-70B",
|
| 1785 |
+
backend="vLLM 0.8.x",
|
| 1786 |
+
initial_log=(
|
| 1787 |
+
"[vLLM] Config dtype: float16\n"
|
| 1788 |
+
"[vLLM] Actual weights: bfloat16\n"
|
| 1789 |
+
"[vLLM] Runtime conversion float16 config -> bfloat16 weights\n"
|
| 1790 |
+
"[vLLM] Extra memory for conversion buffers: 35GB"
|
| 1791 |
+
),
|
| 1792 |
+
initial_snippet=(
|
| 1793 |
+
"# config.json\n"
|
| 1794 |
+
'{\n'
|
| 1795 |
+
' "torch_dtype": "float16"\n'
|
| 1796 |
+
' // Actual checkpoint is bfloat16\n'
|
| 1797 |
+
' // Mismatch causes runtime conversion overhead\n'
|
| 1798 |
+
'}\n'
|
| 1799 |
+
),
|
| 1800 |
+
specialist_opinions={
|
| 1801 |
+
"runtime": SpecialistOpinion("ROCm runtime healthy. Memory available.", 0.78, False),
|
| 1802 |
+
"dispatch": SpecialistOpinion("Backend dispatch fine.", 0.65, False),
|
| 1803 |
+
"kernel": SpecialistOpinion(
|
| 1804 |
+
"Kernels running with dtype conversion overhead. "
|
| 1805 |
+
"Config says fp16 but weights are bf16, so vLLM converts at load time.", 0.82, True
|
| 1806 |
+
),
|
| 1807 |
+
"loader": SpecialistOpinion(
|
| 1808 |
+
"Config torch_dtype=float16 doesn't match checkpoint dtype=bfloat16. "
|
| 1809 |
+
"Fix config to say bfloat16 to avoid conversion overhead.", 0.93, True
|
| 1810 |
+
),
|
| 1811 |
+
},
|
| 1812 |
+
inspect_results=InspectResult(
|
| 1813 |
+
logs="[vLLM] Config: float16, Checkpoint: bfloat16\n[vLLM] Allocating conversion buffers: 35GB\n[vLLM] Total memory: model(35GB) + conversion(35GB) = 70GB",
|
| 1814 |
+
config="torch_dtype: float16\ncheckpoint_dtype: bfloat16\nmismatch: true",
|
| 1815 |
+
snippet="# Config says float16 but checkpoint is bfloat16\n# vLLM allocates both versions during conversion\n# Fix: set torch_dtype='bfloat16' in config.json",
|
| 1816 |
+
metrics="memory_used_gb: 70\nexpected_memory_gb: 35\nconversion_overhead_gb: 35",
|
| 1817 |
+
),
|
| 1818 |
+
specialist_followups={
|
| 1819 |
+
"runtime": "Memory subsystem fine. Just using too much.",
|
| 1820 |
+
"dispatch": "Dispatch fine after conversion.",
|
| 1821 |
+
"kernel": "Conversion overhead is the issue. Fix config to match checkpoint dtype.",
|
| 1822 |
+
"loader": "Set torch_dtype to bfloat16 in config.json.",
|
| 1823 |
+
},
|
| 1824 |
+
))
|
| 1825 |
+
|
| 1826 |
+
scenarios.append(Scenario(
|
| 1827 |
+
id="weight_layout_06",
|
| 1828 |
+
root_cause="weight_layout",
|
| 1829 |
+
correct_fix="fix_weight_mapping",
|
| 1830 |
+
incident_ticket=(
|
| 1831 |
+
"INCIDENT: Rotary position encoding giving wrong angles after checkpoint merge. "
|
| 1832 |
+
"Two LoRA adapters merged into base model, but RoPE inv_freq tensor "
|
| 1833 |
+
"accidentally overwritten with adapter values. Outputs degrade past position 128."
|
| 1834 |
+
),
|
| 1835 |
+
hardware="NVIDIA H100",
|
| 1836 |
+
model_name="Mistral-Large-2",
|
| 1837 |
+
backend="vLLM 0.8.x",
|
| 1838 |
+
initial_log=(
|
| 1839 |
+
"[vLLM] Loading merged checkpoint...\n"
|
| 1840 |
+
"[vLLM] RoPE inv_freq shape: [64] (correct)\n"
|
| 1841 |
+
"[vLLM] RoPE inv_freq values: [0.001, 0.001, ...] (all same — WRONG)\n"
|
| 1842 |
+
"[vLLM] Expected: geometric sequence 1/10000^(2i/d)"
|
| 1843 |
+
),
|
| 1844 |
+
initial_snippet=(
|
| 1845 |
+
"# merge_lora.py\n"
|
| 1846 |
+
"# BUG: LoRA merge accidentally overwrote inv_freq\n"
|
| 1847 |
+
"merged['inv_freq'] = adapter_state['inv_freq'] # adapter had dummy values\n"
|
| 1848 |
+
"# Should have kept base model's inv_freq\n"
|
| 1849 |
+
),
|
| 1850 |
+
specialist_opinions={
|
| 1851 |
+
"runtime": SpecialistOpinion("Runtime fine.", 0.78, False),
|
| 1852 |
+
"dispatch": SpecialistOpinion("Backend dispatch correct.", 0.65, False),
|
| 1853 |
+
"kernel": SpecialistOpinion(
|
| 1854 |
+
"RoPE kernel computes correct rotations for the freq values given. But freq values are wrong.", 0.80, True
|
| 1855 |
+
),
|
| 1856 |
+
"loader": SpecialistOpinion(
|
| 1857 |
+
"LoRA merge script overwrote inv_freq with adapter's dummy values. "
|
| 1858 |
+
"Need to restore base model's inv_freq or regenerate from formula.", 0.95, True
|
| 1859 |
+
),
|
| 1860 |
+
},
|
| 1861 |
+
inspect_results=InspectResult(
|
| 1862 |
+
logs="[RoPE] inv_freq: all values = 0.001 (constant)\n[RoPE] Expected: geometric decay from 1.0 to 1e-4\n[RoPE] Position encoding essentially constant -> no position info after ~128 tokens",
|
| 1863 |
+
config="inv_freq_values: [0.001]*64\nexpected: geometric_series(1/10000, dim=128)\nrope_theta: 10000",
|
| 1864 |
+
snippet="# inv_freq should be: 1 / (theta ** (torch.arange(0, dim, 2) / dim))\n# Instead: all 0.001 from LoRA adapter dummy init\n# Fix: regenerate inv_freq from formula or restore from base model",
|
| 1865 |
+
metrics="quality_0_128: 90%\nquality_128_1k: 25%\nquality_1k_plus: 5%",
|
| 1866 |
+
),
|
| 1867 |
+
specialist_followups={
|
| 1868 |
+
"runtime": "No runtime issue.",
|
| 1869 |
+
"dispatch": "Dispatch correct.",
|
| 1870 |
+
"kernel": "RoPE kernel works. Just getting wrong frequencies.",
|
| 1871 |
+
"loader": "Restore inv_freq from base model. LoRA merge script has a bug that overwrites non-LoRA tensors.",
|
| 1872 |
+
},
|
| 1873 |
+
))
|
| 1874 |
+
|
| 1875 |
+
return scenarios
|
| 1876 |
+
|
| 1877 |
+
|
| 1878 |
+
# Build the full scenario pool
|
| 1879 |
+
SCENARIOS = _make_scenarios()
|
| 1880 |
+
# _01, _03, _04, _05 = train; _02, _06 = eval
|
| 1881 |
+
TRAIN_SCENARIOS = [s for s in SCENARIOS if s.id.endswith(("_01", "_03", "_04", "_05"))]
|
| 1882 |
+
EVAL_SCENARIOS = [s for s in SCENARIOS if s.id.endswith(("_02", "_06"))]
|
| 1883 |
+
|
| 1884 |
+
|
| 1885 |
+
def get_scenario(scenario_id: str | None = None, split: str = "train") -> Scenario:
|
| 1886 |
+
"""Get a scenario by ID, or random from the given split."""
|
| 1887 |
+
if scenario_id:
|
| 1888 |
+
for s in SCENARIOS:
|
| 1889 |
+
if s.id == scenario_id:
|
| 1890 |
+
return s
|
| 1891 |
+
raise ValueError(f"Unknown scenario: {scenario_id}")
|
| 1892 |
+
pool = TRAIN_SCENARIOS if split == "train" else EVAL_SCENARIOS
|
| 1893 |
+
return random.choice(pool)
|
server/stack_doctor_environment.py
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Stack Doctor Environment.
|
| 3 |
+
|
| 4 |
+
An overseer LLM diagnoses sick inference stacks by probing subsystems,
|
| 5 |
+
reconciling conflicting specialist-agent reports, and selecting the
|
| 6 |
+
minimal correct fix.
|
| 7 |
+
|
| 8 |
+
Inspired by real SM12x enablement bugs across vLLM, FlashInfer, SGLang,
|
| 9 |
+
CUTLASS, and Flash-Attention.
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
import json
|
| 15 |
+
from uuid import uuid4
|
| 16 |
+
|
| 17 |
+
from openenv.core.env_server.interfaces import Environment
|
| 18 |
+
from openenv.core.env_server.types import State
|
| 19 |
+
|
| 20 |
+
from models import StackDoctorAction, StackDoctorObservation
|
| 21 |
+
from .scenarios import (
|
| 22 |
+
ROOT_CAUSE_TO_FIX,
|
| 23 |
+
FIX_TO_ROOT_CAUSE,
|
| 24 |
+
ROOT_CAUSES,
|
| 25 |
+
FIXES,
|
| 26 |
+
SPECIALISTS,
|
| 27 |
+
Scenario,
|
| 28 |
+
get_scenario,
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
MAX_STEPS = 6
|
| 32 |
+
|
| 33 |
+
INSPECT_TARGETS = {"logs", "config", "snippet", "metrics"}
|
| 34 |
+
VALID_FIXES = set(FIXES)
|
| 35 |
+
VALID_ROOT_CAUSES = set(ROOT_CAUSES)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class EpisodeState:
|
| 39 |
+
"""Internal mutable episode state (not exposed to agent)."""
|
| 40 |
+
|
| 41 |
+
def __init__(self, scenario: Scenario):
|
| 42 |
+
self.scenario = scenario
|
| 43 |
+
self.step_count = 0
|
| 44 |
+
self.fix_applied = False
|
| 45 |
+
self.fix_was_correct: bool | None = None
|
| 46 |
+
self.done = False
|
| 47 |
+
self.cumulative_reward = 0.0
|
| 48 |
+
self.actions_taken: list[dict] = []
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
class StackDoctorEnvironment(Environment):
|
| 52 |
+
"""
|
| 53 |
+
Stack Doctor: incident-response RL environment for
|
| 54 |
+
inference-stack diagnosis.
|
| 55 |
+
"""
|
| 56 |
+
|
| 57 |
+
SUPPORTS_CONCURRENT_SESSIONS: bool = True
|
| 58 |
+
|
| 59 |
+
def __init__(self):
|
| 60 |
+
self._state = State(episode_id=str(uuid4()), step_count=0)
|
| 61 |
+
self._episode: EpisodeState | None = None
|
| 62 |
+
|
| 63 |
+
def reset(self, seed=None, episode_id=None, **kwargs) -> StackDoctorObservation:
|
| 64 |
+
scenario_id = kwargs.get("scenario_id")
|
| 65 |
+
split = kwargs.get("split", "train")
|
| 66 |
+
scenario = get_scenario(scenario_id, split=split)
|
| 67 |
+
|
| 68 |
+
self._state = State(
|
| 69 |
+
episode_id=episode_id or str(uuid4()),
|
| 70 |
+
step_count=0,
|
| 71 |
+
)
|
| 72 |
+
self._episode = EpisodeState(scenario)
|
| 73 |
+
|
| 74 |
+
specialist_obs = {}
|
| 75 |
+
for name, op in scenario.specialist_opinions.items():
|
| 76 |
+
specialist_obs[name] = {
|
| 77 |
+
"opinion": op.opinion,
|
| 78 |
+
"confidence": op.confidence,
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
return StackDoctorObservation(
|
| 82 |
+
output=(
|
| 83 |
+
"STACK DOCTOR — New incident assigned.\n"
|
| 84 |
+
"Diagnose the root cause, optionally apply a fix, then submit your diagnosis.\n"
|
| 85 |
+
"You have 6 steps. Use them wisely.\n\n"
|
| 86 |
+
"Available actions (send as JSON):\n"
|
| 87 |
+
' {"type":"inspect","target":"logs|config|snippet|metrics"}\n'
|
| 88 |
+
' {"type":"ask_specialist","specialist":"runtime|dispatch|kernel|loader"}\n'
|
| 89 |
+
' {"type":"apply_fix","fix":"relax_arch_check|add_whitelist_entry|fix_runtime_path|switch_backend|update_model_config|fix_weight_mapping"}\n'
|
| 90 |
+
' {"type":"submit","root_cause":"...","fix":"...","justification":"reason for diagnosis"}\n'
|
| 91 |
+
),
|
| 92 |
+
incident_ticket=scenario.incident_ticket,
|
| 93 |
+
hardware=scenario.hardware,
|
| 94 |
+
model_name=scenario.model_name,
|
| 95 |
+
backend=scenario.backend,
|
| 96 |
+
log_excerpt=scenario.initial_log,
|
| 97 |
+
code_snippet=scenario.initial_snippet,
|
| 98 |
+
specialist_opinions=specialist_obs,
|
| 99 |
+
steps_remaining=MAX_STEPS,
|
| 100 |
+
fix_used=False,
|
| 101 |
+
done=False,
|
| 102 |
+
reward=0.0,
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
def step(self, action: StackDoctorAction, **kwargs) -> StackDoctorObservation:
|
| 106 |
+
ep = self._episode
|
| 107 |
+
if ep is None or ep.done:
|
| 108 |
+
return self._terminal_obs("Episode is over. Call reset() to start a new incident.", 0.0)
|
| 109 |
+
|
| 110 |
+
self._state.step_count += 1
|
| 111 |
+
ep.step_count += 1
|
| 112 |
+
|
| 113 |
+
try:
|
| 114 |
+
parsed = json.loads(action.message)
|
| 115 |
+
except (json.JSONDecodeError, TypeError):
|
| 116 |
+
return self._handle_invalid(ep, f"Invalid JSON: {action.message[:200]}")
|
| 117 |
+
|
| 118 |
+
action_type = parsed.get("type")
|
| 119 |
+
|
| 120 |
+
if action_type == "inspect":
|
| 121 |
+
return self._handle_inspect(ep, parsed)
|
| 122 |
+
elif action_type == "ask_specialist":
|
| 123 |
+
return self._handle_ask_specialist(ep, parsed)
|
| 124 |
+
elif action_type == "apply_fix":
|
| 125 |
+
return self._handle_apply_fix(ep, parsed)
|
| 126 |
+
elif action_type == "submit":
|
| 127 |
+
return self._handle_submit(ep, parsed)
|
| 128 |
+
else:
|
| 129 |
+
return self._handle_invalid(ep, f"Unknown action type: {action_type}")
|
| 130 |
+
|
| 131 |
+
@property
|
| 132 |
+
def state(self) -> State:
|
| 133 |
+
return self._state
|
| 134 |
+
|
| 135 |
+
def _handle_inspect(self, ep: EpisodeState, parsed: dict) -> StackDoctorObservation:
|
| 136 |
+
target = parsed.get("target")
|
| 137 |
+
if target not in INSPECT_TARGETS:
|
| 138 |
+
return self._handle_invalid(ep, f"Invalid inspect target: {target}. Use: {INSPECT_TARGETS}")
|
| 139 |
+
|
| 140 |
+
reward = -0.25
|
| 141 |
+
ep.cumulative_reward += reward
|
| 142 |
+
ep.actions_taken.append({"type": "inspect", "target": target})
|
| 143 |
+
|
| 144 |
+
ir = ep.scenario.inspect_results
|
| 145 |
+
result_map = {"logs": ir.logs, "config": ir.config, "snippet": ir.snippet, "metrics": ir.metrics}
|
| 146 |
+
|
| 147 |
+
return self._step_obs(ep, output=f"[INSPECT {target.upper()}]\n{result_map[target]}", reward=reward)
|
| 148 |
+
|
| 149 |
+
def _handle_ask_specialist(self, ep: EpisodeState, parsed: dict) -> StackDoctorObservation:
|
| 150 |
+
specialist = parsed.get("specialist")
|
| 151 |
+
if specialist not in SPECIALISTS:
|
| 152 |
+
return self._handle_invalid(ep, f"Invalid specialist: {specialist}. Use: {SPECIALISTS}")
|
| 153 |
+
|
| 154 |
+
reward = -0.25
|
| 155 |
+
ep.cumulative_reward += reward
|
| 156 |
+
ep.actions_taken.append({"type": "ask_specialist", "specialist": specialist})
|
| 157 |
+
|
| 158 |
+
followup = ep.scenario.specialist_followups.get(specialist, "No additional information.")
|
| 159 |
+
return self._step_obs(ep, output=f"[SPECIALIST: {specialist.upper()}]\n{followup}", reward=reward)
|
| 160 |
+
|
| 161 |
+
def _handle_apply_fix(self, ep: EpisodeState, parsed: dict) -> StackDoctorObservation:
|
| 162 |
+
if ep.fix_applied:
|
| 163 |
+
return self._handle_invalid(ep, "apply_fix already used this episode. You can only apply one fix.")
|
| 164 |
+
|
| 165 |
+
fix = parsed.get("fix")
|
| 166 |
+
if fix not in VALID_FIXES:
|
| 167 |
+
return self._handle_invalid(ep, f"Invalid fix: {fix}. Use one of: {sorted(VALID_FIXES)}")
|
| 168 |
+
|
| 169 |
+
ep.fix_applied = True
|
| 170 |
+
is_correct = fix == ep.scenario.correct_fix
|
| 171 |
+
ep.fix_was_correct = is_correct
|
| 172 |
+
|
| 173 |
+
reward = 3.0 if is_correct else -2.0
|
| 174 |
+
ep.cumulative_reward += reward
|
| 175 |
+
ep.actions_taken.append({"type": "apply_fix", "fix": fix, "correct": is_correct})
|
| 176 |
+
|
| 177 |
+
if is_correct:
|
| 178 |
+
output = f"[FIX APPLIED: {fix}] Fix applied successfully. Systems recovering. Now submit your diagnosis."
|
| 179 |
+
else:
|
| 180 |
+
output = f"[FIX APPLIED: {fix}] Fix applied but the issue persists. Consider your diagnosis carefully."
|
| 181 |
+
|
| 182 |
+
return self._step_obs(ep, output=output, reward=reward)
|
| 183 |
+
|
| 184 |
+
def _handle_submit(self, ep: EpisodeState, parsed: dict) -> StackDoctorObservation:
|
| 185 |
+
root_cause = parsed.get("root_cause")
|
| 186 |
+
fix = parsed.get("fix")
|
| 187 |
+
justification = parsed.get("justification", "")
|
| 188 |
+
|
| 189 |
+
if root_cause not in VALID_ROOT_CAUSES:
|
| 190 |
+
return self._handle_invalid(ep, f"Invalid root_cause: {root_cause}. Use one of: {sorted(VALID_ROOT_CAUSES)}")
|
| 191 |
+
if fix not in VALID_FIXES:
|
| 192 |
+
return self._handle_invalid(ep, f"Invalid fix: {fix}. Use one of: {sorted(VALID_FIXES)}")
|
| 193 |
+
|
| 194 |
+
ep.done = True
|
| 195 |
+
correct_rc = ep.scenario.root_cause
|
| 196 |
+
correct_fix = ep.scenario.correct_fix
|
| 197 |
+
rc_correct = root_cause == correct_rc
|
| 198 |
+
fix_correct = fix == correct_fix
|
| 199 |
+
has_justification = len(justification.strip()) >= 10
|
| 200 |
+
|
| 201 |
+
reward = 0.0
|
| 202 |
+
reward += 8.0 if rc_correct else -4.0
|
| 203 |
+
reward += 8.0 if fix_correct else -4.0
|
| 204 |
+
if (rc_correct and fix_correct) and ep.step_count <= 4:
|
| 205 |
+
reward += 2.0
|
| 206 |
+
if has_justification:
|
| 207 |
+
reward += 1.0
|
| 208 |
+
|
| 209 |
+
ep.cumulative_reward += reward
|
| 210 |
+
ep.actions_taken.append({
|
| 211 |
+
"type": "submit", "root_cause": root_cause, "fix": fix,
|
| 212 |
+
"justification": justification,
|
| 213 |
+
"rc_correct": rc_correct, "fix_correct": fix_correct,
|
| 214 |
+
"has_justification": has_justification,
|
| 215 |
+
})
|
| 216 |
+
|
| 217 |
+
output_lines = ["[DIAGNOSIS SUBMITTED]"]
|
| 218 |
+
output_lines.append(f" Root cause: {root_cause} — {'CORRECT' if rc_correct else 'WRONG (was: ' + correct_rc + ')'}")
|
| 219 |
+
output_lines.append(f" Fix: {fix} — {'CORRECT' if fix_correct else 'WRONG (was: ' + correct_fix + ')'}")
|
| 220 |
+
if has_justification:
|
| 221 |
+
output_lines.append(f" Justification: {justification.strip()}")
|
| 222 |
+
output_lines.append(" JUSTIFICATION BONUS: +1")
|
| 223 |
+
else:
|
| 224 |
+
output_lines.append(" No justification provided (missed +1 bonus)")
|
| 225 |
+
output_lines.append(f" Steps used: {ep.step_count}/{MAX_STEPS}")
|
| 226 |
+
if rc_correct and fix_correct and ep.step_count <= 4:
|
| 227 |
+
output_lines.append(" EFFICIENCY BONUS: +2 (solved in <= 4 steps)")
|
| 228 |
+
output_lines.append(f" Episode reward: {ep.cumulative_reward:.2f}")
|
| 229 |
+
|
| 230 |
+
return self._terminal_obs("\n".join(output_lines), reward)
|
| 231 |
+
|
| 232 |
+
def _handle_invalid(self, ep: EpisodeState, msg: str) -> StackDoctorObservation:
|
| 233 |
+
reward = -2.0
|
| 234 |
+
ep.cumulative_reward += reward
|
| 235 |
+
ep.actions_taken.append({"type": "invalid", "message": msg})
|
| 236 |
+
|
| 237 |
+
if ep.step_count >= MAX_STEPS:
|
| 238 |
+
ep.done = True
|
| 239 |
+
return self._terminal_obs(f"[INVALID ACTION] {msg}\n[EPISODE OVER] Max steps reached. Auto-fail.", reward)
|
| 240 |
+
|
| 241 |
+
return self._step_obs(ep, output=f"[INVALID ACTION] {msg}", reward=reward)
|
| 242 |
+
|
| 243 |
+
def _step_obs(self, ep: EpisodeState, output: str, reward: float) -> StackDoctorObservation:
|
| 244 |
+
remaining = MAX_STEPS - ep.step_count
|
| 245 |
+
if remaining <= 0 and not ep.done:
|
| 246 |
+
ep.done = True
|
| 247 |
+
reward -= 4.0
|
| 248 |
+
ep.cumulative_reward += -4.0
|
| 249 |
+
output += "\n\n[EPISODE OVER] Max steps reached without submission. Auto-fail. Reward: -4"
|
| 250 |
+
|
| 251 |
+
return StackDoctorObservation(
|
| 252 |
+
output=output, incident_ticket=ep.scenario.incident_ticket,
|
| 253 |
+
hardware=ep.scenario.hardware, model_name=ep.scenario.model_name,
|
| 254 |
+
backend=ep.scenario.backend, log_excerpt="", code_snippet="",
|
| 255 |
+
specialist_opinions={}, steps_remaining=remaining, fix_used=ep.fix_applied,
|
| 256 |
+
done=ep.done, reward=reward,
|
| 257 |
+
metadata={"cumulative_reward": ep.cumulative_reward, "step": ep.step_count, "scenario_id": ep.scenario.id},
|
| 258 |
+
)
|
| 259 |
+
|
| 260 |
+
def _terminal_obs(self, output: str, reward: float) -> StackDoctorObservation:
|
| 261 |
+
ep = self._episode
|
| 262 |
+
return StackDoctorObservation(
|
| 263 |
+
output=output, incident_ticket=ep.scenario.incident_ticket if ep else "",
|
| 264 |
+
hardware=ep.scenario.hardware if ep else "", model_name=ep.scenario.model_name if ep else "",
|
| 265 |
+
backend=ep.scenario.backend if ep else "", log_excerpt="", code_snippet="",
|
| 266 |
+
specialist_opinions={}, steps_remaining=0, fix_used=ep.fix_applied if ep else False,
|
| 267 |
+
done=True, reward=reward,
|
| 268 |
+
metadata={"cumulative_reward": ep.cumulative_reward if ep else 0.0, "step": ep.step_count if ep else 0, "scenario_id": ep.scenario.id if ep else ""},
|
| 269 |
+
)
|
server/stack_doctor_mcp.py
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Stack Doctor MCP Environment.
|
| 3 |
+
|
| 4 |
+
Wraps the core Stack Doctor environment with MCP tools that agents
|
| 5 |
+
can discover and invoke. This is the agent-facing interface —
|
| 6 |
+
agents call tools like read_log(), query_specialist(), submit_diagnosis()
|
| 7 |
+
instead of constructing JSON action strings.
|
| 8 |
+
|
| 9 |
+
The training (WebSocket) API still works through _step_impl().
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
import json
|
| 15 |
+
from typing import Any, Optional
|
| 16 |
+
from uuid import uuid4
|
| 17 |
+
|
| 18 |
+
from mcp.server.fastmcp import FastMCP
|
| 19 |
+
from openenv.core.env_server.mcp_environment import MCPEnvironment
|
| 20 |
+
from openenv.core.env_server.types import Action, Observation, State
|
| 21 |
+
|
| 22 |
+
from models import StackDoctorAction, StackDoctorObservation
|
| 23 |
+
from .scenarios import (
|
| 24 |
+
ROOT_CAUSE_TO_FIX,
|
| 25 |
+
FIX_TO_ROOT_CAUSE,
|
| 26 |
+
ROOT_CAUSES,
|
| 27 |
+
FIXES,
|
| 28 |
+
SPECIALISTS,
|
| 29 |
+
Scenario,
|
| 30 |
+
get_scenario,
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
MAX_STEPS = 6
|
| 34 |
+
VALID_FIXES = set(FIXES)
|
| 35 |
+
VALID_ROOT_CAUSES = set(ROOT_CAUSES)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class StackDoctorMCPEnvironment(MCPEnvironment):
|
| 39 |
+
"""
|
| 40 |
+
Stack Doctor with MCP tool interface for agent interaction.
|
| 41 |
+
|
| 42 |
+
Agents discover available tools (read_log, check_config, view_code,
|
| 43 |
+
run_diagnostic, query_specialist, apply_fix, submit_diagnosis) and
|
| 44 |
+
call them to investigate incidents and submit diagnoses.
|
| 45 |
+
"""
|
| 46 |
+
|
| 47 |
+
SUPPORTS_CONCURRENT_SESSIONS: bool = True
|
| 48 |
+
|
| 49 |
+
def __init__(self):
|
| 50 |
+
mcp = FastMCP("stack_doctor")
|
| 51 |
+
self._state_obj = State(episode_id=str(uuid4()), step_count=0)
|
| 52 |
+
self._scenario: Scenario | None = None
|
| 53 |
+
self._step_count = 0
|
| 54 |
+
self._fix_applied = False
|
| 55 |
+
self._fix_was_correct: bool | None = None
|
| 56 |
+
self._done = False
|
| 57 |
+
self._cumulative_reward = 0.0
|
| 58 |
+
self._actions_taken: list[dict] = []
|
| 59 |
+
|
| 60 |
+
env = self # capture for closures
|
| 61 |
+
|
| 62 |
+
@mcp.tool()
|
| 63 |
+
def read_log() -> str:
|
| 64 |
+
"""Read system and application logs for the current incident.
|
| 65 |
+
Returns log output from the affected inference stack including
|
| 66 |
+
error messages, warnings, and system state information.
|
| 67 |
+
Costs 1 step (-0.25 reward)."""
|
| 68 |
+
return env._do_inspect("logs")
|
| 69 |
+
|
| 70 |
+
@mcp.tool()
|
| 71 |
+
def check_config() -> str:
|
| 72 |
+
"""Check configuration files for the current incident.
|
| 73 |
+
Returns relevant configuration parameters including GPU settings,
|
| 74 |
+
backend configuration, model parameters, and environment variables.
|
| 75 |
+
Costs 1 step (-0.25 reward)."""
|
| 76 |
+
return env._do_inspect("config")
|
| 77 |
+
|
| 78 |
+
@mcp.tool()
|
| 79 |
+
def view_code() -> str:
|
| 80 |
+
"""View relevant source code snippets for the current incident.
|
| 81 |
+
Returns code from the affected component showing the likely
|
| 82 |
+
location of the bug or misconfiguration.
|
| 83 |
+
Costs 1 step (-0.25 reward)."""
|
| 84 |
+
return env._do_inspect("snippet")
|
| 85 |
+
|
| 86 |
+
@mcp.tool()
|
| 87 |
+
def run_diagnostic() -> str:
|
| 88 |
+
"""Run performance diagnostics and metrics collection.
|
| 89 |
+
Returns metrics like latency, throughput, GPU utilization,
|
| 90 |
+
error rates, and memory usage for the affected system.
|
| 91 |
+
Costs 1 step (-0.25 reward)."""
|
| 92 |
+
return env._do_inspect("metrics")
|
| 93 |
+
|
| 94 |
+
@mcp.tool()
|
| 95 |
+
def query_specialist(specialist: str) -> str:
|
| 96 |
+
"""Ask a specialist for their analysis of the incident.
|
| 97 |
+
Specialists: 'runtime', 'dispatch', 'kernel', 'loader'.
|
| 98 |
+
WARNING: At least one specialist gives wrong advice per incident.
|
| 99 |
+
Cross-verify specialist opinions before trusting them.
|
| 100 |
+
Costs 1 step (-0.25 reward)."""
|
| 101 |
+
return env._do_ask_specialist(specialist)
|
| 102 |
+
|
| 103 |
+
@mcp.tool()
|
| 104 |
+
def apply_fix(fix: str) -> str:
|
| 105 |
+
"""Apply a fix to the system. Can only be used ONCE per incident.
|
| 106 |
+
Available fixes: 'relax_arch_check', 'add_whitelist_entry',
|
| 107 |
+
'fix_runtime_path', 'switch_backend', 'update_model_config',
|
| 108 |
+
'fix_weight_mapping'.
|
| 109 |
+
Correct fix: +3 reward. Wrong fix: -2 reward."""
|
| 110 |
+
return env._do_apply_fix(fix)
|
| 111 |
+
|
| 112 |
+
@mcp.tool()
|
| 113 |
+
def submit_diagnosis(root_cause: str, fix: str, justification: str = "") -> str:
|
| 114 |
+
"""Submit your final diagnosis. This ends the episode.
|
| 115 |
+
Root causes: 'arch_guard', 'backend_whitelist', 'runtime_loader',
|
| 116 |
+
'backend_selector', 'model_config', 'weight_layout'.
|
| 117 |
+
Fixes: 'relax_arch_check', 'add_whitelist_entry', 'fix_runtime_path',
|
| 118 |
+
'switch_backend', 'update_model_config', 'fix_weight_mapping'.
|
| 119 |
+
justification: A short sentence explaining WHY you chose this root cause
|
| 120 |
+
and fix based on the evidence you gathered. Bonus +1 if provided.
|
| 121 |
+
Correct root_cause: +8. Wrong: -4. Correct fix: +8. Wrong: -4.
|
| 122 |
+
Bonus +2 if solved in 4 or fewer steps. Bonus +1 for justification."""
|
| 123 |
+
return env._do_submit(root_cause, fix, justification)
|
| 124 |
+
|
| 125 |
+
super().__init__(mcp)
|
| 126 |
+
|
| 127 |
+
# ------------------------------------------------------------------
|
| 128 |
+
# MCP tool implementations
|
| 129 |
+
# ------------------------------------------------------------------
|
| 130 |
+
|
| 131 |
+
def _check_episode(self) -> str | None:
|
| 132 |
+
"""Return error message if episode is not active."""
|
| 133 |
+
if self._scenario is None:
|
| 134 |
+
return "No active incident. Call reset() first."
|
| 135 |
+
if self._done:
|
| 136 |
+
return "Episode is over. Call reset() to start a new incident."
|
| 137 |
+
if self._step_count >= MAX_STEPS:
|
| 138 |
+
self._done = True
|
| 139 |
+
return "Max steps reached. Episode over."
|
| 140 |
+
return None
|
| 141 |
+
|
| 142 |
+
def _record_step(self, reward: float, action: dict) -> None:
|
| 143 |
+
self._step_count += 1
|
| 144 |
+
self._state_obj.step_count = self._step_count
|
| 145 |
+
self._cumulative_reward += reward
|
| 146 |
+
self._actions_taken.append(action)
|
| 147 |
+
|
| 148 |
+
def _do_inspect(self, target: str) -> str:
|
| 149 |
+
err = self._check_episode()
|
| 150 |
+
if err:
|
| 151 |
+
return err
|
| 152 |
+
|
| 153 |
+
ir = self._scenario.inspect_results
|
| 154 |
+
result_map = {
|
| 155 |
+
"logs": ir.logs,
|
| 156 |
+
"config": ir.config,
|
| 157 |
+
"snippet": ir.snippet,
|
| 158 |
+
"metrics": ir.metrics,
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
self._record_step(-0.25, {"type": "inspect", "target": target})
|
| 162 |
+
|
| 163 |
+
remaining = MAX_STEPS - self._step_count
|
| 164 |
+
return (
|
| 165 |
+
f"[INSPECT {target.upper()}]\n"
|
| 166 |
+
f"{result_map[target]}\n\n"
|
| 167 |
+
f"[Steps remaining: {remaining} | Reward: -0.25 | Cumulative: {self._cumulative_reward:.2f}]"
|
| 168 |
+
)
|
| 169 |
+
|
| 170 |
+
def _do_ask_specialist(self, specialist: str) -> str:
|
| 171 |
+
err = self._check_episode()
|
| 172 |
+
if err:
|
| 173 |
+
return err
|
| 174 |
+
|
| 175 |
+
if specialist not in SPECIALISTS:
|
| 176 |
+
self._record_step(-2.0, {"type": "invalid", "message": f"Unknown specialist: {specialist}"})
|
| 177 |
+
return f"Invalid specialist '{specialist}'. Available: {SPECIALISTS}. Penalty: -2.0"
|
| 178 |
+
|
| 179 |
+
followup = self._scenario.specialist_followups.get(specialist, "No additional information.")
|
| 180 |
+
self._record_step(-0.25, {"type": "ask_specialist", "specialist": specialist})
|
| 181 |
+
|
| 182 |
+
remaining = MAX_STEPS - self._step_count
|
| 183 |
+
return (
|
| 184 |
+
f"[SPECIALIST: {specialist.upper()}]\n"
|
| 185 |
+
f"{followup}\n\n"
|
| 186 |
+
f"[Steps remaining: {remaining} | Reward: -0.25 | Cumulative: {self._cumulative_reward:.2f}]"
|
| 187 |
+
)
|
| 188 |
+
|
| 189 |
+
def _do_apply_fix(self, fix: str) -> str:
|
| 190 |
+
err = self._check_episode()
|
| 191 |
+
if err:
|
| 192 |
+
return err
|
| 193 |
+
|
| 194 |
+
if self._fix_applied:
|
| 195 |
+
self._record_step(-2.0, {"type": "invalid", "message": "Fix already applied"})
|
| 196 |
+
return "You already applied a fix this episode. Only one fix allowed. Penalty: -2.0"
|
| 197 |
+
|
| 198 |
+
if fix not in VALID_FIXES:
|
| 199 |
+
self._record_step(-2.0, {"type": "invalid", "message": f"Invalid fix: {fix}"})
|
| 200 |
+
return f"Invalid fix '{fix}'. Available: {sorted(VALID_FIXES)}. Penalty: -2.0"
|
| 201 |
+
|
| 202 |
+
self._fix_applied = True
|
| 203 |
+
is_correct = fix == self._scenario.correct_fix
|
| 204 |
+
self._fix_was_correct = is_correct
|
| 205 |
+
reward = 3.0 if is_correct else -2.0
|
| 206 |
+
self._record_step(reward, {"type": "apply_fix", "fix": fix, "correct": is_correct})
|
| 207 |
+
|
| 208 |
+
remaining = MAX_STEPS - self._step_count
|
| 209 |
+
if is_correct:
|
| 210 |
+
return (
|
| 211 |
+
f"[FIX APPLIED: {fix}] Fix applied successfully. Systems recovering.\n"
|
| 212 |
+
f"Now submit your diagnosis with submit_diagnosis().\n\n"
|
| 213 |
+
f"[Steps remaining: {remaining} | Reward: +3.0 | Cumulative: {self._cumulative_reward:.2f}]"
|
| 214 |
+
)
|
| 215 |
+
else:
|
| 216 |
+
return (
|
| 217 |
+
f"[FIX APPLIED: {fix}] Fix applied but the issue persists.\n"
|
| 218 |
+
f"Consider your diagnosis carefully.\n\n"
|
| 219 |
+
f"[Steps remaining: {remaining} | Reward: -2.0 | Cumulative: {self._cumulative_reward:.2f}]"
|
| 220 |
+
)
|
| 221 |
+
|
| 222 |
+
def _do_submit(self, root_cause: str, fix: str, justification: str = "") -> str:
|
| 223 |
+
err = self._check_episode()
|
| 224 |
+
if err:
|
| 225 |
+
return err
|
| 226 |
+
|
| 227 |
+
if root_cause not in VALID_ROOT_CAUSES:
|
| 228 |
+
self._record_step(-2.0, {"type": "invalid", "message": f"Invalid root_cause: {root_cause}"})
|
| 229 |
+
return f"Invalid root_cause '{root_cause}'. Available: {sorted(VALID_ROOT_CAUSES)}. Penalty: -2.0"
|
| 230 |
+
|
| 231 |
+
if fix not in VALID_FIXES:
|
| 232 |
+
self._record_step(-2.0, {"type": "invalid", "message": f"Invalid fix: {fix}"})
|
| 233 |
+
return f"Invalid fix '{fix}'. Available: {sorted(VALID_FIXES)}. Penalty: -2.0"
|
| 234 |
+
|
| 235 |
+
self._done = True
|
| 236 |
+
rc_correct = root_cause == self._scenario.root_cause
|
| 237 |
+
fix_correct = fix == self._scenario.correct_fix
|
| 238 |
+
has_justification = len(justification.strip()) >= 10
|
| 239 |
+
|
| 240 |
+
reward = 0.0
|
| 241 |
+
reward += 8.0 if rc_correct else -4.0
|
| 242 |
+
reward += 8.0 if fix_correct else -4.0
|
| 243 |
+
if rc_correct and fix_correct and self._step_count + 1 <= 4:
|
| 244 |
+
reward += 2.0
|
| 245 |
+
if has_justification:
|
| 246 |
+
reward += 1.0
|
| 247 |
+
|
| 248 |
+
self._record_step(reward, {
|
| 249 |
+
"type": "submit", "root_cause": root_cause, "fix": fix,
|
| 250 |
+
"justification": justification,
|
| 251 |
+
"rc_correct": rc_correct, "fix_correct": fix_correct,
|
| 252 |
+
"has_justification": has_justification,
|
| 253 |
+
})
|
| 254 |
+
|
| 255 |
+
lines = ["[DIAGNOSIS SUBMITTED]"]
|
| 256 |
+
lines.append(f" Root cause: {root_cause} — {'CORRECT' if rc_correct else 'WRONG (was: ' + self._scenario.root_cause + ')'}")
|
| 257 |
+
lines.append(f" Fix: {fix} — {'CORRECT' if fix_correct else 'WRONG (was: ' + self._scenario.correct_fix + ')'}")
|
| 258 |
+
if has_justification:
|
| 259 |
+
lines.append(f" Justification: {justification.strip()}")
|
| 260 |
+
lines.append(" JUSTIFICATION BONUS: +1")
|
| 261 |
+
else:
|
| 262 |
+
lines.append(" No justification provided (missed +1 bonus)")
|
| 263 |
+
lines.append(f" Steps used: {self._step_count}/{MAX_STEPS}")
|
| 264 |
+
if rc_correct and fix_correct and self._step_count <= 4:
|
| 265 |
+
lines.append(" EFFICIENCY BONUS: +2 (solved in <= 4 steps)")
|
| 266 |
+
lines.append(f" Episode reward: {self._cumulative_reward:.2f}")
|
| 267 |
+
|
| 268 |
+
return "\n".join(lines)
|
| 269 |
+
|
| 270 |
+
# ------------------------------------------------------------------
|
| 271 |
+
# OpenEnv Environment interface (for training / WebSocket API)
|
| 272 |
+
# ------------------------------------------------------------------
|
| 273 |
+
|
| 274 |
+
def reset(self, seed=None, episode_id=None, **kwargs) -> StackDoctorObservation:
|
| 275 |
+
scenario_id = kwargs.get("scenario_id")
|
| 276 |
+
split = kwargs.get("split", "train")
|
| 277 |
+
self._scenario = get_scenario(scenario_id, split=split)
|
| 278 |
+
|
| 279 |
+
self._state_obj = State(
|
| 280 |
+
episode_id=episode_id or str(uuid4()),
|
| 281 |
+
step_count=0,
|
| 282 |
+
)
|
| 283 |
+
self._step_count = 0
|
| 284 |
+
self._fix_applied = False
|
| 285 |
+
self._fix_was_correct = None
|
| 286 |
+
self._done = False
|
| 287 |
+
self._cumulative_reward = 0.0
|
| 288 |
+
self._actions_taken = []
|
| 289 |
+
|
| 290 |
+
specialist_obs = {}
|
| 291 |
+
for name, op in self._scenario.specialist_opinions.items():
|
| 292 |
+
specialist_obs[name] = {
|
| 293 |
+
"opinion": op.opinion,
|
| 294 |
+
"confidence": op.confidence,
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
return StackDoctorObservation(
|
| 298 |
+
output=(
|
| 299 |
+
"STACK DOCTOR — New incident assigned.\n"
|
| 300 |
+
"Investigate using the available tools: read_log(), check_config(), "
|
| 301 |
+
"view_code(), run_diagnostic(), query_specialist(name).\n"
|
| 302 |
+
"When ready, apply_fix(fix) and/or submit_diagnosis(root_cause, fix).\n"
|
| 303 |
+
"You have 6 steps. At least one specialist is WRONG — cross-verify.\n"
|
| 304 |
+
),
|
| 305 |
+
incident_ticket=self._scenario.incident_ticket,
|
| 306 |
+
hardware=self._scenario.hardware,
|
| 307 |
+
model_name=self._scenario.model_name,
|
| 308 |
+
backend=self._scenario.backend,
|
| 309 |
+
log_excerpt=self._scenario.initial_log,
|
| 310 |
+
code_snippet=self._scenario.initial_snippet,
|
| 311 |
+
specialist_opinions=specialist_obs,
|
| 312 |
+
steps_remaining=MAX_STEPS,
|
| 313 |
+
fix_used=False,
|
| 314 |
+
done=False,
|
| 315 |
+
reward=0.0,
|
| 316 |
+
)
|
| 317 |
+
|
| 318 |
+
def _step_impl(
|
| 319 |
+
self,
|
| 320 |
+
action: Action,
|
| 321 |
+
timeout_s: Optional[float] = None,
|
| 322 |
+
**kwargs: Any,
|
| 323 |
+
) -> Observation:
|
| 324 |
+
"""Handle non-MCP actions (JSON action strings for training)."""
|
| 325 |
+
if not isinstance(action, StackDoctorAction):
|
| 326 |
+
return self._make_obs("Invalid action type.", -2.0)
|
| 327 |
+
|
| 328 |
+
try:
|
| 329 |
+
parsed = json.loads(action.message)
|
| 330 |
+
except (json.JSONDecodeError, TypeError):
|
| 331 |
+
return self._make_obs(f"Invalid JSON: {action.message[:200]}", -2.0)
|
| 332 |
+
|
| 333 |
+
action_type = parsed.get("type")
|
| 334 |
+
|
| 335 |
+
if action_type == "inspect":
|
| 336 |
+
result = self._do_inspect(parsed.get("target", "logs"))
|
| 337 |
+
elif action_type == "ask_specialist":
|
| 338 |
+
result = self._do_ask_specialist(parsed.get("specialist", ""))
|
| 339 |
+
elif action_type == "apply_fix":
|
| 340 |
+
result = self._do_apply_fix(parsed.get("fix", ""))
|
| 341 |
+
elif action_type == "submit":
|
| 342 |
+
result = self._do_submit(parsed.get("root_cause", ""), parsed.get("fix", ""), parsed.get("justification", ""))
|
| 343 |
+
else:
|
| 344 |
+
self._record_step(-2.0, {"type": "invalid", "message": f"Unknown: {action_type}"})
|
| 345 |
+
result = f"Unknown action type: {action_type}. Penalty: -2.0"
|
| 346 |
+
|
| 347 |
+
# Extract last reward from actions
|
| 348 |
+
last_reward = 0.0
|
| 349 |
+
if self._actions_taken:
|
| 350 |
+
last = self._actions_taken[-1]
|
| 351 |
+
if last.get("type") == "submit":
|
| 352 |
+
# Calculate submit reward
|
| 353 |
+
rc_c = last.get("rc_correct", False)
|
| 354 |
+
fx_c = last.get("fix_correct", False)
|
| 355 |
+
last_reward = (8.0 if rc_c else -4.0) + (8.0 if fx_c else -4.0)
|
| 356 |
+
if rc_c and fx_c and self._step_count <= 4:
|
| 357 |
+
last_reward += 2.0
|
| 358 |
+
if last.get("has_justification", False):
|
| 359 |
+
last_reward += 1.0
|
| 360 |
+
elif last.get("type") == "apply_fix":
|
| 361 |
+
last_reward = 3.0 if last.get("correct") else -2.0
|
| 362 |
+
elif last.get("type") == "invalid":
|
| 363 |
+
last_reward = -2.0
|
| 364 |
+
else:
|
| 365 |
+
last_reward = -0.25
|
| 366 |
+
|
| 367 |
+
return self._make_obs(result, last_reward)
|
| 368 |
+
|
| 369 |
+
def _make_obs(self, output: str, reward: float) -> StackDoctorObservation:
|
| 370 |
+
remaining = MAX_STEPS - self._step_count
|
| 371 |
+
return StackDoctorObservation(
|
| 372 |
+
output=output,
|
| 373 |
+
incident_ticket=self._scenario.incident_ticket if self._scenario else "",
|
| 374 |
+
hardware=self._scenario.hardware if self._scenario else "",
|
| 375 |
+
model_name=self._scenario.model_name if self._scenario else "",
|
| 376 |
+
backend=self._scenario.backend if self._scenario else "",
|
| 377 |
+
log_excerpt="",
|
| 378 |
+
code_snippet="",
|
| 379 |
+
specialist_opinions={},
|
| 380 |
+
steps_remaining=remaining,
|
| 381 |
+
fix_used=self._fix_applied,
|
| 382 |
+
done=self._done,
|
| 383 |
+
reward=reward,
|
| 384 |
+
metadata={
|
| 385 |
+
"cumulative_reward": self._cumulative_reward,
|
| 386 |
+
"step": self._step_count,
|
| 387 |
+
"scenario_id": self._scenario.id if self._scenario else "",
|
| 388 |
+
},
|
| 389 |
+
)
|
| 390 |
+
|
| 391 |
+
@property
|
| 392 |
+
def state(self) -> State:
|
| 393 |
+
return self._state_obj
|
training/Dockerfile
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM pytorch/pytorch:2.5.1-cuda12.4-cudnn9-devel
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
RUN apt-get update && apt-get install -y --no-install-recommends git && rm -rf /var/lib/apt/lists/*
|
| 6 |
+
|
| 7 |
+
RUN pip install --no-cache-dir --upgrade pip
|
| 8 |
+
|
| 9 |
+
# Step 1: Install unsloth first (it pulls the torch version it wants)
|
| 10 |
+
RUN pip install --no-cache-dir \
|
| 11 |
+
"unsloth @ git+https://github.com/unslothai/unsloth.git" \
|
| 12 |
+
unsloth_zoo \
|
| 13 |
+
xformers
|
| 14 |
+
|
| 15 |
+
# Step 2: Now install torchvision to match whatever torch unsloth installed
|
| 16 |
+
RUN pip install --no-cache-dir --upgrade torchvision
|
| 17 |
+
|
| 18 |
+
# Step 3: Install TRL + training deps
|
| 19 |
+
RUN pip install --no-cache-dir \
|
| 20 |
+
"trl>=0.18.2,<=0.24.0" \
|
| 21 |
+
"peft>=0.18.0" \
|
| 22 |
+
"accelerate>=0.34.1" \
|
| 23 |
+
"bitsandbytes>=0.45.5" \
|
| 24 |
+
"datasets>=3.4.1" \
|
| 25 |
+
"transformers>=4.51.3" \
|
| 26 |
+
"huggingface_hub>=0.34.0" \
|
| 27 |
+
sentencepiece \
|
| 28 |
+
hf_transfer \
|
| 29 |
+
torchao \
|
| 30 |
+
triton
|
| 31 |
+
|
| 32 |
+
# Step 4: Install openenv for environment stepping
|
| 33 |
+
RUN pip install --no-cache-dir openenv-core
|
| 34 |
+
|
| 35 |
+
# Copy project code
|
| 36 |
+
COPY . /app/stack_doctor/
|
| 37 |
+
ENV PYTHONPATH="/app/stack_doctor:$PYTHONPATH"
|
| 38 |
+
|
| 39 |
+
CMD ["python", "/app/stack_doctor/training/train_stack_doctor.py"]
|
training/__init__.py
ADDED
|
File without changes
|
training/eval_stack_doctor.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Stack Doctor — Evaluation Script
|
| 3 |
+
|
| 4 |
+
Produces the 4 metrics for judges:
|
| 5 |
+
1. Root-cause accuracy
|
| 6 |
+
2. Fix-family accuracy
|
| 7 |
+
3. Average steps to resolution
|
| 8 |
+
4. Mean reward before vs after RL
|
| 9 |
+
|
| 10 |
+
Can evaluate any model (base or fine-tuned) against held-out eval scenarios.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import json
|
| 14 |
+
import os
|
| 15 |
+
import sys
|
| 16 |
+
|
| 17 |
+
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 18 |
+
PROJECT_DIR = os.path.dirname(SCRIPT_DIR)
|
| 19 |
+
sys.path.insert(0, PROJECT_DIR)
|
| 20 |
+
|
| 21 |
+
from server.stack_doctor_environment import StackDoctorEnvironment
|
| 22 |
+
from server.scenarios import EVAL_SCENARIOS
|
| 23 |
+
from models import StackDoctorAction
|
| 24 |
+
from training.train_stack_doctor import (
|
| 25 |
+
SYSTEM_PROMPT,
|
| 26 |
+
format_scenario_prompt,
|
| 27 |
+
extract_actions,
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def evaluate_model(model, tokenizer, scenarios, label="Model"):
|
| 32 |
+
"""Run model against scenarios and compute metrics."""
|
| 33 |
+
from unsloth import FastLanguageModel
|
| 34 |
+
FastLanguageModel.for_inference(model)
|
| 35 |
+
|
| 36 |
+
total_rc_correct = 0
|
| 37 |
+
total_fix_correct = 0
|
| 38 |
+
total_steps = 0
|
| 39 |
+
total_reward = 0.0
|
| 40 |
+
n = 0
|
| 41 |
+
|
| 42 |
+
for sc in scenarios:
|
| 43 |
+
messages = [
|
| 44 |
+
{"role": "system", "content": SYSTEM_PROMPT},
|
| 45 |
+
{"role": "user", "content": format_scenario_prompt(sc)},
|
| 46 |
+
]
|
| 47 |
+
prompt = tokenizer.apply_chat_template(
|
| 48 |
+
messages, add_generation_prompt=True, tokenize=False,
|
| 49 |
+
)
|
| 50 |
+
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
|
| 51 |
+
outputs = model.generate(
|
| 52 |
+
**inputs,
|
| 53 |
+
max_new_tokens=512,
|
| 54 |
+
temperature=0.3,
|
| 55 |
+
do_sample=True,
|
| 56 |
+
)
|
| 57 |
+
response = tokenizer.decode(outputs[0][inputs.input_ids.shape[1]:], skip_special_tokens=True)
|
| 58 |
+
|
| 59 |
+
actions = extract_actions(response)
|
| 60 |
+
if actions is None:
|
| 61 |
+
total_reward -= 5.0
|
| 62 |
+
n += 1
|
| 63 |
+
continue
|
| 64 |
+
|
| 65 |
+
env = StackDoctorEnvironment()
|
| 66 |
+
env.reset(scenario_id=sc.id)
|
| 67 |
+
|
| 68 |
+
cum_reward = 0.0
|
| 69 |
+
steps = 0
|
| 70 |
+
last_submit = None
|
| 71 |
+
|
| 72 |
+
for action_dict in actions:
|
| 73 |
+
if not isinstance(action_dict, dict):
|
| 74 |
+
continue
|
| 75 |
+
try:
|
| 76 |
+
obs = env.step(StackDoctorAction(message=json.dumps(action_dict)))
|
| 77 |
+
cum_reward += obs.reward
|
| 78 |
+
steps += 1
|
| 79 |
+
if action_dict.get("type") == "submit":
|
| 80 |
+
last_submit = action_dict
|
| 81 |
+
if obs.done:
|
| 82 |
+
break
|
| 83 |
+
except Exception:
|
| 84 |
+
break
|
| 85 |
+
|
| 86 |
+
if last_submit:
|
| 87 |
+
if last_submit.get("root_cause") == sc.root_cause:
|
| 88 |
+
total_rc_correct += 1
|
| 89 |
+
if last_submit.get("fix") == sc.correct_fix:
|
| 90 |
+
total_fix_correct += 1
|
| 91 |
+
|
| 92 |
+
total_steps += steps
|
| 93 |
+
total_reward += cum_reward
|
| 94 |
+
n += 1
|
| 95 |
+
|
| 96 |
+
print(f" {sc.id}: rc={'OK' if last_submit and last_submit.get('root_cause')==sc.root_cause else 'FAIL'} "
|
| 97 |
+
f"fix={'OK' if last_submit and last_submit.get('fix')==sc.correct_fix else 'FAIL'} "
|
| 98 |
+
f"steps={steps} reward={cum_reward:.1f}")
|
| 99 |
+
|
| 100 |
+
print(f"\n{'='*50}")
|
| 101 |
+
print(f"{label} Results ({n} episodes):")
|
| 102 |
+
print(f" Root-cause accuracy: {total_rc_correct/n:.1%}")
|
| 103 |
+
print(f" Fix accuracy: {total_fix_correct/n:.1%}")
|
| 104 |
+
print(f" Avg steps: {total_steps/n:.1f}")
|
| 105 |
+
print(f" Avg reward: {total_reward/n:.1f}")
|
| 106 |
+
print(f"{'='*50}")
|
| 107 |
+
|
| 108 |
+
return {
|
| 109 |
+
"rc_accuracy": total_rc_correct / n,
|
| 110 |
+
"fix_accuracy": total_fix_correct / n,
|
| 111 |
+
"avg_steps": total_steps / n,
|
| 112 |
+
"avg_reward": total_reward / n,
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def main():
|
| 117 |
+
from unsloth import FastLanguageModel
|
| 118 |
+
import argparse
|
| 119 |
+
|
| 120 |
+
parser = argparse.ArgumentParser()
|
| 121 |
+
parser.add_argument("--model", default="unsloth/Qwen3-1.7B", help="Model name or path")
|
| 122 |
+
parser.add_argument("--lora", default=None, help="Path to LoRA adapter")
|
| 123 |
+
args = parser.parse_args()
|
| 124 |
+
|
| 125 |
+
model, tokenizer = FastLanguageModel.from_pretrained(
|
| 126 |
+
model_name=args.model,
|
| 127 |
+
load_in_4bit=True,
|
| 128 |
+
max_seq_length=2048,
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
if args.lora:
|
| 132 |
+
from peft import PeftModel
|
| 133 |
+
model = PeftModel.from_pretrained(model, args.lora)
|
| 134 |
+
|
| 135 |
+
print(f"Evaluating {args.model}" + (f" + {args.lora}" if args.lora else ""))
|
| 136 |
+
print(f"Eval scenarios: {len(EVAL_SCENARIOS)}")
|
| 137 |
+
print()
|
| 138 |
+
|
| 139 |
+
evaluate_model(model, tokenizer, EVAL_SCENARIOS, label=args.model)
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
if __name__ == "__main__":
|
| 143 |
+
main()
|
training/train_stack_doctor.py
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Stack Doctor — GRPO Training Script
|
| 3 |
+
|
| 4 |
+
Train an LLM to diagnose inference-stack incidents using Group Relative
|
| 5 |
+
Policy Optimization (GRPO) with Unsloth + TRL.
|
| 6 |
+
|
| 7 |
+
The model generates a JSON action plan, which gets executed against the
|
| 8 |
+
Stack Doctor environment. Reward = cumulative episode reward.
|
| 9 |
+
|
| 10 |
+
Fleet AI sub-theme: the agent must reconcile conflicting specialist reports
|
| 11 |
+
(some specialists lie) to identify the correct root cause and fix.
|
| 12 |
+
|
| 13 |
+
Usage (Colab with GPU):
|
| 14 |
+
!pip install unsloth trl openenv-core
|
| 15 |
+
!python train_stack_doctor.py
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
import json
|
| 19 |
+
import os
|
| 20 |
+
import sys
|
| 21 |
+
import random
|
| 22 |
+
|
| 23 |
+
# ---------------------------------------------------------------------------
|
| 24 |
+
# 1. Environment setup — add server to path for direct import
|
| 25 |
+
# ---------------------------------------------------------------------------
|
| 26 |
+
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 27 |
+
PROJECT_DIR = os.path.dirname(SCRIPT_DIR)
|
| 28 |
+
sys.path.insert(0, PROJECT_DIR)
|
| 29 |
+
|
| 30 |
+
from server.stack_doctor_environment import StackDoctorEnvironment
|
| 31 |
+
from server.scenarios import SCENARIOS, TRAIN_SCENARIOS, EVAL_SCENARIOS
|
| 32 |
+
from models import StackDoctorAction, StackDoctorObservation
|
| 33 |
+
|
| 34 |
+
# ---------------------------------------------------------------------------
|
| 35 |
+
# 2. Build the system prompt and dataset
|
| 36 |
+
# ---------------------------------------------------------------------------
|
| 37 |
+
|
| 38 |
+
SYSTEM_PROMPT = """You are Stack Doctor, an expert AI agent that diagnoses inference-stack incidents.
|
| 39 |
+
|
| 40 |
+
You receive an incident ticket with hardware/model/backend context, log excerpts, code snippets, and specialist opinions. Some specialists may be wrong — you must reconcile conflicting reports.
|
| 41 |
+
|
| 42 |
+
You must output a JSON array of actions to investigate and then submit your diagnosis. Available actions:
|
| 43 |
+
{"type":"inspect","target":"logs|config|snippet|metrics"}
|
| 44 |
+
{"type":"ask_specialist","specialist":"runtime|dispatch|kernel|loader"}
|
| 45 |
+
{"type":"apply_fix","fix":"relax_arch_check|add_whitelist_entry|fix_runtime_path|switch_backend|update_model_config|fix_weight_mapping"}
|
| 46 |
+
{"type":"submit","root_cause":"arch_guard|backend_whitelist|runtime_loader|backend_selector|model_config|weight_layout","fix":"relax_arch_check|add_whitelist_entry|fix_runtime_path|switch_backend|update_model_config|fix_weight_mapping","justification":"short explanation of your reasoning"}
|
| 47 |
+
|
| 48 |
+
Rules:
|
| 49 |
+
- You have 6 steps max. Each inspect/ask costs -0.25. Wrong fix costs -2. Wrong submit costs -4 per field.
|
| 50 |
+
- Correct submit: +8 per correct field. Efficiency bonus +2 if solved in ≤4 steps.
|
| 51 |
+
- Justification bonus: +1 if you include a justification (≥10 chars) explaining your reasoning.
|
| 52 |
+
- apply_fix can only be used once per episode.
|
| 53 |
+
- submit MUST be your final action.
|
| 54 |
+
- Minimize investigation steps — be decisive.
|
| 55 |
+
- Always include a justification explaining what evidence led to your diagnosis.
|
| 56 |
+
|
| 57 |
+
Output ONLY a JSON array, e.g.:
|
| 58 |
+
[{"type":"inspect","target":"logs"},{"type":"submit","root_cause":"arch_guard","fix":"relax_arch_check","justification":"Logs show sm_121 rejected by arch check despite being SM90-compatible"}]"""
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def format_scenario_prompt(scenario):
|
| 62 |
+
"""Convert a scenario's initial observation into a user prompt."""
|
| 63 |
+
specialist_text = ""
|
| 64 |
+
for name, op in scenario.specialist_opinions.items():
|
| 65 |
+
specialist_text += f"\n {name} (confidence {op.confidence:.2f}): {op.opinion}"
|
| 66 |
+
|
| 67 |
+
return f"""INCIDENT TICKET:
|
| 68 |
+
{scenario.incident_ticket}
|
| 69 |
+
|
| 70 |
+
HARDWARE: {scenario.hardware}
|
| 71 |
+
MODEL: {scenario.model_name}
|
| 72 |
+
BACKEND: {scenario.backend}
|
| 73 |
+
|
| 74 |
+
LOG EXCERPT:
|
| 75 |
+
{scenario.initial_log}
|
| 76 |
+
|
| 77 |
+
CODE SNIPPET:
|
| 78 |
+
{scenario.initial_snippet}
|
| 79 |
+
|
| 80 |
+
SPECIALIST OPINIONS:{specialist_text}
|
| 81 |
+
|
| 82 |
+
Provide your action plan as a JSON array. End with a submit action."""
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def build_dataset(scenarios, n_repeats=50):
|
| 86 |
+
"""Build training dataset: each scenario repeated n times."""
|
| 87 |
+
data = []
|
| 88 |
+
for _ in range(n_repeats):
|
| 89 |
+
for sc in scenarios:
|
| 90 |
+
data.append({
|
| 91 |
+
"prompt": [
|
| 92 |
+
{"role": "system", "content": SYSTEM_PROMPT},
|
| 93 |
+
{"role": "user", "content": format_scenario_prompt(sc)},
|
| 94 |
+
],
|
| 95 |
+
"scenario_id": sc.id,
|
| 96 |
+
})
|
| 97 |
+
random.shuffle(data)
|
| 98 |
+
return data
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
# ---------------------------------------------------------------------------
|
| 102 |
+
# 3. Reward functions
|
| 103 |
+
# ---------------------------------------------------------------------------
|
| 104 |
+
|
| 105 |
+
def extract_actions(text):
|
| 106 |
+
"""Extract JSON action array from model output."""
|
| 107 |
+
text = text.strip()
|
| 108 |
+
# Try to find JSON array in the text
|
| 109 |
+
start = text.find("[")
|
| 110 |
+
end = text.rfind("]")
|
| 111 |
+
if start != -1 and end != -1 and end > start:
|
| 112 |
+
try:
|
| 113 |
+
actions = json.loads(text[start:end + 1])
|
| 114 |
+
if isinstance(actions, list):
|
| 115 |
+
return actions
|
| 116 |
+
except json.JSONDecodeError:
|
| 117 |
+
pass
|
| 118 |
+
# Try parsing the whole thing
|
| 119 |
+
try:
|
| 120 |
+
actions = json.loads(text)
|
| 121 |
+
if isinstance(actions, list):
|
| 122 |
+
return actions
|
| 123 |
+
return [actions] # single action
|
| 124 |
+
except json.JSONDecodeError:
|
| 125 |
+
return None
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def valid_json_reward(completions, **kwargs):
|
| 129 |
+
"""Reward for producing valid JSON action array."""
|
| 130 |
+
scores = []
|
| 131 |
+
for completion in completions:
|
| 132 |
+
response = completion[0]["content"] if isinstance(completion, list) else completion
|
| 133 |
+
actions = extract_actions(response)
|
| 134 |
+
if actions is None:
|
| 135 |
+
scores.append(-3.0)
|
| 136 |
+
elif not any(a.get("type") == "submit" for a in actions):
|
| 137 |
+
scores.append(-1.0) # no submit = useless
|
| 138 |
+
else:
|
| 139 |
+
scores.append(1.0)
|
| 140 |
+
return scores
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
def environment_reward(completions, scenario_id=None, **kwargs):
|
| 144 |
+
"""Execute action plan against Stack Doctor and return episode reward."""
|
| 145 |
+
scores = []
|
| 146 |
+
scenario_ids = kwargs.get("scenario_id", [None] * len(completions))
|
| 147 |
+
|
| 148 |
+
for i, completion in enumerate(completions):
|
| 149 |
+
response = completion[0]["content"] if isinstance(completion, list) else completion
|
| 150 |
+
actions = extract_actions(response)
|
| 151 |
+
|
| 152 |
+
if actions is None:
|
| 153 |
+
scores.append(-5.0)
|
| 154 |
+
continue
|
| 155 |
+
|
| 156 |
+
sid = scenario_ids[i] if i < len(scenario_ids) else None
|
| 157 |
+
env = StackDoctorEnvironment()
|
| 158 |
+
env.reset(scenario_id=sid)
|
| 159 |
+
|
| 160 |
+
cumulative = 0.0
|
| 161 |
+
for action_dict in actions:
|
| 162 |
+
if not isinstance(action_dict, dict):
|
| 163 |
+
cumulative -= 2.0
|
| 164 |
+
continue
|
| 165 |
+
try:
|
| 166 |
+
obs = env.step(StackDoctorAction(message=json.dumps(action_dict)))
|
| 167 |
+
cumulative += obs.reward
|
| 168 |
+
if obs.done:
|
| 169 |
+
break
|
| 170 |
+
except Exception:
|
| 171 |
+
cumulative -= 2.0
|
| 172 |
+
break
|
| 173 |
+
|
| 174 |
+
scores.append(cumulative)
|
| 175 |
+
|
| 176 |
+
return scores
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
def efficiency_reward(completions, **kwargs):
|
| 180 |
+
"""Bonus for shorter action plans that still submit."""
|
| 181 |
+
scores = []
|
| 182 |
+
for completion in completions:
|
| 183 |
+
response = completion[0]["content"] if isinstance(completion, list) else completion
|
| 184 |
+
actions = extract_actions(response)
|
| 185 |
+
if actions is None:
|
| 186 |
+
scores.append(0.0)
|
| 187 |
+
elif len(actions) <= 2 and any(a.get("type") == "submit" for a in actions):
|
| 188 |
+
scores.append(2.0) # very efficient
|
| 189 |
+
elif len(actions) <= 4 and any(a.get("type") == "submit" for a in actions):
|
| 190 |
+
scores.append(1.0)
|
| 191 |
+
else:
|
| 192 |
+
scores.append(0.0)
|
| 193 |
+
return scores
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
def justification_reward(completions, **kwargs):
|
| 197 |
+
"""Reward for including a justification in the submit action."""
|
| 198 |
+
scores = []
|
| 199 |
+
for completion in completions:
|
| 200 |
+
response = completion[0]["content"] if isinstance(completion, list) else completion
|
| 201 |
+
actions = extract_actions(response)
|
| 202 |
+
if actions is None:
|
| 203 |
+
scores.append(0.0)
|
| 204 |
+
continue
|
| 205 |
+
submit_actions = [a for a in actions if a.get("type") == "submit"]
|
| 206 |
+
if not submit_actions:
|
| 207 |
+
scores.append(0.0)
|
| 208 |
+
continue
|
| 209 |
+
justification = submit_actions[-1].get("justification", "")
|
| 210 |
+
if len(justification.strip()) >= 10:
|
| 211 |
+
scores.append(1.0)
|
| 212 |
+
else:
|
| 213 |
+
scores.append(-0.5)
|
| 214 |
+
return scores
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
# ---------------------------------------------------------------------------
|
| 218 |
+
# 4. Training (Unsloth + TRL GRPO)
|
| 219 |
+
# ---------------------------------------------------------------------------
|
| 220 |
+
|
| 221 |
+
def main():
|
| 222 |
+
from unsloth import FastLanguageModel
|
| 223 |
+
from datasets import Dataset
|
| 224 |
+
from trl import GRPOConfig, GRPOTrainer
|
| 225 |
+
import torch
|
| 226 |
+
|
| 227 |
+
max_seq_length = 2048
|
| 228 |
+
lora_rank = 8
|
| 229 |
+
|
| 230 |
+
# Load model
|
| 231 |
+
model, tokenizer = FastLanguageModel.from_pretrained(
|
| 232 |
+
model_name="unsloth/Qwen3-1.7B",
|
| 233 |
+
load_in_4bit=True,
|
| 234 |
+
max_seq_length=max_seq_length,
|
| 235 |
+
)
|
| 236 |
+
|
| 237 |
+
model = FastLanguageModel.get_peft_model(
|
| 238 |
+
model,
|
| 239 |
+
r=lora_rank,
|
| 240 |
+
target_modules=[
|
| 241 |
+
"q_proj", "k_proj", "v_proj", "o_proj",
|
| 242 |
+
"gate_proj", "up_proj", "down_proj",
|
| 243 |
+
],
|
| 244 |
+
lora_alpha=lora_rank * 2,
|
| 245 |
+
use_gradient_checkpointing="unsloth",
|
| 246 |
+
random_state=42,
|
| 247 |
+
)
|
| 248 |
+
|
| 249 |
+
# Build dataset
|
| 250 |
+
train_data = build_dataset(TRAIN_SCENARIOS, n_repeats=80)
|
| 251 |
+
dataset = Dataset.from_list(train_data)
|
| 252 |
+
|
| 253 |
+
# Compute prompt length for config
|
| 254 |
+
sample_prompt = tokenizer.apply_chat_template(
|
| 255 |
+
train_data[0]["prompt"],
|
| 256 |
+
add_generation_prompt=True,
|
| 257 |
+
tokenize=False,
|
| 258 |
+
)
|
| 259 |
+
max_prompt_length = len(tokenizer.encode(sample_prompt)) + 10
|
| 260 |
+
max_completion_length = max_seq_length - max_prompt_length
|
| 261 |
+
|
| 262 |
+
print(f"Prompt length: ~{max_prompt_length} tokens")
|
| 263 |
+
print(f"Completion budget: ~{max_completion_length} tokens")
|
| 264 |
+
print(f"Dataset size: {len(dataset)} episodes")
|
| 265 |
+
print(f"Train scenarios: {len(TRAIN_SCENARIOS)}, Eval scenarios: {len(EVAL_SCENARIOS)}")
|
| 266 |
+
|
| 267 |
+
# GRPO config
|
| 268 |
+
training_args = GRPOConfig(
|
| 269 |
+
temperature=1.0,
|
| 270 |
+
learning_rate=2e-4,
|
| 271 |
+
weight_decay=0.001,
|
| 272 |
+
warmup_ratio=0.1,
|
| 273 |
+
lr_scheduler_type="linear",
|
| 274 |
+
optim="adamw_8bit",
|
| 275 |
+
logging_steps=1,
|
| 276 |
+
per_device_train_batch_size=1,
|
| 277 |
+
gradient_accumulation_steps=1,
|
| 278 |
+
num_generations=2,
|
| 279 |
+
max_prompt_length=max_prompt_length,
|
| 280 |
+
max_completion_length=max_completion_length,
|
| 281 |
+
max_steps=300,
|
| 282 |
+
save_steps=50,
|
| 283 |
+
report_to="none",
|
| 284 |
+
output_dir="outputs",
|
| 285 |
+
)
|
| 286 |
+
|
| 287 |
+
# Trainer
|
| 288 |
+
trainer = GRPOTrainer(
|
| 289 |
+
model=model,
|
| 290 |
+
processing_class=tokenizer,
|
| 291 |
+
reward_funcs=[
|
| 292 |
+
valid_json_reward,
|
| 293 |
+
environment_reward,
|
| 294 |
+
efficiency_reward,
|
| 295 |
+
justification_reward,
|
| 296 |
+
],
|
| 297 |
+
args=training_args,
|
| 298 |
+
train_dataset=dataset,
|
| 299 |
+
)
|
| 300 |
+
|
| 301 |
+
print("Starting GRPO training...")
|
| 302 |
+
trainer.train()
|
| 303 |
+
|
| 304 |
+
# Save
|
| 305 |
+
model.save_pretrained("stack_doctor_lora")
|
| 306 |
+
tokenizer.save_pretrained("stack_doctor_lora")
|
| 307 |
+
print("Training complete. LoRA saved to stack_doctor_lora/")
|
| 308 |
+
|
| 309 |
+
|
| 310 |
+
if __name__ == "__main__":
|
| 311 |
+
main()
|
uv.lock
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|