Repository navigation
perf(data_plane): cheaper TQ kv_retrieve_meta and clear on the controller - #4769
Conversation
90a1e54 to
0553320
Compare
44596f9 to
6f7af4b
Compare
|
/ok to test ed27504 |
…oller kv_retrieve_meta reads a torch tensor once per field (col_mask[col_idx]). On the single-threaded TQ controller that is about a quarter of a put+get cycle. Build the field mask with one .all(dim=0).tolist() instead. The controller is a Ray actor, so the method is overridden in a controller subclass started by tq.init. The override is a plain copy of the stock method; a sha256 of the stock source makes it fail loudly when the TQ pin moves. Measured on one controller (real ZMQ, 20k live rows): +15% saturation throughput (5.6k -> 6.5k req/s) over geometric growth alone. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Zhiyu Li <zhiyul@NVIDIA.com>
…module Move the in-function ray and transfer_queue.interface imports to module top and add the module to pyrefly project-includes. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Zhiyu Li <zhiyul@oci-aga-slurm-1-dm-02.cm.cluster>
FieldMeta.remove_samples rescans every remaining shape of a nested field on each clear, so CLEAR_META grows with the number of live samples. Override it in the controller subclass to rescan only after an eighth of the live samples are gone. A source hash of the stock method fails loudly when the TQ pin moves, as for kv_retrieve_meta. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Zhiyu Li <zhiyul@oci-aga-slurm-1-dm-02.cm.cluster>
…s and a hook test Format both files with the repo's ruff and sort the test imports, which the pre-commit lint-check job requires. Add docstrings to the module's public functions, and a test that the patched controller class carries the kv_retrieve_meta override and installs FieldMeta.remove_samples when started. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Zhiyu Li <zhiyul@oci-aga-slurm-1-dm-02.cm.cluster>
Replace the literal 8 in remove_samples with RESCAN_REMOVED_FRACTION = 1 / 8. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Zhiyu Li <zhiyul@oci-aga-slurm-1-dm-02.cm.cluster>
…ource hashes The two sha256 source hashes tripped the secrets detector as high-entropy hex strings. Check transfer_queue.__version__ against STOCK_TQ_VERSION instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Zhiyu Li <zhiyul@oci-aga-slurm-1-dm-02.cm.cluster>
ed27504 to
fc9191b
Compare
|
/ok to test fc9191b |
terrykong
left a comment
There was a problem hiding this comment.
Thanks for the careful benchmarks and the clear write-up. This review ran both overrides against the stock TQ methods at the pinned commit: the field mask on empty, missing and repeated keys, 300 random clear/add sequences for remove_samples, a real Ray actor, and controller checkpoints loaded in both directions. The results matched every time. No correctness problems found. The CI run on fc9191b tested the current head, and L0 runs all 6 new tests.
Inline:
- (C1) Add tests for the copied
create=Truebranch and for callingconfigure_controller_patch()twice. Please add these in this PR. - (C2) Open the upstream TQ PR, link it as the condition for deleting the module, and reword the version error.
- (C3) Use TQ's
get_loggerin the copy. Low priority.
PR description — please update:
- It still describes a sha256 check of each stock method and two hash tests. The head commit fc9191b replaced both with the
transfer_queue.__version__check. It also says four commits; there are six. - The "+41% vs head" row includes a geometric-growth patch that is not in this PR or in NeMo-RL. Please add a "head + this patch" row (the in-process profile is enough).
- Please footnote that the 300k
remove_samplesrow never reaches a rescan: 2000 clears × 16 rows = 32k removed, which is under 268k ÷ 8. So its steady state is about 0.02 ms, not 0.003 ms. The conclusion still holds.
On the naming question: tq_controller_patch.py fits better. Both overrides run on the controller, the entry point is already configure_controller_patch, and it matches tq_mooncake_checkpoint.py.
CI: gb200_L1_Functional_Tests_SFT failed because the runner lost contact with the server, so no test ran. "CI quality check" failed only because of that job. A rerun of that job should clear both.
Visual explainer: https://terrykong.github.io/gh-pages-poc/terryk/pr-4769-cheaper-controller.html
Generated by Claude Code
terrykong
left a comment
There was a problem hiding this comment.
generally lgtm module the nits
… create and re-config Review on #4769: - "tq_controller_patch.py fits better": rename the module and its test. - C3 "Use TQ's get_logger in the copy": TQ_LOGGING_LEVEL controls the copied log lines again. - C2 "reword the version error": say when to delete the module and when only to bump STOCK_TQ_VERSION, and name the upstream change in the module docstring. - C1 "Add tests for the copied create=True branch and for calling configure_controller_patch() twice". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Zhiyu Li <zhiyul@oci-aga-slurm-1-dm-02.cm.cluster>
612cdfc
|
/ok to test 612cdfc |
What
Two cheaper paths on the single-threaded TransferQueue controller. Both are overrides in a controller subclass, because the controller is a Ray actor.
kv_retrieve_metareads a torch tensor once per field (col_mask[col_idx]). Each read costs microseconds, which is about a quarter of a put+get cycle. The override builds the field mask with one.all(dim=0).tolist(). Results are identical.FieldMeta.remove_samplesrescans the shape of every remaining sample of a nested field on each clear, only to see whether the field can collapse to non-nested. The cost grows with the number of live samples, soCLEAR_METAslows down late in a run. The override rescans only after the field has lostRESCAN_REMOVED_FRACTION(1/8) of its remaining samples. In a large GRPO run (about 131k rollouts per step),CLEAR_METAaveraged 0.3 s and rose to 1.3 s late in the run, andNOTIFY_DATA_UPDATEacknowledgements queue behind it.tq.initstarts the subclass:configure_controller_patch()runs right before it. Each override is a plain copy of the stock method.PatchedController.__init__applies theFieldMetaoverride inside the controller process only, so the driver keeps stockFieldMeta. Building the subclass checkstransfer_queue.__version__againstSTOCK_TQ_VERSION(0.1.11, the pinned commit), so a TQ pin bump fails loudly instead of silently running a stale copy. The error says what to do: delete the module if the new TQ has the upstream change, otherwise diff the two methods and bumpSTOCK_TQ_VERSION.Experiment results:
kv_retrieve_metaIn-process cycle cost at the pinned TQ, without any other controller patch. One cycle is
kv_retrieve_meta(create)for one new key,update_production_statuswith 16 fields,kv_retrieve_meta(get), andset_custom_meta. 1 torch thread, 20k live rows, 3000 cycles per run, 7 interleaved runs, median:The earlier ZMQ benchmark below also included a geometric table-growth patch, which is not in this PR or in NeMo-RL. It shows the effect on top of that patch.
ZMQ benchmark with the growth patch (96 clients, 4 requests per cycle)
One TQ controller (1 torch thread, as a
num_cpus=1Ray actor gets), real ZMQ, 96 closed-loop client processes, 20k live rows, 16 fields. Each client cycle isKV_RETRIEVE_META(create),NOTIFY_DATA_UPDATE,KV_RETRIEVE_META(get),SET_CUSTOM_META. 15 s measured window per run, 2 repetitions, uncapped 90-core node.This run used the earlier mask form (
sum(dim=0) == n, then.tolist()). The PR uses.all(dim=0).tolist(), which gives identical results on 0/1 status.Experiment results:
FieldMeta.remove_samplesOne nested field with all-distinct shapes (the worst case), 16-row clears, 2000 clears per size, TQ at the pinned commit:
* 2000 clears × 16 rows removes 32k samples, which is under 268k ÷ 8, so this row never reaches a rescan. Its steady state is about 0.02 ms, like the 100k row.
These are per field, and a recipe has several nested fields. A rescan still costs O(live samples), but it runs once per eighth of the samples removed, so the average cost per clear stays constant. A field with few samples still rescans on every clear, because one removal is already an eighth of what is left.
Behavior change
A nested field whose shapes have become uniform can stay nested until the next rescan. Readers handle both forms:
BatchMeta.get_shapesusesper_sample_shapeswhen present and the singleshapeotherwise.FieldMetais pickled bysave_checkpoint, soremoved_since_scanbecomes part of a controller checkpoint. It is a class attribute with default 0, so checkpoints load in both directions.Tests
tests/unit/data_plane/test_tq_controller_patch.pyhas 8 tests:STOCK_TQ_VERSION, and another version is rejected.kv_retrieve_metareturns the same indexes and fields as stock on partial-field, missing-key, and empty-key cases, and withcreate=Trueon new and mixed keys (same key mapping afterwards).remove_sampleskeeps a field nested until an eighth of its samples are gone, and serves the same per-sample shapes as stock after every clear.kv_retrieve_metaoverride, starting it installsFieldMeta.remove_samples, and callingconfigure_controller_patch()twice wraps once.The tests ran against TQ at the pinned commit.
ruff0.9.9,pyrefly0.24.2, anddetect-secretspass on the changed files.Notes for review
adapters/tq_controller_patch.py(new), a 2-line hook inadapters/transfer_queue.py,pyrefly.toml, and the test.get_logger, soTQ_LOGGING_LEVELcontrols its lines like stock.remove_sampleschange is onperf/throttle-nested-shape-rescan. Delete this module once TQ takes both changes.🤖 Generated with Claude Code