Skip to content

Commit 95bf7ea

Browse files
author
Le
committed
rename and move allow_child_to_ptrace to common.py to avoid circular deps; update tests
1 parent 25791d6 commit 95bf7ea

7 files changed

Lines changed: 111 additions & 81 deletions

File tree

alf/algorithms/distributed_off_policy_algorithm.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
from alf.environments.alf_environment import AlfEnvironment
3434
from alf.experience_replayers.replay_buffer import ReplayBuffer
3535
from alf.data_structures import Experience, make_experience, StepType
36-
from alf.trainers.evaluator import _allow_child_to_ptrace
36+
from alf.utils.common import allow_child_to_ptrace
3737
from alf.utils.per_process_context import PerProcessContext
3838
from alf.utils import dist_utils
3939
from alf.utils.summary_utils import record_time
@@ -523,7 +523,7 @@ def _create_data_receiver_subprocess(self):
523523
self._ddp_rank),
524524
daemon=True)
525525
process.start()
526-
_allow_child_to_ptrace(process.pid)
526+
allow_child_to_ptrace(process.pid)
527527

528528
def utd(self):
529529
total_exps = int(self._replay_buffer.get_current_position().sum())
@@ -713,7 +713,7 @@ def _create_pull_params_subprocess(self):
713713
self._params_socket_rank),
714714
daemon=True)
715715
process.start()
716-
_allow_child_to_ptrace(process.pid)
716+
allow_child_to_ptrace(process.pid)
717717

718718
def observe_for_replay(self, exp: Experience):
719719
"""Send experience data to the trainer.

alf/experience_replayers/replay_buffer_test.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -328,11 +328,12 @@ def generate_step_types(self, num_envs, max_steps, end_prob):
328328
(False, True),
329329
(True, False),
330330
])
331-
def test_replay_buffer(self, allow_multiprocess, with_replacement):
331+
def test_replay_buffer(self, use_mp_context, with_replacement):
332+
mp_ctx = mp.get_context('spawn') if use_mp_context else None
332333
replay_buffer = ReplayBuffer(data_spec=self.data_spec,
333334
num_environments=self.num_envs,
334335
max_length=self.max_length,
335-
allow_multiprocess=allow_multiprocess)
336+
mp_context=mp_ctx)
336337

337338
batch1 = get_exp_batch([0, 4, 7], self.dim, t=0, x=0.1)
338339
replay_buffer.add_batch(batch1, batch1.env_id)
@@ -702,17 +703,17 @@ class ReplayBufferSharingTest(parameterized.TestCase, alf.test.TestCase):
702703
def test_spawned_process_sharing(self, start_method):
703704
spec = alf.TensorSpec((10, ), dtype=torch.uint8)
704705

706+
ctx = mp.get_context(start_method)
707+
buffer_ctx = ctx if start_method == 'spawn' else None
705708
buffer = ReplayBuffer(data_spec=spec,
706709
num_environments=1,
707710
max_length=10,
708-
device='cpu')
711+
device='cpu',
712+
mp_context=buffer_ctx)
709713

710-
original_start_method = mp.get_start_method()
711-
mp.set_start_method(start_method, force=True)
712-
p = mp.Process(target=_write_to_buffer, args=(buffer, ))
714+
p = ctx.Process(target=_write_to_buffer, args=(buffer, ))
713715
p.start()
714716
p.join()
715-
mp.set_start_method(original_start_method, force=True)
716717

717718
if start_method == 'spawn':
718719
# The buffer in the main process is also changed

alf/trainers/evaluator.py

Lines changed: 1 addition & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414

1515
from absl import logging
1616
from absl import flags
17-
import ctypes
1817
import math
1918
import torch.multiprocessing as mp
2019
import os
@@ -42,30 +41,6 @@
4241
defaults=[None] * 4)
4342

4443

45-
def _allow_child_to_ptrace(child_pid: int) -> None:
46-
"""
47-
In the *parent* process: allow a given child to ptrace us.
48-
This relaxes Yama's ptrace restriction for this specific relationship.
49-
50-
Requires: kernel.yama.ptrace_scope <= 1 (default on many distros).
51-
The value of kernel.yama.ptrace_scope can be checked with:
52-
cat /proc/sys/kernel/yama/ptrace_scope
53-
"""
54-
libc = ctypes.CDLL(ctypes.util.find_library("c"), use_errno=True)
55-
56-
# prctl constants from <linux/prctl.h>:
57-
PR_SET_PTRACER = 0x59616D61
58-
59-
def _check(ret, what):
60-
if ret != 0:
61-
err = ctypes.get_errno()
62-
raise OSError(err, f"{what} failed: {os.strerror(err)}")
63-
64-
# Whitelist the specific child PID
65-
_check(libc.prctl(PR_SET_PTRACER, ctypes.c_ulong(int(child_pid)), 0, 0, 0),
66-
"PR_SET_PTRACER")
67-
68-
6944
class Evaluator(object):
7045
"""Evaluator for performing evaluation on the current algorithm.
7146
@@ -102,7 +77,7 @@ def __init__(self, config: TrainerConfig, conf_file: str):
10277
self._worker.start()
10378

