Skip to content

Commit 0506c76

Browse files
committed
[ExecuTorch][WebGPU] Tests for the adamw WebGPU ops
Pull Request resolved: #20936 Export-delegation + fp64 golden tests for the adamw (`adamw_step`) ops. Key changes: - `test/ops/test_adamw.py` — fp64 in-place param/m/v goldens (incl. non-zero weight-decay) + delegation checks — fp64 library-reference goldens + delegation checks (≥2 cases/op). Co-authored-with: Claude Code. ghstack-source-id: 405026082 @exported-using-ghexport Differential Revision: [D111755127](https://our.internmc.facebook.com/intern/diff/D111755127/)
1 parent 8c73dbf commit 0506c76

1 file changed

Lines changed: 142 additions & 0 deletions

File tree

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
"""AdamW optimizer step (`et_vk.adamw_step`) export + fp64 golden.
8+
9+
`adamw_step(param, m, v, grad, lr, beta1, beta2, eps, weight_decay, bc1, bc2)`
10+
updates the fp32 latent in place: decoupled weight decay, then the bias-corrected
11+
Adam moment update. `bc1`/`bc2` (= 1 - beta^t) are host-precomputed so the kernel
12+
carries no step counter. The op mutates and returns `param`/`m`/`v` (aliased), so
13+
export wraps it in `auto_functionalized`, which VulkanPartitioner tags by name (the
14+
same mutating-op path as `update_cache`). Golden is the fp64 reference, computed
15+
independently so a lossy fp32 op impl cannot fake-pass.
16+
"""
17+
18+
from __future__ import annotations
19+
20+
import unittest
21+
from dataclasses import dataclass
22+
23+
import torch
24+
25+
from executorch.backends.vulkan.partitioner.vulkan_partitioner import VulkanPartitioner
26+
from executorch.exir import to_edge_transform_and_lower
27+
28+
# torch.optim.AdamW defaults; bc1/bc2 are 1 - beta^step (host-precomputed).
29+
LR = 1e-3
30+
BETA1 = 0.9
31+
BETA2 = 0.999
32+
EPS = 1e-8
33+
34+
35+
@dataclass(frozen=True)
36+
class AdamwConfig:
37+
name: str
38+
numel: int
39+
weight_decay: float = 0.01
40+
step: int = 1
41+
42+
43+
CONFIGS = [
44+
AdamwConfig("small", 64),
45+
AdamwConfig("no_wd", 256, weight_decay=0.0), # wd=0 -> the plain Adam path
46+
AdamwConfig("later_step", 1000, step=10), # bias correction well past t=1
47+
]
48+
49+
50+
def _inputs(cfg: AdamwConfig):
51+
"""Deterministic fp32 param/m/v/grad + the host bias corrections."""
52+
g = torch.Generator().manual_seed(0)
53+
param = torch.randn(cfg.numel, generator=g, dtype=torch.float32)
54+
m = torch.randn(cfg.numel, generator=g, dtype=torch.float32) * 0.1
55+
v = torch.rand(cfg.numel, generator=g, dtype=torch.float32) * 0.01
56+
grad = torch.randn(cfg.numel, generator=g, dtype=torch.float32)
57+
bc1 = 1.0 - BETA1**cfg.step
58+
bc2 = 1.0 - BETA2**cfg.step
59+
return param, m, v, grad, bc1, bc2
60+
61+
62+
def _fp64_golden(param, m, v, grad, wd, bc1, bc2):
63+
"""fp64 truth for one AdamW step; mirrors adamw_step.wgsl exactly."""
64+
p = param.double()
65+
g = grad.double()
66+
p = p - LR * wd * p
67+
m64 = BETA1 * m.double() + (1.0 - BETA1) * g
68+
v64 = BETA2 * v.double() + (1.0 - BETA2) * g * g
69+
mhat = m64 / bc1
70+
vhat = v64 / bc2
71+
p = p - LR * mhat / (torch.sqrt(vhat) + EPS)
72+
return p.float(), m64.float(), v64.float()
73+
74+
75+
class _AdamwModule(torch.nn.Module):
76+
def __init__(self, wd: float, bc1: float, bc2: float) -> None:
77+
super().__init__()
78+
self.wd = wd
79+
self.bc1 = bc1
80+
self.bc2 = bc2
81+
82+
def forward(self, param, m, v, grad):
83+
return torch.ops.et_vk.adamw_step(
84+
param, m, v, grad, LR, BETA1, BETA2, EPS, self.wd, self.bc1, self.bc2
85+
)
86+
87+
88+
def _export(cfg: AdamwConfig):
89+
param, m, v, grad, bc1, bc2 = _inputs(cfg)
90+
ep = torch.export.export(
91+
_AdamwModule(cfg.weight_decay, bc1, bc2), (param, m, v, grad)
92+
)
93+
return to_edge_transform_and_lower(
94+
ep, partitioner=[VulkanPartitioner()]
95+
).to_executorch()
96+
97+
98+
def _delegates(et) -> bool:
99+
return any(
100+
d.id == "VulkanBackend"
101+
for plan in et.executorch_program.execution_plan
102+
for d in plan.delegates
103+
)
104+
105+
106+
class TestAdamwStep(unittest.TestCase):
107+
def test_export_delegates(self) -> None:
108+
for cfg in CONFIGS:
109+
with self.subTest(config=cfg.name):
110+
et = _export(cfg)
111+
self.assertTrue(
112+
_delegates(et), f"no VulkanBackend delegate in {cfg.name}"
113+
)
114+
115+
def test_op_matches_fp64_golden(self) -> None:
116+
for cfg in CONFIGS:
117+
with self.subTest(config=cfg.name):
118+
param, m, v, grad, bc1, bc2 = _inputs(cfg)
119+
g_param, g_m, g_v = _fp64_golden(
120+
param, m, v, grad, cfg.weight_decay, bc1, bc2
121+
)
122+
# Op mutates in place; clone so the golden saw the originals.
123+
out_param, out_m, out_v = torch.ops.et_vk.adamw_step(
124+
param.clone(),
125+
m.clone(),
126+
v.clone(),
127+
grad,
128+
LR,
129+
BETA1,
130+
BETA2,
131+
EPS,
132+
cfg.weight_decay,
133+
bc1,
134+
bc2,
135+
)
136+
torch.testing.assert_close(out_param, g_param, atol=5e-4, rtol=1e-3)
137+
torch.testing.assert_close(out_m, g_m, atol=5e-4, rtol=1e-3)
138+
torch.testing.assert_close(out_v, g_v, atol=5e-4, rtol=1e-3)
139+
140+
141+
if __name__ == "__main__":
142+
unittest.main()

0 commit comments

Comments
 (0)