Skip to content

Commit 9d08214

Browse files
committed
Migrate optimizer state across devices (GH-1615)
Adam and SGD previously reused compatible optimizer state buffers even when replacement parameters moved to another device. This left kernels with state and parameters on different devices. Migrate compatible state buffers while preserving their accumulated values. Keep incompatible shape or dtype changes using freshly zeroed buffers, and cover mixed moved and unmoved parameters. Signed-off-by: Eric Shi <ershi@nvidia.com>
1 parent f9fd4eb commit 9d08214

5 files changed

Lines changed: 102 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -204,10 +204,11 @@
204204
capture, which APIC cannot record for replay ([GH-1559](https://github.com/NVIDIA/warp/issues/1559)).
205205
- Fix APIC graph capture for `warp.fem` workflows by making partition node counts graph-capture safe
206206
([GH-1407](https://github.com/NVIDIA/warp/issues/1407)).
207-
- Fix `warp.optim.Adam.set_params()` re-zeroing the optimizer moment buffers on every call for `fp16`
208-
parameters, which silently discarded accumulated state. The reuse check now compares against the
209-
moment-buffer dtype rather than the parameter dtype
210-
([GH-1593](https://github.com/NVIDIA/warp/issues/1593)).
207+
- Fix optimizer state reuse in `warp.optim.Adam.set_params()` and `warp.optim.SGD.set_params()`. Adam no longer
208+
re-zeros moment buffers on every call for `fp16` parameters, and compatible moment and momentum buffers migrate
209+
when replacement parameters move devices, preserving accumulated state and preventing invalid cross-device reuse
210+
([GH-1593](https://github.com/NVIDIA/warp/issues/1593),
211+
[GH-1615](https://github.com/NVIDIA/warp/issues/1615)).
211212

212213
### Documentation
213214

warp/_src/optim/adam.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,16 @@ def set_params(self, params):
133133
# buffer dtype), not ``param.dtype``, otherwise the buffers are re-zeroed on every call.
134134
if self.m[i] is None or self.m[i].shape != param.shape or self.m[i].dtype != dtype:
135135
self.m[i] = wp.zeros(shape=param.shape, dtype=dtype, device=param.device)
136+
elif self.m[i].device != param.device:
137+
migrated_m = wp.empty(shape=param.shape, dtype=dtype, device=param.device)
138+
wp.copy(migrated_m, self.m[i])
139+
self.m[i] = migrated_m
136140
if self.v[i] is None or self.v[i].shape != param.shape or self.v[i].dtype != dtype:
137141
self.v[i] = wp.zeros(shape=param.shape, dtype=dtype, device=param.device)
142+
elif self.v[i].device != param.device:
143+
migrated_v = wp.empty(shape=param.shape, dtype=dtype, device=param.device)
144+
wp.copy(migrated_v, self.v[i])
145+
self.v[i] = migrated_v
138146

139147
def reset_internal_state(self):
140148
"""Reset moment buffers and timestep to zero."""

warp/_src/optim/sgd.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ def set_params(self, params):
8282
param = params[i]
8383
if self.b[i] is None or self.b[i].shape != param.shape or self.b[i].dtype != param.dtype:
8484
self.b[i] = wp.zeros_like(param)
85+
elif self.b[i].device != param.device:
86+
migrated_b = wp.empty_like(param)
87+
wp.copy(migrated_b, self.b[i])
88+
self.b[i] = migrated_b
8589
# Overload the kernel for each parameter so we can precompile the SGD kernel
8690
if param is not None:
8791
wp.overload(sgd_step_kernel, {"g": param, "b": param, "params": param})

warp/tests/test_adam.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,46 @@ def test_adam_set_params_preserves_fp16_state(test, device):
159159
test.assertTrue((opt.v[0].numpy() == 1.0).all(), f"second moment reset for {param_dtype}")
160160

161161

162+
def test_adam_set_params_migrates_state(test, device):
163+
"""Verify compatible moment buffers follow parameters that move devices."""
164+
moved_param = wp.zeros(4, dtype=wp.float32, device="cpu")
165+
unmoved_param = wp.zeros(4, dtype=wp.float32, device="cpu")
166+
opt = warp.optim.Adam([moved_param, unmoved_param], lr=0.02)
167+
168+
moved_m, moved_v = opt.m[0], opt.v[0]
169+
unmoved_m, unmoved_v = opt.m[1], opt.v[1]
170+
moved_m.fill_(1.0)
171+
moved_v.fill_(2.0)
172+
173+
expected_m = moved_m.numpy().copy()
174+
expected_v = moved_v.numpy().copy()
175+
replacement = wp.zeros(4, dtype=wp.float32, device=device)
176+
opt.set_params([replacement, unmoved_param])
177+
178+
test.assertEqual(opt.m[0].device, device)
179+
test.assertEqual(opt.v[0].device, device)
180+
np.testing.assert_array_equal(opt.m[0].numpy(), expected_m)
181+
np.testing.assert_array_equal(opt.v[0].numpy(), expected_v)
182+
test.assertIs(opt.m[1], unmoved_m)
183+
test.assertIs(opt.v[1], unmoved_v)
184+
185+
opt.step([wp.ones_like(replacement), wp.ones_like(unmoved_param)])
186+
replacement.numpy()
187+
unmoved_param.numpy()
188+
189+
expected_m = opt.m[0].numpy().copy()
190+
expected_v = opt.v[0].numpy().copy()
191+
replacement = wp.zeros(4, dtype=wp.float32, device="cpu")
192+
opt.set_params([replacement, unmoved_param])
193+
194+
test.assertEqual(opt.m[0].device, wp.get_device("cpu"))
195+
test.assertEqual(opt.v[0].device, wp.get_device("cpu"))
196+
np.testing.assert_array_equal(opt.m[0].numpy(), expected_m)
197+
np.testing.assert_array_equal(opt.v[0].numpy(), expected_v)
198+
test.assertIs(opt.m[1], unmoved_m)
199+
test.assertIs(opt.v[1], unmoved_v)
200+
201+
162202
devices = get_test_devices()
163203

164204

@@ -172,6 +212,12 @@ class TestAdam(unittest.TestCase):
172212
add_function_test(
173213
TestAdam, "test_adam_set_params_preserves_fp16_state", test_adam_set_params_preserves_fp16_state, devices=devices
174214
)
215+
add_function_test(
216+
TestAdam,
217+
"test_adam_set_params_migrates_state",
218+
test_adam_set_params_migrates_state,
219+
devices=get_cuda_test_devices(),
220+
)
175221

176222

177223
if __name__ == "__main__":

warp/tests/test_sgd.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
import unittest
55

6+
import numpy as np
7+
68
import warp as wp
79
import warp.optim
810
from warp.tests.unittest_utils import *
@@ -205,6 +207,37 @@ def test_sgd_reset_internal_state(test, device):
205207
test.assertAlmostEqual(result2[0], 0.9, places=5)
206208

207209

210+
def test_sgd_set_params_migrates_state(test, device):
211+
"""Verify compatible momentum buffers follow parameters that move devices."""
212+
moved_param = wp.zeros(4, dtype=wp.float32, device="cpu")
213+
unmoved_param = wp.zeros(4, dtype=wp.float32, device="cpu")
214+
opt = warp.optim.SGD([moved_param, unmoved_param], lr=0.02, momentum=0.9)
215+
216+
moved_b = opt.b[0]
217+
unmoved_b = opt.b[1]
218+
moved_b.fill_(1.0)
219+
220+
expected_b = moved_b.numpy().copy()
221+
replacement = wp.zeros(4, dtype=wp.float32, device=device)
222+
opt.set_params([replacement, unmoved_param])
223+
224+
test.assertEqual(opt.b[0].device, device)
225+
np.testing.assert_array_equal(opt.b[0].numpy(), expected_b)
226+
test.assertIs(opt.b[1], unmoved_b)
227+
228+
opt.step([wp.ones_like(replacement), wp.ones_like(unmoved_param)])
229+
replacement.numpy()
230+
unmoved_param.numpy()
231+
232+
expected_b = opt.b[0].numpy().copy()
233+
replacement = wp.zeros(4, dtype=wp.float32, device="cpu")
234+
opt.set_params([replacement, unmoved_param])
235+
236+
test.assertEqual(opt.b[0].device, wp.get_device("cpu"))
237+
np.testing.assert_array_equal(opt.b[0].numpy(), expected_b)
238+
test.assertIs(opt.b[1], unmoved_b)
239+
240+
208241
devices = get_test_devices()
209242

210243

@@ -219,6 +252,12 @@ class TestSGD(unittest.TestCase):
219252
add_function_test(TestSGD, "test_sgd_combined_features", test_sgd_combined_features, devices=devices)
220253
add_function_test(TestSGD, "test_sgd_no_momentum", test_sgd_no_momentum, devices=devices)
221254
add_function_test(TestSGD, "test_sgd_reset_internal_state", test_sgd_reset_internal_state, devices=devices)
255+
add_function_test(
256+
TestSGD,
257+
"test_sgd_set_params_migrates_state",
258+
test_sgd_set_params_migrates_state,
259+
devices=get_cuda_test_devices(),
260+
)
222261

223262

224263
if __name__ == "__main__":

0 commit comments

Comments
 (0)