10479
# Need this to avoid "pidfd_getfd: Operation not permitted"
105-
_allow_child_to_ptrace(self._worker.pid)
80+
common.allow_child_to_ptrace(self._worker.pid)
10681
else:
10782
self._env = create_environment(for_evaluation=True,
10883
num_parallel_environments=num_envs,

alf/utils/common.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
from absl import logging
1818
import contextlib
1919
import copy
20+
import ctypes
21+
import ctypes.util
2022
from fasteners.process_lock import InterProcessLock
2123
from filelock import FileLock
2224
from functools import wraps
@@ -95,6 +97,32 @@ def wrapper(*args, **kwargs):
9597
return decorator
9698

9799

100+
def allow_child_to_ptrace(child_pid: int) -> None:
101+
"""In the *parent* process: allow a given child to ptrace us.
102+
103+
This relaxes Yama's ptrace restriction for this specific relationship.
104+
105+
Requires: ``kernel.yama.ptrace_scope <= 1`` (default on many distros).
106+
The value of ``kernel.yama.ptrace_scope`` can be checked with::
107+
108+
cat /proc/sys/kernel/yama/ptrace_scope
109+
"""
110+
111+
libc = ctypes.CDLL(ctypes.util.find_library("c"), use_errno=True)
112+
113+
# prctl constants from <linux/prctl.h>
114+
PR_SET_PTRACER = 0x59616D61
115+
116+
def _check(ret, what):
117+
if ret != 0:
118+
err = ctypes.get_errno()
119+
raise OSError(err, f"{what} failed: {os.strerror(err)}")
120+
121+
_check(
122+
libc.prctl(PR_SET_PTRACER, ctypes.c_ulong(int(child_pid)), 0, 0, 0),
123+
"PR_SET_PTRACER")
124+
125+
98126
def as_list(x):
99127
"""Convert ``x`` to a list.
100128

alf/utils/common_test.py

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
import torch.nn as nn
2222

2323
import alf
24-
from alf.trainers.evaluator import _allow_child_to_ptrace
2524
import alf.utils.common as common
2625

2726

@@ -97,6 +96,13 @@ def _test_worker(m_):
9796
m_.z[:] = 1.
9897

9998

99+
def _launch_worker_with_ctx(ctx, module):
100+
"""Launch a child process to mutate ``module`` using the provided context."""
101+
process = ctx.Process(target=_test_worker, args=(module, ))
102+
process.start()
103+
process.join()
104+
105+
100106
def _test_tensor_sharing():
101107
"""This function is for testing whether tensors are automatically moved
102108
to shared memory, even when ``Module.share_memory()`` or ``Tensor.share_memory_()``
@@ -116,26 +122,40 @@ def _test_tensor_sharing():
116122

117123
ctx = mp.get_context('spawn')
118124
# Change ``m`` in the child process
119-
process = ctx.Process(target=_test_worker, args=(m, ))
120-
process.start()
121-
_allow_child_to_ptrace(process.pid)
122-
process.join()
125+
_launch_worker_with_ctx(ctx, m)
123126

124127
# numpy array should not be modified
125128
assert np.all(m.y == np.zeros([2]))
126129
# cuda tensor should be modified
127130
if torch.cuda.is_available():
128131
assert torch.all(m.z.cpu() == torch.ones([2]).cpu())
129-
# check that ``m``'s tensor also been modified in the parent process
130-
assert m.x.is_shared() and torch.all(m.x == torch.ones([2]).cpu()), (
131-
"Your pytorch version has a different behavior of sharing CPU tensors "
132-
"between processes. Please report the version to the ALF team.")
132+
# check whether ``m``'s tensor has been modified in the parent process
133+
auto_shared = m.x.is_shared() and torch.all(m.x == torch.ones([2]).cpu())
134+
if auto_shared:
135+
return True
136+
137+
logging.fatal(
138+
"CPU tensors are not automatically shared between processes on this "
139+
"PyTorch build. Need to fall back to explicit share_memory checks.")
140+
141+
# Explicitly share the module and verify the behaviour still works.
142+
m_explicit = _TestModule()
143+
m_explicit.share_memory()
144+
_launch_worker_with_ctx(ctx, m_explicit)
145+
assert torch.all(m_explicit.x == torch.ones([2]).cpu()), (
146+
"Explicit share_memory() failed to propagate tensor updates between "
147+
"processes.")
148+
return False
133149

134150

135151
class TensorSharingTest(alf.test.TestCase):
136152

137153
def test_tensor_sharing(self):
138-
_test_tensor_sharing()
154+
auto_shared = _test_tensor_sharing()
155+
if not auto_shared:
156+
self.skipTest(
157+
"Automatic CPU tensor sharing is disabled on this PyTorch "
158+
f"version ({torch.__version__}).")
139159

140160

141161
if __name__ == '__main__':

alf/utils/data_buffer.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,9 @@ def atomic_wrapper(self, *args, **kwargs):
5858
class RingBuffer(nn.Module):
5959
"""Batched Ring Buffer.
6060
61-
Multiprocessing safe, optionally via: ``allow_multiprocess`` flag, blocking
62-
modes to ``enqueue`` and ``dequeue``, a stop event to terminate blocked
63-
processes, and putting buffer into shared memory.
61+
Multiprocessing safe, optionally via providing ``mp_context`` to enable
62+
blocking modes to ``enqueue`` and ``dequeue``, a stop event to terminate
63+
blocked processes, and putting buffer into shared memory.
6464
6565
This is the underlying implementation of ``ReplayBuffer`` and ``Queue``.
6666
@@ -221,7 +221,7 @@ def enqueue(self, batch, env_ids=None, blocking=False):
221221
"""
222222
if blocking:
223223
assert self._allow_multiprocess, (
224-
"Set allow_multiprocess to enable blocking mode.")
224+
"Pass a multiprocessing context to enable blocking mode.")
225225
env_ids = self.check_convert_env_ids(env_ids)
226226
while not self._stop.is_set():
227227
with self._lock:
@@ -333,7 +333,8 @@ def dequeue(self, env_ids=None, n=1, blocking=False):
333333
assert n <= self._max_length
334334
if blocking:
335335
assert self._allow_multiprocess, [
336-
"Set allow_multiprocess", "to enable blocking mode."
336+
"Pass a multiprocessing context",
337+
"to enable blocking mode."
337338
]
338339
env_ids = self.check_convert_env_ids(env_ids)
339340
while not self._stop.is_set():
@@ -487,7 +488,6 @@ def __init__(self,
487488
num_environments=1,
488489
max_length=capacity,
489490
device=device,
490-
allow_multiprocess=False,
491491
name=name)
492492
self._capacity = torch.as_tensor(self._max_length,
493493
dtype=torch.int64,

alf/utils/data_buffer_test.py

Lines changed: 35 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,25 @@ def get_batch(env_ids, dim, t, x):
6464
reward=r)
6565

6666

67+
def _enqueue_after_delay(ring_buffer, batch, delay=0.04):
68+
alf.set_default_device("cpu")
69+
sleep(delay)
70+
ring_buffer.enqueue(batch, batch.env_id)
71+
72+
73+
def _dequeue_after_delay(ring_buffer):
74+
# cpu tensor on subprocess. Otherwise, spawn method is needed.
75+
alf.set_default_device("cpu")
76+
sleep(0.04)
77+
ring_buffer.dequeue() # 6(deleted), 7, 8, 9
78+
sleep(0.04) # 10, 7, 8, 9
79+
ring_buffer.dequeue() # 10, 7(deleted), 8, 9
80+
81+
82+
def _blocking_dequeue(ring_buffer):
83+
ring_buffer.dequeue(blocking=True)
84+
85+
6786
class RingBufferTest(parameterized.TestCase, alf.test.TestCase):
6887
dim = 20
6988
max_length = 4
@@ -86,15 +105,16 @@ def __init__(self, *args):
86105
('test_sync', False),
87106
('test_async', True),
88107
])
89-
def test_ring_buffer(self, allow_multiprocess):
108+
def test_ring_buffer(self, use_mp_context):
109+
mp_ctx = mp.get_context('spawn') if use_mp_context else None
90110
ring_buffer = RingBuffer(data_spec=self.data_spec,
91111
num_environments=self.num_envs,
92112
max_length=self.max_length,
93-
allow_multiprocess=allow_multiprocess)
113+
mp_context=mp_ctx)
94114

