GeoT optimization 2/4: Datapipes producer/consumer refactor + stream overlap#1742
GeoT optimization 2/4: Datapipes producer/consumer refactor + stream overlap#1742coreyjadams wants to merge 22 commits into
Conversation
|
Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually. Contributors can view more details about this message here. |
Refactor the datapipe prefetch path into a thread-safe host producer (_load_host) plus a main-thread consumer (_consume) with a FIFO submit/consume primitive (io_pump.IOPump), so all device/Warp kernels launch on the consuming thread. Build deferred-sync stream overlap on top: _consume records the preprocessing CUDA event into _events_pending and the DataLoader does one-batch lookahead, inserting compute_stream.wait_event just before each yield so batch N+1 preprocessing overlaps batch N compute. - New io_pump.py (FIFO pump); producer/consumer protocols; _rng fork_generator; core.function_spec.warp_stream_from_torch; refactored readers (base/numpy/zarr/tensorstore_zarr) and datapipes __init__. - MeshDataset parallel disk read + pin (serialize_load_consume=False); DomainMeshReader drop_interior_cells / drop_in_file_boundaries; volume configs enable both. - radius_search pinned non_blocking H2D; recipe train loop pinned async loss D2H. Opt-in timing + torch.profiler labels; streaming + reader tests, docs, and the iterable-dataset tutorial. No Warp keepalive machinery: with the Warp-free SDF (parent branch) the datapipe no longer launches Warp kernels in _consume.
…ressive IO prefetching
76ddb69 to
65cd32b
Compare
Greptile SummaryThis PR refactors the GeoT datapipe layer around a clear producer/consumer split: an
Important Files Changed
Reviews (1): Last reviewed commit: "Update changelog." | Re-trigger Greptile |
| def stop(self) -> None: | ||
| """Stop the dispatcher thread and release its resources. | ||
|
|
||
| Idempotent. Unblocks the dispatcher if it is waiting on a slot, | ||
| then joins it briefly. In-flight background loads already submitted | ||
| via ``dispatch_fn`` are not cancelled; the owning dataset reaps | ||
| them. | ||
| """ | ||
| if self._stop.is_set(): | ||
| return | ||
| self._stop.set() | ||
| # Unblock the dispatcher if it is parked acquiring a slot. | ||
| self._slots.release() | ||
| self._thread.join(timeout=5.0) |
There was a problem hiding this comment.
Silent timeout on dispatcher shutdown
stop() joins the dispatcher thread with a hard 5-second timeout but doesn't log or signal if the thread is still alive when join() returns. If dispatch_fn blocks (e.g. executor queue full, file descriptor exhaustion), the daemon thread continues running past stop() without any indication — the caller in _iter_prefetch's finally block then calls dataset.cancel_prefetch() assuming the pump is cleanly stopped. At process exit the daemon is killed, but during a long run this can accumulate hung threads and suppress the underlying problem. A simple if self._thread.is_alive(): check after join() with a warning would make the failure visible.
| _grid_ids_cpu = torch.tensor( | ||
| [g.id for g in grids], | ||
| dtype=torch.int64, | ||
| pin_memory=torch.cuda.is_available(), | ||
| ) | ||
| grid_ids_tensor = _grid_ids_cpu.to(points.device, non_blocking=True) |
There was a problem hiding this comment.
Pinned memory allocation on every radius-search call
torch.tensor(..., pin_memory=torch.cuda.is_available()) registers a new pinned (page-locked) host allocation via cudaHostAlloc on every forward pass. For a tiny array (one int64 per grid, typically a handful), the CUDA host-memory registration overhead likely dominates and outweighs the non-blocking copy benefit compared to the original single blocking cudaMemcpy. Pinning is efficient only for buffers that are reused across calls. Consider caching the pinned tensor per grid configuration or simply accepting the synchronous allocation here, where the array is small enough that the blocking copy is negligible.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
loliverhennigh
left a comment
There was a problem hiding this comment.
Fine with me, pinning memory like this makes me a bit worried but its ok
peterdsharpe
left a comment
There was a problem hiding this comment.
Reviewed parts I'm codeowner on: dataset yamls, unified recipe changes, and PhysicsNeMo-Mesh changes. All those look good to me!
negin513
left a comment
There was a problem hiding this comment.
Cool to add the iterable and map style datasets.
From my read through:
- The map-style
_iter_prefetchpath has the full N-1 overlap.... This is great!! - But the iterable dataset,
_iter_iterable, doesn't have that lookahead. So it should show the same behavior as the unoptimized workflow...
Does it makes sense to split this into two PRs?
|
/ok to test 255edce |
| [self._base_seed, self._epoch, position] | ||
| ).generate_state(1)[0] | ||
| np.random.seed(int(seed)) | ||
| yield next(sim_iter) |
There was a problem hiding this comment.
claude flagged this for me for this example:
Darcy2D reuses its output buffers: it keepsself.output_k / self.output_p around and does .data.copy_() into them every step, so every batch this yields is the same two tensors. With use_streams=True that's exactly the race the dataloader docstring warns about... ?
| with self._lock: | ||
| if index in self._prefetch_handles: | ||
| return | ||
| handle = self.submit(index, stream) |
There was a problem hiding this comment.
Potential race condition since it is outside the lock?
negin513
left a comment
There was a problem hiding this comment.
LGTM!! One thing before merge is the tutorial 5 inline comment with that example.
Otherwise, look great. Does worth do a profile on it?
Extends this PR's history-independent RNG scheme to the two transform base classes it didn't cover. Their set_epoch reseeded with manual_seed(initial_seed() + epoch), which compounds -- manual_seed updates initial_seed, so the epoch->stream mapping depended on how many times set_epoch had been called, and resuming from a checkpoint at epoch N silently drew different augmentations than a continuous run. set_generator now captures the base seed once and set_epoch reseeds with derive_seed(base_seed, epoch) from _rng, matching the readers' SeedSequence mixing (with a lazy capture for generators assigned outside set_generator). RNG.md and DISTRIBUTIONS.md documented the drifting idiom as the contract; both updated. Two tests pin sequential-vs-direct set_epoch equivalence at the transform and MeshDataset level.
|
Pushed |
PhysicsNeMo Pull Request
This PR is stacked 🥞 on #1741. It requires the torch SDF implementation first.. PR #1741 is merged, so this is ready to go.This PR is a rebuild of datapipe components, particularly at the IO level. It's key changes are the following:
What does it mean to separate IO threads from preprocessing Steams?
The PR introduces the concept of an "IO Pump": it's self priming, so to speak. It takes a few args (queue depth, worker fn), and runs a sentinal thread to keep the queue topped up. Every time an IO payload is consumed, the pump automatically queues another, ensuring the IO is consistently ready to go. The key here is the IO threads can't block the GIL, so async IO is key.
The
IOPumpis FIFO. Exceptions are captured and held until attempted consumption of data: if a preload a a batch fails, it doesn't actually crash everything until we go to consume that batch.While this is based purely on threads today, in principle it's about 80% of the way to enabling processPool concurrency for prefetching too. That will break memory pinning, but for applications that simply cannot release the GIL, it could become a viable option.
For best performance, we should make sure the queue depth is larger than the batch size (so an entire batch is always ready). And if IO latency is greater than model step time, we should preload enough batches to compensate.
GPU Preprocessing with Streams
IO payloads are "consumed" in an N-1 fashion: while batch N is processed, batch N-1 is consumed on the GPU for preprocessing, then collated. The datasets use two functions to isolate work:
_load_hostand_consume._load_hostis what is orchestrated by theIOPump, and is CPU only and threaded._consume, on the other hand, is single threaded (really, the MAIN thread) and handles the queuing of transforms onto the appropriate stream (an arg to_consume) as well as a deferred sync.Cuda Stream Orchestration
Handling the thread and stream orchestration takes a little care. Python threading is relatively easy to queue and block on: we keep the queue primed by filling it to
depth, making sure it has enough work so that data is always ready on the host. During training / inference, at stepN, we eagerly pull batchN+1from the dataset and start the transfer and transformations, and collation. We apply a gating to batchN(which, in this model, was pulled duringN-1processing) with cuda Events to ensure it completes before proceeding - this prevents the model from consuming it before it's ready. During the processing of batchN+1, the same gating on batchN+1is applied right after the processing ofN+2is triggered. In this way, we keep preprocessing flowing concurrently with the model computation.Any data preprocessing that includes blocking syncs (nonzero, HtD blocking, etc) will cause this model to stumble and introduce bubbles in the GPU flow.
RNG Updates
This PR makes some RNG updates to ensure we can have reproducibility regardless of ordering. The IO pump is FIFO but in general we shouldn't necessarily assume FIFO, and RNG reproducibility should be stable under multi-threading.
Iterable Style Dataset
There was a request to support "Simulation only" type datasets, since we have this nice prefetching model now. I included this here, and a tutorial / example as well. It's like a pytorch "iterable" dataset (so no `len`` attribute) but doing on-the-fly generation on the GPU, in side streams, concurrent with model processing. Limited currently to one-step-ahead processing.
Misc Updates
Doc Updates
I am still review the doc updates and readme updates. The PR should not merge without those.
Next Steps
I'd like to get this reviewed now, and think about some follow on PRs:
Description
Checklist
Dependencies
Review Process
All PRs are reviewed by the PhysicsNeMo team before merging.
Depending on which files are changed, GitHub may automatically assign a maintainer for review.
We are also testing AI-based code review tools (e.g., Greptile), which may add automated comments with a confidence score.
This score reflects the AI’s assessment of merge readiness and is not a qualitative judgment of your work, nor is
it an indication that the PR will be accepted / rejected.
AI-generated feedback should be reviewed critically for usefulness.
You are not required to respond to every AI comment, but they are intended to help both authors and reviewers.
Please react to Greptile comments with 👍 or 👎 to provide feedback on their accuracy.