|
Hi community, I'm working with a custom environment where each However, I've encountered a fundamental conflict when using this setup with The Use Case: Data-loading on When num_workers=0, this works perfectly but can be a performance bottleneck, as data loading and preprocessing are slow. The Technical ChallengeThe core issue is that ParallelEnv creates its worker processes as daemonic, while a DataLoader with num_workers > 0 attempts to spawn its own child processes. This leads to Python's well-known restriction: This creates a conflict between torchrl's process management for parallel environments and PyTorch's standard for parallel data loading. What I've ConsideredApproach 1: num_workers = 0 (The Safe-but-Slow Method)This is the most straightforward workaround. It avoids the error but sacrifices performance at a critical point in our training loop. Given that we have many parallel environments, the total throughput is okay, but each Approach 2: Custom Non-Daemonic ParallelEnv (The Powerful-but-Risky Method)I've considered subclassing ParallelEnv and forcing its worker processes to be non-daemonic. This would permit them to have their own children (DataLoader workers). However, the significant drawback is the need for manual and careful process lifecycle management to avoid creating zombie processes, which adds a lot of complexity and risk to the training framework. Discussion Points & Questions Is this a common scenario? Do other users have environments that require heavy, parallelizable I/O or computation during reset()? Are there more elegant, recommended patterns to solve this? Perhaps there's a different way to structure the data loading pipeline with torchrl that I've missed. Would the TorchRL team consider adding a feature to facilitate this? For example, a built-in option in ParallelEnv like This seems like a potentially valuable capability for a broader set of use cases where environments are tightly coupled with large datasets. Thank you for your time and for building such a great library! I look forward to the discussion. |
Replies: 1 comment 1 reply
|
Short version: I would not go down the non-daemonic road. Even if you get it working, it mostly will not buy you the speedup you are after, and there are two cleaner ways out. Why the error exists. The Why it would not help anyway. With N parallel envs each running Option 1, the real fix: preprocess your logs into memory-mapped TensorDicts once, offline. from tensordict import TensorDict
# one-time conversion per log file
td = TensorDict(parse_log(path), batch_size=[n_steps])
td.memmap_(f"/data/prepped/{stem}")Then reset stops being I/O: def _reset(self, tensordict):
stem = random.choice(self.stems)
ep = TensorDict.load_memmap(f"/data/prepped/{stem}")
return self._to_obs(ep)
Option 2, if you must parse raw files at reset: prefetch on a thread. The daemon restriction applies to child processes, not threads, and CPython releases the GIL during blocking file reads, so a background thread genuinely overlaps I/O with your episode compute: from concurrent.futures import ThreadPoolExecutor
def __init__(self, ...):
self._pool = ThreadPoolExecutor(max_workers=1)
self._next = self._pool.submit(self._load, random.choice(self.file_paths))
def _reset(self, tensordict):
ep = self._next.result()
self._next = self._pool.submit(self._load, random.choice(self.file_paths))
return self._to_obs(ep)You know at reset that another file will be needed, so you start loading it immediately and hide the latency behind the episode you are about to run. One caveat on option 1: each Happy to share the full conversion snippet if useful. |
Short version: I would not go down the non-daemonic road. Even if you get it working, it mostly will not buy you the speedup you are after, and there are two cleaner ways out.
Why the error exists. The
AssertionErroris not TorchRL policy, it is Python itself: the multiprocessing docs state that a daemonic process is not allowed to create child processes, precisely so nobody has to reap orphaned grandchildren when a worker dies. SubclassingParallelEnvto make workers non-daemonic means you inherit that reaping job, which is the risk you already identified.Why it would not help anyway. With N parallel envs each running
num_workers=4, you have 4N processes queued against one storage devic…