[DRAFT] refactor(sc): interface-driven staleness samplers (design demo for #3220)#3316
Merged
Merged
Conversation
terrykong
force-pushed
the
terryk/sc-sampler-interface-draft
branch
from
July 23, 2026 00:12
9d325a7 to
d7ee364
Compare
Replace the monolithic StalenessSampler (five mode-encoding flags, three disjoint select branches, a shared evict fitting only two of the modes) with a PromptGroupSampler interface — admit/select/evict + is_on_policy / required_buffer_capacity — and one named policy per behavior: WindowedSampler / WeightFifoSampler / InOrderSampler. The sampler is selected by a pydantic discriminated-union config (name = windowed|weight_fifo|in_order|custom) or a module:ClassName FQN, so invalid knob combinations are unrepresentable and there are no cross-field validations in __init__. admit() moves the rollout-pump dispatch gate + target_step stamping into the sampler (the pump follows whichever policy is selected); InOrderSampler keys evict on target_step so evict/select agree by construction. No legacy-knob path: over_sampling / force_in_order / batch_selection_strategy / max_weight_staleness_versions and warn_if_staleness_window_below_minibatches are removed; the actor no longer owns _max_rollout_version. test_staleness_sampler (monolith) is replaced by test_sampler_interface. DRAFT / design demonstration for the #3220 sampler discussion. Not merged; not run locally (no compatible env). Signed-off-by: Terry Kong <terryk@nvidia.com>
terrykong
force-pushed
the
terryk/sc-sampler-interface-draft
branch
from
July 23, 2026 01:50
d7ee364 to
4e06412
Compare
RayenTian
marked this pull request as ready for review
July 23, 2026 06:37
RayenTian
pushed a commit
that referenced
this pull request
Jul 23, 2026
) (#3316) Signed-off-by: Terry Kong <terryk@nvidia.com>
RayenTian
pushed a commit
that referenced
this pull request
Jul 23, 2026
) (#3316) Signed-off-by: Terry Kong <terryk@nvidia.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Design demonstration for the sampler-refactor discussion on #3220 (the
StalenessSamplerinterface-driven split). This is a strawman to look at, built on top of #3220's head so the diff is just the refactor. Not tested locally (macOS/no-GPU, Linux-only lockfile) — treat every behavior claim as static-only.Posting this instead of a wall of review text so the shape is concrete. Below is before/after for each touched section; permalinks point at the surrounding code (before = base
d3188b24b, after = this branch1398b010).1. Sampler: one flag-driven class → an interface + one policy per behavior
Before —
staleness_sampler.py:21-53:StalenessSampler, five mode-encoding flags whose valid combinations are policed at runtime;selectis three disjoint branches; the sharedevictonly fits two of the three modes.After —
PromptGroupSamplerinterface + one named policy each (WindowedSampler,WeightFifoSampler,InOrderSampler). The three new members (admit, and theis_on_policy/required_buffer_capacityderived facts) are the seam that lets the pumps and validation stop reading raw knobs.Constructor contract (methods only — a
runtime_checkableProtocol can't express__init__):create_samplerbuilds every policy asSampler(buffer, **extra)— the sharedTQReplayBufferis the first positional arg. This is documented on the Protocol, and the custom-FQN branch wraps construction so a sampler that doesn't takebuffer-first fails with a clear message rather than an opaqueTypeError; subclassingBaseSamplersatisfies it.Two review findings get fixed by construction: the evict/select asymmetry (r3617016004 —
InOrderSampler.evictkeys ontarget_step, matching itsselect) and the deadsample_freshest_first(r3617016008 — it exists only onWindowedSampler).2.
SingleControllerConfig: four indirect knobs → one discriminatedsamplerBefore —
single_controller.py:107-125After —
single_controller.py:119-126SamplerConfigis a pydantic discriminated union;name: custom+target: "module:ClassName"loads an out-of-repo sampler, mirroring the existingcreate_checkpoint_engineconvention (nemo_rl/utils/checkpoint_engines/base.py). That's the concrete shape for the custom-sampler support tracked in #2625 item 3.3.
SingleControllerActor.__init__: runtime knob-policing → derived-fact checkBefore —
single_controller.py:271-288(coerce one mode, raise on two invalid knob combos) then:329-336(warn, then translate knobs into the sampler):After —
single_controller.py:260-276: the union makes those states unrepresentable, so the coercion/raises/warn_if_...are gone; the one remaining check reads a fact the sampler owns, so it can't drift from the admission logic:warn_if_staleness_window_below_minibatches(~50 lines) is deleted outright.4.
_rollout_pump: inline gate + actor-owned counter →sampler.admit()Before —
single_controller.py:499-510: the dispatch gate, thetarget_stepderivation, and the_max_rollout_versioncounter all live in the pump (a second copy of the sampler's staleness logic):After —
single_controller.py:445-449: the sampler owns admission and the dispatch counter; the pump follows whichever policy is selected with no gating logic of its own (self._max_rollout_versionis deleted from the actor):No legacy path
Per the discussion, this drops the legacy knobs entirely rather than keeping a shim — the sampler config's
nameis the only switch. Removed: the four config knobs above; thestrict_on_policycoercion + two raises;warn_if_staleness_window_below_minibatches;_max_rollout_versionon the actor; and the monolithicStalenessSampler+ its 526-line test (replaced bytest_sampler_interface.py).Prior art
async_training.staleness_threshold, interpreted by the producer only — same "one owner for staleness" lesson, but a scalar can't express in-order exact matching, and there's no pluggable sampler.buffer_filter = load_function(args.buffer_filter_path)— direct precedent for the FQN hook, but its staleness control is disconnected from the filter, so a custom filter can't change dispatch. Theadmit()-owning policy here is strictly stronger.Not done (draft)
test_sampler_interface.py(and the updated pump tests) are written to the intended contract but unverified.is_on_policyhas no consumer at feat(sc): train path — StalenessSampler + _train_pump rewrite #3220's head (theinvalidate_kv_cachecall lands in feat(sc): setup + entrypoint #3266); included to show the derived-fact seam.async_rl.sampler:block (not done here).🤖 Generated with Claude Code