hyperonnx 1.0.2__tar.gz → 1.0.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. hyperonnx-1.0.3/.claude/CLAUDE.md +27 -0
  2. hyperonnx-1.0.3/.claude/skills/torch-dynamo-onnx-export/SKILL.md +422 -0
  3. hyperonnx-1.0.3/.claude/skills/torch-legacy-onnx-export/SKILL.md +407 -0
  4. {hyperonnx-1.0.2 → hyperonnx-1.0.3}/.gitignore +7 -1
  5. {hyperonnx-1.0.2 → hyperonnx-1.0.3}/PKG-INFO +2 -2
  6. {hyperonnx-1.0.2 → hyperonnx-1.0.3}/hyperonnx/__init__.py +1 -1
  7. {hyperonnx-1.0.2 → hyperonnx-1.0.3}/hyperonnx/hyper_export.py +16 -1
  8. {hyperonnx-1.0.2 → hyperonnx-1.0.3}/hyperonnx/transformers/cache.py +59 -6
  9. {hyperonnx-1.0.2 → hyperonnx-1.0.3}/hyperonnx/transformers/patch.py +30 -1
  10. {hyperonnx-1.0.2 → hyperonnx-1.0.3}/pyproject.toml +3 -3
  11. {hyperonnx-1.0.2 → hyperonnx-1.0.3}/tests/test_export_hyper_onnx.py +7 -3
  12. {hyperonnx-1.0.2 → hyperonnx-1.0.3}/.github/words_bag.txt +0 -0
  13. {hyperonnx-1.0.2 → hyperonnx-1.0.3}/.github/workflows/lint.yml +0 -0
  14. {hyperonnx-1.0.2 → hyperonnx-1.0.3}/.github/workflows/unittest.yml +0 -0
  15. {hyperonnx-1.0.2 → hyperonnx-1.0.3}/.pre-commit-config.yaml +0 -0
  16. {hyperonnx-1.0.2 → hyperonnx-1.0.3}/LICENSE +0 -0
  17. {hyperonnx-1.0.2 → hyperonnx-1.0.3}/README.md +0 -0
  18. {hyperonnx-1.0.2 → hyperonnx-1.0.3}/README_CN.md +0 -0
  19. {hyperonnx-1.0.2 → hyperonnx-1.0.3}/docs/assets/qwen2_omni_vision.gif +0 -0
  20. {hyperonnx-1.0.2 → hyperonnx-1.0.3}/docs/assets/r18-sample.gif +0 -0
  21. {hyperonnx-1.0.2 → hyperonnx-1.0.3}/hyperonnx/auto.py +0 -0
  22. {hyperonnx-1.0.2 → hyperonnx-1.0.3}/hyperonnx/exporter/__init__.py +0 -0
  23. {hyperonnx-1.0.2 → hyperonnx-1.0.3}/hyperonnx/exporter/dynamo.py +0 -0
  24. {hyperonnx-1.0.2 → hyperonnx-1.0.3}/hyperonnx/exporter/torchscript.py +0 -0
  25. {hyperonnx-1.0.2 → hyperonnx-1.0.3}/hyperonnx/exporter/utils.py +0 -0
  26. {hyperonnx-1.0.2 → hyperonnx-1.0.3}/hyperonnx/function_rewriter.py +0 -0
  27. {hyperonnx-1.0.2 → hyperonnx-1.0.3}/hyperonnx/torch_export.py +0 -0
  28. {hyperonnx-1.0.2 → hyperonnx-1.0.3}/hyperonnx/transformers/__init__.py +0 -0
  29. {hyperonnx-1.0.2 → hyperonnx-1.0.3}/hyperonnx/transformers/attention.py +0 -0
  30. {hyperonnx-1.0.2 → hyperonnx-1.0.3}/hyperonnx/typing.py +0 -0
  31. {hyperonnx-1.0.2 → hyperonnx-1.0.3}/hyperonnx/utils.py +0 -0
  32. {hyperonnx-1.0.2 → hyperonnx-1.0.3}/tests/expoter/test_detach_module_outputs.py +0 -0
  33. {hyperonnx-1.0.2 → hyperonnx-1.0.3}/tests/expoter/test_dynamo_build_onnxscript.py +0 -0
  34. {hyperonnx-1.0.2 → hyperonnx-1.0.3}/tests/expoter/test_dynamo_make_custom_op.py +0 -0
  35. {hyperonnx-1.0.2 → hyperonnx-1.0.3}/tests/expoter/test_dynamo_replace_custom_op.py +0 -0
  36. {hyperonnx-1.0.2 → hyperonnx-1.0.3}/tests/expoter/test_non_pod_export.py +0 -0
  37. {hyperonnx-1.0.2 → hyperonnx-1.0.3}/tests/expoter/test_plain_output_tensors.py +0 -0
  38. {hyperonnx-1.0.2 → hyperonnx-1.0.3}/tests/test_auto_trace_method.py +0 -0
  39. {hyperonnx-1.0.2 → hyperonnx-1.0.3}/tests/test_compose_nodes_to_functions.py +0 -0
  40. {hyperonnx-1.0.2 → hyperonnx-1.0.3}/tests/test_export_functions.py +0 -0
  41. {hyperonnx-1.0.2 → hyperonnx-1.0.3}/tests/test_fuse_constants_to_function.py +0 -0
  42. {hyperonnx-1.0.2 → hyperonnx-1.0.3}/tests/transformers/test_attention.py +0 -0
  43. {hyperonnx-1.0.2 → hyperonnx-1.0.3}/tests/transformers/test_static_cache.py +0 -0
