Skip to content

Commit 0015c76

Browse files
authored
Read reduce keepdim when it is serialized as Bool (#24379)
Summary: mean.dim and sum.dim_IntList read keepdim only when it was serialized as Int, but it is serialized as Bool, so a dynamic resize dropped the kept dim. keepdim is now read as Bool or Int (anything else throws), and the output rank is checked against it at build time. This unblocks normalization built from mean(keepdim=True) in dynamic-length models. Reviewed By: SS-JIA Differential Revision: D127010334 Pull Request resolved: #24379
1 parent 0fc43f6 commit 0015c76

5 files changed

Lines changed: 71 additions & 17 deletions

File tree

‎backends/webgpu/runtime/WebGPUUtils.h‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,19 @@ inline float scalar_or(WebGPUGraph& graph, int id, float fallback) {
301301
throw std::runtime_error("scalar_or: expected Double, Int, or None");
302302
}
303303

304+
// Reads a `bool` arg, serialized as Bool; Int is also accepted. Any other
305+
// type, None included, throws instead of defaulting.
306+
inline bool get_bool_or_int(WebGPUGraph& graph, int id, const char* what) {
307+
const auto vt = graph.get_value_type(id);
308+
if (vt == WebGPUGraph::ValueType::Bool) {
309+
return graph.get_bool(id);
310+
}
311+
if (vt == WebGPUGraph::ValueType::Int) {
312+
return graph.get_int(id) != 0;
313+
}
314+
throw std::runtime_error(std::string(what) + " must be a Bool or Int");
315+
}
316+
304317
// Resolves an optional tensor arg (bias/mask/affine) to the binding to use:
305318
// the real tensor's buffer+size if present, else a zero-filled dummy. Caller
306319
// releases `.owned_dummy` (if non-null) after the dispatch is recorded.

‎backends/webgpu/runtime/ops/embedding_q4gsw/EmbeddingQ4gsw.cpp‎

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -113,18 +113,8 @@ void embedding_q4gsw_impl(WebGPUGraph& graph, const std::vector<int>& args) {
113113

114114
// is_linear_weight selects the nibble packing (false: even dim = high nibble;
115115
// true: even dim = low nibble). The shader handles both via a uniform.
116-
bool is_linear = false;
117-
if (graph.get_value_type(is_linear_weight_id) ==
118-
WebGPUGraph::ValueType::Bool) {
119-
is_linear = graph.get_bool(is_linear_weight_id);
120-
} else if (
121-
graph.get_value_type(is_linear_weight_id) ==
122-
WebGPUGraph::ValueType::Int) {
123-
is_linear = graph.get_int(is_linear_weight_id) != 0;
124-
} else {
125-
throw std::runtime_error(
126-
"WebGPU embedding_q4gsw: is_linear_weight must be Bool or Int");
127-
}
116+
const bool is_linear = utils::get_bool_or_int(
117+
graph, is_linear_weight_id, "WebGPU embedding_q4gsw: is_linear_weight");
128118

129119
if (weight.dims.size() < 2 || scales.dims.size() < 2 || out.dims.empty() ||
130120
indices.dims.empty()) {

‎backends/webgpu/runtime/ops/reduce/Reduce.cpp‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,8 @@ void reduce_impl(
9696
const auto& in = graph.get_tensor(in_id);
9797
const auto& out = graph.get_tensor(out_id);
9898

99-
bool keepdim = false;
100-
if (graph.get_value_type(keepdim_id) == WebGPUGraph::ValueType::Int) {
101-
keepdim = graph.get_int(keepdim_id) != 0;
102-
}
99+
const bool keepdim =
100+
utils::get_bool_or_int(graph, keepdim_id, "WebGPU reduce: keepdim");
103101

104102
if (in.dims.empty()) {
105103
throw std::runtime_error("WebGPU reduce: scalar input unsupported");
@@ -119,6 +117,12 @@ void reduce_impl(
119117
if (outer == 0 || r == 0 || inner == 0) {
120118
throw std::runtime_error("WebGPU reduce: zero-sized reduction");
121119
}
120+
const size_t expected_out_rank =
121+
keepdim ? in.dims.size() : in.dims.size() - reduce_dims.size();
122+
if (out.dims.size() != expected_out_rank) {
123+
throw std::runtime_error(
124+
"WebGPU reduce: output rank does not match keepdim");
125+
}
122126

123127
uint64_t in_numel = 1;
124128
for (int64_t d : in.dims) {

‎backends/webgpu/test/native/test_dynamic_shape.cpp‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
// M dyn_sigmoid N dyn_select (select_copy(0,-1), dynamic S)
2121
// O ONE dyn_conv1d graph reused across live input lengths
2222
// P ONE dyn_gelu graph reused above -> at -> above the old 1D dispatch cap
23-
// Q dyn_layer_norm(_rows) (reused graphs)
23+
// Q dyn_layer_norm(_rows) R dyn_mean_keepdim (reused graphs)
2424
// .pte + goldens from test/ops/dynamic_shape/test_dynamic_shape_export.py.
2525
//
2626
// Artifacts dir: $WEBGPU_DYNAMIC_SHAPE_DIR, else argv[1], else
@@ -1933,6 +1933,24 @@ TEST(DynamicShape, LayerNormRowGridReusedGraph) {
19331933
}
19341934
}
19351935

1936+
// R: mean.dim(keepdim=True) over the hidden dim ([1,1,S,1]) and the dynamic S
1937+
// dim ([1,1,1,kHidden]), x - mean(x, -1, keepdim=True), and a keepdim=False
1938+
// mean ([1,1,S]) on ONE loaded graph. keepdim is serialized as a Bool, so a
1939+
// resize that ignored it would drop the kept dim.
1940+
TEST(DynamicShape, MeanKeepdimReusedGraph) {
1941+
Module m(g_dir + "/dyn_mean_keepdim.pte");
1942+
ASSERT_EQ(m.load_forward(), Error::Ok) << "load dyn_mean_keepdim.pte";
1943+
for (int s : {7, 1, 125, 128, 7}) {
1944+
check_outputs(
1945+
m,
1946+
"dyn_mean_keepdim",
1947+
s,
1948+
{1, 1, s, kHidden},
1949+
{{1, 1, s, 1}, {1, 1, 1, kHidden}, {1, 1, s, kHidden}, {1, 1, s}},
1950+
1e-5f);
1951+
}
1952+
}
1953+
19361954
TEST(DynamicShape, SwiGluReusedGraph) {
19371955
Module module(g_dir + "/dyn_swiglu.pte");
19381956
ASSERT_EQ(module.load_forward(), Error::Ok) << "load dyn_swiglu.pte";

‎backends/webgpu/test/ops/dynamic_shape/test_dynamic_shape_export.py‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,15 @@ def forward(self, x: torch.Tensor):
118118
return (out + x if self.residual else out), mean, rstd
119119

120120

121+
class MeanKeepdimModule(torch.nn.Module):
122+
"""mean.dim with keepdim=True over the hidden dim and the dynamic S dim, a
123+
broadcast consumer of the kept dim, and a keepdim=False mean."""
124+
125+
def forward(self, x: torch.Tensor):
126+
m = x.mean(dim=-1, keepdim=True)
127+
return m, x.mean(dim=2, keepdim=True), x - m, x.mean(dim=-1)
128+
129+
121130
class SwiGluModule(torch.nn.Module):
122131
def __init__(
123132
self,
@@ -374,6 +383,18 @@ def export_dynamic_layer_norm_cases(out_dir: str, s_dim) -> None:
374383
)
375384

376385

386+
def export_dynamic_mean_keepdim_cases(out_dir: str, s_dim) -> None:
387+
_export_multi_output_case(
388+
MeanKeepdimModule(),
389+
"dyn_mean_keepdim",
390+
out_dir,
391+
lambda s: (1, 1, s, HIDDEN),
392+
2,
393+
s_dim,
394+
(MAXS, 125, 7, 1),
395+
)
396+
397+
377398
def export_dynamic_gelu_boundary_cases(out_dir: str) -> None:
378399
"""Write a dynamic GELU fixture crossing the old 1D dispatch cap."""
379400
os.makedirs(out_dir, exist_ok=True)
@@ -507,6 +528,9 @@ def export_dynamic_shape_cases(out_dir: str) -> None:
507528
# and with a dynamic row count past the 1D grid limit.
508529
export_dynamic_layer_norm_cases(out_dir, s_dim)
509530

531+
# 2h.2) mean.dim(keepdim=True/False) over the hidden dim and the DYNAMIC S.
532+
export_dynamic_mean_keepdim_cases(out_dir, s_dim)
533+
510534
# 2i) Dynamic SwiGLU graph patterns. The Llama-width fixture forces a 2D
511535
# fused dispatch at M=512; compact variants isolate commutative matching and
512536
# graph/intermediate ownership guards around same-input q4 projections.
@@ -1713,6 +1737,11 @@ def test_export_dynamic_rms(self) -> None:
17131737
"dyn_layer_norm_rows.pte",
17141738
"dyn_layer_norm_rows.S70000.input.bin",
17151739
"dyn_layer_norm_rows.S1000.golden2.bin",
1740+
"dyn_mean_keepdim.pte",
1741+
"dyn_mean_keepdim.S125.input.bin",
1742+
"dyn_mean_keepdim.S125.golden0.bin",
1743+
"dyn_mean_keepdim.S125.golden3.bin",
1744+
"dyn_mean_keepdim.S7.golden2.bin",
17161745
"dyn_linear_bk64.pte",
17171746
"dyn_linear_bk64.S512.input.bin",
17181747
"dyn_linear_bk64.S512.golden.bin",

0 commit comments

Comments
 (0)