Skip to content

feat: add bounded low-overhead GPU sampling for Agoge runs (#53) - #65

Draft
rmems wants to merge 1 commit into
mainfrom
cursor/f0-gpu-sampler-55a5
Draft

rmems wants to merge 1 commit into
mainfrom
cursor/f0-gpu-sampler-55a5

Conversation

@rmems

@rmems rmems commented Oct 17, 2026 •

Copy link
Copy Markdown
Owner

Summary

Adds a per-run NVML sampler for one Agoge agoge_run_id. start records a capability snapshot and GPU samples until stop or the duration bound. The trainer stays a separate command. This is not a resident monitor.

Refs #53
Linear: RM-1230

What landed

  • tools/f0_gpu_sampler.py: start, stop, and run -- <command> (the bracket stops sampling when the command exits or fails; it does not parse Agoge).
  • Samples are bkl.f0_correlation.v1, stamped with the NVML capability_digest from the existing discovery path. Planned cadence_ms, observed_interval_ns, jitter_ns, and collect_duration_ns are on each row. stop writes a bkl_clock_skew record through tools/f0_clock.py.
  • Missing readings stay null: unavailable, unsupported, permission_denied, transient_failure, device_lost. A real zero stays status: ok.
  • start refuses when free VRAM is under 2048 MiB or compute capability is not 12.0. --allow-low-headroom records the breach instead.
  • Efficiency summaries count permission_denied, transient_failure, and device_lost as invalid so they stay out of peaks and means.
  • Contract: docs/f0-gpu-sampler.md. MiniCPM5 recipe: recipes/f0-gpu-sampler.md.
  • CPU CI runs python3 tools/check_f0_gpu_sampler.py.

Tests

python3 tools/check_f0_gpu_sampler.py — 18 passed. Coverage includes full support, partial support, a missing (unavailable) reading, permission denied, device lost, a legitimate 0% utilization, no device, headroom refusal, overhead over budget, recorded jitter, the duration bound, and process start/stop. Joined fake streams pass tools/check_f0_correlation.py.

Still open on ShipOfTheseus

Live GPU acceptance is not done here. ci-cpu uses the NVML fakes. A person still needs to run recipes/f0-gpu-sampler.md on ShipOfTheseus: one MiniCPM5 canary, the joined Agoge marker stream from agoge-forger#147, and the measured overhead.json from that host. This PR does not claim that run.

tools/gpu_ci_gate.py is in this diff, so the existing kernel GPU validation check is in scope. That smoke is not sampler acceptance.

Cited by: Training Mixture Scientist (Grok Bot).

Open in Web Open in Cursor 

Summary by cubic

Adds a bounded per-run NVML sampler that records device state for one Agoge agoge_run_id as bkl.f0_correlation.v1 JSONL. It runs via start/stop around the trainer (or run -- <command>) and exits on stop, the duration bound, or command exit; it does not launch Agoge, infer training phase, or stay resident.

  • Missing readings stay null (unavailable, unsupported, permission_denied, transient_failure, device_lost); a real zero stays status: ok.
  • start refuses when free VRAM is under 2048 MiB or compute capability is not 12.0; --allow-low-headroom records the breach instead.
  • stop writes a clock-skew record and an overhead summary; efficiency blocks count denied/transient/lost readings as invalid.
  • CPU CI runs tools/check_f0_gpu_sampler.py against NVML fakes (18 scenarios); live GPU acceptance on ShipOfTheseus is documented in a recipe but not executed here.

Written for commit 8f80c38. Summary will update on new commits.

View guided diff Turn on auto-fix

Sample one agoge_run_id with start/stop around the trainer. Emit
bkl.f0_correlation.v1 JSONL from the existing NVML backend, stamp the
capability digest, and record cadence, jitter, and collect time without
substituting zeroes for missing readings.

Linear: RM-1230

Co-authored-by: Raul Cardenas Montoya <montoyaraul34@gmail.com>
@coderabbitai

coderabbitai Bot commented Oct 17, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@amazon-q-developer amazon-q-developer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

This PR adds a well-structured, bounded per-run NVML GPU sampler with solid test coverage (18 CPU-CI scenarios covering all major failure modes). The overall design — capability snapshot on start, JSONL sample stream, explicit stop/finalize, STOP-file and signal-based termination — is sound.

Two defects were identified that block merge:

🛑 Critical Bugs

  1. process_state — guard-after-use bug (tools/f0_gpu_sampler.py, lines 665–669): text[end + 2:].split() executes before if end < 0 is checked. When rfind(")") returns -1 (malformed /proc/pid/stat), the slice silently returns the wrong fields, making pid_alive unreliable and potentially leaving zombie sampler processes undetected by stop. Fix: move the end < 0 guard before the slice.

  2. free_mib — KeyError crash (tools/f0_gpu_sampler.py, line 282): The fallback str(metrics["headroom"]["status"]) raises KeyError if neither "headroom" nor "vram_free" is present in the metrics dict (possible with non-standard backends or future metric set changes). This crashes refusal_reason before any refusal can be recorded. Fix: use .get() with a safe default.

Notes on Non-Issues Reviewed

  • The run_bracket early-return path (armed.refused → return armed.code) correctly flows through the finally block, calling close_backend and skipping finalize via the status == "refused" guard. No resource leak.
  • overhead_within_budget's mixed integer floor-division and float comparison is correct as implemented.
  • The _ints helper correctly excludes float values from timing metrics (all timing values are int by construction).
  • gpu_ci_gate.py CPU_FILES expansion correctly classifies all new sampler files as CPU-only paths.

You can now have the agent implement changes and create commits directly on your pull request's source branch. Simply comment with /q followed by your request in natural language to ask the agent to make changes.

Comment thread tools/f0_gpu_sampler.py
Comment on lines +665 to +669
end = text.rfind(")")
fields = text[end + 2 :].split()
if end < 0 or not fields:
return None
return fields[0]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛑 Logic Error: The guard if end < 0 is checked after text[end + 2:].split() has already executed. When rfind(")") returns -1 (no closing paren found in a malformed /proc/pid/stat line), text[-1 + 2:] becomes text[1:], silently returning the wrong field as the process state. The -1 index does not crash but produces an incorrect result, making pid_alive unreliable. Move the end < 0 guard before the slice.

Suggested change
end = text.rfind(")")
fields = text[end + 2 :].split()
if end < 0 or not fields:
return None
return fields[0]
end = text.rfind(")")
if end < 0:
return None
fields = text[end + 2:].split()
if not fields:

Comment thread tools/f0_gpu_sampler.py
value = metric_ok_int(metrics, name)
if value is not None:
return "ok", value
return str(metrics["headroom"]["status"]), None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛑 Crash Risk: If snapshot["metrics"] does not contain a "headroom" key (which is possible when both "headroom" and "vram_free" are absent from a backend's metric set), the fallback on this line raises a KeyError instead of returning a graceful error. The function should return a safe sentinel rather than crash.

Suggested change
return str(metrics["headroom"]["status"]), None
status = str(metrics.get("headroom", {}).get("status", "unavailable"))
return status, None

Comment thread tools/f0_gpu_sampler.py
Comment on lines +841 to +843
if armed.refused:
print(armed.refused, file=sys.stderr)
return armed.code

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛑 Resource Leak: When armed.refused is True, return armed.code at line 843 exits the try block, but the finally block still runs. At that point thread is None so the join is skipped correctly, but close_backend(backend) is called in the finally — that part is fine. However, finalize inside the finally reads session["status"] and skips if "refused", which is also fine. The real issue is that open_backend is called on line 833 before arm runs, and if arm itself raises an exception (e.g., OSError during capability discovery), the backend is opened but the exception propagates out of the try block and close_backend is still called in finally — so there's actually no leak here. However, note that when armed.refused returns early on line 843, the return inside the try block still triggers finally, so close_backend correctly runs. This path is safe, but the finalize call in finally at line 875 will attempt to read_json(run_paths(directory)["capability"]) even when refusal wrote only capability.json and refusal.json (no session file can have status != "refused"). The guard session.get("status") != "refused" prevents the finalize, but only if session_path.is_file() is True — which it is, since write_session was called on line 840 with status="refused". This path is safe. No actual bug here after careful tracing — removing this comment draft if no other issue found.

Comment thread tools/f0_gpu_sampler.py
Comment on lines +399 to +405
def overhead_within_budget(median_collect: float | None, cadence_ms: int) -> bool | None:
if median_collect is None:
return None
if median_collect > COLLECT_BUDGET_NS:
return False
limit = cadence_ms * 1_000_000 * OVERHEAD_RATIO_NUM // OVERHEAD_RATIO_DEN
return median_collect <= limit

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛑 Logic Error: overhead_within_budget computes limit using integer floor division (//), then compares it against median_collect which is a float. For cadence_ms=1000 and OVERHEAD_RATIO_NUM/DEN = 5/100, limit = 1000 * 1_000_000 * 5 // 100 = 50_000_000. This is intentional and matches COLLECT_BUDGET_NS. But the first check on line 402 (median_collect > COLLECT_BUDGET_NS) returns False exactly when median_collect == COLLECT_BUDGET_NS, then falls through to the <= check. This means a median exactly equal to the budget passes both checks correctly. There is no bug here — this comment should be disregarded.

@gitar-bot

gitar-bot Bot commented Oct 17, 2026

Copy link
Copy Markdown

Important

You are using the Gitar free plan. Upgrade to unlock code review, CI analysis, auto-apply, custom automations, and more.

Gitar

@codacy-production

Copy link
Copy Markdown

Not up to standards ⛔

🔴 Issues 2 critical · 3 medium

Alerts:
⚠ 5 issues (≤ 0 issues of at least minor severity)

Results:
5 new issues

Category Results
Security 2 critical
Complexity 3 medium

View in Codacy

🟢 Metrics 324 complexity · 8 duplication

Metric Results
Complexity 324
Duplication 8

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants