Skip to content

Commit c87787e

Browse files
justinchubyCopilot
andauthored
feat: Enable WebGPU Shape op support, remove EliminateShape rewrite rules (#132)
WebGPU now supports the ONNX `Shape` operator natively. This PR removes the workaround that existed for the old constraint. ## What was removed | Item | Reason | |------|--------| | `_eliminate_shape.py` | WebGPU-only workaround — replaces `Shape(attention_mask)` with `ReduceSum+ReduceMax`. No longer needed. | | `_eliminate_shape_test.py` | Tests for the removed module | | `supports_shape` field from `EpCapabilities` | Only WebGPU used it with `False`; removing the field cleans up the API | | `EliminateShape` lowering pass in `_optimizations.py` | Nothing calls it anymore | | `eliminate_shape_rules` from `rewrite_rules` public API | Removed with the implementation | ## What was enabled The `webgpu` EP entry in `_execution_providers.py` no longer sets `supports_shape=False` (field removed). WebGPU models now retain their `Shape` ops, matching all other EPs. ## Test update `test_webgpu_no_shape_nodes` → `test_webgpu_supports_shape_nodes`: assertion flipped from `== 0` to `> 0`, confirming Shape nodes are preserved in WebGPU graphs. ## Stats - 416 lines deleted, 10 lines added - 2317 tests pass --------- Signed-off-by: Justin Chu <justinchuby@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent d31f871 commit c87787e

11 files changed

Lines changed: 30 additions & 421 deletions

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### WebGPU Shape Op Support
11+
12+
#### Changed
13+
14+
- WebGPU now supports the ONNX `Shape` operator natively. The `EliminateShape`
15+
rewrite pass (which replaced `Shape(attention_mask)` with `ReduceSum` +
16+
`ReduceMax`) has been removed.
17+
18+
#### Removed
19+
20+
- **Breaking**: `EpCapabilities.supports_shape` field removed. Custom EPs that
21+
passed `supports_shape=True` or `supports_shape=False` to `EpCapabilities(...)`
22+
will get a `TypeError`. Remove the argument — `Shape` is now universally
23+
supported across all EPs.
24+
- `mobius.rewrite_rules.eliminate_shape_rules` removed from the public API.
25+
26+
---
27+
1028
### Mistral-3 / Pixtral VLM Support
1129

1230
#### Added

docs/ep_quickstart.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,6 @@ register_ep(EpCapabilities(
120120
gqa_dtypes=frozenset({ir.DataType.FLOAT16}),
121121
qkv_pack_dtypes=frozenset({ir.DataType.FLOAT16}),
122122
supports_fused_rope=True,
123-
supports_shape=True,
124123
provider_options={"some_option": "value"},
125124
))
126125

docs/execution_providers.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,6 @@ Some EPs cannot execute certain ops. These are handled by **lowering rules**
9999
|---|---|---|
100100
| No fused RoPE inside GQA | DML | `SeparateRoPE` rewrite: GQA `do_rotary=1` → explicit `RotaryEmbedding` + GQA `do_rotary=0` |
101101
| No packed QKV in GQA | DML | `UnpackQKV` rewrite: packed GQA → 3 separate `MatMul` projections |
102-
| No `Shape` operator | WebGPU | `EliminateShape` rewrite: `Shape(attention_mask)``ReduceSum` + `ReduceMax` |
103102
| No `SkipLayerNorm` kernel | TRT-RTX, onnx-standard | `InlinePass`: expands fused ops using their registered `ir.Function` bodies |
104103
| No `FusedMatMul` kernel | onnx-standard | `InlinePass`: expands to `Transpose + MatMul` |
105104
| No `PackedMultiHeadAttention` kernel | onnx-standard | `InlinePass`: expands to block-diagonal attention bias + standard `Attention` |
@@ -119,7 +118,6 @@ class EpCapabilities:
119118
gqa_dtypes: frozenset[ir.DataType] # dtypes where GQA fusion fires
120119
qkv_pack_dtypes: frozenset[ir.DataType] # dtypes where PackQKV fusion fires
121120
supports_fused_rope: bool = True # False → SeparateRoPE + UnpackQKV
122-
supports_shape: bool = True # False → EliminateShape lowering
123121
supports_skip_layer_norm: bool = True # False → InlinePass expansion
124122
supports_fused_matmul: bool = True # False → Transpose + MatMul via InlinePass
125123
supports_fused_moe: bool = True # False → decompose fused MoE ops
@@ -141,7 +139,7 @@ EpCapabilities(name="cuda", gqa_dtypes={FLOAT16, BFLOAT16},
141139
EpCapabilities(name="dml", gqa_dtypes={FLOAT16},
142140
supports_fused_rope=False)
143141
EpCapabilities(name="webgpu", gqa_dtypes={FLOAT, FLOAT16},
144-
supports_shape=False, default_int4_accuracy_level=4,
142+
default_int4_accuracy_level=4,
145143
provider_options={"enableGraphCapture": "0", ...})
146144
EpCapabilities(name="trt-rtx", gqa_dtypes={FLOAT16, BFLOAT16},
147145
supports_skip_layer_norm=False, enable_graph_capture=True,
@@ -187,7 +185,7 @@ Stage 2b: InlinePass EP-gated. Expands custom ops the EP cannot execute
187185
decomposition for TRT-RTX).
188186
189187
Stage 3: Lowering EP-gated. Structural rewrites for EP constraints.
190-
↓ SeparateRoPE, UnpackQKV, EliminateShape
188+
↓ SeparateRoPE, UnpackQKV
191189
(each only fires if the EP's capabilities require it)
192190
193191
Stage 4: Fold EP-agnostic. Always applied.

src/mobius/__main__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -278,8 +278,6 @@ def _cmd_list(args: argparse.Namespace) -> None:
278278
extras = []
279279
if not caps.supports_fused_rope:
280280
extras.append("no-fused-rope")
281-
if not caps.supports_shape:
282-
extras.append("no-shape")
283281
if not caps.supports_skip_layer_norm:
284282
extras.append("no-skip-layer-norm")
285283
flags = f" [{', '.join(extras)}]" if extras else ""

src/mobius/_execution_providers.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@ class EpCapabilities:
6060
stage).
6161
supports_fused_rope: ``False`` triggers SeparateRoPE + UnpackQKV
6262
lowering (DML).
63-
supports_shape: ``False`` triggers EliminateShape lowering (WebGPU).
6463
supports_skip_layer_norm: ``False`` expands SkipLayerNormalization /
6564
SkipSimplifiedLayerNormalization via InlinePass (TRT-RTX).
6665
supports_fused_moe: ``False`` decomposes fused MoE ops.
@@ -78,7 +77,6 @@ class EpCapabilities:
7877
gqa_dtypes: frozenset[ir.DataType] = dataclasses.field(default_factory=frozenset)
7978
qkv_pack_dtypes: frozenset[ir.DataType] = dataclasses.field(default_factory=frozenset)
8079
supports_fused_rope: bool = True
81-
supports_shape: bool = True
8280
supports_skip_layer_norm: bool = True
8381
supports_fused_moe: bool = True
8482
supports_packed_multi_head_attention: bool = False
@@ -221,7 +219,6 @@ def _register_builtins() -> None:
221219
name="webgpu",
222220
gqa_dtypes=frozenset({ir.DataType.FLOAT, ir.DataType.FLOAT16}),
223221
qkv_pack_dtypes=frozenset({ir.DataType.FLOAT, ir.DataType.FLOAT16}),
224-
supports_shape=False,
225222
default_int4_accuracy_level=4,
226223
provider_options={"enableGraphCapture": "0", "validationMode": "basic"},
227224
),
@@ -245,7 +242,6 @@ def _register_builtins() -> None:
245242
gqa_dtypes=frozenset(), # no GroupQueryAttention
246243
qkv_pack_dtypes=frozenset(), # no PackQKV
247244
supports_fused_rope=False, # no fused RoPE inside GQA (GQA not supported)
248-
supports_shape=True, # Shape is a standard ONNX op — no elimination needed
249245
supports_skip_layer_norm=False, # inline SkipLayerNorm
250246
supports_packed_multi_head_attention=False, # inline PackedMHA
251247
),