@@ -0,0 +1,27 @@
1
+ # HyperONNX Instructions
2
+
3
+ ## Workflow
4
+
5
+ - Check relevant skills in `.claude/skills/` before handling specialized tasks.
6
+ - Prefer minimal, targeted changes that preserve existing project structure and style.
7
+ - Validate the files you change with the smallest relevant command before finishing.
8
+
9
+ ## Python Changes
10
+
11
+ - Follow the existing Python style already used in `hyperonnx/`.
12
+ - When Python files are modified, use the lint skill workflow and prefer `uv run ruff check hyperonnx`.
13
+ - Format Python changes with `uv run ruff format hyperonnx` when formatting is needed.
14
+
15
+ ## Version Compatibility (Hard Constraints)
16
+
17
+ ⚠️ **Supported Versions**: This project is currently tested and working on:
18
+ - **torch < 2.11** (torch 2.5–2.10 verified)
19
+ - **onnxscript < 0.6.0**
20
+
21
+ Both torch >= 2.11 and onnxscript >= 0.6.0 introduce breaking changes to module structure, dtype handling, and ONNX IR that are not yet resolved. **Do not use these newer versions** without explicit future work to support them.
22
+
23
+ **When users ask about newer versions**: Acknowledge the constraint, suggest using compatible versions from `pyproject.toml` optional dependencies, and note that torch >= 2.11 support is a known gap requiring future implementation.
24
+
25
+ ## Commits
26
+
27
+ - If asked to create a commit, use a clear and concise commit message that describes the actual change.
@@ -0,0 +1,422 @@
1
+ ---
2
+ name: torch-dynamo-onnx-export
3
+ description: Use this skill when exporting PyTorch models to ONNX using the modern torch.export (dynamo) path in HyperONNX. Best for models with dynamic control flow, complex tensor operations, and hierarchical custom operators defined via onnxscript.
4
+ argument-hint: Describe the model, custom operations (if any), target opset, and whether you need hierarchical export or simple custom operator definitions.
5
+ ---
6
+
7
+ # Torch Dynamo ONNX Export
8
+
9
+ Use this skill when the task is about the modern Torch ONNX export path in HyperONNX, meaning the `torch.export`-based path with `dynamo=True`.
10
+
11
+ This skill is specific to this repository. It captures the actual workflow used by HyperONNX code and tests, rather than generic PyTorch ONNX advice.
12
+
13
+ ## Torch Behaviour
14
+
15
+ - PyTorch 2.6+ introduced `torch.export` for AOT (Ahead-of-Time) tracing, enabling more dynamic models than TorchScript.
16
+ - PyTorch 2.9+ made `dynamo=True` the default in `torch.onnx.export()`.
17
+ - The dynamo path is based on FX graphs and bytecode analysis, not static tracing.
18
+ - On PyTorch 2.5 and below, `dynamo` parameter may not exist; always check `torch.onnx.export` signature and use the wrapper `torch_export_handle_lower_version` from HyperONNX.
19
+ - In this repository, prefer being explicit: always pass `dynamo=True` to clarify intent when using the modern exporter.
20
+
21
+ ⚠️ **Version Constraint**: This skill is tested and supported on **torch < 2.11** with **onnxscript < 0.6.0**. These versions have breaking changes in ONNX/module structure that require future work to resolve.
22
+
23
+ ## What This Skill Produces
24
+
25
+ Produce one of these outcomes:
26
+
27
+ - A correct export snippet using the modern `torch.export` path with `dynamo=True`.
28
+ - A HyperONNX code change that leverages `torch.export` for better dynamic model support.
29
+ - A custom operator definition using onnxscript and custom_translation_table for the dynamo path.
30
+ - A debugging checklist for why a dynamo export is failing or behaving unexpectedly.
31
+ - A migration guide from legacy (dynamo=False) to modern (dynamo=True) export.
32
+
33
+ ## When To Use It
34
+
35
+ Use this skill when the user asks for any of the following:
36
+
37
+ - Use `torch.onnx.export` with the modern or dynamo path.
38
+ - Force torch.export instead of TorchScript tracing.
39
+ - Export a model with `dynamo=True`.
40
+ - Create custom ONNX operators using onnxscript for the dynamo path.
41
+ - Build a custom_translation_table for hierarchical export.
42
+ - Understand why dynamo export behaves differently from legacy export.
43
+ - Export models with dynamic shapes, loops, or data-dependent control flow.
44
+
45
+ Do not use this skill for generic ONNX export or torch.export advice unrelated to HyperONNX.
46
+
47
+ ## Repository Facts To Ground Decisions
48
+
49
+ - HyperONNX exposes `torch_export_handle_lower_version` in [hyperonnx/torch_export.py](../../../hyperonnx/torch_export.py) to bridge version differences.
50
+ - The main hierarchical export entrypoint is [hyperonnx/hyper_export.py](../../../hyperonnx/hyper_export.py).
51
+ - The dynamo-specific branch for hierarchical replacement lives in [hyperonnx/exporter/dynamo.py](../../../hyperonnx/exporter/dynamo.py).
52
+ - Custom operator definition using onnxscript is built dynamically in [hyperonnx/exporter/dynamo.py](../../../hyperonnx/exporter/dynamo.py) via `build_onnxscript` and IRBuilder.
53
+ - Transformer attention for the dynamo path is handled in [hyperonnx/transformers/attention.py](../../../hyperonnx/transformers/attention.py) via `attention_translation_table()`.
54
+ - Reference tests: [tests/expoter/test_dynamo_replace_custom_op.py](../../../tests/expoter/test_dynamo_replace_custom_op.py), [tests/expoter/test_dynamo_build_onnxscript.py](../../../tests/expoter/test_dynamo_build_onnxscript.py), [tests/expoter/test_dynamo_make_custom_op.py](../../../tests/expoter/test_dynamo_make_custom_op.py).
55
+
56
+ ## Dynamo Export Fundamentals
57
+
58
+ ### Core Concepts
59
+
60
+ **torch.export**: Produces an ExportedProgram (AOT traced graph) that preserves more semantics than TorchScript, including data-dependent control flow and dynamic shapes.
61
+
62
+ **custom_translation_table**: A dictionary mapping PyTorch callables (e.g., custom ops, modules) to their ONNX counterparts (via onnxscript). The dynamo exporter uses this table to translate FX nodes to ONNX operators.
63
+
64
+ **onnxscript**: A framework for defining ONNX functions programmatically. HyperONNX uses `onnxscript.OnnxFunction` and `irbuilder.IRFunction` to build custom operators dynamically.
65
+
66
+ **replace_with_custom_op**: Context manager in [hyperonnx/exporter/dynamo.py](../../../hyperonnx/exporter/dynamo.py) that builds a custom_translation_table from module_spec, yielding it for use in export.
67
+
68
+ **build_onnxscript**: Dynamically constructs an ONNX function from a module's spec (inputs, outputs, dtype info). Used to create custom operator definitions without writing onnxscript manually.
69
+
70
+ ### Key Differences from Legacy Export (TorchScript)
71
+
72
+ | Aspect | Legacy (dynamo=False) | Dynamo (dynamo=True) |
73
+ |--------|----------------------|---------------------|
74
+ | **Tracer** | TorchScript (static tracing) | torch.export (FX bytecode) |
75
+ | **Dynamic Shapes** | Limited | Better supported |
76
+ | **Control Flow** | Loops unrolled, if/else lost | Can be preserved (with torch.cond) |
77
+ | **Custom Op Registration** | torch.onnx.register_custom_op_symbolic | custom_translation_table + onnxscript |
78
+ | **Module Replacement** | DuckForward (torch.autograd.Function) | Direct FX node replacement |
79
+ | **Symbolic Gen** | Automatic from DuckForward.symbolic | Explicit via onnxscript |
80
+ | **Attention Registration** | register_attention_opsets() | attention_translation_table() |
81
+ | **Performance** | Often faster export | Slower export (bytecode analysis) |
82
+
83
+ ## Defining Custom Operators for Dynamo Export
84
+
85
+ ### Pattern 1: Direct onnxscript function (simple custom op)
86
+
87
+ For single custom operations without hierarchical wrapping:
88
+
89
+ ```python
90
+ import onnxscript
91
+ import torch
92
+
93
+ # Step 1: Define an onnxscript function
94
+ _MY_OPSET = onnxscript.values.Opset("custom_domain", 1)
95
+
96
+ @onnxscript.script(_MY_OPSET)
97
+ def my_custom_op(x: onnxscript.FLOAT) -> onnxscript.FLOAT:
98
+ return x * 2.0 + 1.0
99
+
100
+ # Step 2: Build custom_translation_table
101
+ custom_translation_table = {
102
+ torch.ops.custom_lib.my_op: my_custom_op,
103
+ }
104
+
105
+ # Step 3: Export with dynamo=True and the table
106
+ torch.onnx.export(
107
+ model,
108
+ example_inputs,
109
+ 'model.onnx',
110
+ dynamo=True,
111
+ custom_translation_table=custom_translation_table,
112
+ opset_version=18,
113
+ )
114
+ ```
115
+
116
+ Key rules:
117
+ - The onnxscript function signature must match the PyTorch operation signature.
118
+ - The dictionary key must be a callable (e.g., torch.ops, a custom op).
119
+ - Always pass opset_version to align with onnxscript opset expectations.
120
+
121
+ ### Pattern 2: HyperONNX hierarchical export with dynamic onnxscript
122
+
123
+ For complex modules that need hierarchical export, use `replace_with_custom_op` and `build_onnxscript`:
124
+
125
+ ```python
126
+ import torch
127
+ from hyperonnx import export_hyper_onnx
128
+
129
+ class MyCustomModule(torch.nn.Module):
130
+ """A complex module with loops or dynamic shapes."""
131
+ def forward(self, x):
132
+ for i in range(x.shape[0]):
133
+ x = x * 2
134
+ return x
135
+
136
+ model = torch.nn.Sequential(
137
+ torch.nn.Linear(10, 20),
138
+ MyCustomModule(),
139
+ torch.nn.Linear(20, 5),
140
+ )
141
+
142
+ export_hyper_onnx(
143
+ model,
144
+ (torch.randn(1, 10),),
145
+ 'model.onnx',
146
+ hiera=[MyCustomModule], # Export as hierarchical function
147
+ dynamo=True, # Use torch.export, not TorchScript
148
+ opset_version=18,
149
+ )
150
+ ```
151
+
152
+ How it works:
153
+ 1. HyperONNX traces the model and caches module specs (inputs, outputs, dtypes).
154
+ 2. `build_onnxscript` dynamically creates an ONNX function from the module spec.
155
+ 3. `replace_with_custom_op` builds the custom_translation_table.
156
+ 4. torch.onnx.export (with dynamo=True) translates the FX graph using the table.
157
+
158
+ Reference: [tests/expoter/test_dynamo_replace_custom_op.py](../../../tests/expoter/test_dynamo_replace_custom_op.py).
159
+
160
+ ### Pattern 3: Attention-specific translation for dynamo
161
+
162
+ For transformer attention layers:
163
+
164
+ ```python
165
+ import torch
166
+ from hyperonnx.transformers.attention import attention_translation_table
167
+
168
+ # Step 1: Get the translation table for dynamo path
169
+ translation_table = attention_translation_table()
170
+
171
+ # Step 2: Export with dynamo=True and the table
172
+ torch.onnx.export(
173
+ model,
174
+ example_inputs,
175
+ 'model.onnx',
176
+ opset_version=24,
177
+ dynamo=True,
178
+ custom_translation_table=translation_table,
179
+ )
180
+ ```
181
+
182
+ This uses the modern onnxscript-based Attention definition. See [hyperonnx/transformers/attention.py](../../../hyperonnx/transformers/attention.py) for implementation.
183
+
184
+ ## Building Custom Operators with onnxscript
185
+
186
+ ### Overview
187
+
188
+ HyperONNX's `build_onnxscript` function constructs ONNX operators automatically from module specs, eliminating the need to write onnxscript manually. However, you can also write custom onnxscript functions for fine-grained control.
189
+
190
+ ### Auto-generation via build_onnxscript
191
+
192
+ ```python
193
+ from hyperonnx.exporter.dynamo import build_onnxscript
194
+ from hyperonnx.typing import default_module_spec, ModuleSpec
195
+
196
+ # Assume module_spec is populated via trace_module_spec
197
+ spec: ModuleSpec = {...} # Contains: args, output, signature, etc.
198
+
199
+ # Auto-build onnxscript function
200
+ onnx_func = build_onnxscript(spec)
201
+
202
+ # Use in custom_translation_table
203
+ custom_translation_table = {
204
+ MyModule: onnx_func,
205
+ }
206
+ ```
207
+
208
+ ### Manual onnxscript Definition
209
+
210
+ For more control, write onnxscript directly:
211
+
212
+ ```python
213
+ import onnxscript
214
+
215
+ # Define a custom opset
216
+ CUSTOM_OPSET = onnxscript.values.Opset(domain="my_domain", version=1)
217
+
218
+ @onnxscript.script(CUSTOM_OPSET)
219
+ def my_complex_op(x: onnxscript.FLOAT, mask: onnxscript.BOOL) -> onnxscript.FLOAT:
220
+ """Custom operator that applies masking."""
221
+ return x * mask.cast(onnxscript.FLOAT)
222
+
223
+ # Use in translation table
224
+ custom_translation_table = {
225
+ my_pytorch_op: my_complex_op,
226
+ }
227
+ ```
228
+
229
+ Key considerations:
230
+ - onnxscript functions use ONNX IR types (FLOAT, INT32, BOOL, etc.), not PyTorch types.
231
+ - The function signature must match the number of inputs/outputs of the PyTorch operation.
232
+ - Use `.cast()` for type conversions when needed.
233
+
234
+ ## Workflow
235
+
236
+ ### 0. Decide between direct onnxscript and hierarchical export
237
+
238
+ Before writing code, ask:
239
+ - **Is this a single atomic custom operation?** → Use direct onnxscript (Pattern 1).
240
+ - **Is this a module hierarchy that needs reusable ONNX functions?** → Use hierarchical export (Pattern 2).
241
+ - **Is this transformer attention?** → Use the built-in pattern (Pattern 3).
242
+
243
+ If unsure, check:
244
+ - [tests/expoter/test_dynamo_make_custom_op.py](../../../tests/expoter/test_dynamo_make_custom_op.py) for simple custom ops.
245
+ - [tests/expoter/test_dynamo_replace_custom_op.py](../../../tests/expoter/test_dynamo_replace_custom_op.py) for hierarchical examples.
246
+ - [tests/expoter/test_dynamo_build_onnxscript.py](../../../tests/expoter/test_dynamo_build_onnxscript.py) for build_onnxscript usage.
247
+
248
+ ### 1. Check torch version and dynamo availability
249
+
250
+ ```python
251
+ import torch
252
+ from inspect import signature
253
+
254
+ # Verify dynamo parameter exists
255
+ sig = signature(torch.onnx.export)
256
+ if 'dynamo' not in sig.parameters:
257
+ # Use torch_export_handle_lower_version from HyperONNX
258
+ from hyperonnx.torch_export import torch_export_handle_lower_version
259
+ torch_export_handle_lower_version(model, args, f, dynamo=True, ...)
260
+ else:
261
+ torch.onnx.export(model, args, f, dynamo=True, ...)
262
+ ```
263
+
264
+ ### 2. Identify and define custom operators
265
+
266
+ For each custom operator:
267
+ - Determine its input/output signatures.
268
+ - Write an onnxscript function or use build_onnxscript.
269
+ - Add it to custom_translation_table.
270
+
271
+ Example:
272
+ ```python
273
+ custom_translation_table = {}
274
+
275
+ # Add manually-written onnxscript functions
276
+ custom_translation_table[torch.ops.custom_lib.op1] = onnxscript_op1
277
+
278
+ # Or add auto-generated ones from module specs
279
+ custom_translation_table[MyModule] = build_onnxscript(my_module_spec)
280
+ ```
281
+
282
+ ### 3. Build custom_translation_table
283
+
284
+ Use the context manager `replace_with_custom_op` for hierarchical export:
285
+
286
+ ```python
287
+ from hyperonnx.exporter.dynamo import replace_with_custom_op
288
+
289
+ with replace_with_custom_op(model, module_spec) as custom_translation_table:
290
+ torch.onnx.export(
291
+ model,
292
+ example_inputs,
293
+ 'model.onnx',
294
+ dynamo=True,
295
+ custom_translation_table=custom_translation_table,
296
+ opset_version=18,
297
+ )
298
+ ```
299
+
300
+ ### 4. Call torch.onnx.export with dynamo=True
301
+
302
+ ```python
303
+ torch.onnx.export(
304
+ model,
305
+ example_inputs,
306
+ 'model.onnx',
307
+ input_names=['input'],
308
+ output_names=['output'],
309
+ opset_version=18,
310
+ dynamo=True,
311
+ custom_translation_table=custom_translation_table,
312
+ dynamic_shapes={'input': {0: 'batch_size'}}, # Optional: specify dynamic shapes
313
+ )
314
+ ```
315
+
316
+ Key parameters:
317
+ - `dynamo=True`: Use torch.export instead of TorchScript.
318
+ - `custom_translation_table`: Dict mapping callables to onnxscript functions.
319
+ - `dynamic_shapes`: Preferred over dynamic_axes for dynamo path.
320
+ - `opset_version`: Must align with onnxscript opset.
321
+
322
+ ### 5. Handle export failures and fallback
323
+
324
+ The dynamo path may fail on complex models. HyperONNX provides fallback:
325
+
326
+ ```python
327
+ from hyperonnx.hyper_export import export_hyper_onnx
328
+
329
+ # export_hyper_onnx tries fallback automatically
330
+ export_hyper_onnx(
331
+ model,
332
+ args,
333
+ 'model.onnx',
334
+ dynamo=True,
335
+ # If dynamo export fails, HyperONNX may retry with dynamo=False
336
+ )
337
+ ```
338
+
339
+ To disable fallback and fail fast:
340
+ ```python
341
+ # Call torch_export_handle_lower_version directly without fallback
342
+ torch.onnx.export(
343
+ model,
344
+ args,
345
+ f,
346
+ dynamo=True,
347
+ # No fallback logic here
348
+ )
349
+ ```
350
+
351
+ ### 6. Validate the exported ONNX model
352
+
353
+ ```python
354
+ import onnx
355
+
356
+ # Load and check
357
+ onnx_model = onnx.load('model.onnx')
358
+ onnx.checker.check_model(onnx_model, full_check=True)
359
+
360
+ # Verify custom operators appear
361
+ for node in onnx_model.graph.node:
362
+ print(f"{node.op_type} in domain {node.domain}")
363
+ ```
364
+
365
+ ## Common Errors & Recovery
366
+
367
+ | Error | Cause | Fix |
368
+ |-------|-------|-----|
369
+ | `KeyError` in custom_translation_table | torch.ops key not in table | Verify the key exactly matches the PyTorch op reference |
370
+ | `TypeError: ... got an unexpected keyword argument 'dynamo'` | torch.onnx.export too old | Use `torch_export_handle_lower_version` wrapper |
371
+ | onnxscript function signature mismatch | ONNX function has wrong number of inputs/outputs | Verify spec['args'] and spec['output'] match onnxscript function |
372
+ | `UnflatteningError` in build_onnxscript | Output structure not flattened | Use `plain_tensor_container` to extract only tensors |
373
+ | Export succeeds but ONNX model is wrong | dynamic_shapes not specified | Add `dynamic_shapes` parameter for dynamic-shape models |
374
+ | torch.export fails on data-dependent branches | Model has unhandled control flow | Wrap loops/conditionals with `torch.cond` or fall back to dynamo=False |
375
+
376
+ ## Branching Guidance
377
+
378
+ If the request is about library implementation:
379
+ - Prefer HyperONNX abstractions (export_hyper_onnx, replace_with_custom_op).
380
+ - Preserve backward compatibility with older torch versions.
381
+ - Use torch_export_handle_lower_version wrapper for version bridging.
382
+
383
+ If the request is about custom operators:
384
+ - Start with simple onnxscript functions (Pattern 1) for atomic ops.
385
+ - Use build_onnxscript for auto-generation from module specs.
386
+ - Write manual onnxscript only when fine-grained control is needed.
387
+
388
+ If the request is about a failing export:
389
+ - Check if the model uses data-dependent control flow; consider torch.cond.
390
+ - Verify custom_translation_table keys match torch.ops exactly.
391
+ - Check onnxscript function signatures match specs.
392
+ - Try lowering torch version requirement (dynamo may not exist in older PyTorch).
393
+ - Consider falling back to dynamo=False if dynamo export is unreliable for this model.
394
+
395
+ ## Quality Bar
396
+
397
+ A good answer or code change using this skill should:
398
+ - Clearly distinguish between dynamo=True (modern) and dynamo=False (legacy).
399
+ - Use repository-native abstractions (export_hyper_onnx, replace_with_custom_op, build_onnxscript).
400
+ - Specify dynamic_shapes when exporting models with variable batch size or sequence length.
401
+ - Include custom_translation_table definitions with clear onnxscript signatures.
402
+ - Provide validation (onnx.checker.check_model) and explain expected ONNX structure.
403
+ - Mention torch version requirements and fallback behavior.
404
+
405
+ ## Example Prompts
406
+
407
+ - Export this model using dynamo=True and custom attention operators.
408
+ - Add dynamic shape support to this ONNX export.
409
+ - Create a custom_translation_table for my hierarchical model.
410
+ - Why does dynamo export fail on this model with torch.cond?
411
+ - Migrate this export from dynamo=False to dynamo=True.
412
+ - Build an onnxscript function for this custom module.
413
+
414
+ ## Weak Spots To Clarify Before Extending This Skill
415
+
416
+ If you evolve this skill later, clarify these points first:
417
+
418
+ - Which torch versions (2.5, 2.6, 2.8, 2.9+) should be officially supported for dynamo export examples?
419
+ - Should we recommend torch.cond for all data-dependent branches, or only certain patterns?
420
+ - Is build_onnxscript reliable enough to recommend for user-defined modules, or should we prefer manual onnxscript?
421
+ - How to handle dtype inference in build_onnxscript for models with mixed precision (float16, bfloat16)?
422
+ - Should fallback behavior be exposed as a user-facing option, or remain internal to HyperONNX?
@@ -0,0 +1,407 @@
1
+ ---
2
+ name: torch-legacy-onnx-export
3
+ description: Use this skill when working in HyperONNX and you need the legacy Torch ONNX export path with dynamo=False, especially for TorchScript-based export behavior, lower-version compatibility, custom attention registration, or post-export ONNX fixes.
4
+ argument-hint: Describe the model, export entrypoint, torch version, and whether custom attention or custom input structures are involved.
5
+ ---
6
+
7
+ # Torch Legacy ONNX Export
8
+
9
+ Use this skill when the task is about the legacy Torch ONNX export path in HyperONNX, meaning the TorchScript-style path with dynamo=False.
10
+
11
+ This skill is specific to this repository. It captures the actual workflow used by HyperONNX code and tests, rather than generic PyTorch advice.
12
+
13
+ ## Torch Behaviour
14
+
15
+ - PyTorch 2.9 changed `torch.onnx.export(..., dynamo=...)` so that `dynamo=True` is the default.
16
+ - On PyTorch 2.8 and earlier, do not assume the default selects the modern exporter. If legacy behavior matters, set `dynamo=False` explicitly. If modern exporter behavior matters, set `dynamo=True` explicitly.
17
+ - In this repository, prefer being explicit either way. Do not rely on the library default because the default changed across PyTorch versions.
18
+
19
+ ⚠️ **Version Constraint**: This skill is tested and supported on **torch < 2.11** with **onnxscript < 0.6.0**. These versions have breaking changes in ONNX/module structure that require future work to resolve.
20
+
21
+ ## What This Skill Produces
22
+
23
+ Produce one of these outcomes:
24
+
25
+ - A correct export snippet that uses the legacy path with dynamo=False.
26
+ - A HyperONNX code change that preserves legacy export compatibility.
27
+ - A debugging checklist for why a legacy export is failing in this repository.
28
+ - A validation plan that proves the exported ONNX model is structurally correct.
29
+
30
+ ## When To Use It
31
+
32
+ Use this skill when the user asks for any of the following:
33
+
34
+ - Use torch.onnx.export with the old or legacy path.
35
+ - Force TorchScript export instead of dynamo export.
36
+ - Keep HyperONNX compatible with older torch.onnx.export behavior.
37
+ - Export a model with dynamo=False.
38
+ - Make custom attention export work on the TorchScript path.
39
+ - Understand why legacy export behaves differently from dynamo export.
40
+
41
+ Do not use this skill for generic ONNX export advice that is unrelated to HyperONNX.
42
+
43
+ ## Repository Facts To Ground Decisions
44
+
45
+ - HyperONNX exposes a version-bridging wrapper in [hyperonnx/torch_export.py](../../../hyperonnx/torch_export.py) named torch_export_handle_lower_version.
46
+ - The main hierarchical export entrypoint is [hyperonnx/hyper_export.py](../../../hyperonnx/hyper_export.py), which accepts dynamo and may fall back between export modes.
47
+ - The TorchScript branch for hierarchical replacement lives in [hyperonnx/exporter/__init__.py](../../../hyperonnx/exporter/__init__.py).
48
+ - Transformer attention registration for the legacy path is implemented in [hyperonnx/transformers/attention.py](../../../hyperonnx/transformers/attention.py).
49
+ - Repository examples already show dynamo=False in [README.md](../../../README.md) and [README_CN.md](../../../README_CN.md).
50
+ - The duck-typing and symbolic operation system for TorchScript export is in [hyperonnx/exporter/torchscript.py](../../../hyperonnx/exporter/torchscript.py).
51
+
52
+ ## Registering Custom Operators for Legacy Export
53
+
54
+ The legacy TorchScript export path requires explicit symbolic function registration for custom operators. Use this section when the task is to export custom PyTorch operations to ONNX while using dynamo=False.
55
+
56
+ ### Pattern 1: Direct symbolic registration (torch-native style)
57
+
58
+ For simple custom operations that do not need hierarchical module wrapping:
59
+
60
+ ```python
61
+ import torch
62
+ from torch.onnx import register_custom_op_symbolic
63
+
64
+ # Step 1: Define a symbolic function for ONNX
65
+ def symbolic_my_op(g, input, param):
66
+ """ONNX symbolic for my_op.
67
+
68
+ Args:
69
+ g: The ONNX graph context.
70
+ input: The input tensor from PyTorch.
71
+ param: A parameter.
72
+
73
+ Returns:
74
+ ONNX operator node(s).
75
+ """
76
+ return g.op('custom_domain::MyOp', input, param_f=param)
77
+
78
+ # Step 2: Register the symbolic before export
79
+ register_custom_op_symbolic('custom_domain::my_op', symbolic_my_op, opset_version=18)
80
+
81
+ # Step 3: Export with dynamo=False
82
+ torch.onnx.export(
83
+ model,
84
+ example_inputs,
85
+ 'model.onnx',
86
+ opset_version=18,
87
+ dynamo=False,
88
+ )
89
+ ```
90
+
91
+ Key rules:
92
+ - The symbolic function name must match the registered name in `"<domain>::<op>"` format.
93
+ - Always use opset_version consistently between registration and export.
94
+ - Register before calling export; registration persists in the current process.
95
+
96
+ ### Pattern 2: HyperONNX hierarchical module with duck-typed custom op
97
+
98
+ For complex models where the custom operation is itself a module that needs hierarchical export, use the duck-typing system from [hyperonnx/exporter/torchscript.py](../../../hyperonnx/exporter/torchscript.py):
99
+
100
+ ```python
101
+ import torch
102
+ from hyperonnx import export_hyper_onnx
103
+
104
+ class MyCustomModule(torch.nn.Module):
105
+ """A module that implements custom logic."""
106
+ def forward(self, x):
107
+ # Custom computation
108
+ return x * 2 + 1
109
+
110
+ model = torch.nn.Sequential(
111
+ torch.nn.Linear(10, 20),
112
+ MyCustomModule(),
113
+ torch.nn.Linear(20, 5),
114
+ )
115
+
116
+ export_hyper_onnx(
117
+ model,
118
+ (torch.randn(1, 10),),
119
+ 'model.onnx',
120
+ hiera=[MyCustomModule], # Export MyCustomModule as a function
121
+ dynamo=False,
122
+ opset_version=18,
123
+ )
124
+ ```
125
+
126
+ How it works:
127
+ 1. When you pass `hiera=[MyCustomModule]`, HyperONNX will trace and export the module separately.
128
+ 2. The module is then replaced with a duck-type that acts like a custom operator node.
129
+ 3. The DuckForward class (from torchscript.py) handles the ONNX symbolic generation automatically.
130
+
131
+ Reference: [tests/expoter/test_dynamo_replace_custom_op.py](../../../tests/expoter/test_dynamo_replace_custom_op.py) for hierarchical examples.
132
+
133
+ ### Pattern 3: Attention-specific registration
134
+
135
+ For transformer attention layers, HyperONNX provides a pre-built pattern:
136
+
137
+ ```python
138
+ import torch
139
+ from hyperonnx.transformers.attention import register_attention_opsets
140
+
141
+ # Register custom attention ONNX symbolic for the legacy path
142
+ register_attention_opsets()
143
+
144
+ # Then proceed with normal export
145
+ torch.onnx.export(
146
+ model,
147
+ example_inputs,
148
+ 'model.onnx',
149
+ opset_version=24,
150
+ dynamo=False,
151
+ )
152
+ ```
153
+
154
+ This registration only affects the TorchScript (legacy) path. See "Handle transformer attention correctly" in Workflow step 4 for post-export adjustments.
155
+
156
+ ## Understanding torchscript.py: Duck-Typing and Symbolic Export
157
+
158
+ The [hyperonnx/exporter/torchscript.py](../../../hyperonnx/exporter/torchscript.py) module implements a clever pattern for exporting PyTorch modules as ONNX custom operators without requiring explicit symbolic functions. Here is how it works:
159
+
160
+ ### Core Concepts
161
+
162
+ **DuckForward**: A torch.autograd.Function subclass that:
163
+ - Stores the precomputed output in module_spec at forward time.
164
+ - Implements a `symbolic()` static method that generates an ONNX node instead of computing.
165
+ - Acts as a "duck type" – it quacks like a normal forward pass during tracing, but produces ONNX during export.
166
+
167
+ **make_duck_forward**: Creates a DuckForward instance with closure over a specific module_spec.
168
+
169
+ **replace_duck_forward**: Context manager that temporarily replaces module.forward with duck-typed versions.
170
+
171
+ ### Workflow (Three Phases)
172
+
173
+ **Phase 1: Tracing and Caching**
174
+
175
+ Before export, HyperONNX calls `trace_module_spec()` which:
176
+ 1. Registers forward hooks on all modules in the hiera list.
177
+ 2. Runs the model with example inputs to capture: input signatures, actual output values, loop count.
178
+ 3. Stores all this in module_spec.
179
+
180
+ **Phase 2: Duck-Typing (During torch.onnx.export)**
181
+
182
+ When `torch.onnx.export` traces the model with dynamo=False:
183
+ 1. `replace_duck_forward()` context manager replaces each hiera module's forward with a duck-typed version.
184
+ 2. Each duck forward is a DuckForward.apply() call that:
185
+ - During normal tracing: calls DuckForward.forward(), returns the precomputed cached output.
186
+ - During ONNX symbolic tracing: calls DuckForward.symbolic(), returns an ONNX op node.
187
+ 3. The exporter builds the ONNX graph, inserting custom op nodes where the duck forwards were.
188
+
189
+ **Phase 3: Function Embedding**
190
+
191
+ After export, HyperONNX uses other rewriters to:
192
+ 1. Embed the actual ONNX subgraph for each module into a function definition.
193
+ 2. Replace the custom op nodes with function calls.
194
+
195
+ ### Key Implementation Details
196
+
197
+ **Forward method**: Stores precomputed output in module_spec, indexed by loop counter. Handles multiple calls to the same module and flattens nested output structures.
198
+
199
+ **Symbolic method**: Uses the module's type_name (set during tracing) as the ONNX op name. Filters out None args and specifies the number of outputs.
200
+
201
+ **Forward wrapper (_forward function)**:
202
+ - Runs in inference mode to avoid backprop.
203
+ - Flattens input structures the same way as the real forward.
204
+ - Reorders kwargs to match the original function signature.
205
+ - Calls DuckForward.apply which triggers either forward or symbolic depending on context.
206
+
207
+ ### Why This Pattern Matters for Legacy Export
208
+
209
+ 1. **No need for explicit symbolic functions**: The duck-typing pattern generates ONNX nodes automatically.
210
+ 2. **Handles complex module interactions**: Multiple calls, nested outputs, custom input types are all cached.
211
+ 3. **Integrates with TorchScript tracer**: The exporter sees normal forward calls; duck-typing is transparent.
212
+ 4. **Preserves correctness**: By caching actual outputs during tracing, the ONNX graph matches PyTorch semantics.
213
+
214
+ ### When to Use vs. When Not To Use
215
+
216
+ **Use duck-typing (torchscript.py) when:**
217
+ - You are using HyperONNX hierarchical export (passing hiera list).
218
+ - The module is complex enough that writing a correct symbolic function is error-prone.
219
+ - You want the output to be an embedded ONNX function, not just a custom op node.
220
+
221
+ **Use direct symbolic registration when:**
222
+ - You only have a single custom operator, not a module hierarchy.
223
+ - You already have a working symbolic function.
224
+ - You want the ONNX output to reference an external custom op domain.
225
+
226
+ ### Common Gotchas
227
+
228
+ 1. **Mismatched signatures**: If the cached output from tracing does not match the actual forward call structure, duck forward will return wrong data. Always verify with trace_module_spec.
229
+ 2. **Module called multiple times**: Duck forward increments loops and indexes into loop_outputs. Ensure tracing and export see the same number of calls.
230
+ 3. **Non-tensor outputs**: The symbolic method must return an op or tuple of ops. Use plain_tensor_container to extract just tensors.
231
+ 4. **Backward incompatibility**: Do not call backward on a DuckForward result. The backward method raises RuntimeError by design.
232
+
233
+ ## Workflow
234
+
235
+ ### 0. Decide between direct symbolic registration and duck-typing
236
+
237
+ Before writing code, ask:
238
+ - **Is this a single custom op that will appear in the final ONNX?** → Use direct symbolic registration (Pattern 1).
239
+ - **Is this a PyTorch module that should become a reusable ONNX function?** → Use HyperONNX hierarchical export with duck-typing (Pattern 2).
240
+ - **Is this transformer attention?** → Use the built-in pattern (Pattern 3) and refer to step 4 of Workflow.
241
+
242
+ If unsure, check the test files:
243
+ - [tests/expoter/test_dynamo_replace_custom_op.py](../../../tests/expoter/test_dynamo_replace_custom_op.py) for hierarchical examples.
244
+ - [hyperonnx/transformers/attention.py](../../../hyperonnx/transformers/attention.py) for attention-specific patterns.
245
+
246
+ ### 1. Identify the export entrypoint
247
+
248
+ First determine which layer of the repository the task belongs to:
249
+
250
+ - If the task is about HyperONNX library internals, prefer torch_export_handle_lower_version instead of calling raw torch.onnx.export directly.
251
+ - If the task is about exporting a full hierarchical model, work through export_hyper_onnx in [hyperonnx/hyper_export.py](../../../hyperonnx/hyper_export.py).
252
+ - If the task is a direct usage example or a focused test, raw torch.onnx.export is acceptable when it matches the repository’s current test style.
253
+
254
+ ### 2. Choose the export mode deliberately
255
+
256
+ Use this decision logic:
257
+
258
+ - If the requirement is legacy or TorchScript export, set dynamo=False explicitly.
259
+ - If the user only says export must work and does not require legacy behavior, note that HyperONNX may try fallback modes internally in some paths.
260
+ - If the code is in trace_module_spec, remember the repository intentionally uses dynamo=False there to keep tracing behavior aligned with TorchScript export semantics.
261
+
262
+ ### 3. Apply the correct calling pattern
263
+
264
+ For repository code changes:
265
+
266
+ - Preserve the wrapper call shape used in [hyperonnx/torch_export.py](../../../hyperonnx/torch_export.py).
267
+ - Keep kwargs handling compatible with torch versions below 2.5.0, where kwargs are merged into args.
268
+ - Do not remove the OperatorExportTypes.ONNX_ATEN_FALLBACK behavior for older torch versions unless the task explicitly requires it and the change is validated.
269
+
270
+ For direct usage examples:
271
+
272
+ - Pass dynamo=False explicitly.
273
+ - Provide input_names and output_names when the model interface is non-trivial.
274
+ - Keep opset_version explicit when export semantics matter.
275
+
276
+ Minimal example:
277
+
278
+ ```python
279
+ import torch
280
+
281
+ model = MyModule().eval()
282
+ example_inputs = (x, y)
283
+
284
+ torch.onnx.export(
285
+ model,
286
+ example_inputs,
287
+ "model.onnx",
288
+ input_names=["x", "y"],
289
+ output_names=["out"],
290
+ opset_version=19,
291
+ dynamo=False,
292
+ )
293
+ ```
294
+
295
+ ### 4. Handle transformer attention correctly
296
+
297
+ If the task involves HyperONNX transformer attention, the legacy path has a required branch:
298
+
299
+ - Before export on the TorchScript path, call register_attention_opsets() from [hyperonnx/transformers/attention.py](../../../hyperonnx/transformers/attention.py).
300
+ - This registration exists specifically for the torchscript export path with dynamo=False.
301
+ - If the export also relies on the custom Attention op becoming default-domain ONNX Attention opset 24, promote the exported model with promote_onnx_model_to_opset24() after export.
302
+
303
+ Pattern:
304
+
305
+ ```python
306
+ import onnx
307
+ import torch
308
+
309
+ from hyperonnx.transformers.attention import (
310
+ promote_onnx_model_to_opset24,
311
+ register_attention_opsets,
312
+ )
313
+
314
+ register_attention_opsets()
315
+
316
+ torch.onnx.export(
317
+ model,
318
+ example_inputs,
319
+ output_path,
320
+ opset_version=24,
321
+ dynamo=False,
322
+ )
323
+
324
+ onnx_model = onnx.load_model(str(output_path))
325
+ onnx_model = promote_onnx_model_to_opset24(onnx_model)
326
+ onnx.checker.check_model(onnx_model, full_check=True)
327
+ ```
328
+
329
+ Reference implementation: [tests/transformers/test_attention.py](../../../tests/transformers/test_attention.py)
330
+
331
+ ### 5. Watch for input-structure limitations
332
+
333
+ Legacy export does not make arbitrary Python objects exportable.
334
+
335
+ Use these rules from the repository tests:
336
+
337
+ - Plain custom objects fail export.
338
+ - Tuple or list subclasses may pass flattening but still lose class behavior.
339
+ - A torch.Tensor subclass can sometimes preserve the needed behavior well enough for export.
340
+
341
+ Reference: [tests/expoter/test_non_pod_export.py](../../../tests/expoter/test_non_pod_export.py)
342
+
343
+ If the user is trying to export non-POD inputs, explain that the fix is usually to flatten or tensorize inputs, not to keep arbitrary Python objects in the ONNX boundary.
344
+
345
+ ### 6. Validate the exported model
346
+
347
+ Minimum completion checks:
348
+
349
+ - The export artifact exists or the in-memory export object is non-null.
350
+ - onnx.checker.check_model passes.
351
+ - The chosen export path really used dynamo=False when legacy behavior was required.
352
+ - If custom attention is involved, verify the expected Attention node and domain/opset normalization.
353
+ - If changing library code, verify at least the smallest relevant test for the touched path.
354
+
355
+ Good repository-aligned validation targets:
356
+
357
+ - [tests/transformers/test_attention.py](../../../tests/transformers/test_attention.py)
358
+ - [tests/transformers/test_static_cache.py](../../../tests/transformers/test_static_cache.py)
359
+ - [tests/expoter/test_dynamo_replace_custom_op.py](../../../tests/expoter/test_dynamo_replace_custom_op.py) for contrast with the dynamo path
360
+
361
+ ## Branching Guidance
362
+
363
+ If the request is about library implementation:
364
+
365
+ - Prefer HyperONNX wrappers and existing abstractions.
366
+ - Preserve backward-compatibility branches.
367
+ - Avoid replacing the wrapper with direct raw torch.onnx.export unless there is a strong reason.
368
+
369
+ If the request is about examples or docs:
370
+
371
+ - Show the shortest correct snippet with dynamo=False.
372
+ - Mention any required registration step up front.
373
+ - Link the example back to the repository’s existing test or README pattern.
374
+
375
+ If the request is about a failing export:
376
+
377
+ - Check whether the failure is caused by unsupported input structures.
378
+ - Check whether the user forgot dynamo=False.
379
+ - Check whether custom attention registration was skipped.
380
+ - Check whether post-export opset promotion is missing.
381
+ - Check whether the code path should have used torch_export_handle_lower_version.
382
+
383
+ ## Quality Bar
384
+
385
+ A good answer or code change using this skill should:
386
+
387
+ - Distinguish clearly between legacy TorchScript export and dynamo export.
388
+ - Use repository-native entrypoints and helpers.
389
+ - Mention version-compatibility handling when touching core export code.
390
+ - Include concrete validation, not just export code.
391
+ - Avoid generic ONNX advice that ignores HyperONNX’s actual implementation.
392
+
393
+ ## Example Prompts
394
+
395
+ - Use the legacy torch.onnx.export path for this HyperONNX test.
396
+ - Add a sample that exports with dynamo=False and validates the ONNX.
397
+ - Debug why this custom attention export fails on the TorchScript path.
398
+ - Update HyperONNX export code without breaking lower torch versions.
399
+ - Show me the correct legacy export flow for a transformer module in this repo.
400
+
401
+ ## Weak Spots To Clarify Before Extending This Skill
402
+
403
+ If you evolve this skill later, clarify these points first:
404
+
405
+ - Whether the team wants to prefer legacy export in docs, or only keep it as a compatibility path.
406
+ - Which torch version range should be treated as officially supported for legacy export examples.
407
+ - Whether future transformer examples should standardize on post-export opset promotion or hide that behind a helper.
@@ -1,5 +1,6 @@
1
1
  .*/
2
- !.github/
2
+ !/.claude/
3
+ !/.github/
3
4
  *.egg-info
4
5
  __pycache__
5
6
  # pdm build tool
@@ -14,3 +15,8 @@ tests/models/
14
15
 
15
16
  uv.lock
16
17
  .coverage
18
+
19
+ # agents
20
+ /.claude/settings.json
21
+ /.github/agents/
22
+ /.github/skills/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperonnx
3
- Version: 1.0.2
3
+ Version: 1.0.3
4
4
  Summary: Copyright (C) 2025 The HYPERONNX Authors.
5
5
  Author-email: Wenyi Tang <wenyitang@outlook.com>
6
6
  Requires-Python: >=3.11
@@ -9,7 +9,7 @@ Classifier: License :: OSI Approved :: Apache Software License
9
9
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
10
10
  Classifier: Topic :: Software Development
11
11
  License-File: LICENSE
12
- Requires-Dist: onnxifier>=2.0.1
12
+ Requires-Dist: onnxifier>=2.0.3
13
13
  Requires-Dist: onnxscript<0.6.0
14
14
  Requires-Dist: onnx>=1.18.0
15
15
  Requires-Dist: torch<2.11 ; extra == "cpu"
@@ -19,4 +19,4 @@ from .hyper_export import export_hyper_onnx
19
19
  from .utils import HYPER_DOMAIN
