Repository navigation
Link received payload objects into the core cache instead of copying - #125
javieratomic wants to merge 1 commit into
Conversation
610bc96 to
b181fc8
Compare
b181fc8 to
3978c31
Compare
3978c31 to
c80cacf
Compare
A rank export receives each cache payload object as a complete companion wrapper in a synced temporary file in the core cache. `store_slice` then hashes the received payload, copies it into a new wrapper in cache staging, syncs the copy, and hashes the installed copy again: three full reads and one write and sync per object, on the receiving stream's thread. When the received file already is the wrapper `store_slice` would write, the copy, its write and its sync repeat work whose result is already on disk. In a DeepSeek V4.1 Flash TP3 export on three DGX Spark GB10 hosts with an empty core cache, rank zero receives 203.5 GB from the other two ranks this way. Before this change the receiver always called `store_slice`. Now it first calls the new `Root::adopt_companion`, which links the received file itself into cache staging when it is byte-for-byte that wrapper; both hashes remain. The link step is shared with directory publication, which already hard-linked source files that are canonical wrappers from its own code in `export.rs`; that code moves into `container::link_companion`. It checks the length and leading bytes against `companion_prefix`, links the opened file through its descriptor so that a replaced path cannot be linked instead, checks that the link names the same inode, and hashes the linked file against the declared identity; a mismatched identity is an error. For a file that is not the canonical wrapper, or a link the filesystem refuses, `link_companion` returns false, `adopt_companion` returns `None`, and the receiver stores the slice as before. Adoption checks and links the descriptor the receiver opened, the same one `store_slice` copies from. An already cached identity returns the verified cached object, as `store_slice` does. Publication is unchanged: the object is made immutable, linked under its identity and verified once more, and the receiver closes its writable descriptor before the file is published. Core format, cache layout and object identities are unchanged, and directory publication keeps its behavior, error kinds and messages. Adoption needs Linux (`linkat` through `/proc/self/fd`); on other platforms received objects are copied as before. Expected result: with an empty core cache, "Receive rank export payloads" no longer copies, writes and syncs each received object. On an earlier tree, with an earlier version of this adoption, that phase took 280.6 s instead of 457.8 s; this commit itself was not measured on its current base. Evidence: helios `cargo test --release --features server,runtime-core --lib`: 1335 passed, 0 failed, 26 ignored, with FLARE_TEST_TEMPLATE_DIR prepared by tests/frontend/prepare_templates.py. heliopause `cargo test --release`: 190 passed, 0 failed, 3 ignored. A helios test covers adoption by link (same inode); a wrapper with an extra tensor and a wrapper of the right length with different header bytes, both declined; a mismatched identity, which is rejected; a second arrival of a cached identity, which returns the cached object; and that cache staging is empty after the declined, rejected and cached cases. A heliopause test checks that the receiver's install step adopts a canonical wrapper (same inode) and copies another framing into a new file. The existing directory publication tests cover the shared link step. The 203.5 GB is the byte count reported by "Receive rank export payloads" in an export on `d2b5c5f3` without this change, with an empty core cache; there that phase took 462.3 s. The measurement above used full TP3 exports with the same model and export command, an empty core cache (then named the package cache) and an empty forward compilation cache, built from `ca1f960e` plus an earlier version of NVIDIA#124 that published cache payloads concurrently and without the directory publication changes merged as `e13ecb74`; its adoption step linked the received file by path instead of through `link_companion`. There, with the default 8 transfer streams, "Receive rank export payloads" took 280.6 s with adoption and 457.8 s without it (477.2 s without it and `--transfer-streams 16`), and the whole export phase took 1473.2 s against 1677.4 s; the package exported with adoption served with `flare run` and answered a 262k-token prompt that asks for 16 values placed through the context with all 16 correct, with identical output with an empty and with a warm prefix cache. One export per configuration was measured. The payload is still read twice after it arrives (identity check and post-installation verification) instead of being hashed while it streams. Design-Signed-Off-By: Claude Opus 5.5; design-review=cfcf Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Javier López <59578422+javieratomic@users.noreply.github.com>
c80cacf to
8c97421
Compare
|
Rebased onto |
Name the upstream project and repository after their rename, point every link at NVIDIA/hyperflare-inference, note that NVIDIA#124 and NVIDIA#125 were rebased onto upstream's package-to-core rename, add the NVIDIA#124 re-measurement, and say where the prefill-scratch prototype's branches are. Design-Signed-Off-By: Claude Opus 5.5; design-review=1c01 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Javier López <59578422+javieratomic@users.noreply.github.com>
When a core is assembled, `core_engram` resolves each Engram plane's payload source in turn, and `payload_source` hashes each plane, or copies and hashes it when it spans several files, with one synchronous reader. On DeepSeek V4.1 Flash the two weight planes are about 98 GB each, so "Assemble core payloads" spends most of its time hashing one plane at a time while one reader at a time cannot keep the NVMe device busy. Before this change `core_engram` validated each table and resolved its planes one after another. Now it first validates every table and lists its planes (shared weights and scales, or each shard's weights and scales) with the same checks as before; the new `plane_sources` then resolves those planes with `payload_source` on up to eight workers, largest first, stops scheduling at the first error and returns it once running planes finish. The sources come back in declared order, and the tables, shards and installed payloads are built from them in the same order as before, so the published layout does not change. Measured result, on an earlier base: on a TP3 export on three DGX Spark GB10 hosts, "Assemble package payloads" (now "Assemble core payloads") took 169.1 s instead of 262.0 s. Evidence: helios `cargo test --release --features server,runtime-core --lib`: 1334 passed, 0 failed, 26 ignored. heliopause `cargo test --release`: 190 passed, 0 failed, 3 ignored, including `core_engram_preserves_shared_and_partitioned_plane_layouts`. The new test `plane_sources_keep_declared_order_and_report_failures` resolves three planes of different sizes, one of them spanning two source files, checks that they come back in declared order with the right identities and sizes, and checks that a plane beyond the payload fails. The measurement used full exports with the same model and recipe flags, one per configuration, on builds based on `a161b17f` that also contained earlier versions of NVIDIA#122, NVIDIA#124 and NVIDIA#125 and the local checkpoint reading proposed in NVIDIA#126, none of which change this phase in those versions; the export completed and committed the core, including the data of all three ranks. This model has four planes, so four readers run at once; the other two planes are about 3 GB each, so most of the remaining time is the two weight planes, each still hashed by one reader. The change was not measured on `bbad44fe`, nor together with the read-ahead digest of NVIDIA#124 (`50c664a3`), which also shortens this phase. Design-Signed-Off-By: Claude Opus 5.5; design-review=cfcf Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Javier López <59578422+javieratomic@users.noreply.github.com>
Why
A rank export receives each cache payload object as a complete companion wrapper in a synced temporary file in the core cache.
store_slicethen hashes the received payload, copies it into a new wrapper in cache staging, syncs the copy, and hashes the installed copy again: three full reads and one write and sync per object, on the receiving stream's thread. When the received file already is the wrapperstore_slicewould write, the copy, its write and its sync repeat work whose result is already on disk. In a DeepSeek V4.1 Flash TP3 export on three DGX Spark GB10 hosts with an empty core cache, rank zero receives 203.5 GB from the other two ranks this way.What changed
Before, the receiver always called
store_slice. Now it first calls the newRoot::adopt_companion, which links the received file itself into cache staging when it is byte-for-byte that wrapper. That removes the copy read, the write and the sync; both hashes remain.The link step is shared with directory publication, which already hard-linked source files that are canonical wrappers from its own code in
export.rs; that code moves intocontainer::link_companion, which:companion_prefix;For a file that is not the canonical wrapper, or a link the filesystem refuses,
link_companionreturns false,adopt_companionreturnsNone, and the receiver stores the slice as before. Adoption checks and links the descriptor the receiver opened, the same onestore_slicecopies from. An already cached identity returns the verified cached object, asstore_slicedoes. Publication is unchanged: the object is made immutable, linked under its identity and verified once more, and the receiver closes its writable descriptor before the file is published.Core format, cache layout and object identities are unchanged. Directory publication keeps its behavior, error kinds and messages. Adoption needs Linux (
linkatthrough/proc/self/fd); on other platforms received objects are copied as before.Result
Expected: with an empty core cache, "Receive rank export payloads" no longer copies, writes and syncs each received object. Measured on an earlier tree with an earlier version of this adoption (below), that phase took 280.6 s instead of 457.8 s. This PR has not been measured on its current base.
Evidence
Tests on this branch:
cargo test --release --features server,runtime-core --lib, withFLARE_TEST_TEMPLATE_DIRprepared bytests/frontend/prepare_templates.py: 1335 passed, 0 failed, 26 ignored.cargo test --release: 190 passed, 0 failed, 3 ignored.The 203.5 GB is the byte count reported by "Receive rank export payloads" in an export on
d2b5c5f3without this change, with an empty core cache; there that phase took 462.3 s.Measurement on an earlier tree: full TP3 exports on three DGX Spark GB10 hosts with the same model and export command, an empty core cache (then named the package cache) and an empty forward compilation cache. The tree was
ca1f960eplus an earlier version of #124 that published cache payloads concurrently, without the directory publication changes merged ase13ecb74. Its adoption step linked the received file by path instead of through the sharedlink_companion. Phase names are as reported then.--transfer-streams 16)The package exported with adoption served with
flare run. It answered a 262k-token prompt that asks for 16 values placed through the context with all 16 correct, with identical output with an empty and with a warm prefix cache.Limitations:
Slice::digestof payloads of at least 32 MiB while fewer than 16 digests are reading ahead, which bothstore_sliceand adoption use for the identity check. The two changes were not measured together.🤖 Generated with Claude Code