src/mobius/_optimizations.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
2. **Fusion** — promote standard ops to EP-supported fused ops
1212
(GQA, SkipNorm, GeluFusion). Gated by ``(ep, dtype)`` and ``model_role``.
1313
3. **Lowering** — decompose ops the EP cannot execute
14-
(SeparateRoPE, EliminateShape).
14+
(SeparateRoPE).
1515
4. **Fold** — final dead-node removal and constant folding.
1616
1717
All EP knowledge is encoded in :class:`~mobius._execution_providers.EpCapabilities`
@@ -57,7 +57,6 @@
5757
from mobius._passes import FoldConcatInitializersPass, FoldTransposedInitializerPass
5858
from mobius.functions import register_function_bodies
5959
from mobius.rewrite_rules import (
60-
eliminate_shape_rules,
6160
gelu_fusion_rules,
6261
group_query_attention_rules,
6362
pack_qkv_for_gqa_rules,
@@ -298,9 +297,6 @@ def _get_optimization_passes(
298297
lower.append(("SeparateRoPE", list(separate_rope_rules())))
299298
lower.append(("UnpackQKV", list(unpack_qkv_rules())))
300299

301-
if not caps.supports_shape:
302-
lower.append(("EliminateShape", list(eliminate_shape_rules())))
303-
304300
return fuse, lower
305301

306302

@@ -325,7 +321,7 @@ def optimize_model(
325321
2. **Fusion** — promote standard ops to EP-supported fused ops
326322
(e.g. GQA, SkipNorm, GeluFusion). Gated by ``(ep, dtype)`` and role.
327323
3. **Lowering** — decompose ops the EP cannot execute
328-
(e.g. SeparateRoPE for DML, EliminateShape for WebGPU).
324+
(e.g. SeparateRoPE for DML).
329325
4. **Fold** — final dead-node removal and constant folding.
330326
331327
After fusion, if GQA was expected for ``(ep, dtype)`` but zero

src/mobius/components/_common.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -191,11 +191,9 @@ def create_attention_bias(
191191
# Get query_length and total_length from shapes.
192192
# query_length comes from input_ids dim 1 (the query; e.g. 1 during decode).
193193
# total_length comes from attention_mask dim 1 (past + current tokens).
194-
# Using attention_mask for total_length lets the EliminateShape WebGPU rule
195-
# eliminate that Shape op. Using input_ids for query_length is semantically
196-
# correct: during decode input_ids is (batch, 1), so query_length=1 and
197-
# start = total_length - 1, giving the last row of q_indices.
198-
# On WebGPU (concrete dims), Shape(input_ids, 1) is constant-folded away.
194+
# Using input_ids for query_length is semantically correct: during decode
195+
# input_ids is (batch, 1), so query_length=1 and start = total_length - 1,
196+
# giving the last row of q_indices.
199197
query_length = op.Shape(input_ids, start=1, end=2) # 1-D [1]
200198
total_length = op.Shape(attention_mask, start=1, end=2) # 1-D [1]
201199
start = op.Sub(total_length, query_length)

src/mobius/rewrite_rules/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030

3131
__all__ = [
3232
"bias_gelu_rules",
33-
"eliminate_shape_rules",
3433
"gelu_fusion_rules",
3534
"group_query_attention_rules",
3635
"layer_norm_fusion_rules",
@@ -43,7 +42,6 @@
4342
]
4443

4544
from mobius.rewrite_rules._bias_gelu import bias_gelu_rules
46-
from mobius.rewrite_rules._eliminate_shape import eliminate_shape_rules
4745
from mobius.rewrite_rules._gelu_fusion import gelu_fusion_rules
4846
from mobius.rewrite_rules._group_query_attention import (
4947
group_query_attention_rules,

src/mobius/rewrite_rules/_eliminate_shape.py

Lines changed: 0 additions & 221 deletions
This file was deleted.

0 commit comments

Comments
 (0)