20
20
 
21
21
  __all__ = ["auto_trace_method", "export_hyper_onnx", "HYPER_DOMAIN"]
22
- __version__ = "1.0.2"
22
+ __version__ = "1.0.3"
@@ -209,6 +209,8 @@ def _export_hiera(
209
209
  dynamo: bool,
210
210
  external_data: bool,
211
211
  do_optimization: bool,
212
+ fold_nodes_to_functions: bool,
213
+ fuse_constants_to_function: bool,
212
214
  external_directory: str | PathLike | None,
213
215
  module_spec: dict[Module, ModuleSpec],
214
216
  hiera: Collection[type[Module]],
@@ -244,6 +246,8 @@ def _export_hiera(
244
246
  hiera=hiera,
245
247
  module_spec=child_spec,
246
248
  do_optimization=do_optimization,
249
+ fold_nodes_to_functions=fold_nodes_to_functions,
250
+ fuse_constants_to_function=fuse_constants_to_function,
247
251
  external_directory=external_directory,
248
252
  )
249
253
  # update since child_spec may be updated
@@ -318,6 +322,8 @@ def export_hyper_onnx( # noqa: C901
318
322
  hiera: Collection[type[Module]] | None = None,
319
323
  module_spec: dict[Module, ModuleSpec] | None = None,
320
324
  do_optimization: bool = True,
325
+ fold_nodes_to_functions: bool = True,
326
+ fuse_constants_to_function: bool = True,
321
327
  external_directory: str | PathLike | None = None,
322
328
  **_: Any, # ignored options
323
329
  ) -> Any | None:
@@ -347,6 +353,10 @@ def export_hyper_onnx( # noqa: C901
347
353
  module_spec (Optional[Dict[Module, ModuleSpec]]): A dictionary to store the
348
354
  detail spec of modules.
349
355
  do_optimization: Whether to optimize the exported ONNX model.
356
+ fold_nodes_to_functions: Whether to compose individual nodes into a function.
357
+ Defaults to True.
358
+ fuse_constants_to_function: Whether to fuse constants into function.
359
+ Defaults to True.
350
360
  external_directory (Optional[str | PathLike]): The directory to save the onnx
351
361
  model exported to be composed. If not specified, the model will be saved
352
362
  in the memory. Set to True if functions to be composed are too large.
@@ -389,7 +399,8 @@ def export_hyper_onnx( # noqa: C901
389
399
  if _dyn == dynamo:
390
400
  logger.warning(
391
401
  f" Failed to export {model_typename}, "
392
- f"try use dynamo={not _dyn}"
402
+ f"try use dynamo={not _dyn}, "
403
+ "set ONNXIFIER_LOG_LEVEL=DEBUG to see error logs."
393
404
  )
394
405
  logger.debug(f" <<<\n{ex}")
395
406
  raise RuntimeError(f" Failed to export {model_typename}.")
@@ -411,6 +422,8 @@ def export_hyper_onnx( # noqa: C901
411
422
  dynamo=dynamo,
412
423
  external_data=external_data,
413
424
  do_optimization=do_optimization,
425
+ fold_nodes_to_functions=fold_nodes_to_functions,
426
+ fuse_constants_to_function=fuse_constants_to_function,
414
427
  external_directory=external_directory,
415
428
  module_spec=module_spec,
416
429
  hiera=hiera,
@@ -460,7 +473,9 @@ def export_hyper_onnx( # noqa: C901
460
473
  passes = [ComposeOnnxAsFunctionRewriter(HYPER_DOMAIN, tuple(module_spec.values()))]
461
474
  if do_optimization:
462
475
  passes.extend(OPTIMIZER_PASSES)
476
+ if fold_nodes_to_functions:
463
477
  passes.append(ComposeNodesToFunctionsRewriter(model_typename))
478
+ if fuse_constants_to_function:
464
479
  passes.append(FuseConstantsToFunctionRewriter())
465
480
  with chdir(external_directory):
466
481
  graph = PassManager(
@@ -33,6 +33,8 @@ class StaticCache(Tensor):
33
33
  We should treat DynamicCache as static for onnx export.
34
34
  """
35
35
 
36
+ # pylint: disable=unused-argument
37
+
36
38
  def update(
37
39
  self,
38
40
  key_states: Tensor,
@@ -61,6 +63,12 @@ class StaticCache(Tensor):
61
63
 
62
64
  layer_kv = self[layer_idx]
63
65
  keys, values = layer_kv.unbind()
66
+ if torch.onnx.is_in_onnx_export():
67
+ # We don't want to update cache because it's inplace. During exporting,
68
+ # the forward could be called multiple times and the cache will be falsely
69
+ # updated, so we skip cache update in onnx exporting phase.
70
+ keys = keys.clone()
71
+ values = values.clone()
64
72
  # Some old models give None for `cache_position` or even omit passing
65
73
  # `cache_kwargs` when used as cross-attention, in which case we should copy
66
74
  # the whole Layer (key_states.shape[-2] == self.max_cache_len)
@@ -82,6 +90,40 @@ class StaticCache(Tensor):
82
90
 
83
91
  return keys, values
84
92
 
93
+ def update_conv_state(
94
+ self, conv_states: torch.Tensor, layer_idx: int, **kwargs
95
+ ) -> torch.Tensor:
96
+ """
97
+ Updates the cache with the new `conv_states` for the layer `layer_idx`.
98
+
99
+ Parameters:
100
+ conv_states (`torch.Tensor`):
101
+ The new conv states to cache.
102
+ layer_idx (`int`):
103
+ The index of the layer to cache the states for.
104
+
105
+ Return:
106
+ `torch.Tensor`: The updated conv states.
107
+ """
108
+ raise NotImplementedError("Qwen GDN conv state is not supported yet.")
109
+
110
+ def update_recurrent_state(
111
+ self, recurrent_states: torch.Tensor, layer_idx: int, **kwargs
112
+ ) -> torch.Tensor:
113
+ """
114
+ Updates the cache with the new `recurrent_states` for the layer `layer_idx`.
115
+
116
+ Parameters:
117
+ smm_states (`torch.Tensor`):
118
+ The new ssm states to cache.
119
+ layer_idx (`int`):
120
+ The index of the layer to cache the states for.
121
+
122
+ Return:
123
+ `torch.Tensor`: The updated ssm states.
124
+ """
125
+ raise NotImplementedError("Qwen GDN recurrent state is not supported yet.")
126
+
85
127
  def early_initialization(
86
128
  self,
87
129
  batch_size: int,
@@ -107,11 +149,9 @@ class StaticCache(Tensor):
107
149
  """Returns the sequence length of the cache for the given layer."""
108
150
  if layer_idx >= self.shape[0]:
109
151
  return 0
110
- return int(self[0, 0].any(dim=-1).sum().item())
152
+ return int(self[layer_idx, 0].any(dim=-1).sum().item())
111
153
 
112
- def get_mask_sizes(
113
- self, cache_position: torch.Tensor, layer_idx: int
114
- ) -> tuple[int, int]:
154
+ def get_mask_sizes(self, query_length: int, layer_idx: int) -> tuple[int, int]:
115
155
  """
116
156
  Return a tuple (kv_length, kv_offset) corresponding to the length and offset
117
157
  that will be returned for the given layer at `layer_idx`. The masks are then
@@ -119,8 +159,9 @@ class StaticCache(Tensor):
119
159
  for each layer.
120
160
  """
121
161
  if layer_idx >= self.shape[0]:
122
- return cache_position.shape[0], 0
123
- return self.shape[4], 0
162
+ if isinstance(query_length, torch.Tensor) and query_length.ndim > 0:
163
+ return query_length.shape[0], 0
164
+ return self.max_cache_len, 0
124
165
 
125
166
  def get_max_cache_shape(self, layer_idx: int = 0) -> int:
126
167
  """
@@ -140,6 +181,18 @@ class StaticCache(Tensor):
140
181
  if self.get_seq_length() > 0:
141
182
  self.copy_(self.index_select(0, beam_idx.to(self.device)))
142
183
 
184
+ def crop(self, max_length: int):
185
+ """Do nothing for static cache."""
186
+ return
187
+
188
+ def batch_repeat_interleave(self, repeats: int):
189
+ """Do nothing for static cache."""
190
+ return
191
+
192
+ def batch_select_indices(self, indices: torch.Tensor):
193
+ """Do nothing for static cache."""
194
+ return
195
+
143
196
  @property
144
197
  def max_batch_size(self) -> int:
145
198
  """Return the maximum batch size of the cache"""
@@ -1,5 +1,5 @@
1
1
  """
2
- Copyright (C) 2025 The HYPERONNX Authors.
2
+ Copyright (C) 2026 The HYPERONNX Authors.
3
3
 
4
4
  Licensed under the Apache License, Version 2.0 (the "License");
5
5
  you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@ import importlib
18
18
  from contextlib import contextmanager
19
19
  from unittest.mock import patch
20
20
 
21
+ import torch
21
22
  from packaging.version import Version
22
23
 
23
24
 
@@ -34,6 +35,7 @@ def patch_transformers():
34
35
  TRANSFORMERS_HAS_SDPA_ATTENTION = Version("4.48.0")
35
36
  TRANSFORMERS_HAS_SDPA_MASK = Version("4.53.0")
36
37
  TRANSFORMERS_FIX_VMAP = Version("5.0.0")
38
+ TRANSFORMERS_DEPRECATE_CACHE_POSITION = Version("5.4.0")
37
39
  CURR_VER = Version(transformers.__version__)
38
40
 
39
41
  patches = []
@@ -56,6 +58,33 @@ def patch_transformers():
56
58
  "transformers.integrations.sdpa_attention.use_gqa_in_sdpa", use_gqa_in_sdpa
57
59
  )
58
60
  patches.append(p2)
61
+ if CURR_VER >= TRANSFORMERS_DEPRECATE_CACHE_POSITION:
62
+ original_sdpa_mask = transformers.masking_utils.sdpa_mask
63
+
64
+ def patch_sdpa_mask(
65
+ batch_size: int,
66
+ q_length: int,
67
+ kv_length: int,
68
+ *args,
69
+ **kwargs,
70
+ ):
71
+ # In onnx exporting (torch.onnx.is_in_onnx_export()==True), the shape of a
72
+ # Tensor is still a tensor:
73
+ # type(tensor([1]).shape[0]) == torch.Tensor
74
+ # So the wrap in sdpa_mask is wrong in this case. We forcely convert the
75
+ # shape to integer.
76
+ def _to_int(x):
77
+ if isinstance(x, torch.Tensor) and x.ndim == 0:
78
+ return int(x.item())
79
+ return x
80
+
81
+ q_length = _to_int(q_length)
82
+ kv_length = _to_int(kv_length)
83
+
84
+ return original_sdpa_mask(batch_size, q_length, kv_length, *args, **kwargs)
85
+
86
+ p3 = patch("transformers.masking_utils.sdpa_mask", patch_sdpa_mask)
87
+ patches.append(p3)
59
88
 
60
89
  try:
61
90
  for p in patches:
@@ -28,7 +28,7 @@ classifiers = [
28
28
  "Topic :: Software Development",
29
29
  ]
30
30
  dynamic = ["version", "description"]
31
- dependencies = ["onnxifier>=2.0.1", "onnxscript<0.6.0", "onnx>=1.18.0"]
31
+ dependencies = ["onnxifier>=2.0.3", "onnxscript<0.6.0", "onnx>=1.18.0"]
32
32
 
33
33
  [project.urls]
34
34
  Home = "https://github.com/LoSealL/hyperonnx"
@@ -47,7 +47,7 @@ dev = [
47
47
  "pyright>=1.1.408",
48
48
  "pytest",
49
49
  "pytest-cov",
50
- "transformers>=4.48.0,<5.4.0",
50
+ "transformers>=4.48.0",
51
51
  ]
52
52
 
53
53
  [tool.setuptools]
@@ -56,7 +56,7 @@ packages = ["hyperonnx"]
56
56
 
57
57
  [tool.pyright]
58
58
  include = ["hyperonnx"]
59
- exclude = ["tests"]
59
+ exclude = ["**/node_modules", "**/__pycache__", "**/.*", ".venv", "tests"]
60
60
 
61
61
  [tool.pytest.ini_options]
62
62
  testpaths = ["tests"]
@@ -140,6 +140,7 @@ def test_export_resnet_with_basicblock_and_bottleneck(dynamo):
140
140
  input_names=["input.1"],
141
141
  hiera=[ResNet, BasicBlock, Bottleneck],
142
142
  do_optimization=False,
143
+ fold_nodes_to_functions=False,
143
144
  dynamo=dynamo,
144
145
  )
145
146
  model = onnx.load_from_string(f.getvalue())
@@ -229,7 +230,6 @@ def test_export_llama_transformers(dynamo, cache, tmp_path):
229
230
  input_ids=torch.randint(config.vocab_size, [1, 16]).long(),
230
231
  attention_mask=torch.ones([1, 16]),
231
232
  position_ids=torch.arange(16).long()[None],
232
- cache_position=torch.arange(16).long(),
233
233
  )
234
234
  if cache:
235
235
  # example_inputs["use_cache"] = True
@@ -243,10 +243,12 @@ def test_export_llama_transformers(dynamo, cache, tmp_path):
243
243
  )
244
244
  with torch.no_grad():
245
245
  ref_output = llama(**example_inputs)
246
+ if cache:
247
+ example_inputs["past_key_values"].fill_(0)
246
248
  with BytesIO() as f, patch_transformers(), torch.no_grad():
247
- input_names = ["input_ids", "attention_mask", "position_ids", "cache_position"]
249
+ input_names = ["input_ids", "attention_mask", "position_ids"]
248
250
  if cache:
249
- input_names.insert(-1, "past_key_values")
251
+ input_names.append("past_key_values")
250
252
  output_names = ["last_hidden_state"]
251
253
  if cache:
252
254
  output_names.append("present_key_values")
@@ -311,6 +313,8 @@ def test_export_rnnlike(dynamo):
311
313
  hiera=[RNNCell],
312
314
  input_names=["x"],
313
315
  do_optimization=False,
316
+ fold_nodes_to_functions=False,
317
+ fuse_constants_to_function=False,
314
318
  dynamo=dynamo,
315
319
  )
316
320
  onnx_model = onnx.load_from_string(f.getvalue())
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes