Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/prime_rl/dashboard/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ cluster head node, a laptop against a mounted outputs dir:
uv sync --extra dashboard && uv run dashboard [output_dir ...]
```

The trace viewer's **Messages** mode keeps structured `message.content` and
`trace.tools` visibly separate. **Rendered** decodes each selected branch's
recorded post-renderer `token_ids` as one sequence, retaining special tokens;
it never reconstructs a chat template. The recorded IDs remain the source of
truth, and the viewer reports when IDs, the renderer model, or its tokenizer
are unavailable. Text, advantage, logprob, mask, and content signals apply to
both views; Rendered with Text uses the exact full-sequence decode.

This package is fully AI-generated and maintained by agents - it is not meant to be read or edited by humans. Change it by asking an agent, and verify through the browser smoke tests.
The integration suite covers it end to end (`tests/integration/dashboard_smoke.py`
runs after every integration test).
73 changes: 66 additions & 7 deletions src/prime_rl/dashboard/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -767,6 +767,56 @@ def decode_pieces(model: str, ids: list[int]) -> list[str] | None:
return pieces


def trace_node_paths(trace: dict) -> list[list[int]]:
"""Root-to-leaf node indexes in the same order as the trace viewer."""
nodes = trace.get("nodes") or []
has_child = {node.get("parent") for node in nodes if isinstance(node, dict) and isinstance(node.get("parent"), int)}
paths = []
for leaf in (index for index in range(len(nodes)) if index not in has_child):
path = []
seen = set()
index = leaf
while isinstance(index, int) and 0 <= index < len(nodes) and index not in seen:
seen.add(index)
path.append(index)
parent = nodes[index].get("parent") if isinstance(nodes[index], dict) else None
index = parent if isinstance(parent, int) else None
paths.append(list(reversed(path)))
return paths


def rendered_token_text(trace: dict, model: str | None) -> dict:
"""Decode recorded post-renderer IDs as full branch sequences."""
nodes = trace.get("nodes") or []
paths = trace_node_paths(trace)
if not any(isinstance(node, dict) and node.get("token_ids") for node in nodes):
return {"status": "missing_token_ids", "model": model, "paths": []}
if not model:
return {"status": "missing_model", "model": None, "paths": []}
tokenizer = get_tokenizer(model)
if tokenizer is None:
return {"status": "tokenizer_unavailable", "model": model, "paths": []}

def decode_path(path: list[int]) -> dict:
ids = [token_id for index in path for token_id in (nodes[index].get("token_ids") or [])]
try:
text = tokenizer.decode(ids, skip_special_tokens=False)
except Exception:
text = None
return {"nodes": path, "token_count": len(ids), "text": text}

rendered_paths = [decode_path(path) for path in paths]
all_nodes = list(range(len(nodes)))
all_nodes_rendered = decode_path(all_nodes)
status = "ok" if all(path["text"] is not None for path in rendered_paths + [all_nodes_rendered]) else "decode_error"
return {
"status": status,
"model": model,
"paths": rendered_paths,
"all_nodes": all_nodes_rendered,
}


@app.get("/api/runs/{run}/rollouts/{step}/{kind}/{subset}/series")
def episode_series(run: str, step: int, kind: str, subset: str, etag: str | None = None, after: int = 0) -> dict:
"""Per-episode series over a traces file (x = episode order): reward, shape, and the
Expand Down Expand Up @@ -797,25 +847,34 @@ def value(s: dict, key: str):


@app.get("/api/runs/{run}/rollouts/{step}/{kind}/{subset}/{line}")
def get_episode(run: str, step: int, kind: str, subset: str, line: int, tokens: bool = False) -> dict:
def get_episode(
run: str,
step: int,
kind: str,
subset: str,
line: int,
tokens: bool = False,
rendered: bool = False,
) -> dict:
path = traces_path(run, step, kind, subset)
offsets = line_offsets(path)
if not 0 <= line < len(offsets):
raise HTTPException(404, "episode line out of range")
with path.open("rb") as f:
f.seek(offsets[line])
rec = orjson.loads(f.readline())
if not tokens:
if not tokens and not rendered:
return rec
fallback_model = model_name(main_config(get_run_dir(run))[1])
for trace in rec.get("traces") or []:
client = ((trace.get("agent") or {}).get("config") or {}).get("client") or {}
model = client.get("renderer_model_name") or fallback_model
if not model:
continue
for node in trace.get("nodes") or []:
if node.get("token_ids"):
node["token_strs"] = decode_pieces(model, node["token_ids"])
if tokens and model:
for node in trace.get("nodes") or []:
if node.get("token_ids"):
node["token_strs"] = decode_pieces(model, node["token_ids"])
if rendered:
trace["rendered_tokens"] = rendered_token_text(trace, model)
return rec


Expand Down
Loading