Skip to content

feat: log toolcall and thinktag violation rate into single controller - #3765

Merged
yfw merged 6 commits into
mainfrom
arnavk/violation_metrics
Aug 29, 2026
Merged

feat: log toolcall and thinktag violation rate into single controller#3765
yfw merged 6 commits into
mainfrom
arnavk/violation_metrics

Conversation

@arnavk-nvidia

@arnavk-nvidia arnavk-nvidia commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

What does this PR do ?

Logs metrics of toolcall and think tag violation rate in single controller similar to the one existing in grpo.py.
In single controller the message log gets dropped hence the metrics are calculated in payload itself.

Issues

List issues that this PR closes (syntax):

Usage

  • You can potentially add a usage example below
# Add a code snippet demonstrating how to use this

Before your PR is "Ready for review"

Pre checks:

  • Make sure you read and followed Contributor guidelines
  • Did you write any new necessary tests?
  • Did you run the unit tests and functional tests locally? Visit our Testing Guide for how to run tests
  • Did you add or update any necessary documentation? Visit our Document Development Guide for how to write, build and test the docs.

Additional Information

  • ...

@arnavk-nvidia
arnavk-nvidia requested review from a team as code owners August 21, 2026 23:44
@copy-pr-bot

copy-pr-bot Bot commented Aug 21, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@arnavk-nvidia
arnavk-nvidia requested a review from yfw August 21, 2026 23:44
@arnavk-nvidia arnavk-nvidia added the CI:Lfast Runs a fast test suite and re-use nightly `main` container (but sync dependencies to PRs version) label Aug 21, 2026
@arnavk-nvidia arnavk-nvidia changed the title adding tool-call and think-tag violation rate into single controller feat: log toolcall and thinktag violation rate into single controller Aug 21, 2026

@arnavk-nvidia arnavk-nvidia left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Automated review found one bug that will fail CI.

Generated by Claude Code

Comment thread tests/unit/single_controller/test_sc_utils_helpers.py Outdated
Signed-off-by: Arnav Kundu <arnavk@nvidia.com>
…p_metrics test

Positional [0] was landing in the seq_logprob_error_metrics parameter
instead of num_invalid_tool_calls, causing a TypeError instead of the
asserted empty dict.

Signed-off-by: Arnav Kundu <arnavk@nvidia.com>
@arnavk-nvidia
arnavk-nvidia force-pushed the arnavk/violation_metrics branch from 76b70ce to c51e9ff Compare August 28, 2026 16:42
@arnavk-nvidia

Copy link
Copy Markdown
Contributor Author

/ok to test c51e9ff

@yfw yfw left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for closing this gap — the SC path had no violation telemetry while the sync GRPO path did, and riding the documented tags sidecar with metric names matching grpo.py is the right shape for it. Nice quick turnaround on the keyword-arg fix in c51e9ff too.

2 action items, 1 follow-up. Findings re-verified against c51e9ff after the force-push.

The branch is still red on tests it doesn't touch. addopts carries -x, so a plain run stops at the first failure and hides the rest:

$ uv run --group test pytest tests/unit/single_controller/ tests/unit/experience/ -q --maxfail=0
7 failed, 702 passed

AI-1 — _train_pump_controller needs the new _step_log_dict keys

All 7 remaining failures are test_train_pump_* / test_advantage_stage_* in tests/unit/single_controller/test_single_controller_actor.py — a file this PR doesn't touch — and they share one cause:

    for tag in meta.tags or []:
        for key in VIOLATION_TAG_KEYS:
>           self._step_log_dict[key].append(int(tag.get(key, 0)))
E           KeyError: 'num_invalid_tool_calls'
nemo_rl/algorithms/single_controller.py:1967: KeyError

The new drain loop hard-indexes _step_log_dict. Production seeds every key at single_controller.py:313-319, but _train_pump_controller hand-builds it with four at line 1112, and its metas carry tags, so the loop actually runs. That file is unchanged from main, so this is a regression from the production change rather than a stale test.

Action: add the keys to the fixture, plus the import. Verified locally — 45 passed on that file, from 7 failed.

# tests/unit/single_controller/test_single_controller_actor.py
from nemo_rl.experience.payload import VIOLATION_TAG_KEYS
...
    ctrl._step_log_dict = {
        "rewards": [],
        "masked_advantages": [],
        "sequence_lengths": [],
        "seq_logprob_error_metrics": [],
        **{key: [] for key in VIOLATION_TAG_KEYS},
    }

Please fix in this PR — CI stays red otherwise. Worth fixing in the fixture rather than by moving the drain loop below the _advantage_estimator is None early-out: the other _advantage_stage tests pass only because their metas have no tags, which is a fragile reason to be green.

AI-2 — one comment on VIOLATION_TAG_KEYS

VIOLATION_TAG_KEYS is now load-bearing in four independent places: the tuple itself, the __init__ seed, the reduce_advantage_pump_metrics signature, and that test fixture. Nothing links them statically — the **self._step_log_dict splat at single_controller.py:1081 is opaque to both ruff and pyrefly. The KeyError above is one failure mode; adding a 4th key to the tuple without editing the utils.py signature is the other, and it surfaces as TypeError: ... unexpected keyword argument at the first train step.

Action: a one-line comment on the tuple ("keep in sync with reduce_advantage_pump_metrics kwargs"). Explicitly not asking for a refactor — collapsing the three kwargs into a single dict parameter moves the complexity rather than removing it, and it would break the deliberate metric-name parity with grpo.py that motivates this PR.

Follow-up — an assertion on the drained counts

Nothing currently asserts what the drain loop does: the other _advantage_stage tests build metas without tags, so it runs zero iterations, and the 7 above only enter it incidentally. Since you're editing that fixture anyway, an assertion on the drained counts would also pin the tag.get(key, 0) default for rows enqueued before this feature — the rolling-upgrade case. Fine as a follow-up if you'd rather keep this PR tight.

Context — no action

ruff check and pyrefly check are clean on all changed files. We looked at these and are explicitly not asking for them: popping the non-tensor violation_counts key out of the BatchedDataDict (it's inert — pack_payload filters on isinstance(v, torch.Tensor) and the batch is dropped right after); using stamp_tags instead of pack_payload (wrong lifecycle — stamp_tags is for scalars derived later in the train loop); and unifying _violation_counts with grpo's counting loop (that loop also writes penalty advantages and threads token_offset, so merging them would be over-abstraction).

Generated by Claude Code

Comment thread nemo_rl/algorithms/single_controller_utils/utils.py
Comment thread nemo_rl/algorithms/single_controller_utils/utils.py
Comment thread tests/unit/experience/test_payload.py
…cstring

Signed-off-by: Arnav Kundu <arnavk@nvidia.com>
@arnavk-nvidia

Copy link
Copy Markdown
Contributor Author

/ok to test 8c2ba3d

Test doubles that hand-construct _step_log_dict (bypassing __init__)
predate the violation-count keys, so _advantage_stage raised KeyError
on them. Use setdefault instead of a direct index.

Also updates test_rollout_pump_writes_expected_tq_data's tag-schema
assertion, which asserted the old weight_version-only tag shape and no
longer matches pack_payload's output now that it stamps violation
counts too.

Signed-off-by: Arnav Kundu <arnavk@nvidia.com>
@arnavk-nvidia

Copy link
Copy Markdown
Contributor Author

/ok to test af2c889

yfw added 2 commits August 28, 2026 14:50
Signed-off-by: Yi-Fu Wu <yifu.wu@gmail.com>
Signed-off-by: Yi-Fu Wu <yifu.wu@gmail.com>
@yfw

yfw commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

/ok to test 59fb697

@yfw
yfw enabled auto-merge (squash) August 28, 2026 21:51
@yfw
yfw merged commit c76ee60 into main Aug 29, 2026
86 checks passed
@yfw
yfw deleted the arnavk/violation_metrics branch August 29, 2026 00:42
asolergi-nv added a commit that referenced this pull request Aug 29, 2026
PR3 (#3591) was SQUASH-merged into main as b3b6713, so none of its commits are
ancestors of main while PR4 still carries all of them. Git therefore sees PR3's whole
diff as independently added on both sides, which is why all 16 conflicts name b3b6713
and why the PR showed CONFLICTING despite the content being identical.

That made the classification, not the content, the work. For each conflicted file: is
main's version byte-identical to PR3's head (3d9ce21), and does PR4 add anything beyond
it? Three groups fell out.

GROUP A -- pure squash artefacts, resolved by taking OURS (10 files)
  fleet_health.py, collective_weight_synchronizer.py, membership.py,
  nccl_reshard_weight_synchronizer.py, grpo_sc_generation_shard_recovery.sh,
  test_watchdog_pump.py, test_membership.py, test_reconcile_communicator.py,
  test_reshard_rebuild.py, test_weight_synchronizer.py
  main == PR3 exactly and no other PR touched them, so PR4's side is main's content plus
  PR4's delta. Taking ours loses nothing.

GROUP B -- PR4 contributes nothing, resolved by taking THEIRS (2 files)
  single_controller_utils/setup.py  (#3480, #3727, #3821 on top of PR3)
  tests/unit/single_controller/test_refit_recovery.py  (#3480 on top of PR3)

GROUP C -- genuine merges (4 files), one per upstream PR below.

The six upstream PRs that contributed real content, and what each needed:

  #3480 recover replay buffer from native TQ checkpoints
        single_controller.py: rollout_recovery imports. Kept alongside ours.
        setup.py, test_refit_recovery.py, L1 harness: group B / additive.
  #3765 log toolcall and thinktag violation rate
        single_controller.py: VIOLATION_TAG_KEYS. Auto-merged, verified present.
  #3727 support non-colocated MInf
        single_controller.py: MegatronGeneration import, kept alongside ours.
        L1 harness: grpo_megatron_generation_gym_single_controller.sh entry.
  #3821 warm-start the value model from a critic-pretrain checkpoint
        config.py: the max_num_epochs validator. Ours only adds restart_dead_shards to
        FleetHealthConfig, so both survive; verified the field landed in the right class
        and the validator is intact.
  #3655 nemo-lens telemetry
        vllm_generation.py: the @trace_fn decorator on generate. Ours adds restart_shard
        in a different region; both kept.
  #3839 pause generation during in-flight refit
        vllm_generation.py: pause_generation_for_refit / resume_generation_after_refit.
        Auto-merged, verified present -- worth knowing it exists, since it pauses engines
        around a refit and this PR restarts them.

Verified after resolving: no conflict markers; all four lint hooks clean (the single
pyrefly error is the pre-existing unrelated transfer_queue import); 1122 unit tests pass;
both submodule pointers and uv.lock/pyproject byte-identical to main.

Both sides' work was checked individually rather than assumed: EngineSupervisor wiring,
restart_dead_shards, restart_shard, recreate_worker, desired_membership and the report_refit
call on our side; the six items above on main's.

Note for anyone reproducing locally: #3655 adds a nemo-lens dependency that the pre-merge
container image does not carry, so tests fail at import with ModuleNotFoundError: nemo
until the venv is refreshed. Plain upstream/main fails the same way in that image; it is
not a merge defect.

Signed-off-by: asolergibert <asolergibert@nvidia.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CI:Lfast Runs a fast test suite and re-use nightly `main` container (but sync dependencies to PRs version)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants