Skip to content

Seed the neighbor-sampling RNG per rank instead of leaving it unset - #730

Draft
kmontemayor2-sc wants to merge 3 commits into
mainfrom
kmonte/seed_neighbor_sampling_per_rank
Draft

Seed the neighbor-sampling RNG per rank instead of leaving it unset#730
kmontemayor2-sc wants to merge 3 commits into
mainfrom
kmonte/seed_neighbor_sampling_per_rank

Conversation

@kmontemayor2-sc

Copy link
Copy Markdown
Collaborator

create_sampling_config hardcoded seed=None into GLT's SamplingConfig, so NeighborSampler never called RandomSeedManager::setSeed -- the only setSeed call site in GLT -- and is_set stayed false for the whole run.

That is not just a determinism choice. CPURandomSampler::UniformSample reads RandomSeedManager::getInstance().getSeed() on every call, once per source row of a batch, and discards the result because the std::mt19937 it feeds is thread_local static and already constructed. Unseeded, getSeed() constructs a std::random_device and draws from it, roughly 5 us of real work per source row for a value that is thrown away.

Measured on an unmodified GLT build, setting any seed took the sampler call from ~1450 us to ~50 us (~29x) with byte-identical neighbor output. In a multi-node GPU inference workload it took per-batch data loading from 47-184 ms to 21-25 ms and left recall unchanged.

The seed is derived from the global rank so ranks do not draw identical samples, mixed with a run-level base that defaults to wall-clock time so successive runs still vary as unseeded sampling did. The derived seed is logged with its base so a run stays reproducible after the fact. crc32 is used rather than hash(), whose string hashing is randomized per process, and its range matches setSeed(unsigned int) exactly so no masking is needed.

Callers may still pass an explicit seed, which is used verbatim.

kmontemayor and others added 3 commits July 31, 2026 23:25
`create_sampling_config` hardcoded `seed=None` into GLT's `SamplingConfig`, so
`NeighborSampler` never called `RandomSeedManager::setSeed` -- the only `setSeed`
call site in GLT -- and `is_set` stayed false for the whole run.

That is not just a determinism choice. `CPURandomSampler::UniformSample` reads
`RandomSeedManager::getInstance().getSeed()` on every call, once per source row of
a batch, and discards the result because the `std::mt19937` it feeds is
`thread_local static` and already constructed. Unseeded, `getSeed()` constructs a
`std::random_device` and draws from it, roughly 5 us of real work per source row
for a value that is thrown away.

Measured on an unmodified GLT build, setting any seed took the sampler call from
~1450 us to ~50 us (~29x) with byte-identical neighbor output. In a multi-node GPU
inference workload it took per-batch data loading from 47-184 ms to 21-25 ms and
left recall unchanged.

The seed is derived from the global rank so ranks do not draw identical samples,
mixed with a run-level base that defaults to wall-clock time so successive runs
still vary as unseeded sampling did. The derived seed is logged with its base so a
run stays reproducible after the fact. `crc32` is used rather than `hash()`, whose
string hashing is randomized per process, and its range matches
`setSeed(unsigned int)` exactly so no masking is needed.

Callers may still pass an explicit seed, which is used verbatim.
Point the GLT source references at permalinks pinned to the commit
`gigl/scripts/install_glt.sh` installs, so they stay valid as upstream moves.
Line numbers verified against that commit.

Trim the measured numbers down to the headline result, drop the note about
`crc32` already matching `setSeed(unsigned int)`, and add a TODO to remove the
workaround if upstream hoists the `getSeed()` call into the engine initializer
so the unseeded path stops paying per-row entropy.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The per-rank crc32 derivation existed to make the seed reproducible from a
single recorded base. That reproducibility is not wanted, and paying for it
cost more than it was worth: the seed had to be built outside
create_sampling_config so the caller could supply a rank, which meant both
DistNeighborLoader and DistABLPLoader had to change, and a public
derive_sampling_seed had to exist purely as plumbing.

Drawing a random uint32 inside create_sampling_config gets the same
properties for the parts that matter. Any seed is enough for the
performance fix, since only RandomSeedManager::is_set is load-bearing, not
the value. Distinctness across ranks still holds because each rank builds
its own loader and so draws its own seed; collisions are ~1e-6 at fleet
scale and are harmless anyway, as two ranks sharing a seed still sample
different source nodes. It is also better in one case the derivation
handled badly: two loaders in the same process previously got identical
seeds, since rank and wall-clock second were both the same.

Callers are untouched, and the explanation of why a seed is set at all now
lives on create_sampling_config, where someone reading the seed argument
will find it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant