Skip to content

Commit 6af9fe9

Browse files
authored
Qualcomm AI Engine Direct - Add templated AdamW optimizer to training extension (pytorch#22916)
Based on [pytorch#18848](pytorch#18848) and [pytorch#22857](pytorch#22857). ### Summary - Carries forward the AdamW optimizer work from [pytorch#18848](pytorch#18848), design and implemented by @BryanBradfo. - Incorporates the allocation-safety and test-coverage improvements from [pytorch#22857](pytorch#22857), implemented by @psiddh in response to reviewer feedback on the original AdamW implementation. - Consolidates the prior work into a single PR and addresses additional reviewer feedback from previous review rounds. - Adds Float and Half AdamW support. - Adds dtype, shape, layout, contiguity, and CPU validation before parameter updates. ### Test plan buck2 test //extension/training/optimizer/test:adamw_test passes all 9 tests.
1 parent d3ad4be commit 6af9fe9

7 files changed

Lines changed: 731 additions & 1 deletion

File tree

‎extension/training/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ current state.
88
## Layout
99
- `examples/` : Example end to end flows from model definition to optimizer.step()
1010
- `module/`: Utility class to provide an improved UX when using ExecuTorch for Training.
11-
- `optimizer/`: Cpp implementations of various optimizers, currently only SGD though Adam is planned.
11+
- `optimizer/`: C++ implementations of various optimizers, currently SGD and AdamW. Adam is planned.
1212
- `test/`: Tests that cover multiple subdirs.
1313

1414
## Technical Birds Eye view
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
#include <executorch/extension/training/optimizer/adamw.h>
10+
11+
#include <executorch/extension/tensor/tensor_ptr.h>
12+
#include <executorch/runtime/core/error.h>
13+
#include <executorch/runtime/core/exec_aten/util/scalar_type_util.h>
14+
#include <executorch/runtime/core/exec_aten/util/tensor_util.h>
15+
16+
#include <cmath>
17+
#include <cstdlib>
18+
#include <cstring>
19+
20+
using executorch::aten::Tensor;
21+
using executorch::extension::make_tensor_ptr;
22+
using ::executorch::runtime::Error;
23+
24+
namespace executorch {
25+
namespace extension {
26+
namespace training {
27+
namespace optimizer {
28+
29+
namespace {
30+
// out[i] = a[i] + alpha * b[i]
31+
template <typename scalar_t>
32+
void add_out_hack(
33+
const Tensor& a,
34+
const Tensor& b,
35+
const double alpha,
36+
Tensor& out) {
37+
auto a_ptr = a.const_data_ptr<scalar_t>();
38+
auto b_ptr = b.const_data_ptr<scalar_t>();
39+
auto out_ptr = out.mutable_data_ptr<scalar_t>();
40+
for (ssize_t i = 0; i < a.numel(); ++i) {
41+
out_ptr[i] = static_cast<scalar_t>(
42+
static_cast<double>(a_ptr[i]) + static_cast<double>(b_ptr[i]) * alpha);
43+
}
44+
}
45+
46+
// out[i] = a[i] * alpha
47+
template <typename scalar_t>
48+
void mul_out_hack(const Tensor& a, const double alpha, Tensor& out) {
49+
auto a_ptr = a.const_data_ptr<scalar_t>();
50+
auto out_ptr = out.mutable_data_ptr<scalar_t>();
51+
for (ssize_t i = 0; i < a.numel(); ++i) {
52+
out_ptr[i] = static_cast<scalar_t>(static_cast<double>(a_ptr[i]) * alpha);
53+
}
54+
}
55+
56+
// Fused second-moment update: v[i] = beta2 * v[i] + (1 - beta2) * g[i]^2.
57+
// Avoids materializing a separate g^2 tensor.
58+
template <typename scalar_t>
59+
void addcmul_sq_out_hack(
60+
const Tensor& v,
61+
const Tensor& g,
62+
const double beta2,
63+
Tensor& out) {
64+
auto v_ptr = v.const_data_ptr<scalar_t>();
65+
auto g_ptr = g.const_data_ptr<scalar_t>();
66+
auto out_ptr = out.mutable_data_ptr<scalar_t>();
67+
const double one_minus_beta2 = 1.0 - beta2;
68+
for (ssize_t i = 0; i < v.numel(); ++i) {
69+
const double gi = static_cast<double>(g_ptr[i]);
70+
out_ptr[i] = static_cast<scalar_t>(
71+
static_cast<double>(v_ptr[i]) * beta2 + one_minus_beta2 * gi * gi);
72+
}
73+
}
74+
75+
// Fused AdamW parameter update:
76+
// p[i] -= lr * (m[i] / bias_correction1) /
77+
// (sqrt(v[i] / bias_correction2) + eps)
78+
// Performed in double precision internally to limit accumulated FP error on
79+
// the division-by-sqrt path.
80+
template <typename scalar_t>
81+
void adamw_update_hack(
82+
Tensor& p,
83+
const Tensor& m,
84+
const Tensor& v,
85+
const double lr,
86+
const double bias_correction1,
87+
const double bias_correction2,
88+
const double eps) {
89+
auto p_ptr = p.mutable_data_ptr<scalar_t>();
90+
auto m_ptr = m.const_data_ptr<scalar_t>();
91+
auto v_ptr = v.const_data_ptr<scalar_t>();
92+
const double inv_bc1 = 1.0 / bias_correction1;
93+
const double inv_sqrt_bc2 = 1.0 / std::sqrt(bias_correction2);
94+
for (ssize_t i = 0; i < p.numel(); ++i) {
95+
const double m_hat = static_cast<double>(m_ptr[i]) * inv_bc1;
96+
const double v_hat_sqrt =
97+
std::sqrt(static_cast<double>(v_ptr[i])) * inv_sqrt_bc2;
98+
p_ptr[i] = static_cast<scalar_t>(
99+
static_cast<double>(p_ptr[i]) - lr * m_hat / (v_hat_sqrt + eps));
100+
}
101+
}
102+
} // namespace
103+
104+
bool AdamWParamGroup::has_options() const {
105+
return options_ != nullptr;
106+
}
107+
108+
AdamWOptions& AdamWParamGroup::options() {
109+
return *options_.get();
110+
}
111+
112+
const AdamWOptions& AdamWParamGroup::options() const {
113+
return *options_.get();
114+
}
115+
116+
void AdamWParamGroup::set_options(std::unique_ptr<AdamWOptions> options) {
117+
options_ = std::move(options);
118+
}
119+
120+
const std::map<std::string_view, executorch::aten::Tensor>&
121+
AdamWParamGroup::named_parameters() const {
122+
return named_parameters_;
123+
}
124+
125+
void AdamW::add_param_group(const AdamWParamGroup& param_group) {
126+
AdamWParamGroup param_group_(param_group.named_parameters());
127+
if (!param_group.has_options()) {
128+
param_group_.set_options(defaults_.clone());
129+
} else {
130+
param_group_.set_options(param_group.options().clone());
131+
}
132+
param_groups_.emplace_back(std::move(param_group_));
133+
}
134+
135+
Error AdamW::step(const std::map<std::string_view, executorch::aten::Tensor>&
136+
named_gradients) {
137+
for (auto& group : param_groups_) {
138+
auto& options = group.options();
139+
const double lr = options.lr();
140+
const double beta1 = options.beta1();
141+
const double beta2 = options.beta2();
142+
const double eps = options.eps();
143+
const double weight_decay = options.weight_decay();
144+
145+
for (auto param_iter = group.named_parameters().begin();
146+
param_iter != group.named_parameters().end();
147+
++param_iter) {
148+
const auto& named_gradient = named_gradients.find(param_iter->first);
149+
if (named_gradient == named_gradients.end()) {
150+
continue;
151+
}
152+
auto g = named_gradient->second;
153+
auto p = param_iter->second;
154+
const auto scalar_type = p.scalar_type();
155+
156+
if (scalar_type != executorch::aten::ScalarType::Float &&
157+
scalar_type != executorch::aten::ScalarType::Half) {
158+
return Error::InvalidArgument;
159+
}
160+
161+
if (p.sizes() != g.sizes() || scalar_type != g.scalar_type() ||
162+
!executorch::ET_RUNTIME_NAMESPACE::tensor_is_contiguous(p) ||
163+
!executorch::ET_RUNTIME_NAMESPACE::tensor_is_contiguous(g) ||
164+
!p.device().is_cpu() || !g.device().is_cpu()) {
165+
return Error::InvalidArgument;
166+
}
167+
168+
// Look up or lazily allocate the per-parameter state (two moment
169+
// buffers sized and shaped like the gradient, plus a step counter).
170+
// Done before p is touched: the allocation below can fail, and a
171+
// half-applied step that decayed p without advancing its moments would
172+
// decay p twice if the caller retried.
173+
auto param_state_it = state_.find(p.unsafeGetTensorImpl());
174+
AdamWParamState* state_ptr = nullptr;
175+
if (param_state_it == state_.end()) {
176+
// Moment buffers use the parameter dtype to preserve on-device memory
177+
// usage. Half state trades numerical precision for a smaller footprint.
178+
void* m_buf_ptr = malloc(g.nbytes());
179+
void* v_buf_ptr = malloc(g.nbytes());
180+
if ((m_buf_ptr == nullptr || v_buf_ptr == nullptr) && g.nbytes() != 0) {
181+
free(m_buf_ptr);
182+
free(v_buf_ptr);
183+
return Error::MemoryAllocationFailed;
184+
}
185+
if (g.nbytes() != 0) {
186+
std::memset(m_buf_ptr, 0, g.nbytes());
187+
std::memset(v_buf_ptr, 0, g.nbytes());
188+
}
189+
190+
std::vector<executorch::aten::SizesType> sizes(
191+
g.sizes().begin(), g.sizes().end());
192+
// The buffers come from malloc, so they live on the CPU regardless of
193+
// where the gradient does.
194+
const auto cpu =
195+
executorch::aten::Device(executorch::aten::DeviceType::CPU);
196+
auto m_ptr = make_tensor_ptr(
197+
sizes,
198+
m_buf_ptr,
199+
g.scalar_type(),
200+
cpu,
201+
executorch::aten::TensorShapeDynamism::STATIC,
202+
[](void* p) { free(p); });
203+
auto v_ptr = make_tensor_ptr(
204+
sizes,
205+
v_buf_ptr,
206+
g.scalar_type(),
207+
cpu,
208+
executorch::aten::TensorShapeDynamism::STATIC,
209+
[](void* p) { free(p); });
210+
211+
auto state = std::make_unique<AdamWParamState>(
212+
std::move(m_ptr), std::move(v_ptr));
213+
state_ptr = state.get();
214+
state_[p.unsafeGetTensorImpl()] = std::move(state);
215+
} else {
216+
state_ptr = param_state_it->second.get();
217+
}
218+
219+
// Create a minimal context for error handling in ET_SWITCH
220+
struct {
221+
[[noreturn]] void fail(Error /* error */) {
222+
ET_CHECK_MSG(false, "Unsupported dtype in AdamW::step");
223+
}
224+
} ctx;
225+
226+
ET_SWITCH_FLOATH_TYPES(scalar_type, ctx, "AdamW::step", CTYPE, [&] {
227+
// Decoupled weight decay: p <- p - lr * weight_decay * p. Applied
228+
// to the parameter directly, BEFORE the moment-based update, and
229+
// NOT folded into the gradient. This is the defining property of
230+
// AdamW (Loshchilov & Hutter, 2019).
231+
if (weight_decay != 0.0) {
232+
add_out_hack<CTYPE>(p, p, -lr * weight_decay, p);
233+
}
234+
235+
state_ptr->increment_step_count();
236+
const int64_t step = state_ptr->step_count();
237+
238+
Tensor& exp_avg = state_ptr->exp_avg();
239+
Tensor& exp_avg_sq = state_ptr->exp_avg_sq();
240+
241+
// First moment: m <- beta1 * m + (1 - beta1) * g
242+
mul_out_hack<CTYPE>(exp_avg, beta1, exp_avg);
243+
add_out_hack<CTYPE>(exp_avg, g, 1.0 - beta1, exp_avg);
244+
245+
// Second moment: v <- beta2 * v + (1 - beta2) * g^2
246+
addcmul_sq_out_hack<CTYPE>(exp_avg_sq, g, beta2, exp_avg_sq);
247+
248+
// Bias-corrected update.
249+
const double bias_correction1 = 1.0 - std::pow(beta1, step);
250+
const double bias_correction2 = 1.0 - std::pow(beta2, step);
251+
adamw_update_hack<CTYPE>(
252+
p,
253+
exp_avg,
254+
exp_avg_sq,
255+
lr,
256+
bias_correction1,
257+
bias_correction2,
258+
eps);
259+
});
260+
}
261+
}
262+
return Error::Ok;
263+
}
264+
265+
AdamW::~AdamW() = default;
266+
267+
} // namespace optimizer
268+
} // namespace training
269+
} // namespace extension
270+
} // namespace executorch

0 commit comments

Comments
 (0)