95115
batch1 = get_batch([1, 2, 3, 5, 6], self.dim, t=1, x=0.4)
96-
if not allow_multiprocess:
97-
# enqueue: blocking mode only available under allow_multiprocess
116+
if not use_mp_context:
117+
# enqueue: blocking mode only available when multiprocessing is enabled
98118
self.assertRaises(AssertionError,
99119
ring_buffer.enqueue,
100120
batch1,
@@ -107,8 +127,8 @@ def test_ring_buffer(self, allow_multiprocess):
107127
# test that the created batch has gradients
108128
self.assertTrue(batch1.x.requires_grad)
109129
ring_buffer.enqueue(batch1, batch1.env_id)
110-
if not allow_multiprocess:
111-
# dequeue: blocking mode only available under allow_multiprocess
130+
if not use_mp_context:
131+
# dequeue: blocking mode only available when multiprocessing is enabled
112132
self.assertRaises(AssertionError,
113133
ring_buffer.dequeue,
114134
env_ids=batch1.env_id,
@@ -159,17 +179,13 @@ def test_ring_buffer(self, allow_multiprocess):
159179
ring_buffer.remove_up_to(3)
160180
self.assertEqual(ring_buffer._current_size, torch.tensor([0] * 8))
161181

162-
if allow_multiprocess:
182+
if use_mp_context:
163183
# Test block on dequeue without enough data
164-
def delayed_enqueue(ring_buffer, batch):
165-
alf.set_default_device("cpu")
166-
sleep(0.04)
167-
ring_buffer.enqueue(batch, batch.env_id)
168-
169-
p = mp.Process(target=delayed_enqueue,
170-
args=(ring_buffer,
171-
alf.nest.map_structure(
172-
lambda x: x.cpu(), batch1)))
184+
batch1_cpu = alf.nest.map_structure(
185+
lambda tensor: tensor.detach().cpu()
186+
if isinstance(tensor, torch.Tensor) else tensor, batch1)
187+
p = mp_ctx.Process(target=_enqueue_after_delay,
188+
args=(ring_buffer, batch1_cpu))
173189
p.start()
174190
batch = ring_buffer.dequeue(env_ids=batch1.env_id, blocking=True)
175191
self.assertEqual(batch.step_type, torch.tensor([[9]] * 5))
@@ -180,26 +196,16 @@ def delayed_enqueue(ring_buffer, batch):
180196
batch2 = get_batch(range(0, 8), self.dim, t=t, x=0.4)
181197
ring_buffer.enqueue(batch2)
182198

183-
def delayed_dequeue():
184-
# cpu tensor on subprocess. Otherwise, spawn method is needed.
185-
alf.set_default_device("cpu")
186-
sleep(0.04)
187-
ring_buffer.dequeue() # 6(deleted), 7, 8, 9
188-
sleep(0.04) # 10, 7, 8, 9
189-
ring_buffer.dequeue() # 10, 7(deleted), 8, 9
190-
191-
p = mp.Process(target=delayed_dequeue)
199+
p = mp_ctx.Process(target=_dequeue_after_delay,
200+
args=(ring_buffer, ))
192201
p.start()
193202
batch2 = get_batch(range(0, 8), self.dim, t=10, x=0.4)
194203
ring_buffer.enqueue(batch2, blocking=True)
195204
p.join()
196205
self.assertEqual(ring_buffer._current_size[0], torch.tensor(3))
197206

198207
# Test stop queue event
199-
def blocking_dequeue(ring_buffer):
200-
ring_buffer.dequeue(blocking=True)
201-
202-
p = mp.Process(target=blocking_dequeue, args=(ring_buffer, ))
208+
p = mp_ctx.Process(target=_blocking_dequeue, args=(ring_buffer, ))
203209
ring_buffer.clear()
204210
p.start()
205211
sleep(0.02) # for subprocess to enter while loop

0 commit comments

Comments
 (0)