Repository navigation
perf: keep record streams as numpy arrays - #2980
Merged
Merged
Conversation
to_record built each per-token stream as a Python list and rounded the float streams with a per-value round() call. Pydantic then walked every element. On real 128k-token episodes this costs ~50 ms per episode, all of it holding the GIL. Records now carry the node streams as C-contiguous numpy arrays, with floats in float64 and the rounded streams rounded with np.round. A numpy-aware encoder (orjson with OPT_SERIALIZE_NUMPY) writes the same bytes as before. JSON-mode dumps round with np.round too and give the same output. Callers that encode records with the stdlib json convert them first with json_safe_record: the judge envs and the eval push, whose SDK encodes with the stdlib json. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
ApprovabilityVerdict: Needs Human Approval Macroscope's review found this PR requires human approval — This changes the default Trace/Episode record representation from Python lists to NumPy arrays and routes production judge snapshots through a new orjson-based serializer. It also pins runtime dependencies to an external Git revision, so the public serialization contract and installation impact need human review. You can add or adjust custom eligibility rules. Learn more. |
orjson writes the numpy streams of to_record() directly, so the judge envs skip the list copy that json_safe_record makes for the stdlib encoder. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 25, 2026
The prime SDK now encodes records with orjson and OPT_SERIALIZE_NUMPY, so callers no longer convert the stream arrays to lists first. The node streams join the record after the pydantic dump, without the field reorder: pydantic's JSON mode cannot emit numpy arrays, and its own encoder is about 3.5x slower on real episodes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mirrors pydantic's model_dump and model_dump_json pair. Pydantic's own methods stay untouched: JSON mode must return plain JSON types, the msgpack wire relies on python mode, and nested models never call an override. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mikasenghaas
commented
Oct 26, 2026
to_record and to_json take a pydantic exclude spec, merged with the record's own excludes, so a judge drops ok and the hidden reasoning fields without editing the record or calling orjson itself. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
samsja
previously approved these changes
Oct 26, 2026
…prime#1109 The SDK encodes the numpy record streams with orjson from #1109 on. TODO: replace the git pin with the PyPI release asap and raise the prime-runs minimum to it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mikasenghaas
added a commit
to PrimeIntellect-ai/prime-rl
that referenced
this pull request
Oct 27, 2026
## Summary - Bump verifiers to PrimeIntellect-ai/verifiers#2980. With it, `Episode.to_record()` returns the per-token node streams as numpy arrays. - The file monitor's orjson (`OPT_SERIALIZE_NUMPY`) writes them in one pass, so the file monitor needs no code change. - The bump adds `orjson` to the verifiers dependencies. `uv.lock` is relocked for it. - The prime monitors keep passing the episodes to the SDK. From PrimeIntellect-ai/prime#1109 on, the SDK encodes the numpy streams with orjson. - `test_episode_records.py` checks the upload path with the SDK's own encoders: the trace line, the samples batch and the training viewer table. **Pinned until release:** PrimeIntellect-ai/prime#1109 is merged but not released yet, so `prime-runs` and `prime-traces` are pinned to its merge commit on prime main (`adf8d25`), with a TODO to replace the pins with the PyPI release and raise the `prime-runs` minimum. `deps/verifiers` is on verifiers main with PrimeIntellect-ai/verifiers#2980 (f166cd5). With the released SDK, the prime monitors' uploads fail with `TypeError`. ## Verification Real data: one 2,048-episode training batch with 193M tokens, replayed through the real `TrainSink` and `Orchestrator.finalize_train_batch` with a no-op sender. | | Before | After | |---|---|---| | File monitor serialization, single-threaded, per batch | 103.3 s | 18.1 s | | `FileMonitor.log_episodes` during arrival, per batch | 113.4 s | 19.9 s | | Ship with the file and prime serializers running in background threads | 1015.7 s | 114.2 s | - Ship time runs from taking the batch to shipping it. The last row is a worst case: each monitor's real per-episode serializer loops in its own thread, as with a backlog. These threads hold the GIL in long C calls and starve the ship's pure-Python steps. Without them, the ship on this base takes ~42 s. - In the last row, before is `main` with the released SDK, and after is this stack with PrimeIntellect-ai/prime#1109. GC time in the ship drops from 103.7 s to 0.6 s, and the longest pause from 4.52 s to 0.01 s. - The prime serializer thread still costs 147 ms per episode after. Of that, 12 ms is the encode and the rest is the SDK's level-9 gzip, which releases the GIL. - The prime SDK trace line drops from 114.7 ms per episode (`main` and the released SDK) to 12.2 ms (this stack with PrimeIntellect-ai/prime#1109) on 512 episodes, with the same content. - `pytest tests/unit/orchestrator tests/unit/utils tests/unit/train/sft/test_prime_monitor.py tests/unit/test_dashboard_artifacts.py` with the PrimeIntellect-ai/prime#1109 SDK: 181 passed. With the released SDK, `test_received_episode_uploads_plain_json` fails with `TypeError: Object of type ndarray is not JSON serializable`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Temporary git pins on core upload/training SDK packages change resolution and reproducibility until PyPI catches up; behavior change is limited to serialization of episode traces but affects all prime monitor uploads. > > **Overview** > **Pins `prime-runs` and `prime-traces` to the Prime main commit that ships PrimeIntellect-ai/prime#1109** (orjson-based trace/record encoding for numpy per-token streams), via `tool.uv.sources`, an override on `prime-traces`, and a refreshed `uv.lock`. TODOs call out swapping these git pins for the next PyPI releases. > > **Aligns upload-path tests** in `test_episode_records.py` with the SDK: trace lines use `prime_traces._encode_record`, and sample JSON uses `prime_runs._http.encode_json`, with an explicit check that encoded `token_ids` remain plain JSON lists—not the old `json.dumps` / `to_mapping` stand-ins. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit d5b8db3. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
kcoopermiller
added a commit
that referenced
this pull request
Oct 29, 2026
## Summary Replaces the git pins added in #2980 with the PyPI release of PrimeIntellect-ai/prime#1109 (orjson-encoded records and numpy streams). That release is PrimeIntellect-ai/prime#1126, which ships `prime-runs` 0.1.5 and `prime-traces` 0.0.11. Both are now on PyPI. `uv.lock` moves `prime-runs` and `prime-traces` from the `adf8d25c` git sources to PyPI 0.1.5 / 0.0.11. No other package moves. The lockfile also changes from `revision = 4` to `5`, because uv 0.13 writes `default-groups` into it. ## Changes - `prime-runs>=0.1.4` → `prime-runs>=0.1.5`. - Removes the `prime-runs` git source in `[tool.uv.sources]`, pinned to `adf8d25c`. - Removes the `prime-traces` git override in `override-dependencies`, pinned to the same commit. `prime-runs` 0.1.5 requires `prime-traces>=0.0.11`, so verifiers no longer needs to pin it. - Adds `prime-traces = false` to `[tool.uv.exclude-newer-package]`. Without it, the 7-day `exclude-newer` cooldown hides `prime-traces` 0.0.11 and the lock fails to resolve. `prime-runs` is already exempt. ## Checks - In a fresh venv, `pip install prime-runs==0.1.5` resolves `prime-traces` 0.0.11. - `uv sync` installs `prime-runs` 0.1.5 and `prime-traces` 0.0.11. - `ruff check .` passes. - `tests/unit`: 243 passed, 10 skipped. The skips need `harbor` or `PRIME_API_KEY`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- Macroscope's pull request summary starts here --> <!-- Macroscope will only edit the content between these invisible markers, and the markers themselves will not be visible in the GitHub rendered markdown. --> <!-- If you delete either of the start / end markers from your PR's description, Macroscope will append its summary at the bottom of the description. --> > [!NOTE] > ### Require prime-runs 0.1.5 from PyPI > Bumps the minimum `prime-runs` version to 0.1.5 in [pyproject.toml](https://github.com/PrimeIntellect-ai/verifiers/pull/3001/files#diff-50c86b7ed8ac2cf95bd48334961bf0530cdc77b5a56f852c5c61b89d735fd711) and removes the uv sources and override-dependencies entries that pulled `prime-runs` and `prime-traces` from specific Git revisions. Adds `prime-traces = false` to `tool.uv.exclude-newer-package` so the cutoff does not apply to it. Behavioral Change: both packages now resolve from PyPI instead of pinned Git commits. > > <!-- Macroscope's changelog starts here --> > #### Changes since #3001 opened > > - Updated the dependency lock to require `prime-runs` version 0.1.5 from PyPI. [2d3f59a] > <!-- Macroscope's changelog ends here --> > > <!-- Macroscope's review summary starts here --> > > <sup><a href="https://app.macroscope.com">Macroscope</a> summarized 1b749f4.</sup> > <!-- Macroscope's review summary ends here --> > <!-- Macroscope's pull request summary ends here --> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Dependency and lockfile-only change with no application code edits; behavior shifts from pinned git commits to released PyPI versions already exercised via the prior pins. > > **Overview** > This PR **drops temporary git pins** for `prime-runs` and `prime-traces` and **consumes the published PyPI releases** instead (`prime-runs` **0.1.5**, `prime-traces` **0.0.11** via `prime-runs`' dependency). > > **`pyproject.toml`** raises the minimum to `prime-runs>=0.1.5`, removes the `prime-runs` git entry under `[tool.uv.sources]`, removes the `prime-traces` git `override-dependencies` block (and related TODO comments), and adds **`prime-traces = false`** to `[tool.uv.exclude-newer-package]` so the 7-day `exclude-newer` window does not block resolving the new trace package. > > **`uv.lock`** reflects PyPI sources for both packages, drops the manifest override for `prime-traces`, and bumps lock **revision** to 5 (uv writing `default-groups` on the root package). > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 2d3f59a. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.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.
Summary
Episode.to_record()andTrace.to_record()return the per-token node streams as C-contiguous numpy arrays. That coverstoken_ids,mask,is_content, the rounded float streams andloss_weights.np.round.OPT_SERIALIZE_NUMPYwrites each stream in one pass.round(), and pydantic serialized every element.to_record()dumps the model without the streams, then adds them as arrays. Pydantic's JSON mode cannot emit numpy arrays, and its own encoder (model_dump_json) is 3.5x slower on real episodes (see Verification).Episode.to_json()andTrace.to_json():to_record()encoded with orjson, as compact UTF-8 bytes. The pair mirrors pydantic'smodel_dump/model_dump_json, which stay unchanged.to_record()andto_json()takeexclude, a pydantic exclude spec merged with the record's own excludes. The per-token streams are always included.model_dump(mode="json"),model_dump_json) round withnp.roundinstead of a per-valueround(). The output is the same.to_json(exclude=...), which dropsokand the hidden reasoning fields. Before, they edited the record dict. Addorjsonas a dependency.vf-eval --pushkeeps passing the episodes to the prime SDK. The SDK encodes them with orjson from perf: encode json with orjson in prime-runs and prime-traces prime#1109 on.prime-rl counterpart: PrimeIntellect-ai/prime-rl#4257. In a replayed batch with PrimeIntellect-ai/prime#1109, the ship under monitor contention drops from 1016 s to 114 s.
Pinned until release: PrimeIntellect-ai/prime#1109 is merged but not released yet, so
prime-runs(andprime-tracesthroughoverride-dependencies) are pinned to its merge commit on prime main (adf8d25), with a TODO to replace them with the PyPI release asap and raise theprime-runsminimum. The pins only apply to this repo's lock; consumers need the release.Breaking
to_record()node streams are numpy arrays, not lists.json.dumps(record)with the stdlibjsonraisesTypeError.OPT_SERIALIZE_NUMPY.to_record(), so their uploads fail withTypeError.prime-runs.Verification
Real data: one 2,048-episode training batch with 193M tokens. Each step below ran single-threaded over the whole batch.
to_record()+ orjson (trace-file write)A second check ran on 256 episodes of the same batch:
to_record()+ orjson (this branch)model_dump_jsonwith the same excludesmodel_dump(mode="json")+ orjsonto_json()have the content of the plain JSON dump, for traces and episodes, with and without rounding;excludemerges with the record's own excludes and keeps the streams;round_streammatchesround()on 100k float32 values.tests/unit: all passed (Docker,PRIME_API_KEYand harbor tests skipped).🤖 Generated with Claude Code
Note
Medium Risk
Changes the trace/episode serialization contract (NumPy in records, bytes from
to_json) and pins Prime packages from git, which affects eval uploads and any code that assumed list-shaped streams or stdlib JSON.Overview
Trace and episode JSON records now attach per-token fields (
token_ids,mask,is_content, float training streams,loss_weights) as NumPy arrays instead of Python lists.to_record()dumps the model without those streams, then merges them viaadd_record_streams/MessageNode.record_streams; float streams usenp.round(round_stream) at the configured precision.Adds
Trace.to_json()andEpisode.to_json()— compact UTF-8 bytes fromorjsonwithOPT_SERIALIZE_NUMPY. Bothto_recordandto_jsonaccept an optionalexcludemerged withRECORD_DUMP_EXCLUDEviamerge_exclude(judge envs use this to dropokand hidden reasoning instead of mutating dicts).Dependencies: runtime
orjson, and temporary git pins forprime-runs/prime-tracesuntil a PyPI release ships prime#1109-compatible encoding.Breaking:
to_record()dicts are no longerjson.dumps-safe; callers must useto_jsonor orjson with NumPy support.Reviewed by Cursor Bugbot for commit 4ae941f. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Keep record streams as NumPy arrays and encode records with orjson
Trace.to_recordandEpisode.to_recordnow attach node streams (token IDs, mask, content flags, rounded float streams, loss weights) as NumPy arrays, with float streams rounded perfloat_decimalsviaround_streamin graph.pyTrace.to_jsonandEpisode.to_jsonencode records with orjson's NumPy option; NaN and infinity becomenullto_recordaccepts anexcludeargument merged with built-in record exclusions viamerge_excludein trace.py;JudgeTask.from_traceandRankTask.recordnow useTrace.to_jsonwith exclusions instead of mutating records and using stdlib JSONorjson>=3.10.0as a dependency in pyproject.tomlnull, and records contain arrays instead of lists for stream fields; check consumers ofto_json/to_recordoutputMacroscope summarized 4ae941f.