Auto-sync: 2026-06-25 22:43:25 (part 17)
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/memory.py +1108 -0
- outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/metrics.py +484 -0
- outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/mkldnn_ir.py +1361 -0
- outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/mkldnn_lowerings.py +1399 -0
- outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/mock_cache.py +274 -0
- outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/ops_handler.py +1207 -0
- outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/optimize_indexing.py +126 -0
- outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/output_code.py +1202 -0
- outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/package/__init__.py +1 -0
- outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/package/build_package.py +15 -0
- outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/package/package.py +138 -0
- outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/pattern_matcher.py +2495 -0
- outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/quantized_lowerings.py +171 -0
- outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/remote_cache.py +432 -0
- outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/remote_gemm_autotune_cache.py +20 -0
- outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/rocm_multiarch_utils.py +310 -0
- outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/__init__.py +0 -0
- outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/autotune_cache.py +667 -0
- outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/benchmarking.py +549 -0
- outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/cache_dir_utils.py +54 -0
- outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/caching/__init__.py +32 -0
- outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/caching/config.py +167 -0
- outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/caching/context.py +310 -0
- outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/caching/encoders.py +68 -0
- outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/caching/exceptions.py +96 -0
- outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/caching/implementations.py +422 -0
- outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/caching/interfaces.py +1280 -0
- outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/caching/locks.py +207 -0
- outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/caching/memoizers.py +7 -0
- outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/caching/utils.py +66 -0
- outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/compile_tasks.py +150 -0
- outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/coordinate_descent_tuner.py +412 -0
- outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/debug_utils.py +138 -0
- outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/halide_helpers.py +155 -0
- outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/hints.py +236 -0
- outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/proton_utils.py +325 -0
- outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/runtime_utils.py +829 -0
- outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/static_triton_launcher.py +351 -0
- outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/triton_compat.py +186 -0
- outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/triton_helpers.py +850 -0
- outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/triton_heuristics.py +0 -0
- outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/triton_lazy_compile.py +243 -0
- outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/scheduler.py +0 -0
- outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/script.ld +8 -0
- outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/select_algorithm.py +0 -0
- outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/shape_propagation.py +148 -0
- outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/sizevars.py +1193 -0
- outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/standalone_compile.py +491 -0
- outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/stream_constants.py +5 -0
- outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/stream_utils.py +39 -0
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/memory.py
ADDED
|
@@ -0,0 +1,1108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import collections
|
| 4 |
+
import dataclasses
|
| 5 |
+
import heapq
|
| 6 |
+
import logging
|
| 7 |
+
from typing import TYPE_CHECKING, TypedDict
|
| 8 |
+
|
| 9 |
+
import torch
|
| 10 |
+
from torch._environment import is_fbcode
|
| 11 |
+
from torch._utils_internal import signpost_event
|
| 12 |
+
from torch.utils._ordered_set import OrderedSet
|
| 13 |
+
|
| 14 |
+
from . import config
|
| 15 |
+
from .ir import MultiOutputLayout, NoneLayout
|
| 16 |
+
from .utils import get_dtype_size, is_nonfreeable_buffers
|
| 17 |
+
from .virtualized import V
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
if TYPE_CHECKING:
|
| 21 |
+
from collections.abc import Callable
|
| 22 |
+
|
| 23 |
+
from .dependencies import Dep
|
| 24 |
+
from .scheduler import BaseSchedulerNode, SchedulerBuffer
|
| 25 |
+
|
| 26 |
+
from .dependencies import WeakDep
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
torch_log = logging.getLogger(__name__)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
@dataclasses.dataclass
|
| 33 |
+
class PeakMemoryResult:
|
| 34 |
+
order: list[BaseSchedulerNode]
|
| 35 |
+
peak_memory: int
|
| 36 |
+
method: str
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
@dataclasses.dataclass
|
| 40 |
+
class MemoryPlanningInfoForBuffer:
|
| 41 |
+
size_alloc: int = 0
|
| 42 |
+
size_free: int = 0
|
| 43 |
+
# succ_nodes used for buffer lifetime/freeing (excludes is_fake WeakDeps)
|
| 44 |
+
succ_nodes: OrderedSet[BaseSchedulerNode] = dataclasses.field(
|
| 45 |
+
default_factory=OrderedSet
|
| 46 |
+
)
|
| 47 |
+
# succ_nodes used for node ordering (includes is_fake WeakDeps)
|
| 48 |
+
succ_nodes_for_ordering: OrderedSet[BaseSchedulerNode] = dataclasses.field(
|
| 49 |
+
default_factory=OrderedSet
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
def __post_init__(self) -> None:
|
| 53 |
+
torch._check(
|
| 54 |
+
len(self.succ_nodes) <= len(self.succ_nodes_for_ordering),
|
| 55 |
+
lambda: f"succ_nodes must be a subset of succ_nodes_for_ordering. "
|
| 56 |
+
f"len(succ_nodes)={len(self.succ_nodes)}, len(succ_nodes_for_ordering)={len(self.succ_nodes_for_ordering)}",
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
@dataclasses.dataclass
|
| 61 |
+
class MemoryPlanningInfoForNode:
|
| 62 |
+
index: int = 0
|
| 63 |
+
size: int = 0
|
| 64 |
+
pred_buffers: OrderedSet[SchedulerBuffer | FreeableInputBuffer] = dataclasses.field(
|
| 65 |
+
default_factory=OrderedSet
|
| 66 |
+
)
|
| 67 |
+
pred_nodes: OrderedSet[BaseSchedulerNode] = dataclasses.field(
|
| 68 |
+
default_factory=OrderedSet
|
| 69 |
+
)
|
| 70 |
+
succ_nodes: OrderedSet[BaseSchedulerNode] = dataclasses.field(
|
| 71 |
+
default_factory=OrderedSet
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
@dataclasses.dataclass
|
| 76 |
+
class FreeableInputBuffer:
|
| 77 |
+
name: str
|
| 78 |
+
mpi_buffer: MemoryPlanningInfoForBuffer = dataclasses.field(
|
| 79 |
+
default_factory=MemoryPlanningInfoForBuffer
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
def get_name(self) -> str:
|
| 83 |
+
return self.name
|
| 84 |
+
|
| 85 |
+
def __hash__(self) -> int:
|
| 86 |
+
return hash(self.name)
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def get_freeable_input_buf(
|
| 90 |
+
nodes: list[BaseSchedulerNode],
|
| 91 |
+
graph_inputs: OrderedSet[str],
|
| 92 |
+
) -> dict[str, FreeableInputBuffer]:
|
| 93 |
+
"""
|
| 94 |
+
Create and keep track of all input buffers that can be freed during the program
|
| 95 |
+
|
| 96 |
+
Returns:
|
| 97 |
+
A dictionary containing all freeable input buffers, keyed by their names.
|
| 98 |
+
"""
|
| 99 |
+
|
| 100 |
+
def _dep_size_hint(dep: Dep) -> int:
|
| 101 |
+
return V.graph.get_dep_size_hint(dep)
|
| 102 |
+
|
| 103 |
+
# get freeable input buffers' successor nodes for memory lifetime (excludes is_fake WeakDeps)
|
| 104 |
+
# and for ordering (includes all deps)
|
| 105 |
+
dep_name_to_succ_nodes: dict[str, OrderedSet[BaseSchedulerNode]] = (
|
| 106 |
+
collections.defaultdict(OrderedSet)
|
| 107 |
+
)
|
| 108 |
+
dep_name_to_succ_nodes_for_ordering: dict[str, OrderedSet[BaseSchedulerNode]] = (
|
| 109 |
+
collections.defaultdict(OrderedSet)
|
| 110 |
+
)
|
| 111 |
+
dep_name_to_size: dict[str, int] = dict()
|
| 112 |
+
|
| 113 |
+
for node in nodes:
|
| 114 |
+
for dep in node.read_writes.reads:
|
| 115 |
+
if dep.name in graph_inputs:
|
| 116 |
+
if not is_nonfreeable_buffers(dep):
|
| 117 |
+
# All deps contribute to ordering, but fake weak deps do not contribute to
|
| 118 |
+
# memory liveness
|
| 119 |
+
dep_name_to_succ_nodes_for_ordering[dep.name].add(node)
|
| 120 |
+
dep_name_to_size[dep.name] = _dep_size_hint(dep)
|
| 121 |
+
if not (isinstance(dep, WeakDep) and dep.is_fake):
|
| 122 |
+
dep_name_to_succ_nodes[dep.name].add(node)
|
| 123 |
+
|
| 124 |
+
# create FreeableInputBuffer objects and add them to the returned dictionary
|
| 125 |
+
name_to_freeable_input_buf: dict[str, FreeableInputBuffer] = dict()
|
| 126 |
+
for dep_name in dep_name_to_succ_nodes_for_ordering:
|
| 127 |
+
name_to_freeable_input_buf[dep_name] = FreeableInputBuffer(
|
| 128 |
+
dep_name,
|
| 129 |
+
MemoryPlanningInfoForBuffer(
|
| 130 |
+
size_free=dep_name_to_size[dep_name],
|
| 131 |
+
succ_nodes=dep_name_to_succ_nodes[dep_name],
|
| 132 |
+
succ_nodes_for_ordering=dep_name_to_succ_nodes_for_ordering[dep_name],
|
| 133 |
+
),
|
| 134 |
+
)
|
| 135 |
+
return name_to_freeable_input_buf
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
def compute_size_for_scheduler_buffer(
|
| 139 |
+
name_to_buf: dict[str, SchedulerBuffer],
|
| 140 |
+
) -> dict[str, tuple[int, int]]:
|
| 141 |
+
"""
|
| 142 |
+
Compute the size of each scheduler buffer, including (1) memory allocated when
|
| 143 |
+
it is created and (2) memory deallocated when it is freed.
|
| 144 |
+
|
| 145 |
+
We specially handle the case of MultiOutputLayout.
|
| 146 |
+
Consider the following case:
|
| 147 |
+
buf0 = some_ops_with_multi_outputs(...)
|
| 148 |
+
buf1 = buf0[0] # assume 10 bytes
|
| 149 |
+
buf2 = buf0[1] # assume 20 bytes
|
| 150 |
+
In such cases,
|
| 151 |
+
buf0: at creation, 30 bytes allocated, when deleted, 0 bytes freed
|
| 152 |
+
buf1: at creation, 0 bytes allocated, when deleted, 10 bytes freed
|
| 153 |
+
buf2: at creation, 0 bytes allocated, when deleted, 20 bytes freed
|
| 154 |
+
|
| 155 |
+
When an operation mutates a buffer in-place, the scheduler creates a new buffer name
|
| 156 |
+
to track the "before" and "after" states, even though they share the same memory.
|
| 157 |
+
|
| 158 |
+
The mutated buffer represents a rename with zero allocation and deallocation cost.
|
| 159 |
+
During dependency tracking, we transfer dependencies from the mutated name back to
|
| 160 |
+
the original buffer, ensuring the original memory is only freed when all aliases
|
| 161 |
+
are done.
|
| 162 |
+
|
| 163 |
+
This handles cases where a buffer has multiple non-overlapping aliases - rather than
|
| 164 |
+
trying to assign free costs to individual aliases, we forward all alias dependencies
|
| 165 |
+
to the original buffer.
|
| 166 |
+
|
| 167 |
+
Consider:
|
| 168 |
+
buf0 = op0()
|
| 169 |
+
buf1 = mutation_op_(buf0)
|
| 170 |
+
del buf0
|
| 171 |
+
...
|
| 172 |
+
op(buf1)
|
| 173 |
+
del buf1
|
| 174 |
+
|
| 175 |
+
The only memory events are the creation prior to op0, and the deletion following buf1.
|
| 176 |
+
|
| 177 |
+
Returns:
|
| 178 |
+
A dictionary mapping a scheduler buffer to a tuple of (size_alloc, size_free).
|
| 179 |
+
"""
|
| 180 |
+
from .ir import MultiOutput
|
| 181 |
+
from .scheduler import OutputNode
|
| 182 |
+
|
| 183 |
+
sched_buf_to_size: dict[str, tuple[int, int]] = dict()
|
| 184 |
+
|
| 185 |
+
def _compute_and_update_buf_size(
|
| 186 |
+
sched_buf: SchedulerBuffer, user_of_MultiOutputLayout: bool = False
|
| 187 |
+
) -> int:
|
| 188 |
+
if sched_buf.get_name() in V.graph.scheduler.mutation_real_name:
|
| 189 |
+
sched_buf_to_size[sched_buf.get_name()] = (0, 0)
|
| 190 |
+
return 0
|
| 191 |
+
elif isinstance(sched_buf.node.layout, NoneLayout):
|
| 192 |
+
sched_buf_to_size[sched_buf.get_name()] = (0, 0)
|
| 193 |
+
return 0
|
| 194 |
+
elif isinstance(sched_buf.node.layout, MultiOutputLayout):
|
| 195 |
+
size_alloc = 0
|
| 196 |
+
for user in sched_buf.users:
|
| 197 |
+
if isinstance(user.node, OutputNode):
|
| 198 |
+
continue
|
| 199 |
+
for buf in user.node.get_outputs():
|
| 200 |
+
if isinstance(buf.node, MultiOutput):
|
| 201 |
+
size_alloc += _compute_and_update_buf_size(buf, True)
|
| 202 |
+
sched_buf_to_size[sched_buf.get_name()] = (
|
| 203 |
+
0 if user_of_MultiOutputLayout else size_alloc,
|
| 204 |
+
0,
|
| 205 |
+
)
|
| 206 |
+
return size_alloc
|
| 207 |
+
else:
|
| 208 |
+
buf_size = V.graph.sizevars.optimization_hint(
|
| 209 |
+
sched_buf.node.get_numel(), fallback=0
|
| 210 |
+
) * get_dtype_size(sched_buf.node.get_dtype())
|
| 211 |
+
sched_buf_to_size[sched_buf.get_name()] = (
|
| 212 |
+
0 if user_of_MultiOutputLayout else buf_size,
|
| 213 |
+
buf_size,
|
| 214 |
+
)
|
| 215 |
+
return buf_size
|
| 216 |
+
|
| 217 |
+
for sched_buf in name_to_buf.values():
|
| 218 |
+
# skip if sched_buf is already processed as an user of another SchedulerBuffer
|
| 219 |
+
# whose layout is of the type MultiOutputLayout
|
| 220 |
+
if sched_buf.get_name() not in sched_buf_to_size:
|
| 221 |
+
_compute_and_update_buf_size(sched_buf)
|
| 222 |
+
|
| 223 |
+
return sched_buf_to_size
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
def assign_memory_planning_info_for_scheduler_buffers(
|
| 227 |
+
nodes: list[BaseSchedulerNode],
|
| 228 |
+
name_to_buf: dict[str, SchedulerBuffer],
|
| 229 |
+
) -> None:
|
| 230 |
+
"""
|
| 231 |
+
For each SchedulerBuffer, assign its size info and successor nodes.
|
| 232 |
+
A buffer's successor nodes determines when a buffer can be freed.
|
| 233 |
+
"""
|
| 234 |
+
# get buffer sizes
|
| 235 |
+
sched_buf_to_size = compute_size_for_scheduler_buffer(name_to_buf)
|
| 236 |
+
|
| 237 |
+
# get buffer's successor nodes for memory lifetime (excludes is_fake WeakDeps)
|
| 238 |
+
# and for ordering (includes all deps)
|
| 239 |
+
dep_name_to_succ_nodes: dict[str, OrderedSet[BaseSchedulerNode]] = (
|
| 240 |
+
collections.defaultdict(OrderedSet)
|
| 241 |
+
)
|
| 242 |
+
dep_name_to_succ_nodes_for_ordering: dict[str, OrderedSet[BaseSchedulerNode]] = (
|
| 243 |
+
collections.defaultdict(OrderedSet)
|
| 244 |
+
)
|
| 245 |
+
for node in nodes:
|
| 246 |
+
for dep in node.unmet_dependencies:
|
| 247 |
+
# All deps contribute to ordering, but fake weak deps do not contribute to
|
| 248 |
+
# memory liveness
|
| 249 |
+
dep_name_to_succ_nodes_for_ordering[dep.name].add(node)
|
| 250 |
+
if not (isinstance(dep, WeakDep) and dep.is_fake):
|
| 251 |
+
dep_name_to_succ_nodes[dep.name].add(node)
|
| 252 |
+
|
| 253 |
+
# iterate in reverse, so dependencies are picked up transitively.
|
| 254 |
+
for mutating_buf_name, real_buf_name in reversed(
|
| 255 |
+
V.graph.scheduler.mutation_real_name.items()
|
| 256 |
+
):
|
| 257 |
+
dep_name_to_succ_nodes[real_buf_name] |= dep_name_to_succ_nodes[
|
| 258 |
+
mutating_buf_name
|
| 259 |
+
]
|
| 260 |
+
dep_name_to_succ_nodes_for_ordering[real_buf_name] |= (
|
| 261 |
+
dep_name_to_succ_nodes_for_ordering[mutating_buf_name]
|
| 262 |
+
)
|
| 263 |
+
|
| 264 |
+
# populate the MemoryPlanningInfoForBuffer attribute to each scheduler buffer
|
| 265 |
+
# note: there are scheduler buffers not in dep_name_to_succ_nodes (e.g., graph outputs)
|
| 266 |
+
for buf_name in name_to_buf:
|
| 267 |
+
name_to_buf[buf_name].mpi_buffer = MemoryPlanningInfoForBuffer(
|
| 268 |
+
size_alloc=sched_buf_to_size[buf_name][0],
|
| 269 |
+
size_free=sched_buf_to_size[buf_name][1],
|
| 270 |
+
succ_nodes=dep_name_to_succ_nodes[buf_name],
|
| 271 |
+
succ_nodes_for_ordering=dep_name_to_succ_nodes_for_ordering[buf_name],
|
| 272 |
+
)
|
| 273 |
+
|
| 274 |
+
|
| 275 |
+
def assign_memory_planning_info_for_scheduler_nodes(
|
| 276 |
+
nodes: list[BaseSchedulerNode],
|
| 277 |
+
name_to_fused_node: dict[str, BaseSchedulerNode],
|
| 278 |
+
name_to_buf: dict[str, SchedulerBuffer],
|
| 279 |
+
name_to_freeable_input_buf: dict[str, FreeableInputBuffer],
|
| 280 |
+
) -> None:
|
| 281 |
+
"""
|
| 282 |
+
Assign to each scheduler node its predecessor and successor nodes.
|
| 283 |
+
"""
|
| 284 |
+
|
| 285 |
+
node_to_pred_nodes: dict[BaseSchedulerNode, OrderedSet[BaseSchedulerNode]] = (
|
| 286 |
+
collections.defaultdict(OrderedSet)
|
| 287 |
+
)
|
| 288 |
+
node_to_succ_nodes: dict[BaseSchedulerNode, OrderedSet[BaseSchedulerNode]] = {}
|
| 289 |
+
node_to_pred_buffers: dict[
|
| 290 |
+
BaseSchedulerNode, OrderedSet[SchedulerBuffer | FreeableInputBuffer]
|
| 291 |
+
] = collections.defaultdict(OrderedSet)
|
| 292 |
+
|
| 293 |
+
# collect all predecessors using existing successor mappings
|
| 294 |
+
for node in nodes:
|
| 295 |
+
succ_nodes = OrderedSet(
|
| 296 |
+
succ_node
|
| 297 |
+
for buffer in node.get_outputs()
|
| 298 |
+
for succ_node in buffer.mpi_buffer.succ_nodes_for_ordering
|
| 299 |
+
)
|
| 300 |
+
node_to_succ_nodes[node] = succ_nodes
|
| 301 |
+
|
| 302 |
+
# For each successor, add current node as its predecessor
|
| 303 |
+
for succ_node in succ_nodes:
|
| 304 |
+
node_to_pred_nodes[succ_node].add(node)
|
| 305 |
+
|
| 306 |
+
# For each output buffer, add it as predecessor to its successor nodes
|
| 307 |
+
# Use succ_nodes (not succ_nodes_for_ordering) since pred_buffers is used
|
| 308 |
+
# for memory lifetime tracking, not ordering
|
| 309 |
+
for buffer in node.get_outputs():
|
| 310 |
+
for succ_node in buffer.mpi_buffer.succ_nodes:
|
| 311 |
+
node_to_pred_buffers[succ_node].add(buffer)
|
| 312 |
+
|
| 313 |
+
for freeable_buffer in name_to_freeable_input_buf.values():
|
| 314 |
+
for succ_node in freeable_buffer.mpi_buffer.succ_nodes:
|
| 315 |
+
node_to_pred_buffers[succ_node].add(freeable_buffer)
|
| 316 |
+
|
| 317 |
+
# Second pass: assign memory planning info using completed predecessor mappings
|
| 318 |
+
for index, node in enumerate(nodes):
|
| 319 |
+
size_alloc = sum(buffer.mpi_buffer.size_alloc for buffer in node.get_outputs())
|
| 320 |
+
succ_nodes = node_to_succ_nodes[node]
|
| 321 |
+
pred_nodes = node_to_pred_nodes[node]
|
| 322 |
+
|
| 323 |
+
# make sure we do not make node a successor or predecessor of itself
|
| 324 |
+
succ_nodes.discard(node)
|
| 325 |
+
pred_nodes.discard(node)
|
| 326 |
+
|
| 327 |
+
node.mpi_node = MemoryPlanningInfoForNode(
|
| 328 |
+
index=index,
|
| 329 |
+
size=size_alloc,
|
| 330 |
+
pred_buffers=node_to_pred_buffers[node],
|
| 331 |
+
pred_nodes=node_to_pred_nodes[node],
|
| 332 |
+
succ_nodes=succ_nodes,
|
| 333 |
+
)
|
| 334 |
+
|
| 335 |
+
|
| 336 |
+
# map each scheduler buffer to its size, start step, and end step
|
| 337 |
+
@dataclasses.dataclass
|
| 338 |
+
class BufferInfo:
|
| 339 |
+
buffer: SchedulerBuffer | FreeableInputBuffer
|
| 340 |
+
size_alloc: int
|
| 341 |
+
size_free: int
|
| 342 |
+
start_step: int
|
| 343 |
+
end_step: int
|
| 344 |
+
|
| 345 |
+
|
| 346 |
+
def compute_memory_timeline(
|
| 347 |
+
nodes: list[BaseSchedulerNode],
|
| 348 |
+
name_to_freeable_input_buf: dict[str, FreeableInputBuffer],
|
| 349 |
+
graph_outputs: OrderedSet[str],
|
| 350 |
+
) -> tuple[
|
| 351 |
+
list[BufferInfo],
|
| 352 |
+
dict[BaseSchedulerNode, int],
|
| 353 |
+
dict[FreeableInputBuffer | SchedulerBuffer, BaseSchedulerNode],
|
| 354 |
+
]:
|
| 355 |
+
"""
|
| 356 |
+
Compute buffer allocation and deallocation sizes and map their
|
| 357 |
+
lifetime to the node schedule
|
| 358 |
+
"""
|
| 359 |
+
|
| 360 |
+
# get the execution step of each node, this will be used to determine
|
| 361 |
+
# the end_step of buffers
|
| 362 |
+
node_to_step: dict[BaseSchedulerNode, int] = {
|
| 363 |
+
node: step for step, node in enumerate(nodes)
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
# get buffers' size and liveliness information
|
| 367 |
+
buf_info_list: list[BufferInfo] = []
|
| 368 |
+
buf_to_snode_last_use: dict[
|
| 369 |
+
FreeableInputBuffer | SchedulerBuffer, BaseSchedulerNode
|
| 370 |
+
] = {}
|
| 371 |
+
|
| 372 |
+
def _get_end_step_and_snode(
|
| 373 |
+
buf: FreeableInputBuffer | SchedulerBuffer,
|
| 374 |
+
) -> tuple[int, BaseSchedulerNode | None]:
|
| 375 |
+
max_step: int = -1
|
| 376 |
+
max_step_snode: BaseSchedulerNode | None = None
|
| 377 |
+
succ_nodes = buf.mpi_buffer.succ_nodes
|
| 378 |
+
if succ_nodes:
|
| 379 |
+
for succ_node in succ_nodes:
|
| 380 |
+
step = node_to_step[succ_node]
|
| 381 |
+
if step > max_step:
|
| 382 |
+
max_step = step
|
| 383 |
+
max_step_snode = succ_node
|
| 384 |
+
assert max_step_snode is not None
|
| 385 |
+
return max_step, max_step_snode
|
| 386 |
+
|
| 387 |
+
# 1. for freeable input buffers
|
| 388 |
+
for buf_name, input_buf in name_to_freeable_input_buf.items():
|
| 389 |
+
end_step = -1
|
| 390 |
+
if buf_name not in graph_outputs:
|
| 391 |
+
end_step, end_step_snode = _get_end_step_and_snode(input_buf)
|
| 392 |
+
assert end_step_snode is not None
|
| 393 |
+
buf_to_snode_last_use[input_buf] = end_step_snode
|
| 394 |
+
|
| 395 |
+
buf_info_list.append(
|
| 396 |
+
BufferInfo(
|
| 397 |
+
input_buf,
|
| 398 |
+
input_buf.mpi_buffer.size_free,
|
| 399 |
+
input_buf.mpi_buffer.size_free,
|
| 400 |
+
0,
|
| 401 |
+
end_step,
|
| 402 |
+
)
|
| 403 |
+
)
|
| 404 |
+
|
| 405 |
+
# 2. for scheduler buffers
|
| 406 |
+
for step, node in enumerate(nodes):
|
| 407 |
+
for sched_buf in node.get_outputs():
|
| 408 |
+
# note: it is possible for a non-graph-output sched_buf to have no succ_nodes and
|
| 409 |
+
# to be only used by its defining op (e.g., due to fusion when all consumers of
|
| 410 |
+
# the buffer are fused with its defining op). In such cases, end_step is step.
|
| 411 |
+
buf_name = sched_buf.get_name()
|
| 412 |
+
end_step = -1
|
| 413 |
+
if buf_name not in graph_outputs:
|
| 414 |
+
end_step, end_step_snode = _get_end_step_and_snode(sched_buf)
|
| 415 |
+
if end_step == -1:
|
| 416 |
+
end_step = step
|
| 417 |
+
buf_to_snode_last_use[sched_buf] = node
|
| 418 |
+
else:
|
| 419 |
+
assert end_step_snode is not None
|
| 420 |
+
buf_to_snode_last_use[sched_buf] = end_step_snode
|
| 421 |
+
|
| 422 |
+
buf_info_list.append(
|
| 423 |
+
BufferInfo(
|
| 424 |
+
sched_buf,
|
| 425 |
+
sched_buf.mpi_buffer.size_alloc,
|
| 426 |
+
sched_buf.mpi_buffer.size_free,
|
| 427 |
+
step,
|
| 428 |
+
end_step,
|
| 429 |
+
)
|
| 430 |
+
)
|
| 431 |
+
|
| 432 |
+
return buf_info_list, node_to_step, buf_to_snode_last_use
|
| 433 |
+
|
| 434 |
+
|
| 435 |
+
def estimate_peak_memory(
|
| 436 |
+
nodes: list[BaseSchedulerNode],
|
| 437 |
+
name_to_freeable_input_buf: dict[str, FreeableInputBuffer],
|
| 438 |
+
graph_outputs: OrderedSet[str],
|
| 439 |
+
) -> tuple[int, list[int]]:
|
| 440 |
+
"""
|
| 441 |
+
Given a list of nodes in their execution order, estimate the peak memory, by
|
| 442 |
+
keeping track of the liveliness of SchedulerBuffers and FreeableInputBuffers.
|
| 443 |
+
|
| 444 |
+
Returns:
|
| 445 |
+
int: peak memory
|
| 446 |
+
List[int]: memory usage at each node (or each step).
|
| 447 |
+
"""
|
| 448 |
+
|
| 449 |
+
buf_info_list, _, _ = compute_memory_timeline(
|
| 450 |
+
nodes, name_to_freeable_input_buf, graph_outputs
|
| 451 |
+
)
|
| 452 |
+
|
| 453 |
+
# incremental memory changes at each step
|
| 454 |
+
memory = [0 for _ in range(len(nodes) + 1)]
|
| 455 |
+
|
| 456 |
+
# for each buffer, update memory when created and when freed
|
| 457 |
+
for buf_info in buf_info_list:
|
| 458 |
+
memory[buf_info.start_step] += buf_info.size_alloc
|
| 459 |
+
memory[buf_info.end_step + 1] -= buf_info.size_free
|
| 460 |
+
|
| 461 |
+
# get peak memory by compute the cumulative memories
|
| 462 |
+
max_memory = 0
|
| 463 |
+
cur_memory = 0
|
| 464 |
+
memories_at_nodes = []
|
| 465 |
+
for t in range(len(nodes) + 1):
|
| 466 |
+
cur_memory += memory[t]
|
| 467 |
+
memories_at_nodes.append(cur_memory)
|
| 468 |
+
max_memory = max(max_memory, cur_memory)
|
| 469 |
+
|
| 470 |
+
return (max_memory, memories_at_nodes)
|
| 471 |
+
|
| 472 |
+
|
| 473 |
+
@dataclasses.dataclass
|
| 474 |
+
class SNodeMemory:
|
| 475 |
+
size_alloc: int
|
| 476 |
+
size_free: int
|
| 477 |
+
|
| 478 |
+
|
| 479 |
+
def estimate_peak_memory_allocfree(
|
| 480 |
+
nodes: list[BaseSchedulerNode],
|
| 481 |
+
name_to_freeable_input_buf: dict[str, FreeableInputBuffer],
|
| 482 |
+
graph_outputs: OrderedSet[str],
|
| 483 |
+
) -> tuple[
|
| 484 |
+
int,
|
| 485 |
+
list[tuple[int, int]],
|
| 486 |
+
dict[BaseSchedulerNode, SNodeMemory],
|
| 487 |
+
dict[FreeableInputBuffer | SchedulerBuffer, BaseSchedulerNode],
|
| 488 |
+
]:
|
| 489 |
+
"""
|
| 490 |
+
Alternative version of estimate_peak_memory, that respects the fact,
|
| 491 |
+
that every SchedulerNode has multiple phases:
|
| 492 |
+
1. alloc ( outputs )
|
| 493 |
+
2. run_kernel
|
| 494 |
+
3. dealloc last_use buffers
|
| 495 |
+
estimate_peak_memory collapses memory into one value: size_alloc - size_free
|
| 496 |
+
While peak memory happens after alloc.
|
| 497 |
+
|
| 498 |
+
Duplicating the code to not migrate all callsites at once,
|
| 499 |
+
In future usages of estimate_peak_memory will migrate to this version.
|
| 500 |
+
"""
|
| 501 |
+
|
| 502 |
+
buf_info_list, _, buf_to_snode_last_use = compute_memory_timeline(
|
| 503 |
+
nodes, name_to_freeable_input_buf, graph_outputs
|
| 504 |
+
)
|
| 505 |
+
|
| 506 |
+
# incremental memory changes at each step
|
| 507 |
+
step_idx_allocfree = [SNodeMemory(0, 0) for _ in range(len(nodes))]
|
| 508 |
+
|
| 509 |
+
# for each buffer, update memory when created and when freed
|
| 510 |
+
for buf_info in buf_info_list:
|
| 511 |
+
step_idx_allocfree[buf_info.start_step].size_alloc += buf_info.size_alloc
|
| 512 |
+
if buf_info.end_step != -1:
|
| 513 |
+
step_idx_allocfree[buf_info.end_step].size_free += buf_info.size_free
|
| 514 |
+
|
| 515 |
+
snodes_allocfree = {}
|
| 516 |
+
for i, node in enumerate(nodes):
|
| 517 |
+
snodes_allocfree[node] = step_idx_allocfree[i]
|
| 518 |
+
|
| 519 |
+
max_memory = 0
|
| 520 |
+
cur_memory = 0
|
| 521 |
+
snodes_curr_memory = []
|
| 522 |
+
for t in range(len(nodes)):
|
| 523 |
+
alloc = step_idx_allocfree[t].size_alloc
|
| 524 |
+
free = step_idx_allocfree[t].size_free
|
| 525 |
+
cur_memory += alloc
|
| 526 |
+
post_alloc = cur_memory
|
| 527 |
+
max_memory = max(max_memory, cur_memory)
|
| 528 |
+
cur_memory -= free
|
| 529 |
+
post_free = cur_memory
|
| 530 |
+
snodes_curr_memory.append((post_alloc, post_free))
|
| 531 |
+
|
| 532 |
+
return (
|
| 533 |
+
max_memory,
|
| 534 |
+
snodes_curr_memory,
|
| 535 |
+
snodes_allocfree,
|
| 536 |
+
buf_to_snode_last_use,
|
| 537 |
+
)
|
| 538 |
+
|
| 539 |
+
|
| 540 |
+
def topological_sort_lpmf(
|
| 541 |
+
nodes: list[BaseSchedulerNode],
|
| 542 |
+
name_to_freeable_input_buf: dict[str, FreeableInputBuffer],
|
| 543 |
+
name_to_buf: dict[str, SchedulerBuffer],
|
| 544 |
+
graph_outputs: OrderedSet[str],
|
| 545 |
+
) -> list[BaseSchedulerNode]:
|
| 546 |
+
"""
|
| 547 |
+
A bfs-based greedy topological order. LPMF stands for "Least Peak Memory First".
|
| 548 |
+
|
| 549 |
+
The idea is from this paper:
|
| 550 |
+
Buffer memory optimization for video codec application modeled in Simulink
|
| 551 |
+
https://www.cs.york.ac.uk/rts/docs/DAC-1964-2006/PAPERS/2006/DAC06/PDFFILES/P0689.PDF
|
| 552 |
+
|
| 553 |
+
The algorithm maintains the max memory so far.
|
| 554 |
+
At every iteration, for each scheduleable node, it computes:
|
| 555 |
+
- how much memory needs to be allocated for the output buffers of this node;
|
| 556 |
+
- how much memory can be freed as a result of executing this node.
|
| 557 |
+
This gives us two values for each node:
|
| 558 |
+
(1) mem1: memory during the execution of the node;
|
| 559 |
+
(2) mem2: memory after executing the node, after some input buffers are freed.
|
| 560 |
+
The greedy approach select as follows:
|
| 561 |
+
(i) if there are nodes whose mem1 values are below the max memory so far,
|
| 562 |
+
then pick the node with the lowest mem2 value;
|
| 563 |
+
(ii) otherwise, pick the one with the lowest mem1 value.
|
| 564 |
+
"""
|
| 565 |
+
|
| 566 |
+
class NodeInfo(TypedDict):
|
| 567 |
+
indegree: int
|
| 568 |
+
memory_to_free: int
|
| 569 |
+
|
| 570 |
+
class BufferInfo(TypedDict):
|
| 571 |
+
outdegree: int
|
| 572 |
+
|
| 573 |
+
node_info: dict[BaseSchedulerNode, NodeInfo] = dict()
|
| 574 |
+
buf_info: dict[SchedulerBuffer | FreeableInputBuffer, BufferInfo] = dict()
|
| 575 |
+
|
| 576 |
+
# compute nodes' number of unmet dependencies (for schedulability)
|
| 577 |
+
# initialize the list of nodes ready to be scheduled
|
| 578 |
+
nodes_to_schedule: OrderedSet[BaseSchedulerNode] = OrderedSet()
|
| 579 |
+
for node in nodes:
|
| 580 |
+
node_info[node] = {
|
| 581 |
+
"indegree": len(node.mpi_node.pred_nodes),
|
| 582 |
+
"memory_to_free": 0,
|
| 583 |
+
}
|
| 584 |
+
if node_info[node]["indegree"] == 0:
|
| 585 |
+
nodes_to_schedule.add(node)
|
| 586 |
+
|
| 587 |
+
# compute buffers' number of unmet successors (used to decide when to free)
|
| 588 |
+
for buf in list(name_to_buf.values()) + list(name_to_freeable_input_buf.values()):
|
| 589 |
+
buf_info[buf] = {
|
| 590 |
+
"outdegree": len(buf.mpi_buffer.succ_nodes)
|
| 591 |
+
+ (1 if buf.get_name() in graph_outputs else 0)
|
| 592 |
+
}
|
| 593 |
+
|
| 594 |
+
# initialize memory estimations
|
| 595 |
+
live_memory = sum(
|
| 596 |
+
input_buf.mpi_buffer.size_free
|
| 597 |
+
for input_buf in name_to_freeable_input_buf.values()
|
| 598 |
+
)
|
| 599 |
+
|
| 600 |
+
# this is the total output memory, which is a lower bound for peak memory
|
| 601 |
+
# we do not include the memory of non freeable input buffers
|
| 602 |
+
output_memory = 0
|
| 603 |
+
for buf_name in graph_outputs:
|
| 604 |
+
if buf_name in name_to_buf:
|
| 605 |
+
output_memory += name_to_buf[buf_name].mpi_buffer.size_free
|
| 606 |
+
elif buf_name in name_to_freeable_input_buf:
|
| 607 |
+
output_memory += name_to_freeable_input_buf[buf_name].mpi_buffer.size_free
|
| 608 |
+
max_memory = max(live_memory, output_memory)
|
| 609 |
+
memory_gap = max_memory - live_memory
|
| 610 |
+
|
| 611 |
+
# compute the amount of memory that is allocated when a node is scheduled
|
| 612 |
+
# and the amount of memory that can be freed when a node is scheduled
|
| 613 |
+
for node in nodes:
|
| 614 |
+
# 1. if a buffer read by this node is last used by this node
|
| 615 |
+
for buf in node.mpi_node.pred_buffers:
|
| 616 |
+
if buf_info[buf]["outdegree"] == 1:
|
| 617 |
+
node_info[node]["memory_to_free"] += buf.mpi_buffer.size_free
|
| 618 |
+
# 2. if a buffer written by this node is used internally and not used later
|
| 619 |
+
for buf in node.get_outputs():
|
| 620 |
+
if buf_info[buf]["outdegree"] == 0:
|
| 621 |
+
node_info[node]["memory_to_free"] += buf.mpi_buffer.size_free
|
| 622 |
+
|
| 623 |
+
# schedule nodes one at a time
|
| 624 |
+
schedule: list[BaseSchedulerNode] = []
|
| 625 |
+
size_threshold = config.size_threshold_for_succ_based_strategy
|
| 626 |
+
num_iters: int = 0
|
| 627 |
+
while num_iters < len(nodes) and nodes_to_schedule:
|
| 628 |
+
# select a node to schedule:
|
| 629 |
+
if (
|
| 630 |
+
size_threshold > 0
|
| 631 |
+
and min(node.mpi_node.size for node in nodes_to_schedule) > size_threshold
|
| 632 |
+
):
|
| 633 |
+
selected_node = min(
|
| 634 |
+
nodes_to_schedule,
|
| 635 |
+
key=lambda node: min(
|
| 636 |
+
(
|
| 637 |
+
succ_node.mpi_node.index
|
| 638 |
+
for succ_node in node.mpi_node.succ_nodes
|
| 639 |
+
),
|
| 640 |
+
default=len(nodes),
|
| 641 |
+
),
|
| 642 |
+
)
|
| 643 |
+
else:
|
| 644 |
+
selected_node = min(
|
| 645 |
+
nodes_to_schedule,
|
| 646 |
+
key=lambda node: (
|
| 647 |
+
node.mpi_node.size if node.mpi_node.size > memory_gap else 0,
|
| 648 |
+
node.mpi_node.size - node_info[node]["memory_to_free"],
|
| 649 |
+
node.mpi_node.index,
|
| 650 |
+
),
|
| 651 |
+
)
|
| 652 |
+
nodes_to_schedule.remove(selected_node)
|
| 653 |
+
schedule.append(selected_node)
|
| 654 |
+
num_iters += 1
|
| 655 |
+
|
| 656 |
+
# update memory usage
|
| 657 |
+
live_memory += selected_node.mpi_node.size
|
| 658 |
+
max_memory = max(max_memory, live_memory)
|
| 659 |
+
live_memory -= node_info[selected_node]["memory_to_free"]
|
| 660 |
+
memory_gap = max_memory - live_memory
|
| 661 |
+
|
| 662 |
+
# update successor nodes and nodes_to_schedule
|
| 663 |
+
for succ_node in selected_node.mpi_node.succ_nodes:
|
| 664 |
+
assert node_info[succ_node]["indegree"] > 0
|
| 665 |
+
node_info[succ_node]["indegree"] -= 1
|
| 666 |
+
if node_info[succ_node]["indegree"] == 0:
|
| 667 |
+
nodes_to_schedule.add(succ_node)
|
| 668 |
+
|
| 669 |
+
# update predecessor nodes
|
| 670 |
+
for buf in selected_node.mpi_node.pred_buffers:
|
| 671 |
+
assert buf_info[buf]["outdegree"] > 0
|
| 672 |
+
buf_info[buf]["outdegree"] -= 1
|
| 673 |
+
if buf_info[buf]["outdegree"] == 1:
|
| 674 |
+
for succ_node in buf.mpi_buffer.succ_nodes:
|
| 675 |
+
node_info[succ_node]["memory_to_free"] += buf.mpi_buffer.size_free
|
| 676 |
+
|
| 677 |
+
if num_iters > len(nodes):
|
| 678 |
+
raise RuntimeError("Failed to schedule, while loop ran too long for lpmf")
|
| 679 |
+
|
| 680 |
+
return schedule
|
| 681 |
+
|
| 682 |
+
|
| 683 |
+
def topological_sort_bfs(nodes: list[BaseSchedulerNode]) -> list[BaseSchedulerNode]:
|
| 684 |
+
"""
|
| 685 |
+
A BFS topological sort that selects nodes whose dependencies are executed the
|
| 686 |
+
earliest. This follows a FIFO idea. Specifically, at every iteration, for each node
|
| 687 |
+
that is schedulable, we gather the order in which its predecessor nodes are executed,
|
| 688 |
+
and this sorted list of execution orders of predecessor nodes defines the priority.
|
| 689 |
+
We select the node whose predecessors nodes are executed the earliest. The FIFO
|
| 690 |
+
idea aims to reduce the liveness duration of buffers created.
|
| 691 |
+
"""
|
| 692 |
+
|
| 693 |
+
class NodeInfo(TypedDict):
|
| 694 |
+
indegree: int
|
| 695 |
+
order: int
|
| 696 |
+
|
| 697 |
+
node_info: dict[BaseSchedulerNode, NodeInfo] = dict()
|
| 698 |
+
|
| 699 |
+
@dataclasses.dataclass
|
| 700 |
+
class NodeWithPriority:
|
| 701 |
+
priority: list[int]
|
| 702 |
+
node: BaseSchedulerNode
|
| 703 |
+
|
| 704 |
+
def __lt__(self, other: NodeWithPriority) -> bool:
|
| 705 |
+
if self.priority == other.priority:
|
| 706 |
+
return self.node.mpi_node.index < other.node.mpi_node.index
|
| 707 |
+
return self.priority < other.priority
|
| 708 |
+
|
| 709 |
+
def _node_priority(node: BaseSchedulerNode) -> list[int]:
|
| 710 |
+
# priority is the order in which predecessor nodes are executed
|
| 711 |
+
assert node_info[node]["indegree"] == 0
|
| 712 |
+
exec_orders = sorted(
|
| 713 |
+
OrderedSet(
|
| 714 |
+
node_info[pred_node]["order"] for pred_node in node.mpi_node.pred_nodes
|
| 715 |
+
)
|
| 716 |
+
)
|
| 717 |
+
return exec_orders
|
| 718 |
+
|
| 719 |
+
# compute nodes' number of unmet dependencies (for schedulability)
|
| 720 |
+
# initialize the list of nodes ready to be scheduled
|
| 721 |
+
nodes_to_schedule: list[NodeWithPriority] = []
|
| 722 |
+
for node in nodes:
|
| 723 |
+
node_info[node] = {"indegree": len(node.mpi_node.pred_nodes), "order": -1}
|
| 724 |
+
if node_info[node]["indegree"] == 0:
|
| 725 |
+
heapq.heappush(
|
| 726 |
+
nodes_to_schedule, NodeWithPriority(_node_priority(node), node)
|
| 727 |
+
)
|
| 728 |
+
|
| 729 |
+
# schedule nodes one at a time
|
| 730 |
+
schedule: list[BaseSchedulerNode] = []
|
| 731 |
+
num_iters: int = 0
|
| 732 |
+
while num_iters < len(nodes) and nodes_to_schedule:
|
| 733 |
+
# select a node to schedule
|
| 734 |
+
selected_node = heapq.heappop(nodes_to_schedule).node
|
| 735 |
+
node_info[selected_node]["order"] = len(schedule)
|
| 736 |
+
schedule.append(selected_node)
|
| 737 |
+
num_iters += 1
|
| 738 |
+
|
| 739 |
+
# update successor nodes and nodes_to_schedule
|
| 740 |
+
for succ_node in selected_node.mpi_node.succ_nodes:
|
| 741 |
+
assert node_info[succ_node]["indegree"] > 0
|
| 742 |
+
node_info[succ_node]["indegree"] -= 1
|
| 743 |
+
if node_info[succ_node]["indegree"] == 0:
|
| 744 |
+
heapq.heappush(
|
| 745 |
+
nodes_to_schedule,
|
| 746 |
+
NodeWithPriority(_node_priority(succ_node), succ_node),
|
| 747 |
+
)
|
| 748 |
+
|
| 749 |
+
if num_iters > len(nodes):
|
| 750 |
+
raise RuntimeError("Failed to schedule, while loop ran too long for bfs")
|
| 751 |
+
|
| 752 |
+
return schedule
|
| 753 |
+
|
| 754 |
+
|
| 755 |
+
def topological_sort_dfs(nodes: list[BaseSchedulerNode]) -> list[BaseSchedulerNode]:
|
| 756 |
+
"""
|
| 757 |
+
This is a DFS topological sort. The setup is similar to `topological_sort_schedule`
|
| 758 |
+
in scheduler.py. The difference is the order nodes are visited in the outer loop.
|
| 759 |
+
In `topological_sort_schedule`, nodes are visited in their original order.
|
| 760 |
+
In this function, nodes are visited based on their priority -- for each node, we
|
| 761 |
+
compute the total memory of all buffers it reads from or writes to, and we visit
|
| 762 |
+
the nodes in ascending order of this priority.
|
| 763 |
+
"""
|
| 764 |
+
seen: OrderedSet[BaseSchedulerNode] = OrderedSet()
|
| 765 |
+
name_to_node: dict[str, BaseSchedulerNode] = dict()
|
| 766 |
+
result: list[BaseSchedulerNode] = []
|
| 767 |
+
size_with_reads: dict[BaseSchedulerNode, int] = dict()
|
| 768 |
+
|
| 769 |
+
def visit(n: BaseSchedulerNode) -> None:
|
| 770 |
+
if n not in seen:
|
| 771 |
+
seen.add(n)
|
| 772 |
+
dep_nodes = [
|
| 773 |
+
name_to_node[dep.name]
|
| 774 |
+
for dep in n.unmet_dependencies
|
| 775 |
+
if dep.name in name_to_node
|
| 776 |
+
]
|
| 777 |
+
for node in sorted(
|
| 778 |
+
dep_nodes, key=lambda n: (size_with_reads[n], n.mpi_node.index)
|
| 779 |
+
):
|
| 780 |
+
visit(node)
|
| 781 |
+
result.append(n)
|
| 782 |
+
|
| 783 |
+
for node in nodes:
|
| 784 |
+
for name in node.get_buffer_names():
|
| 785 |
+
name_to_node[name] = node
|
| 786 |
+
|
| 787 |
+
for node in nodes:
|
| 788 |
+
size_with_reads[node] = node.mpi_node.size + sum(
|
| 789 |
+
pred_buf.mpi_buffer.size_free for pred_buf in node.mpi_node.pred_buffers
|
| 790 |
+
)
|
| 791 |
+
for node in sorted(nodes, key=lambda n: (size_with_reads[n], n.mpi_node.index)):
|
| 792 |
+
visit(node)
|
| 793 |
+
|
| 794 |
+
return result
|
| 795 |
+
|
| 796 |
+
|
| 797 |
+
def validate_graph_acyclic(nodes: list[BaseSchedulerNode]) -> None:
|
| 798 |
+
"""
|
| 799 |
+
Validate that the graph is acyclic by checking predecessor relationships.
|
| 800 |
+
|
| 801 |
+
Raises:
|
| 802 |
+
RuntimeError: If a cycle is detected in the graph
|
| 803 |
+
"""
|
| 804 |
+
# DFS coloring scheme for cycle detection:
|
| 805 |
+
# WHITE (0): Node has not been visited yet
|
| 806 |
+
# GRAY (1): Node is currently being processed (in the recursion stack)
|
| 807 |
+
# BLACK (2): Node has been completely processed (finished exploring all its predecessors)
|
| 808 |
+
# A back edge (cycle) is detected when we encounter a GRAY node during DFS traversal
|
| 809 |
+
WHITE, GRAY, BLACK = 0, 1, 2
|
| 810 |
+
color = dict.fromkeys(nodes, WHITE)
|
| 811 |
+
path: list[BaseSchedulerNode] = [] # Track current DFS path
|
| 812 |
+
|
| 813 |
+
def dfs_visit(node: BaseSchedulerNode) -> None:
|
| 814 |
+
if color[node] == BLACK:
|
| 815 |
+
return
|
| 816 |
+
|
| 817 |
+
if color[node] == GRAY:
|
| 818 |
+
path.append(node)
|
| 819 |
+
path_info = " -> ".join([node.get_name() for node in path])
|
| 820 |
+
|
| 821 |
+
raise RuntimeError(
|
| 822 |
+
f"Cycle detected in memory planning graph"
|
| 823 |
+
f"Path containing cycle (i -> j: j is a dependency of i): {path_info} "
|
| 824 |
+
f"This indicates invalid dependency relationships in the scheduler graph"
|
| 825 |
+
)
|
| 826 |
+
|
| 827 |
+
color[node] = GRAY
|
| 828 |
+
path.append(node)
|
| 829 |
+
|
| 830 |
+
for pred_node in node.mpi_node.pred_nodes:
|
| 831 |
+
assert pred_node != node
|
| 832 |
+
dfs_visit(pred_node)
|
| 833 |
+
|
| 834 |
+
path.pop()
|
| 835 |
+
color[node] = BLACK
|
| 836 |
+
|
| 837 |
+
# Start DFS from all unvisited nodes
|
| 838 |
+
for node in nodes:
|
| 839 |
+
if color[node] == WHITE:
|
| 840 |
+
dfs_visit(node)
|
| 841 |
+
|
| 842 |
+
|
| 843 |
+
def validate_unique_buffer_names(
|
| 844 |
+
nodes: list[BaseSchedulerNode],
|
| 845 |
+
name_to_buf: dict[str, SchedulerBuffer],
|
| 846 |
+
name_to_freeable_input_buf: dict[str, FreeableInputBuffer],
|
| 847 |
+
) -> None:
|
| 848 |
+
"""
|
| 849 |
+
Validate that for each node's output buffer, the name_to_buf mapping is correct.
|
| 850 |
+
For each output buffer buf, we should have name_to_buf[buf.get_name()] == buf.
|
| 851 |
+
Also validate that no buffer names overlap with freeable input buffer names.
|
| 852 |
+
|
| 853 |
+
Raises:
|
| 854 |
+
RuntimeError: If buffer name mapping is incorrect or names overlap
|
| 855 |
+
"""
|
| 856 |
+
for node in nodes:
|
| 857 |
+
for buf in node.get_outputs():
|
| 858 |
+
buf_name = buf.get_name()
|
| 859 |
+
|
| 860 |
+
# Check if buffer name exists in the mapping
|
| 861 |
+
if buf_name not in name_to_buf:
|
| 862 |
+
raise RuntimeError(
|
| 863 |
+
f"{buf_name} from {node.get_name()} is not found in name_to_buf mapping."
|
| 864 |
+
f" This indicates a missing buffer mapping."
|
| 865 |
+
)
|
| 866 |
+
|
| 867 |
+
# Check if the mapping points to the correct buffer object
|
| 868 |
+
if name_to_buf[buf_name] != buf:
|
| 869 |
+
raise RuntimeError(
|
| 870 |
+
f"Buffer name mapping is incorrect for '{buf_name}'."
|
| 871 |
+
f"Expected name_to_buf['{buf_name}'] to be {buf.debug_str()}"
|
| 872 |
+
f"but got {name_to_buf[buf_name].debug_str()}"
|
| 873 |
+
f"This indicates some buffers share the same name"
|
| 874 |
+
)
|
| 875 |
+
|
| 876 |
+
# Check if buffer name conflicts with freeable input buffer names
|
| 877 |
+
if buf_name in name_to_freeable_input_buf:
|
| 878 |
+
raise RuntimeError(
|
| 879 |
+
f"Buffer name conflict detected: '{buf_name}' from node {node.get_name()} "
|
| 880 |
+
f"is also used as a freeable input buffer name. "
|
| 881 |
+
)
|
| 882 |
+
|
| 883 |
+
|
| 884 |
+
def prepare_planning_info(
|
| 885 |
+
nodes: list[BaseSchedulerNode],
|
| 886 |
+
name_to_buf: dict[str, SchedulerBuffer],
|
| 887 |
+
name_to_fused_node: dict[str, BaseSchedulerNode],
|
| 888 |
+
graph_inputs: OrderedSet[str],
|
| 889 |
+
graph_outputs: OrderedSet[str],
|
| 890 |
+
) -> tuple[int, dict[str, FreeableInputBuffer]]:
|
| 891 |
+
"""
|
| 892 |
+
Prepare planning info. As nodes are scheduled one at a time, these help
|
| 893 |
+
keep track of when a buffer can be freed, and when a node can be scheduled
|
| 894 |
+
|
| 895 |
+
Returns:
|
| 896 |
+
int: peak memory estimation
|
| 897 |
+
dict[str, FreeableInputBuffer]: name to freeable input buffer
|
| 898 |
+
"""
|
| 899 |
+
name_to_freeable_input_buf = get_freeable_input_buf(nodes, graph_inputs)
|
| 900 |
+
assign_memory_planning_info_for_scheduler_buffers(nodes, name_to_buf)
|
| 901 |
+
assign_memory_planning_info_for_scheduler_nodes(
|
| 902 |
+
nodes, name_to_fused_node, name_to_buf, name_to_freeable_input_buf
|
| 903 |
+
)
|
| 904 |
+
|
| 905 |
+
# the default
|
| 906 |
+
estimated_peak_memory, _ = estimate_peak_memory(
|
| 907 |
+
nodes, name_to_freeable_input_buf, graph_outputs
|
| 908 |
+
)
|
| 909 |
+
|
| 910 |
+
return estimated_peak_memory, name_to_freeable_input_buf
|
| 911 |
+
|
| 912 |
+
|
| 913 |
+
def reorder_for_peak_memory(
|
| 914 |
+
nodes: list[BaseSchedulerNode],
|
| 915 |
+
name_to_buf: dict[str, SchedulerBuffer],
|
| 916 |
+
name_to_fused_node: dict[str, BaseSchedulerNode],
|
| 917 |
+
graph_inputs: OrderedSet[str],
|
| 918 |
+
graph_outputs: OrderedSet[str],
|
| 919 |
+
methods: list[Callable[..., list[BaseSchedulerNode]]] = [ # noqa: B006
|
| 920 |
+
topological_sort_lpmf,
|
| 921 |
+
topological_sort_bfs,
|
| 922 |
+
topological_sort_dfs,
|
| 923 |
+
],
|
| 924 |
+
) -> list[BaseSchedulerNode]:
|
| 925 |
+
"""
|
| 926 |
+
Try a few heuristics based topological sort algorithms, and pick the one whose
|
| 927 |
+
resulting topological order has the lowest peak memory estimation.
|
| 928 |
+
"""
|
| 929 |
+
|
| 930 |
+
torch_log.info("Reordering for peak memory -- %d nodes", len(nodes))
|
| 931 |
+
|
| 932 |
+
estimated_peak_memory, name_to_freeable_input_buf = prepare_planning_info(
|
| 933 |
+
nodes,
|
| 934 |
+
name_to_buf,
|
| 935 |
+
name_to_fused_node,
|
| 936 |
+
graph_inputs,
|
| 937 |
+
graph_outputs,
|
| 938 |
+
)
|
| 939 |
+
|
| 940 |
+
# export graph for simulator if needed
|
| 941 |
+
if config.reorder_for_peak_memory_debug:
|
| 942 |
+
export_graph_for_simulator(
|
| 943 |
+
nodes,
|
| 944 |
+
name_to_freeable_input_buf,
|
| 945 |
+
name_to_fused_node,
|
| 946 |
+
graph_inputs,
|
| 947 |
+
graph_outputs,
|
| 948 |
+
)
|
| 949 |
+
|
| 950 |
+
# Validate planning info before proceeding with reordering
|
| 951 |
+
try:
|
| 952 |
+
validate_graph_acyclic(nodes)
|
| 953 |
+
validate_unique_buffer_names(nodes, name_to_buf, name_to_freeable_input_buf)
|
| 954 |
+
except RuntimeError:
|
| 955 |
+
torch_log.exception("Memory planning validation failed")
|
| 956 |
+
if not is_fbcode(): # TODO: remove after ensuring OSS side is safe
|
| 957 |
+
raise
|
| 958 |
+
|
| 959 |
+
# keep track of the peak memory estimates of different methods
|
| 960 |
+
peak_memory_diff_methods: list[PeakMemoryResult] = []
|
| 961 |
+
peak_memory_diff_methods.append(
|
| 962 |
+
PeakMemoryResult(nodes, estimated_peak_memory, "baseline")
|
| 963 |
+
)
|
| 964 |
+
torch_log.info("Baseline peak memory: %d", estimated_peak_memory)
|
| 965 |
+
|
| 966 |
+
# other methods
|
| 967 |
+
for method in methods:
|
| 968 |
+
try:
|
| 969 |
+
if method is topological_sort_lpmf:
|
| 970 |
+
order = method(
|
| 971 |
+
nodes, name_to_freeable_input_buf, name_to_buf, graph_outputs
|
| 972 |
+
)
|
| 973 |
+
else:
|
| 974 |
+
order = method(nodes)
|
| 975 |
+
assert len(order) == len(nodes)
|
| 976 |
+
peak_memory, _ = estimate_peak_memory(
|
| 977 |
+
order, name_to_freeable_input_buf, graph_outputs
|
| 978 |
+
)
|
| 979 |
+
peak_memory_diff_methods.append(
|
| 980 |
+
PeakMemoryResult(order, peak_memory, method.__name__)
|
| 981 |
+
)
|
| 982 |
+
torch_log.info("%s peak memory: %d", method.__name__, peak_memory)
|
| 983 |
+
except Exception:
|
| 984 |
+
torch_log.exception("Failed to reorder for %s", method.__name__)
|
| 985 |
+
if not is_fbcode(): # TODO: remove after ensuring OSS side is safe
|
| 986 |
+
raise
|
| 987 |
+
|
| 988 |
+
signpost_event(
|
| 989 |
+
category="inductor",
|
| 990 |
+
name="memory",
|
| 991 |
+
parameters={
|
| 992 |
+
"orm": {elem.method: elem.peak_memory for elem in peak_memory_diff_methods},
|
| 993 |
+
},
|
| 994 |
+
)
|
| 995 |
+
|
| 996 |
+
# get the optimal one
|
| 997 |
+
best_result = min(peak_memory_diff_methods, key=lambda x: x.peak_memory)
|
| 998 |
+
|
| 999 |
+
return best_result.order
|
| 1000 |
+
|
| 1001 |
+
|
| 1002 |
+
def export_graph_for_simulator(
|
| 1003 |
+
nodes: list[BaseSchedulerNode],
|
| 1004 |
+
name_to_freeable_input_buf: dict[str, FreeableInputBuffer],
|
| 1005 |
+
name_to_fused_node: dict[str, BaseSchedulerNode],
|
| 1006 |
+
graph_inputs: OrderedSet[str],
|
| 1007 |
+
graph_outputs: OrderedSet[str],
|
| 1008 |
+
) -> None:
|
| 1009 |
+
"""
|
| 1010 |
+
This is for debugging purposes. It will dump a json file that records graph information.
|
| 1011 |
+
The graph can then be used in a simulator: https://fburl.com/code/3l3d3qi4
|
| 1012 |
+
"""
|
| 1013 |
+
|
| 1014 |
+
class ORMBuffer(TypedDict):
|
| 1015 |
+
name: str
|
| 1016 |
+
size_alloc: int
|
| 1017 |
+
size_free: int
|
| 1018 |
+
size: int # for backward compatibility
|
| 1019 |
+
is_input: bool
|
| 1020 |
+
is_output: bool
|
| 1021 |
+
deps: list[str]
|
| 1022 |
+
unmet_deps: list[str]
|
| 1023 |
+
|
| 1024 |
+
class ORMNode(TypedDict):
|
| 1025 |
+
name: str
|
| 1026 |
+
buffer_names: list[str]
|
| 1027 |
+
|
| 1028 |
+
class ORMGraph(TypedDict):
|
| 1029 |
+
nodes: list[ORMNode]
|
| 1030 |
+
buffers: list[ORMBuffer]
|
| 1031 |
+
|
| 1032 |
+
orm_buffers: list[ORMBuffer] = []
|
| 1033 |
+
orm_nodes: list[ORMNode] = []
|
| 1034 |
+
|
| 1035 |
+
# get orm buffers for freeable input buffers
|
| 1036 |
+
for buf_name, input_buf in name_to_freeable_input_buf.items():
|
| 1037 |
+
orm_buf_input_buffer: ORMBuffer = {
|
| 1038 |
+
"name": buf_name,
|
| 1039 |
+
"size_alloc": input_buf.mpi_buffer.size_free,
|
| 1040 |
+
"size_free": input_buf.mpi_buffer.size_free,
|
| 1041 |
+
"size": input_buf.mpi_buffer.size_free,
|
| 1042 |
+
"is_input": True,
|
| 1043 |
+
"is_output": buf_name in graph_outputs,
|
| 1044 |
+
"deps": [],
|
| 1045 |
+
"unmet_deps": [],
|
| 1046 |
+
}
|
| 1047 |
+
orm_buffers.append(orm_buf_input_buffer)
|
| 1048 |
+
|
| 1049 |
+
# get orm buffers for scheduler buffers
|
| 1050 |
+
name_to_buf: dict[str, SchedulerBuffer] = {
|
| 1051 |
+
buf.get_name(): buf for node in nodes for buf in node.get_outputs()
|
| 1052 |
+
} # need to reassign due to probably node pruning
|
| 1053 |
+
for buf_name, sched_buf in name_to_buf.items():
|
| 1054 |
+
if sched_buf.defining_op is None:
|
| 1055 |
+
continue
|
| 1056 |
+
deps = [
|
| 1057 |
+
pred_buf.get_name()
|
| 1058 |
+
for pred_buf in name_to_fused_node[
|
| 1059 |
+
sched_buf.defining_op.get_name()
|
| 1060 |
+
].mpi_node.pred_buffers
|
| 1061 |
+
]
|
| 1062 |
+
orm_buf_scheduler_buffer: ORMBuffer = {
|
| 1063 |
+
"name": buf_name,
|
| 1064 |
+
"size_alloc": sched_buf.mpi_buffer.size_alloc,
|
| 1065 |
+
"size_free": sched_buf.mpi_buffer.size_free,
|
| 1066 |
+
"size": sched_buf.mpi_buffer.size_free,
|
| 1067 |
+
"is_input": False,
|
| 1068 |
+
"is_output": buf_name in graph_outputs,
|
| 1069 |
+
"deps": deps,
|
| 1070 |
+
"unmet_deps": [
|
| 1071 |
+
buf_name for buf_name in deps if buf_name not in graph_inputs
|
| 1072 |
+
],
|
| 1073 |
+
}
|
| 1074 |
+
orm_buffers.append(orm_buf_scheduler_buffer)
|
| 1075 |
+
|
| 1076 |
+
# get orm nodes
|
| 1077 |
+
for node in nodes:
|
| 1078 |
+
orm_node: ORMNode = {
|
| 1079 |
+
"name": node.get_name(),
|
| 1080 |
+
"buffer_names": list(node.get_buffer_names()),
|
| 1081 |
+
}
|
| 1082 |
+
orm_nodes.append(orm_node)
|
| 1083 |
+
|
| 1084 |
+
# create the graph object
|
| 1085 |
+
g: ORMGraph = {
|
| 1086 |
+
"nodes": orm_nodes,
|
| 1087 |
+
"buffers": orm_buffers,
|
| 1088 |
+
}
|
| 1089 |
+
|
| 1090 |
+
# dump the graph
|
| 1091 |
+
import json
|
| 1092 |
+
import os
|
| 1093 |
+
|
| 1094 |
+
import torch
|
| 1095 |
+
from functorch.compile import get_graph_being_compiled
|
| 1096 |
+
|
| 1097 |
+
name = os.path.splitext(get_graph_being_compiled())[0] + "_fused"
|
| 1098 |
+
|
| 1099 |
+
g_str = json.dumps(g, indent=2)
|
| 1100 |
+
|
| 1101 |
+
torch._logging.trace_structured(
|
| 1102 |
+
"artifact",
|
| 1103 |
+
metadata_fn=lambda: {
|
| 1104 |
+
"name": name,
|
| 1105 |
+
"encoding": "string",
|
| 1106 |
+
},
|
| 1107 |
+
payload_fn=lambda: g_str,
|
| 1108 |
+
)
|
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/metrics.py
ADDED
|
@@ -0,0 +1,484 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import csv
|
| 4 |
+
import dataclasses
|
| 5 |
+
import inspect
|
| 6 |
+
import os
|
| 7 |
+
import re
|
| 8 |
+
from dataclasses import dataclass
|
| 9 |
+
from functools import lru_cache
|
| 10 |
+
from typing import TYPE_CHECKING
|
| 11 |
+
|
| 12 |
+
from torch._inductor import config
|
| 13 |
+
from torch._inductor.utils import get_benchmark_name
|
| 14 |
+
from torch.utils._ordered_set import OrderedSet
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
# Prevent circular import
|
| 18 |
+
if TYPE_CHECKING:
|
| 19 |
+
from collections.abc import Callable
|
| 20 |
+
|
| 21 |
+
from torch._inductor.runtime.triton_compat import Config
|
| 22 |
+
from torch._inductor.scheduler import BaseSchedulerNode
|
| 23 |
+
|
| 24 |
+
# counter for tracking how many kernels have been generated
|
| 25 |
+
generated_kernel_count = 0
|
| 26 |
+
generated_cpp_vec_kernel_count = 0
|
| 27 |
+
num_bytes_accessed = 0
|
| 28 |
+
nodes_num_elem: list[
|
| 29 |
+
tuple[
|
| 30 |
+
BaseSchedulerNode,
|
| 31 |
+
int,
|
| 32 |
+
]
|
| 33 |
+
] = []
|
| 34 |
+
node_runtimes: list[tuple[BaseSchedulerNode, float]] = []
|
| 35 |
+
|
| 36 |
+
# counters for tracking fusions
|
| 37 |
+
ir_nodes_pre_fusion = 0
|
| 38 |
+
|
| 39 |
+
# counters for tracking to_dtype inserted
|
| 40 |
+
cpp_to_dtype_count = 0
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
@dataclasses.dataclass
|
| 44 |
+
class CppOuterLoopFusedCount:
|
| 45 |
+
inner_kernel_number: int
|
| 46 |
+
local_buffer_number: int = 0
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
# The length counts the number of outer loop fusions.
|
| 50 |
+
cpp_outer_loop_fused_inner_counts: list[CppOuterLoopFusedCount] = []
|
| 51 |
+
|
| 52 |
+
num_comprehensive_padding = 0
|
| 53 |
+
num_matches_for_scatter_upon_const_tensor = 0
|
| 54 |
+
|
| 55 |
+
num_loop_reordering = 0
|
| 56 |
+
num_auto_chunking: int = 0
|
| 57 |
+
|
| 58 |
+
# counter for parallel reduction.
|
| 59 |
+
parallel_reduction_count = 0
|
| 60 |
+
|
| 61 |
+
codegen_mix_order_reduction = 0
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
# reset all counters
|
| 65 |
+
def reset() -> None:
|
| 66 |
+
global generated_kernel_count
|
| 67 |
+
global generated_cpp_vec_kernel_count
|
| 68 |
+
global num_bytes_accessed, nodes_num_elem
|
| 69 |
+
global ir_nodes_pre_fusion
|
| 70 |
+
global cpp_to_dtype_count
|
| 71 |
+
global cpp_outer_loop_fused_inner_counts
|
| 72 |
+
global num_comprehensive_padding
|
| 73 |
+
global num_matches_for_scatter_upon_const_tensor
|
| 74 |
+
global num_loop_reordering
|
| 75 |
+
global parallel_reduction_count
|
| 76 |
+
global codegen_mix_order_reduction
|
| 77 |
+
global num_auto_chunking
|
| 78 |
+
|
| 79 |
+
generated_kernel_count = 0
|
| 80 |
+
generated_cpp_vec_kernel_count = 0
|
| 81 |
+
num_bytes_accessed = 0
|
| 82 |
+
nodes_num_elem.clear()
|
| 83 |
+
node_runtimes.clear()
|
| 84 |
+
ir_nodes_pre_fusion = 0
|
| 85 |
+
cpp_to_dtype_count = 0
|
| 86 |
+
cpp_outer_loop_fused_inner_counts.clear()
|
| 87 |
+
num_comprehensive_padding = 0
|
| 88 |
+
num_matches_for_scatter_upon_const_tensor = 0
|
| 89 |
+
num_loop_reordering = 0
|
| 90 |
+
parallel_reduction_count = 0
|
| 91 |
+
codegen_mix_order_reduction = 0
|
| 92 |
+
num_auto_chunking = 0
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
@dataclass
|
| 96 |
+
class CachedMetricsDeltas:
|
| 97 |
+
"""
|
| 98 |
+
The subset of metrics we want update across cache hits, e.g., the
|
| 99 |
+
FxGraphCache.
|
| 100 |
+
"""
|
| 101 |
+
|
| 102 |
+
generated_kernel_count: int
|
| 103 |
+
generated_cpp_vec_kernel_count: int
|
| 104 |
+
ir_nodes_pre_fusion: int
|
| 105 |
+
cpp_to_dtype_count: int
|
| 106 |
+
num_bytes_accessed: int
|
| 107 |
+
num_matches_for_scatter_upon_const_tensor: int
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def get_metric_fields() -> list[str]:
|
| 111 |
+
return [field.name for field in dataclasses.fields(CachedMetricsDeltas)]
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
class CachedMetricsHelper:
|
| 115 |
+
"""
|
| 116 |
+
A helper class to help calculate and apply counter deltas for those
|
| 117 |
+
metrics we want to save with cache entries (e.g., FxGraphCache) and
|
| 118 |
+
apply on a cache hit.
|
| 119 |
+
"""
|
| 120 |
+
|
| 121 |
+
def __init__(self) -> None:
|
| 122 |
+
self.cached_metrics = {}
|
| 123 |
+
for metric in get_metric_fields():
|
| 124 |
+
self.cached_metrics[metric] = globals()[metric]
|
| 125 |
+
|
| 126 |
+
def get_deltas(self) -> CachedMetricsDeltas:
|
| 127 |
+
delta_metrics = {}
|
| 128 |
+
for metric in get_metric_fields():
|
| 129 |
+
delta_metrics[metric] = globals()[metric] - self.cached_metrics[metric]
|
| 130 |
+
|
| 131 |
+
return CachedMetricsDeltas(**delta_metrics)
|
| 132 |
+
|
| 133 |
+
@staticmethod
|
| 134 |
+
def apply_deltas(delta: CachedMetricsDeltas) -> None:
|
| 135 |
+
for metric in get_metric_fields():
|
| 136 |
+
globals()[metric] += getattr(delta, metric)
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
REGISTERED_METRIC_TABLES: dict[str, MetricTable] = {}
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
@dataclass
|
| 143 |
+
class MetricTable:
|
| 144 |
+
table_name: str
|
| 145 |
+
column_names: list[str]
|
| 146 |
+
|
| 147 |
+
num_rows_added: int = 0
|
| 148 |
+
|
| 149 |
+
def add_row(self, row_fn: Callable[[], dict[str, str | float | None]]) -> None:
|
| 150 |
+
if self.table_name not in enabled_metric_tables():
|
| 151 |
+
return
|
| 152 |
+
|
| 153 |
+
row_dict = row_fn()
|
| 154 |
+
assert len(self.column_names) == len(row_dict), (
|
| 155 |
+
f"{len(self.column_names)} v.s. {len(row_dict)}"
|
| 156 |
+
)
|
| 157 |
+
assert OrderedSet(self.column_names) == OrderedSet(row_dict.keys()), (
|
| 158 |
+
f"{OrderedSet(self.column_names)} v.s. {OrderedSet(row_dict.keys())}"
|
| 159 |
+
)
|
| 160 |
+
|
| 161 |
+
bn = get_benchmark_name()
|
| 162 |
+
# assert bn is not None
|
| 163 |
+
row = [bn] + [row_dict[column_name] for column_name in self.column_names]
|
| 164 |
+
assert all(isinstance(i, (str, float, type(None))) for i in row)
|
| 165 |
+
self._write_row(row)
|
| 166 |
+
|
| 167 |
+
def output_filename(self) -> str:
|
| 168 |
+
return f"metric_table_{self.table_name}.csv"
|
| 169 |
+
|
| 170 |
+
def write_header(self) -> None:
|
| 171 |
+
filename = self.output_filename()
|
| 172 |
+
with open(filename, "w") as fd:
|
| 173 |
+
writer = csv.writer(fd, lineterminator="\n")
|
| 174 |
+
writer.writerow(["model_name"] + self.column_names)
|
| 175 |
+
|
| 176 |
+
def _write_row(self, row: list[str | float | None]) -> None:
|
| 177 |
+
filename = self.output_filename()
|
| 178 |
+
if self.num_rows_added == 0 and not os.path.exists(filename):
|
| 179 |
+
self.write_header()
|
| 180 |
+
|
| 181 |
+
self.num_rows_added += 1
|
| 182 |
+
|
| 183 |
+
for idx, orig_val in enumerate(row):
|
| 184 |
+
if isinstance(orig_val, float):
|
| 185 |
+
new_val = f"{orig_val:.6f}"
|
| 186 |
+
elif orig_val is None:
|
| 187 |
+
new_val = ""
|
| 188 |
+
else:
|
| 189 |
+
new_val = orig_val
|
| 190 |
+
row[idx] = new_val
|
| 191 |
+
|
| 192 |
+
with open(filename, "a") as fd:
|
| 193 |
+
writer = csv.writer(fd, lineterminator="\n")
|
| 194 |
+
writer.writerow(row)
|
| 195 |
+
|
| 196 |
+
@staticmethod
|
| 197 |
+
def register_table(name: str, column_names: list[str]) -> None:
|
| 198 |
+
table = MetricTable(name, column_names)
|
| 199 |
+
REGISTERED_METRIC_TABLES[name] = table
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
MetricTable.register_table(
|
| 203 |
+
"slow_fusion",
|
| 204 |
+
[
|
| 205 |
+
"kernel1_path",
|
| 206 |
+
"kernel1_latency",
|
| 207 |
+
"kernel2_path",
|
| 208 |
+
"kernel2_latency",
|
| 209 |
+
"fused_kernel_path",
|
| 210 |
+
"fused_kernel_latency",
|
| 211 |
+
"slow_down_ratio",
|
| 212 |
+
],
|
| 213 |
+
)
|
| 214 |
+
|
| 215 |
+
# track the fusion statistics for each graph
|
| 216 |
+
MetricTable.register_table(
|
| 217 |
+
"graph_stats",
|
| 218 |
+
[
|
| 219 |
+
"graph_id",
|
| 220 |
+
"num_nodes_before_fusion",
|
| 221 |
+
"num_nodes_after_fusion",
|
| 222 |
+
],
|
| 223 |
+
)
|
| 224 |
+
|
| 225 |
+
# track the perf difference between persistent reduction and non-persistent
|
| 226 |
+
# reductions
|
| 227 |
+
MetricTable.register_table(
|
| 228 |
+
"persistent_red_perf",
|
| 229 |
+
[
|
| 230 |
+
"kernel0_path",
|
| 231 |
+
"kernel1_path",
|
| 232 |
+
"kernel2_path",
|
| 233 |
+
"kernel3_path",
|
| 234 |
+
"kernel0_latency",
|
| 235 |
+
"kernel1_latency",
|
| 236 |
+
"kernel2_latency",
|
| 237 |
+
"kernel3_latency",
|
| 238 |
+
"size_hints",
|
| 239 |
+
"reduction_hint",
|
| 240 |
+
],
|
| 241 |
+
)
|
| 242 |
+
|
| 243 |
+
# Log the fusion failures due to indexing mismatch
|
| 244 |
+
MetricTable.register_table(
|
| 245 |
+
"fusion_failure_due_to_indexing_mismatch",
|
| 246 |
+
[
|
| 247 |
+
"pre_grad_graph_id",
|
| 248 |
+
"post_grad_graph_id",
|
| 249 |
+
"node1_name",
|
| 250 |
+
"node2_name",
|
| 251 |
+
"node1_debug_str",
|
| 252 |
+
"node2_debug_str",
|
| 253 |
+
"common_buffer_names",
|
| 254 |
+
"failure_reason",
|
| 255 |
+
],
|
| 256 |
+
)
|
| 257 |
+
|
| 258 |
+
# Log metadata for pointwise/reduction kernels. E.g., model name, kernel path, numel, rnumel, reduction hint
|
| 259 |
+
MetricTable.register_table(
|
| 260 |
+
"kernel_metadata",
|
| 261 |
+
[
|
| 262 |
+
"kernel_name",
|
| 263 |
+
"kernel_path",
|
| 264 |
+
"kernel_category", # pointwise/reduction/foreach etc.
|
| 265 |
+
"size_hints",
|
| 266 |
+
"reduction_hint",
|
| 267 |
+
"line_of_code",
|
| 268 |
+
"num_load",
|
| 269 |
+
"num_store",
|
| 270 |
+
"num_for_loop",
|
| 271 |
+
"num_atomic_add",
|
| 272 |
+
"num_args",
|
| 273 |
+
# xyz numel can be different to size_hints since size_hints are rounded
|
| 274 |
+
# up to the nearest power of 2.
|
| 275 |
+
# Inductor kernel will burn in the xyz numel in kernel code for static
|
| 276 |
+
# shape kernels.
|
| 277 |
+
# Logging them will be helpful to find unaligned shape for reduction
|
| 278 |
+
"xnumel",
|
| 279 |
+
"ynumel",
|
| 280 |
+
"rnumel",
|
| 281 |
+
"kernel_args_num_gb",
|
| 282 |
+
],
|
| 283 |
+
)
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
def _parse_kernel_fn_code(kernel_module_code: str) -> str:
|
| 287 |
+
"""
|
| 288 |
+
The kernel_module_code is the python module that contains kernel function code.
|
| 289 |
+
kernel function is the proper triton kernel function annotated with
|
| 290 |
+
@triton.jit
|
| 291 |
+
"""
|
| 292 |
+
from .codecache import PyCodeCache
|
| 293 |
+
from .wrapper_benchmark import get_triton_kernel
|
| 294 |
+
|
| 295 |
+
mod = PyCodeCache.load(kernel_module_code)
|
| 296 |
+
kernel = get_triton_kernel(mod)
|
| 297 |
+
# kernel is a CachingAutotune; kernel.fn is the JITFunction;
|
| 298 |
+
# kernel.fn.fn is the function being decorate by triton.jit
|
| 299 |
+
return inspect.getsource(kernel.fn.fn)
|
| 300 |
+
|
| 301 |
+
|
| 302 |
+
def _parse_kernel_line_of_code(proper_kernel_fn_code: str) -> int:
|
| 303 |
+
"""
|
| 304 |
+
Return the line of code for the kernel excluding the decorators.
|
| 305 |
+
"""
|
| 306 |
+
return len(proper_kernel_fn_code.splitlines())
|
| 307 |
+
|
| 308 |
+
|
| 309 |
+
def _parse_size_hints(kernel_module_code: str, kernel_category: str) -> str | None:
|
| 310 |
+
if kernel_category == "foreach":
|
| 311 |
+
# foreach kernel does not have size_hints
|
| 312 |
+
return None
|
| 313 |
+
m = re.search(r"size_hints=(\[[0-9, ]*\]),", kernel_module_code)
|
| 314 |
+
assert m, "size_hints missing!"
|
| 315 |
+
return m.group(1)
|
| 316 |
+
|
| 317 |
+
|
| 318 |
+
def _parse_reduction_hint(kernel_category: str, kernel_module_code: str) -> str | None:
|
| 319 |
+
if kernel_category not in ("reduction", "persistent_reduction"):
|
| 320 |
+
return None
|
| 321 |
+
m = re.search(r"reduction_hint=ReductionHint\.(\w*),", kernel_module_code)
|
| 322 |
+
assert m, "reduction_hint not found in kernel source code!"
|
| 323 |
+
return m.group(1)
|
| 324 |
+
|
| 325 |
+
|
| 326 |
+
def _count_pattern(proper_kernel_fn_code: str, pattern: str) -> int:
|
| 327 |
+
return proper_kernel_fn_code.count(pattern)
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
def _count_args(proper_kernel_fn_code: str) -> int:
|
| 331 |
+
def_line = proper_kernel_fn_code.splitlines()[0]
|
| 332 |
+
assert def_line.startswith("def ")
|
| 333 |
+
start_idx = def_line.index("(")
|
| 334 |
+
end_idx = def_line.index("):")
|
| 335 |
+
decl_csv = def_line[start_idx + 1 : end_idx]
|
| 336 |
+
comps = decl_csv.split(",")
|
| 337 |
+
return len(comps)
|
| 338 |
+
|
| 339 |
+
|
| 340 |
+
def _parse_proper_kernel_fn_code(kernel_fn_code: str) -> str:
|
| 341 |
+
"""
|
| 342 |
+
Skip decorators.
|
| 343 |
+
"""
|
| 344 |
+
start_pos = kernel_fn_code.index("def ")
|
| 345 |
+
return kernel_fn_code[start_pos:]
|
| 346 |
+
|
| 347 |
+
|
| 348 |
+
def _parse_numel(proper_kernel_fn_code: str, numel_arg_name: str) -> int | None:
|
| 349 |
+
m = re.search(f"{numel_arg_name} = ([\\d]+)", proper_kernel_fn_code)
|
| 350 |
+
if m:
|
| 351 |
+
return int(m.group(1))
|
| 352 |
+
else:
|
| 353 |
+
return None
|
| 354 |
+
|
| 355 |
+
|
| 356 |
+
def _parse_kernel_args_num_gb(
|
| 357 |
+
kernel_fn_code: str, kernel_category: str
|
| 358 |
+
) -> float | None:
|
| 359 |
+
"""
|
| 360 |
+
inductor meta looks like:
|
| 361 |
+
inductor_meta={... 'mutated_arg_names': [], 'no_x_dim': False, 'kernel_num_gb': 2.0},
|
| 362 |
+
"""
|
| 363 |
+
m = re.search(r".kernel_num_gb.:\s*([0-9.]+)", kernel_fn_code)
|
| 364 |
+
if m:
|
| 365 |
+
return float(m.group(1))
|
| 366 |
+
else:
|
| 367 |
+
"""
|
| 368 |
+
There are a few cases that kernel_num_gdb field can be missing:
|
| 369 |
+
1. the field will be missing if config.benchmark_kernel and
|
| 370 |
+
config.profile_bandwidth are false
|
| 371 |
+
2. even if config.benchmark_kernel or config.profile_bandwidth is true.
|
| 372 |
+
foreach kernel does not have kernel_num_gb field in the metadata
|
| 373 |
+
"""
|
| 374 |
+
return None
|
| 375 |
+
|
| 376 |
+
|
| 377 |
+
def log_kernel_metadata(
|
| 378 |
+
kernel_name: str, kernel_path: str, kernel_module_code: str
|
| 379 |
+
) -> None:
|
| 380 |
+
"""
|
| 381 |
+
An utility to log kernel metadata. We may parse metadata from kernel source code here.
|
| 382 |
+
|
| 383 |
+
It's fine to parse the generated kernel code here since the logging is
|
| 384 |
+
disabled by default. It would hurt compilation time.
|
| 385 |
+
"""
|
| 386 |
+
from .wrapper_benchmark import get_kernel_category_by_source_code
|
| 387 |
+
|
| 388 |
+
kernel_category = get_kernel_category_by_source_code(kernel_module_code)
|
| 389 |
+
reduction_hint = _parse_reduction_hint(kernel_category, kernel_module_code)
|
| 390 |
+
size_hints = _parse_size_hints(kernel_module_code, kernel_category)
|
| 391 |
+
kernel_fn_code = _parse_kernel_fn_code(kernel_module_code)
|
| 392 |
+
|
| 393 |
+
proper_kernel_fn_code = _parse_proper_kernel_fn_code(kernel_fn_code)
|
| 394 |
+
|
| 395 |
+
# the line of code excluding the decortors
|
| 396 |
+
kernel_line_of_code = _parse_kernel_line_of_code(proper_kernel_fn_code)
|
| 397 |
+
|
| 398 |
+
get_metric_table("kernel_metadata").add_row(
|
| 399 |
+
lambda: {
|
| 400 |
+
"kernel_name": kernel_name,
|
| 401 |
+
"kernel_path": kernel_path,
|
| 402 |
+
"kernel_category": kernel_category,
|
| 403 |
+
"size_hints": size_hints,
|
| 404 |
+
"reduction_hint": reduction_hint,
|
| 405 |
+
"line_of_code": kernel_line_of_code,
|
| 406 |
+
"num_load": _count_pattern(proper_kernel_fn_code, "tl.load"),
|
| 407 |
+
"num_store": _count_pattern(proper_kernel_fn_code, "tl.store"),
|
| 408 |
+
"num_for_loop": _count_pattern(proper_kernel_fn_code, "for "),
|
| 409 |
+
"num_atomic_add": _count_pattern(proper_kernel_fn_code, "tl.atomic_add"),
|
| 410 |
+
"num_args": _count_args(proper_kernel_fn_code),
|
| 411 |
+
"xnumel": _parse_numel(proper_kernel_fn_code, "xnumel"),
|
| 412 |
+
"ynumel": _parse_numel(proper_kernel_fn_code, "ynumel"),
|
| 413 |
+
"rnumel": _parse_numel(proper_kernel_fn_code, "rnumel"),
|
| 414 |
+
"kernel_args_num_gb": _parse_kernel_args_num_gb(
|
| 415 |
+
kernel_fn_code, kernel_category
|
| 416 |
+
),
|
| 417 |
+
}
|
| 418 |
+
)
|
| 419 |
+
|
| 420 |
+
|
| 421 |
+
def purge_old_log_files() -> None:
|
| 422 |
+
"""
|
| 423 |
+
Purge the old log file at the beginning when the benchmark script runs.
|
| 424 |
+
Should do it in the parent process rather than the child processes running
|
| 425 |
+
each individual model.
|
| 426 |
+
"""
|
| 427 |
+
for name, table in REGISTERED_METRIC_TABLES.items():
|
| 428 |
+
if name in enabled_metric_tables():
|
| 429 |
+
filename = table.output_filename()
|
| 430 |
+
if os.path.exists(filename):
|
| 431 |
+
os.unlink(filename)
|
| 432 |
+
|
| 433 |
+
table.write_header()
|
| 434 |
+
|
| 435 |
+
|
| 436 |
+
def enabled_metric_tables() -> OrderedSet[str]:
|
| 437 |
+
return enabled_metric_tables_impl(config.enabled_metric_tables)
|
| 438 |
+
|
| 439 |
+
|
| 440 |
+
@lru_cache
|
| 441 |
+
def enabled_metric_tables_impl(config_str: str) -> OrderedSet[str]:
|
| 442 |
+
enabled: OrderedSet[str] = OrderedSet()
|
| 443 |
+
for name in config_str.split(","):
|
| 444 |
+
name = name.strip()
|
| 445 |
+
if not name:
|
| 446 |
+
continue
|
| 447 |
+
assert name in REGISTERED_METRIC_TABLES, (
|
| 448 |
+
f"Metric table name {name} is not registered"
|
| 449 |
+
)
|
| 450 |
+
enabled.add(name)
|
| 451 |
+
return enabled
|
| 452 |
+
|
| 453 |
+
|
| 454 |
+
def is_metric_table_enabled(name: str) -> bool:
|
| 455 |
+
return name in enabled_metric_tables()
|
| 456 |
+
|
| 457 |
+
|
| 458 |
+
def get_metric_table(name: str) -> MetricTable:
|
| 459 |
+
assert name in REGISTERED_METRIC_TABLES, f"Metric table {name} is not defined"
|
| 460 |
+
return REGISTERED_METRIC_TABLES[name]
|
| 461 |
+
|
| 462 |
+
|
| 463 |
+
MetricTable.register_table(
|
| 464 |
+
"kernel_autotune",
|
| 465 |
+
[
|
| 466 |
+
"kernel_path",
|
| 467 |
+
"kernel_name",
|
| 468 |
+
"triton_config",
|
| 469 |
+
"latency_ms",
|
| 470 |
+
],
|
| 471 |
+
)
|
| 472 |
+
|
| 473 |
+
|
| 474 |
+
def log_kernel_autotune_result(
|
| 475 |
+
kernel_path: str, kernel_name: str, config: Config, latency: float
|
| 476 |
+
) -> None:
|
| 477 |
+
get_metric_table("kernel_autotune").add_row(
|
| 478 |
+
lambda: {
|
| 479 |
+
"kernel_path": kernel_path,
|
| 480 |
+
"kernel_name": kernel_name,
|
| 481 |
+
"triton_config": str(config),
|
| 482 |
+
"latency_ms": latency,
|
| 483 |
+
}
|
| 484 |
+
)
|
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/mkldnn_ir.py
ADDED
|
@@ -0,0 +1,1361 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# mypy: allow-untyped-defs
|
| 2 |
+
from collections.abc import Sequence
|
| 3 |
+
from typing import Any, Optional
|
| 4 |
+
|
| 5 |
+
import sympy
|
| 6 |
+
|
| 7 |
+
import torch
|
| 8 |
+
from torch._prims_common import make_channels_last_strides_for, StrideType
|
| 9 |
+
from torch.utils._ordered_set import OrderedSet
|
| 10 |
+
|
| 11 |
+
from .ir import (
|
| 12 |
+
ExternKernelAlloc,
|
| 13 |
+
FixedLayout,
|
| 14 |
+
FlexibleLayout,
|
| 15 |
+
get_device_type,
|
| 16 |
+
ir_node_to_tensor,
|
| 17 |
+
IRNode,
|
| 18 |
+
is_contiguous_storage_and_layout,
|
| 19 |
+
Layout,
|
| 20 |
+
may_convert_to_optional,
|
| 21 |
+
MultiOutput,
|
| 22 |
+
MultiOutputLayout,
|
| 23 |
+
MutationOutput,
|
| 24 |
+
NoneLayout,
|
| 25 |
+
TensorBox,
|
| 26 |
+
)
|
| 27 |
+
from .utils import convert_shape_to_inductor, pad_listlike, SUPPORTED_MKLDNN_DEVICES
|
| 28 |
+
from .virtualized import V
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def _prepare_convolution_fusion_create(
|
| 32 |
+
cls,
|
| 33 |
+
x: "TensorBox",
|
| 34 |
+
weight: "TensorBox",
|
| 35 |
+
bias: "TensorBox",
|
| 36 |
+
padding: Sequence[int],
|
| 37 |
+
stride: Sequence[int],
|
| 38 |
+
dilation: Sequence[int],
|
| 39 |
+
groups: int,
|
| 40 |
+
transposed: bool = False,
|
| 41 |
+
output_padding: Sequence[int] | None = None,
|
| 42 |
+
quantize_args: list["TensorBox"] | None = None,
|
| 43 |
+
other: Optional["TensorBox"] = None,
|
| 44 |
+
):
|
| 45 |
+
"""
|
| 46 |
+
This function is a helper function to prepare inputs, layout and constant args
|
| 47 |
+
for convolution post-op fusion's create function, including deciding the output
|
| 48 |
+
layout (channels first or channels last), realizing inputs and make them etc. The
|
| 49 |
+
function only supports the CPU/XPU device since conv post-op fusion kernel is only
|
| 50 |
+
supported on CPU/XPU right now.
|
| 51 |
+
"""
|
| 52 |
+
|
| 53 |
+
# Port from aten/src/ATen/native/ConvUtils.h: _conv_input_size
|
| 54 |
+
def _conv_input_size(
|
| 55 |
+
output_size, weight_size, padding, output_padding, stride, dilation, groups
|
| 56 |
+
):
|
| 57 |
+
assert len(output_size) == len(weight_size), "Expect input dim == weight dim"
|
| 58 |
+
dim = len(output_size)
|
| 59 |
+
assert dim > 2, "Expect input dim > 2"
|
| 60 |
+
|
| 61 |
+
BATCH_DIM = 0
|
| 62 |
+
WEIGHT_INPUT_CHANNELS_DIM = 1
|
| 63 |
+
input_size = []
|
| 64 |
+
input_size.append(output_size[BATCH_DIM])
|
| 65 |
+
input_size.append(weight_size[WEIGHT_INPUT_CHANNELS_DIM] * groups)
|
| 66 |
+
for d in range(2, dim):
|
| 67 |
+
kernel = (weight_size[d] - 1) * dilation[d - 2] + 1
|
| 68 |
+
input_size_d = (
|
| 69 |
+
(output_size[d] - 1) * stride[d - 2]
|
| 70 |
+
- (padding[d - 2] * 2)
|
| 71 |
+
+ kernel
|
| 72 |
+
+ output_padding[d - 2]
|
| 73 |
+
)
|
| 74 |
+
input_size.append(input_size_d)
|
| 75 |
+
return list(map(int, input_size))
|
| 76 |
+
|
| 77 |
+
# Port from aten/src/ATen/native/ConvUtils.h: _conv_output_size
|
| 78 |
+
def _conv_output_size(input_size, weight_size, padding, stride, dilation=None):
|
| 79 |
+
has_dilation = dilation is not None
|
| 80 |
+
dim = len(input_size)
|
| 81 |
+
output_size = []
|
| 82 |
+
output_size.append(input_size[0])
|
| 83 |
+
output_size.append(weight_size[0])
|
| 84 |
+
for d in range(2, dim):
|
| 85 |
+
# pyrefly: ignore [unsupported-operation]
|
| 86 |
+
dilation_ = dilation[d - 2] if has_dilation else 1
|
| 87 |
+
kernel = dilation_ * (weight_size[d] - 1) + 1
|
| 88 |
+
output_size_d = (input_size[d] + (2 * padding[d - 2]) - kernel) // stride[
|
| 89 |
+
d - 2
|
| 90 |
+
] + 1
|
| 91 |
+
output_size.append(output_size_d)
|
| 92 |
+
return output_size
|
| 93 |
+
|
| 94 |
+
# The size of prepacked_weight is the prepacked weight size of deconv:
|
| 95 |
+
# Groups > 1: [g*o, i/g, ...]
|
| 96 |
+
# Groups == 1: [o, i, ...]
|
| 97 |
+
# Returns original weight size in [i, o, ...]
|
| 98 |
+
def _original_deconv_weight_size(
|
| 99 |
+
prepacked_weight,
|
| 100 |
+
groups,
|
| 101 |
+
):
|
| 102 |
+
prepacked_weight_size = prepacked_weight.size()
|
| 103 |
+
dim = len(prepacked_weight_size)
|
| 104 |
+
assert dim > 2, "Expect weight dim > 2"
|
| 105 |
+
if groups > 1:
|
| 106 |
+
weight_size = []
|
| 107 |
+
weight_size.append(prepacked_weight_size[1] * groups)
|
| 108 |
+
weight_size.append(prepacked_weight_size[0] / groups)
|
| 109 |
+
weight_size.extend(prepacked_weight_size[d] for d in range(2, dim))
|
| 110 |
+
else:
|
| 111 |
+
weight_size = prepacked_weight.transpose(0, 1).size()
|
| 112 |
+
return weight_size
|
| 113 |
+
|
| 114 |
+
x.realize()
|
| 115 |
+
weight.realize()
|
| 116 |
+
if bias is not None:
|
| 117 |
+
bias.realize()
|
| 118 |
+
with V.graph.fake_mode:
|
| 119 |
+
# TODO <Leslie> cleaned up the fake_tensor trace as Linear implementation
|
| 120 |
+
x_fake = ir_node_to_tensor(x)
|
| 121 |
+
weight_fake = ir_node_to_tensor(weight)
|
| 122 |
+
dims = len(x_fake.size()) - 2
|
| 123 |
+
assert 0 < len(padding) <= dims
|
| 124 |
+
assert 0 < len(dilation) <= dims
|
| 125 |
+
assert 0 < len(stride) <= dims
|
| 126 |
+
padding = pad_listlike(padding, dims)
|
| 127 |
+
dilation = pad_listlike(dilation, dims)
|
| 128 |
+
stride = pad_listlike(stride, dims)
|
| 129 |
+
if output_padding is None:
|
| 130 |
+
output_padding = pad_listlike([0], dims)
|
| 131 |
+
else:
|
| 132 |
+
assert 0 < len(output_padding) <= dims
|
| 133 |
+
output_padding = pad_listlike(output_padding, dims)
|
| 134 |
+
assert isinstance(groups, (int, sympy.core.numbers.Integer))
|
| 135 |
+
if transposed:
|
| 136 |
+
# When transposed, the size of the prepacked oneDNN weight is different
|
| 137 |
+
# from the PyTorch weight. We're not able to run aten conv with such
|
| 138 |
+
# size. We infer the output size from the input params here:
|
| 139 |
+
weight_size = _original_deconv_weight_size(weight_fake, groups)
|
| 140 |
+
input_size = x_fake.size()
|
| 141 |
+
output_size = _conv_input_size(
|
| 142 |
+
input_size,
|
| 143 |
+
weight_size,
|
| 144 |
+
padding,
|
| 145 |
+
output_padding,
|
| 146 |
+
stride,
|
| 147 |
+
dilation,
|
| 148 |
+
groups,
|
| 149 |
+
)
|
| 150 |
+
else:
|
| 151 |
+
x_shape = list(x_fake.shape)
|
| 152 |
+
weight_shape = list(weight_fake.shape)
|
| 153 |
+
if len(x_shape) != len(weight_shape):
|
| 154 |
+
assert len(x_shape) == 3 and len(weight_shape) == 4
|
| 155 |
+
weight_shape.pop(2)
|
| 156 |
+
output_size = _conv_output_size(
|
| 157 |
+
x_shape,
|
| 158 |
+
weight_shape,
|
| 159 |
+
padding,
|
| 160 |
+
stride,
|
| 161 |
+
dilation,
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
req_stride_order = [0] + list(reversed(range(1, len(stride) + 1)))
|
| 165 |
+
req_stride_order = [len(req_stride_order)] + req_stride_order
|
| 166 |
+
|
| 167 |
+
x = cls.require_stride_order(x, req_stride_order)
|
| 168 |
+
|
| 169 |
+
# We won't do weight prepack for Conv if dynamic_shapes or if is xpu.
|
| 170 |
+
# In static shape cases, since weight is prepacked, we'll always force output to be channels last in the Conv kernel.
|
| 171 |
+
# In dynamic shape cases, for input with channels = 1, like tensor of size (s0, 1, 28, 28) and stride (784, 784, 28, 1),
|
| 172 |
+
# x = cls.require_stride_order(x, req_stride_order) where req_stride_order is in the channels last order
|
| 173 |
+
# won't change the stride of this tensor since stride for dimensions of size 1 is ignored. While in Conv kernel,
|
| 174 |
+
# this tensor is considered as channels first and the output will be in contiguous format.
|
| 175 |
+
# To align the behavior of the Conv kernel, we set the output_stride in such case to be contiguous instead of channels last.
|
| 176 |
+
dynamic_shapes = not all(isinstance(i, int) for i in (output_size))
|
| 177 |
+
if (
|
| 178 |
+
dynamic_shapes or get_device_type(x) == "xpu"
|
| 179 |
+
) and is_contiguous_storage_and_layout(x):
|
| 180 |
+
output_stride: StrideType = FlexibleLayout.contiguous_strides(output_size)
|
| 181 |
+
# Currently we don't support channel last for the situation that stride of input's batch dim is 0,
|
| 182 |
+
# eg. input_size = (1, 1280, 64, 64), but input_stride=(0, 1, 81920, 1280).
|
| 183 |
+
# So we use NCHW hear instead.
|
| 184 |
+
# Different with cpu, cpu conv always use channels_last for convolution when weight is prepacked,
|
| 185 |
+
# but xpu does not do the prepack, so the problem exposed here is only for xpu.
|
| 186 |
+
# TODO support channels_last for such zero stride input.
|
| 187 |
+
elif get_device_type(x) == "xpu" and x.get_stride()[0] == 0:
|
| 188 |
+
output_stride = FlexibleLayout.contiguous_strides(output_size)
|
| 189 |
+
else:
|
| 190 |
+
output_stride = make_channels_last_strides_for(output_size)
|
| 191 |
+
|
| 192 |
+
assert get_device_type(x) == get_device_type(weight)
|
| 193 |
+
assert get_device_type(x) in SUPPORTED_MKLDNN_DEVICES
|
| 194 |
+
inputs = [x]
|
| 195 |
+
|
| 196 |
+
if quantize_args is not None:
|
| 197 |
+
x_scale, x_zero_point, w_scale, w_zero_point = quantize_args
|
| 198 |
+
x_scale.realize()
|
| 199 |
+
x_zero_point.realize()
|
| 200 |
+
w_scale.realize()
|
| 201 |
+
w_zero_point.realize()
|
| 202 |
+
inputs = inputs + [x_scale, x_zero_point] + [weight] + [w_scale, w_zero_point]
|
| 203 |
+
else:
|
| 204 |
+
inputs += [weight]
|
| 205 |
+
|
| 206 |
+
if other is not None:
|
| 207 |
+
other = cls.require_stride_order(other, req_stride_order)
|
| 208 |
+
assert isinstance(other, TensorBox)
|
| 209 |
+
inputs += [other]
|
| 210 |
+
|
| 211 |
+
kernel_layout = FixedLayout(
|
| 212 |
+
x.get_device_or_error(),
|
| 213 |
+
x.get_dtype(),
|
| 214 |
+
convert_shape_to_inductor(output_size),
|
| 215 |
+
convert_shape_to_inductor(output_stride),
|
| 216 |
+
)
|
| 217 |
+
constant_args = [padding, stride, dilation, groups]
|
| 218 |
+
if transposed:
|
| 219 |
+
constant_args.insert(1, output_padding)
|
| 220 |
+
|
| 221 |
+
if bias is not None:
|
| 222 |
+
inputs.append(bias)
|
| 223 |
+
else:
|
| 224 |
+
constant_args.insert(0, bias)
|
| 225 |
+
return inputs, constant_args, kernel_layout, req_stride_order, other
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
def _prepare_linear_fusion_create(
|
| 229 |
+
cls,
|
| 230 |
+
x: "TensorBox",
|
| 231 |
+
weight: "TensorBox",
|
| 232 |
+
bias: "TensorBox",
|
| 233 |
+
quantize_args: list["TensorBox"] | None = None,
|
| 234 |
+
other: Optional["TensorBox"] = None,
|
| 235 |
+
binary_sum: bool = False,
|
| 236 |
+
):
|
| 237 |
+
"""
|
| 238 |
+
This function is a helper function to prepare inputs, layout and constant args
|
| 239 |
+
for linear post-op fusion's create function. The function only supports the CPU device
|
| 240 |
+
since linear post-op fusion kernel is only supported on CPU right now.
|
| 241 |
+
"""
|
| 242 |
+
x.realize()
|
| 243 |
+
weight.realize()
|
| 244 |
+
if bias is not None:
|
| 245 |
+
bias.realize()
|
| 246 |
+
|
| 247 |
+
*m, _ = x.get_size()
|
| 248 |
+
# The weight has been transposed during the qlinear weight prepack process.
|
| 249 |
+
# https://github.com/pytorch/pytorch/blob/4979f9c0d72490970e2019bb1d2284f83d93f76b/
|
| 250 |
+
# aten/src/ATen/native/quantized/cpu/qlinear_prepack.cpp#L291
|
| 251 |
+
_, oc = weight.get_size()
|
| 252 |
+
output_size = list(m) + [oc]
|
| 253 |
+
req_stride_order = list(reversed(range(len(x.get_size()))))
|
| 254 |
+
|
| 255 |
+
x = cls.require_stride_order(x, req_stride_order)
|
| 256 |
+
assert get_device_type(x) == get_device_type(weight)
|
| 257 |
+
assert get_device_type(x) in SUPPORTED_MKLDNN_DEVICES
|
| 258 |
+
inputs = [x]
|
| 259 |
+
|
| 260 |
+
if quantize_args is not None:
|
| 261 |
+
x_scale, x_zero_point, w_scale, w_zero_point = quantize_args
|
| 262 |
+
x_scale.realize()
|
| 263 |
+
x_zero_point.realize()
|
| 264 |
+
w_scale.realize()
|
| 265 |
+
w_zero_point.realize()
|
| 266 |
+
inputs = inputs + [x_scale, x_zero_point] + [weight] + [w_scale, w_zero_point]
|
| 267 |
+
else:
|
| 268 |
+
inputs += [weight]
|
| 269 |
+
|
| 270 |
+
if other is not None:
|
| 271 |
+
if binary_sum:
|
| 272 |
+
other = cls.require_stride_order(other, req_stride_order)
|
| 273 |
+
inputs = inputs + [other]
|
| 274 |
+
|
| 275 |
+
output_stride = FlexibleLayout.contiguous_strides(output_size)
|
| 276 |
+
kernel_layout = FixedLayout(
|
| 277 |
+
x.get_device(),
|
| 278 |
+
x.get_dtype(),
|
| 279 |
+
output_size,
|
| 280 |
+
output_stride,
|
| 281 |
+
)
|
| 282 |
+
constant_args: list[Any] = []
|
| 283 |
+
|
| 284 |
+
if bias is not None:
|
| 285 |
+
inputs.append(bias)
|
| 286 |
+
else:
|
| 287 |
+
constant_args.insert(0, bias)
|
| 288 |
+
return inputs, constant_args, kernel_layout, req_stride_order, other
|
| 289 |
+
|
| 290 |
+
|
| 291 |
+
def _create_output_node(packed):
|
| 292 |
+
output_ir = MultiOutput(
|
| 293 |
+
packed.get_layout(),
|
| 294 |
+
packed,
|
| 295 |
+
[],
|
| 296 |
+
)
|
| 297 |
+
packed.layout = MultiOutputLayout(device=packed.get_device())
|
| 298 |
+
packed.outputs = [output_ir]
|
| 299 |
+
return output_ir
|
| 300 |
+
|
| 301 |
+
|
| 302 |
+
class ConvolutionUnary(ExternKernelAlloc):
|
| 303 |
+
def __init__(
|
| 304 |
+
self,
|
| 305 |
+
layout,
|
| 306 |
+
inputs,
|
| 307 |
+
constant_args=(),
|
| 308 |
+
) -> None:
|
| 309 |
+
self.device_type = get_device_type(inputs[0])
|
| 310 |
+
super().__init__(
|
| 311 |
+
layout,
|
| 312 |
+
inputs,
|
| 313 |
+
constant_args,
|
| 314 |
+
None,
|
| 315 |
+
op_overload=torch.ops.mkldnn._convolution_pointwise.default,
|
| 316 |
+
cpp_kernel_name=f"aoti_torch_{self.device_type}_mkldnn__convolution_pointwise",
|
| 317 |
+
)
|
| 318 |
+
|
| 319 |
+
def codegen(self, wrapper):
|
| 320 |
+
wrapper.include_extra_header(
|
| 321 |
+
f"torch/csrc/inductor/aoti_torch/c/shim_{self.device_type}.h"
|
| 322 |
+
)
|
| 323 |
+
super().codegen(wrapper)
|
| 324 |
+
|
| 325 |
+
@classmethod
|
| 326 |
+
def create(
|
| 327 |
+
cls,
|
| 328 |
+
x: "TensorBox",
|
| 329 |
+
weight: "TensorBox",
|
| 330 |
+
bias: "TensorBox",
|
| 331 |
+
padding_: list[int],
|
| 332 |
+
stride_: list[int],
|
| 333 |
+
dilation_: list[int],
|
| 334 |
+
groups: int,
|
| 335 |
+
attr,
|
| 336 |
+
scalars: list[Any] | None,
|
| 337 |
+
algorithm,
|
| 338 |
+
):
|
| 339 |
+
(
|
| 340 |
+
inputs,
|
| 341 |
+
constant_args,
|
| 342 |
+
kernel_layout,
|
| 343 |
+
_,
|
| 344 |
+
_,
|
| 345 |
+
) = _prepare_convolution_fusion_create(
|
| 346 |
+
cls, x, weight, bias, padding_, stride_, dilation_, groups
|
| 347 |
+
)
|
| 348 |
+
constant_args = constant_args + [
|
| 349 |
+
attr,
|
| 350 |
+
may_convert_to_optional(scalars),
|
| 351 |
+
algorithm,
|
| 352 |
+
]
|
| 353 |
+
packed = ConvolutionUnary(
|
| 354 |
+
layout=kernel_layout,
|
| 355 |
+
inputs=inputs,
|
| 356 |
+
constant_args=constant_args,
|
| 357 |
+
)
|
| 358 |
+
return _create_output_node(packed)
|
| 359 |
+
|
| 360 |
+
|
| 361 |
+
class ConvolutionBinary(ExternKernelAlloc):
|
| 362 |
+
def __init__(
|
| 363 |
+
self,
|
| 364 |
+
layout,
|
| 365 |
+
inputs,
|
| 366 |
+
constant_args=(),
|
| 367 |
+
cpp_constant_args=(),
|
| 368 |
+
) -> None:
|
| 369 |
+
self.device_type = get_device_type(inputs[0])
|
| 370 |
+
super().__init__(
|
| 371 |
+
layout,
|
| 372 |
+
inputs,
|
| 373 |
+
constant_args,
|
| 374 |
+
None,
|
| 375 |
+
op_overload=torch.ops.mkldnn._convolution_pointwise.binary,
|
| 376 |
+
cpp_kernel_name=f"aoti_torch_{self.device_type}_mkldnn__convolution_pointwise_binary",
|
| 377 |
+
)
|
| 378 |
+
self.cpp_constant_args = cpp_constant_args
|
| 379 |
+
|
| 380 |
+
def codegen(self, wrapper):
|
| 381 |
+
wrapper.include_extra_header(
|
| 382 |
+
f"torch/csrc/inductor/aoti_torch/c/shim_{self.device_type}.h"
|
| 383 |
+
)
|
| 384 |
+
super().codegen(wrapper)
|
| 385 |
+
|
| 386 |
+
@classmethod
|
| 387 |
+
def create(
|
| 388 |
+
cls,
|
| 389 |
+
x: "TensorBox",
|
| 390 |
+
other: "TensorBox",
|
| 391 |
+
weight: "TensorBox",
|
| 392 |
+
bias: "TensorBox",
|
| 393 |
+
padding_: list[int],
|
| 394 |
+
stride_: list[int],
|
| 395 |
+
dilation_: list[int],
|
| 396 |
+
groups: int,
|
| 397 |
+
binary_attr: str,
|
| 398 |
+
binary_alpha: float | None,
|
| 399 |
+
unary_attr: str | None,
|
| 400 |
+
unary_scalars: list[Any] | None,
|
| 401 |
+
unary_algorithm: str | None,
|
| 402 |
+
):
|
| 403 |
+
(
|
| 404 |
+
inputs,
|
| 405 |
+
constant_args,
|
| 406 |
+
kernel_layout,
|
| 407 |
+
req_stride_order,
|
| 408 |
+
_,
|
| 409 |
+
) = _prepare_convolution_fusion_create(
|
| 410 |
+
cls, x, weight, bias, padding_, stride_, dilation_, groups
|
| 411 |
+
)
|
| 412 |
+
# pyrefly: ignore [bad-assignment]
|
| 413 |
+
other = cls.require_stride_order(other, req_stride_order)
|
| 414 |
+
inputs.insert(1, other)
|
| 415 |
+
constant_args = constant_args + [
|
| 416 |
+
binary_attr,
|
| 417 |
+
binary_alpha,
|
| 418 |
+
unary_attr,
|
| 419 |
+
may_convert_to_optional(unary_scalars),
|
| 420 |
+
unary_algorithm,
|
| 421 |
+
]
|
| 422 |
+
packed = ConvolutionBinary(
|
| 423 |
+
layout=kernel_layout,
|
| 424 |
+
inputs=inputs,
|
| 425 |
+
constant_args=constant_args,
|
| 426 |
+
)
|
| 427 |
+
return _create_output_node(packed)
|
| 428 |
+
|
| 429 |
+
|
| 430 |
+
class ConvolutionBinaryInplace(ExternKernelAlloc):
|
| 431 |
+
def __init__(
|
| 432 |
+
self,
|
| 433 |
+
kernel_layout,
|
| 434 |
+
inputs,
|
| 435 |
+
constant_args=(),
|
| 436 |
+
) -> None:
|
| 437 |
+
# Due to constrain of op.call, other (Tensor&) should be at input[0]
|
| 438 |
+
self.device_type = get_device_type(inputs[0])
|
| 439 |
+
reordered_inputs = [inputs[1], inputs[0]] + inputs[2:]
|
| 440 |
+
|
| 441 |
+
super().__init__(
|
| 442 |
+
kernel_layout,
|
| 443 |
+
reordered_inputs,
|
| 444 |
+
constant_args,
|
| 445 |
+
None,
|
| 446 |
+
op_overload=torch.ops.mkldnn._convolution_pointwise_.binary,
|
| 447 |
+
cpp_kernel_name=f"aoti_torch_{self.device_type}_mkldnn__convolution_pointwise_binary_",
|
| 448 |
+
)
|
| 449 |
+
|
| 450 |
+
self.mutation_outputs = [
|
| 451 |
+
MutationOutput(NoneLayout(device=inputs[0].get_device()), inputs[0], self),
|
| 452 |
+
MutationOutput(NoneLayout(device=inputs[1].get_device()), inputs[1], self),
|
| 453 |
+
]
|
| 454 |
+
|
| 455 |
+
def codegen(self, wrapper):
|
| 456 |
+
wrapper.include_extra_header(
|
| 457 |
+
f"torch/csrc/inductor/aoti_torch/c/shim_{self.device_type}.h"
|
| 458 |
+
)
|
| 459 |
+
super().codegen(wrapper)
|
| 460 |
+
|
| 461 |
+
def get_unbacked_symbol_defs(self) -> OrderedSet[sympy.Symbol]:
|
| 462 |
+
return OrderedSet()
|
| 463 |
+
|
| 464 |
+
@classmethod
|
| 465 |
+
def create(
|
| 466 |
+
cls,
|
| 467 |
+
x: "TensorBox",
|
| 468 |
+
other: "TensorBox",
|
| 469 |
+
weight: "TensorBox",
|
| 470 |
+
bias: "TensorBox",
|
| 471 |
+
padding_: list[int],
|
| 472 |
+
stride_: list[int],
|
| 473 |
+
dilation_: list[int],
|
| 474 |
+
groups: int,
|
| 475 |
+
binary_attr: str,
|
| 476 |
+
binary_alpha: float | None,
|
| 477 |
+
unary_attr: str | None,
|
| 478 |
+
unary_scalars: list[Any] | None,
|
| 479 |
+
unary_algorithm: str | None,
|
| 480 |
+
):
|
| 481 |
+
(
|
| 482 |
+
inputs,
|
| 483 |
+
constant_args,
|
| 484 |
+
_,
|
| 485 |
+
req_stride_order,
|
| 486 |
+
_,
|
| 487 |
+
) = _prepare_convolution_fusion_create(
|
| 488 |
+
cls, x, weight, bias, padding_, stride_, dilation_, groups
|
| 489 |
+
)
|
| 490 |
+
# pyrefly: ignore [bad-assignment]
|
| 491 |
+
other = cls.require_stride_order(other, req_stride_order)
|
| 492 |
+
inputs.insert(1, other)
|
| 493 |
+
constant_args = constant_args + [
|
| 494 |
+
binary_attr,
|
| 495 |
+
binary_alpha,
|
| 496 |
+
unary_attr,
|
| 497 |
+
may_convert_to_optional(unary_scalars),
|
| 498 |
+
unary_algorithm,
|
| 499 |
+
]
|
| 500 |
+
packed = ConvolutionBinaryInplace(
|
| 501 |
+
kernel_layout=NoneLayout(device=inputs[1].get_device()), # type: ignore[arg-type]
|
| 502 |
+
inputs=inputs,
|
| 503 |
+
constant_args=constant_args,
|
| 504 |
+
)
|
| 505 |
+
# This op mutates in place which means that the result is not the
|
| 506 |
+
# target but rather the input that is being mutated
|
| 507 |
+
# init reorders the inputs, so inputs[1] becomes packed.inputs[0]
|
| 508 |
+
return packed.inputs[0]
|
| 509 |
+
|
| 510 |
+
|
| 511 |
+
class ConvolutionTransposeUnary(ExternKernelAlloc):
|
| 512 |
+
def __init__(
|
| 513 |
+
self,
|
| 514 |
+
layout,
|
| 515 |
+
inputs,
|
| 516 |
+
constant_args=(),
|
| 517 |
+
) -> None:
|
| 518 |
+
self.device_type = get_device_type(inputs[0])
|
| 519 |
+
super().__init__(
|
| 520 |
+
layout,
|
| 521 |
+
inputs,
|
| 522 |
+
constant_args,
|
| 523 |
+
None,
|
| 524 |
+
op_overload=torch.ops.mkldnn._convolution_transpose_pointwise.default,
|
| 525 |
+
cpp_kernel_name=f"aoti_torch_{self.device_type}_mkldnn__convolution_transpose_pointwise",
|
| 526 |
+
)
|
| 527 |
+
|
| 528 |
+
def codegen(self, wrapper):
|
| 529 |
+
wrapper.include_extra_header(
|
| 530 |
+
f"torch/csrc/inductor/aoti_torch/c/shim_{self.device_type}.h"
|
| 531 |
+
)
|
| 532 |
+
super().codegen(wrapper)
|
| 533 |
+
|
| 534 |
+
@classmethod
|
| 535 |
+
def create(
|
| 536 |
+
cls,
|
| 537 |
+
x: "TensorBox",
|
| 538 |
+
weight: "TensorBox",
|
| 539 |
+
bias: "TensorBox",
|
| 540 |
+
padding_: list[int],
|
| 541 |
+
output_padding_: list[int],
|
| 542 |
+
stride_: list[int],
|
| 543 |
+
dilation_: list[int],
|
| 544 |
+
groups_: int,
|
| 545 |
+
attr,
|
| 546 |
+
scalars: list[Any] | None,
|
| 547 |
+
algorithm,
|
| 548 |
+
):
|
| 549 |
+
transposed = True
|
| 550 |
+
(
|
| 551 |
+
inputs,
|
| 552 |
+
constant_args,
|
| 553 |
+
kernel_layout,
|
| 554 |
+
_,
|
| 555 |
+
_,
|
| 556 |
+
) = _prepare_convolution_fusion_create(
|
| 557 |
+
cls,
|
| 558 |
+
x,
|
| 559 |
+
weight,
|
| 560 |
+
bias,
|
| 561 |
+
padding_,
|
| 562 |
+
stride_,
|
| 563 |
+
dilation_,
|
| 564 |
+
groups_,
|
| 565 |
+
transposed,
|
| 566 |
+
output_padding_,
|
| 567 |
+
)
|
| 568 |
+
constant_args = constant_args + [
|
| 569 |
+
attr,
|
| 570 |
+
may_convert_to_optional(scalars),
|
| 571 |
+
algorithm,
|
| 572 |
+
]
|
| 573 |
+
packed = ConvolutionTransposeUnary(
|
| 574 |
+
layout=kernel_layout,
|
| 575 |
+
inputs=inputs,
|
| 576 |
+
constant_args=constant_args,
|
| 577 |
+
)
|
| 578 |
+
return _create_output_node(packed)
|
| 579 |
+
|
| 580 |
+
|
| 581 |
+
class QConvPointWisePT2E(ExternKernelAlloc):
|
| 582 |
+
def __init__(
|
| 583 |
+
self,
|
| 584 |
+
layout,
|
| 585 |
+
inputs,
|
| 586 |
+
constant_args=(),
|
| 587 |
+
) -> None:
|
| 588 |
+
"""
|
| 589 |
+
if bias is not None
|
| 590 |
+
- inputs = [x, w, b, weight_scale, weight_zp]
|
| 591 |
+
- const_args is: [stride, padding, dilation, groups, x_scale, x_zp, o_scale, o_zp,
|
| 592 |
+
fp32_output, unary_attr, unary_scalars, unary_algorithm]
|
| 593 |
+
else
|
| 594 |
+
- inputs = [x, w, weight_scale, weight_zp]
|
| 595 |
+
- const_args is: [bias, stride, padding, dilation, groups, x_scale, x_zp, o_scale, o_zp,
|
| 596 |
+
fp32_output, unary_attr, unary_scalars, unary_algorithm]
|
| 597 |
+
"""
|
| 598 |
+
self.device_type = get_device_type(inputs[0])
|
| 599 |
+
self.has_bias = len(inputs) == 5
|
| 600 |
+
super().__init__(
|
| 601 |
+
layout,
|
| 602 |
+
inputs,
|
| 603 |
+
constant_args,
|
| 604 |
+
None,
|
| 605 |
+
op_overload=torch.ops.onednn.qconv_pointwise.tensor,
|
| 606 |
+
cpp_kernel_name=f"aoti_torch_{self.device_type}__qconv_pointwise_tensor",
|
| 607 |
+
)
|
| 608 |
+
|
| 609 |
+
def codegen(self, wrapper):
|
| 610 |
+
wrapper.include_extra_header(
|
| 611 |
+
f"torch/csrc/inductor/aoti_torch/c/shim_{self.device_type}.h"
|
| 612 |
+
)
|
| 613 |
+
super().codegen(wrapper)
|
| 614 |
+
if isinstance(self.layout, Layout):
|
| 615 |
+
self.codegen_size_asserts(wrapper)
|
| 616 |
+
|
| 617 |
+
@classmethod
|
| 618 |
+
def create(
|
| 619 |
+
cls,
|
| 620 |
+
qx: "TensorBox",
|
| 621 |
+
x_scale: "TensorBox",
|
| 622 |
+
x_zero_point: "TensorBox",
|
| 623 |
+
qw: "TensorBox", # qw
|
| 624 |
+
w_scale: "TensorBox",
|
| 625 |
+
w_zero_point,
|
| 626 |
+
bias: "TensorBox",
|
| 627 |
+
stride: list[int],
|
| 628 |
+
padding: list[int],
|
| 629 |
+
dilation: list[int],
|
| 630 |
+
groups: int,
|
| 631 |
+
output_scale: float,
|
| 632 |
+
output_zero_point: int,
|
| 633 |
+
output_dtype,
|
| 634 |
+
attr,
|
| 635 |
+
scalars,
|
| 636 |
+
algorithm,
|
| 637 |
+
):
|
| 638 |
+
transposed = False
|
| 639 |
+
output_padding = None
|
| 640 |
+
(
|
| 641 |
+
inputs,
|
| 642 |
+
constant_args,
|
| 643 |
+
kernel_layout,
|
| 644 |
+
_,
|
| 645 |
+
_,
|
| 646 |
+
) = _prepare_convolution_fusion_create(
|
| 647 |
+
cls,
|
| 648 |
+
qx,
|
| 649 |
+
qw,
|
| 650 |
+
bias,
|
| 651 |
+
padding,
|
| 652 |
+
stride,
|
| 653 |
+
dilation,
|
| 654 |
+
groups,
|
| 655 |
+
transposed,
|
| 656 |
+
output_padding,
|
| 657 |
+
[x_scale, x_zero_point, w_scale, w_zero_point], # type: ignore[list-item]
|
| 658 |
+
)
|
| 659 |
+
# swap padding and stride to align with functional conv arg order
|
| 660 |
+
if bias is None:
|
| 661 |
+
constant_args[1], constant_args[2] = constant_args[2], constant_args[1]
|
| 662 |
+
else:
|
| 663 |
+
constant_args[0], constant_args[1] = constant_args[1], constant_args[0]
|
| 664 |
+
|
| 665 |
+
constant_args = constant_args + [
|
| 666 |
+
output_scale,
|
| 667 |
+
output_zero_point,
|
| 668 |
+
output_dtype,
|
| 669 |
+
attr,
|
| 670 |
+
may_convert_to_optional(scalars),
|
| 671 |
+
algorithm,
|
| 672 |
+
]
|
| 673 |
+
|
| 674 |
+
assert output_dtype is not None
|
| 675 |
+
if output_dtype in [torch.float32, torch.bfloat16]:
|
| 676 |
+
# in _prepare_convolution_fusion_create, we use x.dtype (uint8) to create kernel_layout
|
| 677 |
+
# if we set output_dtype is not None, the output buf should be output_dtype instead of uint8.
|
| 678 |
+
kernel_layout.dtype = output_dtype
|
| 679 |
+
|
| 680 |
+
return QConvPointWisePT2E(
|
| 681 |
+
layout=kernel_layout,
|
| 682 |
+
inputs=inputs,
|
| 683 |
+
constant_args=constant_args,
|
| 684 |
+
)
|
| 685 |
+
|
| 686 |
+
|
| 687 |
+
class QConvPointWiseBinaryPT2E(ExternKernelAlloc):
|
| 688 |
+
def __init__(
|
| 689 |
+
self,
|
| 690 |
+
layout,
|
| 691 |
+
inputs,
|
| 692 |
+
constant_args=(),
|
| 693 |
+
) -> None:
|
| 694 |
+
"""
|
| 695 |
+
Needs input/weight/output qparams
|
| 696 |
+
if bias is not None
|
| 697 |
+
- inputs = [x, x_scale, x_zp, w, w_scale, w_zp, accum, b]
|
| 698 |
+
- const_args = [stride, padding, dilation, groups, o_scale, o_zp,
|
| 699 |
+
output_dtype, accum_scale, accum_zp, binary_attr, alpha, unary_attr, unary_scalars, unary_algorithm]
|
| 700 |
+
else
|
| 701 |
+
- inputs = [x, x_scale, x_zp, w, w_scale, w_zp, accum]
|
| 702 |
+
- const_args [b, stride, padding, dilation, groups, o_scale, o_zp,
|
| 703 |
+
output_dtype, accum_scale, accum_zp, binary_attr, alpha, unary_attr, unary_scalars, unary_algorithm]
|
| 704 |
+
"""
|
| 705 |
+
self.device_type = get_device_type(inputs[0])
|
| 706 |
+
self.has_bias = len(inputs) == 8
|
| 707 |
+
self.idx_for_inplace_sum = 6
|
| 708 |
+
super().__init__(
|
| 709 |
+
layout,
|
| 710 |
+
inputs,
|
| 711 |
+
constant_args,
|
| 712 |
+
None,
|
| 713 |
+
op_overload=torch.ops.onednn.qconv2d_pointwise.binary_tensor,
|
| 714 |
+
cpp_kernel_name=(
|
| 715 |
+
f"aoti_torch_{self.device_type}__qconv2d_pointwise_binary_tensor"
|
| 716 |
+
),
|
| 717 |
+
)
|
| 718 |
+
|
| 719 |
+
def codegen(self, wrapper):
|
| 720 |
+
wrapper.include_extra_header(
|
| 721 |
+
f"torch/csrc/inductor/aoti_torch/c/shim_{self.device_type}.h"
|
| 722 |
+
)
|
| 723 |
+
super().codegen(wrapper)
|
| 724 |
+
if isinstance(self.layout, Layout):
|
| 725 |
+
self.codegen_size_asserts(wrapper)
|
| 726 |
+
|
| 727 |
+
def get_mutation_names(self) -> Sequence[str]:
|
| 728 |
+
return [self.input_name(self.idx_for_inplace_sum)]
|
| 729 |
+
|
| 730 |
+
def get_unbacked_symbol_defs(self) -> OrderedSet[sympy.Symbol]:
|
| 731 |
+
return OrderedSet()
|
| 732 |
+
|
| 733 |
+
@classmethod
|
| 734 |
+
def create(
|
| 735 |
+
cls,
|
| 736 |
+
qx: "TensorBox",
|
| 737 |
+
x_scale: "TensorBox",
|
| 738 |
+
x_zero_point: "TensorBox",
|
| 739 |
+
qw: "TensorBox", # packed_weight
|
| 740 |
+
w_scale,
|
| 741 |
+
w_zero_point,
|
| 742 |
+
qaccum: "TensorBox",
|
| 743 |
+
bias: "TensorBox",
|
| 744 |
+
stride: list[int],
|
| 745 |
+
padding: list[int],
|
| 746 |
+
dilation: list[int],
|
| 747 |
+
groups: int,
|
| 748 |
+
output_scale: "TensorBox",
|
| 749 |
+
output_zero_point: "TensorBox",
|
| 750 |
+
output_dtype,
|
| 751 |
+
accum_scale,
|
| 752 |
+
accum_zero_point,
|
| 753 |
+
binary_attr,
|
| 754 |
+
alpha,
|
| 755 |
+
unary_attr,
|
| 756 |
+
unary_scalars,
|
| 757 |
+
unary_algorithm,
|
| 758 |
+
):
|
| 759 |
+
transposed = False
|
| 760 |
+
output_padding = None
|
| 761 |
+
(
|
| 762 |
+
inputs,
|
| 763 |
+
constant_args,
|
| 764 |
+
_kernel_layout,
|
| 765 |
+
req_stride_order,
|
| 766 |
+
qaccum,
|
| 767 |
+
) = _prepare_convolution_fusion_create(
|
| 768 |
+
cls,
|
| 769 |
+
qx,
|
| 770 |
+
qw,
|
| 771 |
+
bias,
|
| 772 |
+
padding,
|
| 773 |
+
stride,
|
| 774 |
+
dilation,
|
| 775 |
+
groups,
|
| 776 |
+
transposed,
|
| 777 |
+
output_padding,
|
| 778 |
+
[x_scale, x_zero_point, w_scale, w_zero_point],
|
| 779 |
+
qaccum,
|
| 780 |
+
)
|
| 781 |
+
|
| 782 |
+
# swap padding and stride to align with functional conv arg order
|
| 783 |
+
if bias is None:
|
| 784 |
+
constant_args[1], constant_args[2] = constant_args[2], constant_args[1]
|
| 785 |
+
else:
|
| 786 |
+
constant_args[0], constant_args[1] = constant_args[1], constant_args[0]
|
| 787 |
+
|
| 788 |
+
constant_args = constant_args + [
|
| 789 |
+
output_scale,
|
| 790 |
+
output_zero_point,
|
| 791 |
+
output_dtype,
|
| 792 |
+
accum_scale,
|
| 793 |
+
accum_zero_point,
|
| 794 |
+
binary_attr,
|
| 795 |
+
alpha,
|
| 796 |
+
unary_attr,
|
| 797 |
+
may_convert_to_optional(unary_scalars),
|
| 798 |
+
unary_algorithm,
|
| 799 |
+
]
|
| 800 |
+
|
| 801 |
+
assert binary_attr == "sum", (
|
| 802 |
+
"For now, only post op sum is supported in QConvPointWiseBinaryPT2E."
|
| 803 |
+
)
|
| 804 |
+
|
| 805 |
+
V.graph.mark_buffer_mutated(qaccum.get_name())
|
| 806 |
+
packed = QConvPointWiseBinaryPT2E(
|
| 807 |
+
layout=NoneLayout(device=qaccum.get_device()),
|
| 808 |
+
inputs=inputs,
|
| 809 |
+
constant_args=constant_args,
|
| 810 |
+
)
|
| 811 |
+
|
| 812 |
+
# Return accum since it has been inplace changed.
|
| 813 |
+
return packed.inputs[packed.idx_for_inplace_sum]
|
| 814 |
+
|
| 815 |
+
|
| 816 |
+
class MKLPackedLinear(ExternKernelAlloc):
|
| 817 |
+
def __init__(
|
| 818 |
+
self,
|
| 819 |
+
layout,
|
| 820 |
+
inputs,
|
| 821 |
+
constant_args=(),
|
| 822 |
+
) -> None:
|
| 823 |
+
super().__init__(
|
| 824 |
+
layout,
|
| 825 |
+
inputs,
|
| 826 |
+
constant_args,
|
| 827 |
+
None,
|
| 828 |
+
op_overload=torch.ops.mkl._mkl_linear.default,
|
| 829 |
+
)
|
| 830 |
+
|
| 831 |
+
def codegen(self, wrapper):
|
| 832 |
+
wrapper.include_extra_header("torch/csrc/inductor/aoti_torch/c/shim_cpu.h")
|
| 833 |
+
super().codegen(wrapper)
|
| 834 |
+
|
| 835 |
+
@classmethod
|
| 836 |
+
def create(cls, x, packed_w, orig_w, B, batch_size):
|
| 837 |
+
x = cls.require_stride1(cls.realize_input(x))
|
| 838 |
+
orig_w = cls.require_stride1(cls.realize_input(orig_w))
|
| 839 |
+
*m, _ = x.get_size()
|
| 840 |
+
oc, _ = orig_w.get_size()
|
| 841 |
+
output_size = list(m) + [oc]
|
| 842 |
+
output_stride = FlexibleLayout.contiguous_strides(output_size)
|
| 843 |
+
inputs = [x, packed_w, orig_w]
|
| 844 |
+
constant_args = [batch_size]
|
| 845 |
+
if B is not None:
|
| 846 |
+
inputs += [B]
|
| 847 |
+
else:
|
| 848 |
+
constant_args.insert(0, None)
|
| 849 |
+
|
| 850 |
+
device = x.get_device()
|
| 851 |
+
assert device is not None
|
| 852 |
+
return MKLPackedLinear(
|
| 853 |
+
layout=FixedLayout(device, x.get_dtype(), output_size, output_stride),
|
| 854 |
+
inputs=inputs,
|
| 855 |
+
constant_args=constant_args,
|
| 856 |
+
)
|
| 857 |
+
|
| 858 |
+
|
| 859 |
+
class LinearUnary(ExternKernelAlloc):
|
| 860 |
+
def __init__(
|
| 861 |
+
self,
|
| 862 |
+
layout,
|
| 863 |
+
inputs,
|
| 864 |
+
constant_args=(),
|
| 865 |
+
) -> None:
|
| 866 |
+
self.device_type = get_device_type(inputs[0])
|
| 867 |
+
super().__init__(
|
| 868 |
+
layout,
|
| 869 |
+
inputs,
|
| 870 |
+
constant_args,
|
| 871 |
+
None,
|
| 872 |
+
op_overload=torch.ops.mkldnn._linear_pointwise.default,
|
| 873 |
+
cpp_kernel_name=f"aoti_torch_{self.device_type}__linear_pointwise",
|
| 874 |
+
)
|
| 875 |
+
|
| 876 |
+
def codegen(self, wrapper):
|
| 877 |
+
wrapper.include_extra_header(
|
| 878 |
+
f"torch/csrc/inductor/aoti_torch/c/shim_{self.device_type}.h"
|
| 879 |
+
)
|
| 880 |
+
super().codegen(wrapper)
|
| 881 |
+
|
| 882 |
+
@classmethod
|
| 883 |
+
def create(cls, x, w, B, attr, scalars, algorithm):
|
| 884 |
+
x = cls.require_contiguous(cls.realize_input(x))
|
| 885 |
+
w = cls.require_contiguous(cls.realize_input(w))
|
| 886 |
+
|
| 887 |
+
*m, _ic = x.get_size()
|
| 888 |
+
oc, _ic = w.get_size()
|
| 889 |
+
output_size = list(m) + [oc]
|
| 890 |
+
inputs = [x, w]
|
| 891 |
+
constant_args = [attr, scalars if scalars else [-1], algorithm]
|
| 892 |
+
if B is not None:
|
| 893 |
+
B = cls.require_contiguous(cls.realize_input(B))
|
| 894 |
+
inputs.append(B)
|
| 895 |
+
else:
|
| 896 |
+
constant_args.insert(0, None)
|
| 897 |
+
|
| 898 |
+
device = x.get_device()
|
| 899 |
+
assert device is not None
|
| 900 |
+
|
| 901 |
+
packed = LinearUnary(
|
| 902 |
+
layout=FixedLayout(
|
| 903 |
+
device=device,
|
| 904 |
+
dtype=x.get_dtype(),
|
| 905 |
+
size=output_size,
|
| 906 |
+
),
|
| 907 |
+
inputs=inputs,
|
| 908 |
+
constant_args=constant_args,
|
| 909 |
+
)
|
| 910 |
+
return _create_output_node(packed)
|
| 911 |
+
|
| 912 |
+
def apply_constraint(self):
|
| 913 |
+
pass
|
| 914 |
+
|
| 915 |
+
|
| 916 |
+
class LinearBinary(ExternKernelAlloc):
|
| 917 |
+
kernel = "torch.ops.mkldnn._linear_pointwise.binary"
|
| 918 |
+
|
| 919 |
+
def __init__(
|
| 920 |
+
self,
|
| 921 |
+
layout,
|
| 922 |
+
inputs,
|
| 923 |
+
constant_args=(),
|
| 924 |
+
) -> None:
|
| 925 |
+
self.device_type = get_device_type(inputs[0])
|
| 926 |
+
super().__init__(
|
| 927 |
+
layout,
|
| 928 |
+
inputs,
|
| 929 |
+
constant_args,
|
| 930 |
+
None,
|
| 931 |
+
op_overload=torch.ops.mkldnn._linear_pointwise.binary,
|
| 932 |
+
cpp_kernel_name=f"aoti_torch_{self.device_type}__linear_pointwise_binary",
|
| 933 |
+
)
|
| 934 |
+
|
| 935 |
+
def codegen(self, wrapper):
|
| 936 |
+
wrapper.include_extra_header(
|
| 937 |
+
f"torch/csrc/inductor/aoti_torch/c/shim_{self.device_type}.h"
|
| 938 |
+
)
|
| 939 |
+
super().codegen(wrapper)
|
| 940 |
+
|
| 941 |
+
@classmethod
|
| 942 |
+
def create(cls, x, y, w, B, attr):
|
| 943 |
+
x = cls.require_contiguous(cls.realize_input(x))
|
| 944 |
+
y = cls.require_contiguous(cls.realize_input(y))
|
| 945 |
+
w = cls.require_contiguous(cls.realize_input(w))
|
| 946 |
+
|
| 947 |
+
*m, _ic = x.get_size()
|
| 948 |
+
oc, _ic = w.get_size()
|
| 949 |
+
output_size = list(m) + [oc]
|
| 950 |
+
inputs = [x, y, w]
|
| 951 |
+
constant_args = [attr]
|
| 952 |
+
if B is not None:
|
| 953 |
+
B = cls.require_contiguous(cls.realize_input(B))
|
| 954 |
+
inputs.append(B)
|
| 955 |
+
else:
|
| 956 |
+
constant_args.insert(0, B)
|
| 957 |
+
|
| 958 |
+
device = x.get_device()
|
| 959 |
+
assert device is not None
|
| 960 |
+
packed = LinearBinary(
|
| 961 |
+
layout=FixedLayout(
|
| 962 |
+
device=device,
|
| 963 |
+
dtype=x.get_dtype(),
|
| 964 |
+
size=output_size,
|
| 965 |
+
),
|
| 966 |
+
inputs=inputs,
|
| 967 |
+
constant_args=constant_args,
|
| 968 |
+
)
|
| 969 |
+
return _create_output_node(packed)
|
| 970 |
+
|
| 971 |
+
def apply_constraint(self):
|
| 972 |
+
pass
|
| 973 |
+
|
| 974 |
+
|
| 975 |
+
class QLinearPointwisePT2E(ExternKernelAlloc):
|
| 976 |
+
def __init__(
|
| 977 |
+
self,
|
| 978 |
+
layout,
|
| 979 |
+
inputs,
|
| 980 |
+
constant_args=(),
|
| 981 |
+
has_bias=True,
|
| 982 |
+
) -> None:
|
| 983 |
+
"""
|
| 984 |
+
if bias is not None
|
| 985 |
+
- inputs = [x, w, b, weight_scale, weight_zp]
|
| 986 |
+
- const_args is: [x_scale, x_zp, o_scale, o_zp,
|
| 987 |
+
fp32_output, unary_attr, unary_scalars, unary_algorithm]
|
| 988 |
+
else
|
| 989 |
+
- inputs = [x, w, weight_scale, weight_zp]
|
| 990 |
+
- const_args is: [bias, x_scale, x_zp, o_scale, o_zp,
|
| 991 |
+
fp32_output, unary_attr, unary_scalars, unary_algorithm]
|
| 992 |
+
"""
|
| 993 |
+
self.device_type = get_device_type(inputs[0])
|
| 994 |
+
self.has_bias = has_bias
|
| 995 |
+
super().__init__(
|
| 996 |
+
layout,
|
| 997 |
+
inputs,
|
| 998 |
+
constant_args,
|
| 999 |
+
None,
|
| 1000 |
+
op_overload=(torch.ops.onednn.qlinear_pointwise.tensor),
|
| 1001 |
+
cpp_kernel_name=(
|
| 1002 |
+
f"aoti_torch_{self.device_type}__qlinear_pointwise_tensor"
|
| 1003 |
+
),
|
| 1004 |
+
)
|
| 1005 |
+
|
| 1006 |
+
def codegen(self, wrapper):
|
| 1007 |
+
wrapper.include_extra_header(
|
| 1008 |
+
f"torch/csrc/inductor/aoti_torch/c/shim_{self.device_type}.h"
|
| 1009 |
+
)
|
| 1010 |
+
super().codegen(wrapper)
|
| 1011 |
+
|
| 1012 |
+
if isinstance(self.layout, Layout):
|
| 1013 |
+
self.codegen_size_asserts(wrapper)
|
| 1014 |
+
|
| 1015 |
+
@classmethod
|
| 1016 |
+
def create(
|
| 1017 |
+
cls,
|
| 1018 |
+
qx: "TensorBox",
|
| 1019 |
+
x_scale: "TensorBox",
|
| 1020 |
+
x_zero_point: "TensorBox",
|
| 1021 |
+
qw: "TensorBox", # packed_weight
|
| 1022 |
+
w_scale: "TensorBox",
|
| 1023 |
+
w_zero_point: "TensorBox",
|
| 1024 |
+
bias: "TensorBox",
|
| 1025 |
+
output_scale: float,
|
| 1026 |
+
output_zero_point: int,
|
| 1027 |
+
output_dtype,
|
| 1028 |
+
post_op_name,
|
| 1029 |
+
post_op_args,
|
| 1030 |
+
post_op_algorithm,
|
| 1031 |
+
):
|
| 1032 |
+
(inputs, constant_args, kernel_layout, _, _) = _prepare_linear_fusion_create(
|
| 1033 |
+
cls,
|
| 1034 |
+
qx,
|
| 1035 |
+
qw,
|
| 1036 |
+
bias,
|
| 1037 |
+
[x_scale, x_zero_point, w_scale, w_zero_point],
|
| 1038 |
+
)
|
| 1039 |
+
|
| 1040 |
+
constant_args = constant_args + [
|
| 1041 |
+
output_scale,
|
| 1042 |
+
output_zero_point,
|
| 1043 |
+
output_dtype,
|
| 1044 |
+
post_op_name,
|
| 1045 |
+
may_convert_to_optional(post_op_args),
|
| 1046 |
+
post_op_algorithm,
|
| 1047 |
+
]
|
| 1048 |
+
|
| 1049 |
+
assert output_dtype is not None
|
| 1050 |
+
if output_dtype in [torch.float32, torch.bfloat16]:
|
| 1051 |
+
# in _prepare_linear_fusion_create, we use x.dtype (uint8) to create kernel_layout
|
| 1052 |
+
# if we set fp32_output, the output buf should be dtype float32 instead of uint8.
|
| 1053 |
+
kernel_layout.dtype = output_dtype
|
| 1054 |
+
|
| 1055 |
+
return QLinearPointwisePT2E(
|
| 1056 |
+
layout=kernel_layout,
|
| 1057 |
+
inputs=inputs,
|
| 1058 |
+
constant_args=constant_args,
|
| 1059 |
+
has_bias=(bias is not None),
|
| 1060 |
+
)
|
| 1061 |
+
|
| 1062 |
+
|
| 1063 |
+
class QLinearPointwiseBinaryPT2E(ExternKernelAlloc):
|
| 1064 |
+
def __init__(
|
| 1065 |
+
self,
|
| 1066 |
+
layout,
|
| 1067 |
+
inputs,
|
| 1068 |
+
constant_args=(),
|
| 1069 |
+
has_bias=True,
|
| 1070 |
+
) -> None:
|
| 1071 |
+
"""
|
| 1072 |
+
if bias is not None
|
| 1073 |
+
- inputs = [x, w, x_scale, x_zp, weight_scale, weight_zp, x2, bias]
|
| 1074 |
+
- const_args is: [o_scale, o_zp,
|
| 1075 |
+
fp32_output, binary_attr, alpha, unary_attr, unary_scalars, unary_algorithm]
|
| 1076 |
+
else
|
| 1077 |
+
- inputs = [x, w, x_scale, x_zp, weight_scale, weight_zp, x2]
|
| 1078 |
+
- const_args is: [bias, o_scale, o_zp,
|
| 1079 |
+
fp32_output, binary_attr, alpha, unary_attr, unary_scalars, unary_algorithm]
|
| 1080 |
+
"""
|
| 1081 |
+
self.device_type = get_device_type(inputs[0])
|
| 1082 |
+
self.has_bias = has_bias
|
| 1083 |
+
self.idx_for_inplace_sum = 6
|
| 1084 |
+
super().__init__(
|
| 1085 |
+
layout,
|
| 1086 |
+
inputs,
|
| 1087 |
+
constant_args,
|
| 1088 |
+
None,
|
| 1089 |
+
op_overload=(torch.ops.onednn.qlinear_pointwise.binary_tensor),
|
| 1090 |
+
cpp_kernel_name=f"aoti_torch_{self.device_type}__qlinear_pointwise_binary_tensor",
|
| 1091 |
+
)
|
| 1092 |
+
|
| 1093 |
+
def codegen(self, wrapper):
|
| 1094 |
+
wrapper.include_extra_header(
|
| 1095 |
+
f"torch/csrc/inductor/aoti_torch/c/shim_{self.device_type}.h"
|
| 1096 |
+
)
|
| 1097 |
+
super().codegen(wrapper)
|
| 1098 |
+
if isinstance(self.layout, Layout):
|
| 1099 |
+
self.codegen_size_asserts(wrapper)
|
| 1100 |
+
|
| 1101 |
+
def get_mutation_names(self) -> Sequence[str]:
|
| 1102 |
+
binary_post_op = self.constant_args[-5]
|
| 1103 |
+
if binary_post_op == "sum":
|
| 1104 |
+
input = self.inputs[self.idx_for_inplace_sum]
|
| 1105 |
+
assert isinstance(input, IRNode)
|
| 1106 |
+
return [input.get_name()]
|
| 1107 |
+
else:
|
| 1108 |
+
return []
|
| 1109 |
+
|
| 1110 |
+
@classmethod
|
| 1111 |
+
def create(
|
| 1112 |
+
cls,
|
| 1113 |
+
qx: "TensorBox",
|
| 1114 |
+
x_scale: "TensorBox",
|
| 1115 |
+
x_zero_point: "TensorBox",
|
| 1116 |
+
qw: "TensorBox", # packed_weight
|
| 1117 |
+
w_scale: "TensorBox",
|
| 1118 |
+
w_zero_point: "TensorBox",
|
| 1119 |
+
other: "TensorBox",
|
| 1120 |
+
bias: "TensorBox",
|
| 1121 |
+
output_scale: float,
|
| 1122 |
+
output_zero_point: int,
|
| 1123 |
+
output_dtype,
|
| 1124 |
+
other_scale,
|
| 1125 |
+
other_zp,
|
| 1126 |
+
binary_post_op,
|
| 1127 |
+
binary_alpha,
|
| 1128 |
+
unary_post_op,
|
| 1129 |
+
unary_post_op_args,
|
| 1130 |
+
unary_post_op_algorithm,
|
| 1131 |
+
):
|
| 1132 |
+
(
|
| 1133 |
+
inputs,
|
| 1134 |
+
constant_args,
|
| 1135 |
+
kernel_layout,
|
| 1136 |
+
req_stride_order,
|
| 1137 |
+
other,
|
| 1138 |
+
) = _prepare_linear_fusion_create(
|
| 1139 |
+
cls,
|
| 1140 |
+
qx,
|
| 1141 |
+
qw,
|
| 1142 |
+
bias,
|
| 1143 |
+
[x_scale, x_zero_point, w_scale, w_zero_point],
|
| 1144 |
+
other,
|
| 1145 |
+
binary_post_op == "sum",
|
| 1146 |
+
)
|
| 1147 |
+
|
| 1148 |
+
constant_args = constant_args + [
|
| 1149 |
+
output_scale,
|
| 1150 |
+
output_zero_point,
|
| 1151 |
+
output_dtype,
|
| 1152 |
+
other_scale,
|
| 1153 |
+
other_zp,
|
| 1154 |
+
binary_post_op,
|
| 1155 |
+
binary_alpha,
|
| 1156 |
+
unary_post_op,
|
| 1157 |
+
may_convert_to_optional(unary_post_op_args),
|
| 1158 |
+
unary_post_op_algorithm,
|
| 1159 |
+
]
|
| 1160 |
+
|
| 1161 |
+
if binary_post_op == "sum":
|
| 1162 |
+
V.graph.mark_buffer_mutated(other.get_name())
|
| 1163 |
+
packed = QLinearPointwiseBinaryPT2E(
|
| 1164 |
+
layout=NoneLayout(device=other.get_device()),
|
| 1165 |
+
inputs=inputs,
|
| 1166 |
+
constant_args=constant_args,
|
| 1167 |
+
has_bias=(bias is not None),
|
| 1168 |
+
)
|
| 1169 |
+
# Return other since it has been inplace changed.
|
| 1170 |
+
return packed.inputs[packed.idx_for_inplace_sum]
|
| 1171 |
+
|
| 1172 |
+
assert output_dtype is not None
|
| 1173 |
+
if output_dtype in [torch.float32, torch.bfloat16]:
|
| 1174 |
+
# in _prepare_linear_fusion_create, we use x.dtype (uint8) to create kernel_layout
|
| 1175 |
+
# if we set fp32_output, the output buf should be dtype float32 instead of uint8.
|
| 1176 |
+
kernel_layout.dtype = output_dtype
|
| 1177 |
+
|
| 1178 |
+
return QLinearPointwiseBinaryPT2E(
|
| 1179 |
+
layout=kernel_layout,
|
| 1180 |
+
inputs=inputs,
|
| 1181 |
+
constant_args=constant_args,
|
| 1182 |
+
has_bias=(bias is not None),
|
| 1183 |
+
)
|
| 1184 |
+
|
| 1185 |
+
|
| 1186 |
+
class MkldnnRnnLayer(ExternKernelAlloc):
|
| 1187 |
+
def __init__(
|
| 1188 |
+
self,
|
| 1189 |
+
layout,
|
| 1190 |
+
inputs,
|
| 1191 |
+
constant_args=(),
|
| 1192 |
+
) -> None:
|
| 1193 |
+
super().__init__(
|
| 1194 |
+
layout,
|
| 1195 |
+
inputs,
|
| 1196 |
+
constant_args,
|
| 1197 |
+
None,
|
| 1198 |
+
op_overload=torch.ops.aten.mkldnn_rnn_layer.default,
|
| 1199 |
+
)
|
| 1200 |
+
|
| 1201 |
+
@classmethod
|
| 1202 |
+
def create(
|
| 1203 |
+
cls,
|
| 1204 |
+
x: "TensorBox",
|
| 1205 |
+
w0: "TensorBox",
|
| 1206 |
+
w1: "TensorBox",
|
| 1207 |
+
w2: "TensorBox",
|
| 1208 |
+
w3: "TensorBox",
|
| 1209 |
+
hx: "TensorBox",
|
| 1210 |
+
cx: "TensorBox",
|
| 1211 |
+
reverse: bool,
|
| 1212 |
+
batch_sizes: list[int],
|
| 1213 |
+
mode: int,
|
| 1214 |
+
hidden_size: int,
|
| 1215 |
+
num_layers: int,
|
| 1216 |
+
has_biases: bool,
|
| 1217 |
+
bidirectional: bool,
|
| 1218 |
+
batch_first: bool,
|
| 1219 |
+
train: bool,
|
| 1220 |
+
):
|
| 1221 |
+
# pyrefly: ignore [bad-assignment]
|
| 1222 |
+
x = cls.require_stride1(cls.realize_input(x))
|
| 1223 |
+
# If batch_first, x has been permuted in lstm before entering the mkldnn_rnn_layer.
|
| 1224 |
+
# Make sure x is contiguous in batch_first case.
|
| 1225 |
+
x.freeze_layout()
|
| 1226 |
+
# pyrefly: ignore [bad-assignment]
|
| 1227 |
+
w0 = cls.require_stride1(cls.realize_input(w0))
|
| 1228 |
+
# pyrefly: ignore [bad-assignment]
|
| 1229 |
+
w1 = cls.require_stride1(cls.realize_input(w1))
|
| 1230 |
+
# pyrefly: ignore [bad-assignment]
|
| 1231 |
+
w2 = cls.require_stride1(cls.realize_input(w2))
|
| 1232 |
+
# pyrefly: ignore [bad-assignment]
|
| 1233 |
+
w3 = cls.require_stride1(cls.realize_input(w3))
|
| 1234 |
+
# pyrefly: ignore [bad-assignment]
|
| 1235 |
+
hx = cls.require_stride1(cls.realize_input(hx))
|
| 1236 |
+
hx.freeze_layout()
|
| 1237 |
+
# pyrefly: ignore [bad-assignment]
|
| 1238 |
+
cx = cls.require_stride1(cls.realize_input(cx))
|
| 1239 |
+
cx.freeze_layout()
|
| 1240 |
+
|
| 1241 |
+
input_size = x.get_size()
|
| 1242 |
+
assert len(input_size) == 3, "Expect lstm input to be 3D"
|
| 1243 |
+
# batch_first is handled in the lstm OP. When entering
|
| 1244 |
+
# rnn_layer here, we'll always have batch_first = False
|
| 1245 |
+
seq_length, mini_batch, input_size = input_size
|
| 1246 |
+
output_shape = [seq_length, mini_batch, hidden_size]
|
| 1247 |
+
|
| 1248 |
+
hy_shape = hx.get_size()
|
| 1249 |
+
cy_shape = cx.get_size()
|
| 1250 |
+
|
| 1251 |
+
inputs = [x, w0, w1, w2, w3, hx, cx]
|
| 1252 |
+
constant_args = [
|
| 1253 |
+
reverse,
|
| 1254 |
+
batch_sizes,
|
| 1255 |
+
mode,
|
| 1256 |
+
hidden_size,
|
| 1257 |
+
num_layers,
|
| 1258 |
+
has_biases,
|
| 1259 |
+
bidirectional,
|
| 1260 |
+
batch_first,
|
| 1261 |
+
train,
|
| 1262 |
+
]
|
| 1263 |
+
|
| 1264 |
+
device = x.get_device()
|
| 1265 |
+
assert device is not None
|
| 1266 |
+
packed = MkldnnRnnLayer(
|
| 1267 |
+
MultiOutputLayout(device=device),
|
| 1268 |
+
inputs=inputs,
|
| 1269 |
+
constant_args=constant_args,
|
| 1270 |
+
)
|
| 1271 |
+
|
| 1272 |
+
def get_strides_of_lstm_output(output_shape, batch_first):
|
| 1273 |
+
assert len(output_shape) == 3, "Expect output_shape to be 3D"
|
| 1274 |
+
return FlexibleLayout.contiguous_strides(output_shape)
|
| 1275 |
+
|
| 1276 |
+
# C shim call requires all the outputs to be passed in, and thus the last
|
| 1277 |
+
# dummy return value is added.
|
| 1278 |
+
output_sizes = [output_shape, hy_shape, cy_shape, [1]]
|
| 1279 |
+
output_strides = [
|
| 1280 |
+
get_strides_of_lstm_output(output_shape, batch_first),
|
| 1281 |
+
FlexibleLayout.contiguous_strides(hy_shape),
|
| 1282 |
+
FlexibleLayout.contiguous_strides(cy_shape),
|
| 1283 |
+
[1],
|
| 1284 |
+
]
|
| 1285 |
+
output_ir = [
|
| 1286 |
+
MultiOutput(
|
| 1287 |
+
FixedLayout(
|
| 1288 |
+
x.get_device(), # type: ignore[arg-type]
|
| 1289 |
+
x.get_dtype(),
|
| 1290 |
+
output_size,
|
| 1291 |
+
output_stride,
|
| 1292 |
+
),
|
| 1293 |
+
packed,
|
| 1294 |
+
[(tuple, i)],
|
| 1295 |
+
)
|
| 1296 |
+
for i, (output_size, output_stride) in enumerate(
|
| 1297 |
+
zip(output_sizes, output_strides)
|
| 1298 |
+
)
|
| 1299 |
+
]
|
| 1300 |
+
packed.outputs = output_ir
|
| 1301 |
+
|
| 1302 |
+
return output_ir
|
| 1303 |
+
|
| 1304 |
+
def codegen(self, wrapper):
|
| 1305 |
+
wrapper.include_extra_header("torch/csrc/inductor/aoti_torch/c/shim_cpu.h")
|
| 1306 |
+
return super().codegen(wrapper)
|
| 1307 |
+
|
| 1308 |
+
|
| 1309 |
+
# Add this IR so that we can include shim_cpu.h for cpp_wrapper
|
| 1310 |
+
class WeightInt4PackMatmul(ExternKernelAlloc):
|
| 1311 |
+
def __init__(
|
| 1312 |
+
self,
|
| 1313 |
+
layout,
|
| 1314 |
+
inputs,
|
| 1315 |
+
constant_args=(),
|
| 1316 |
+
) -> None:
|
| 1317 |
+
"""
|
| 1318 |
+
inputs = [x, w, qGroupSize, qScalesAndZeros]
|
| 1319 |
+
constant_args = ()
|
| 1320 |
+
"""
|
| 1321 |
+
assert len(inputs) == 4
|
| 1322 |
+
assert len(constant_args) == 0
|
| 1323 |
+
super().__init__(
|
| 1324 |
+
layout,
|
| 1325 |
+
inputs,
|
| 1326 |
+
constant_args,
|
| 1327 |
+
None,
|
| 1328 |
+
op_overload=(torch.ops.quantized.int4mm_packed_weight_cpu.default),
|
| 1329 |
+
cpp_kernel_name=("aoti_torch_cpu__weight_int4pack_mm_cpu_tensor"),
|
| 1330 |
+
)
|
| 1331 |
+
|
| 1332 |
+
def codegen(self, wrapper):
|
| 1333 |
+
wrapper.include_extra_header("torch/csrc/inductor/aoti_torch/c/shim_cpu.h")
|
| 1334 |
+
super().codegen(wrapper)
|
| 1335 |
+
|
| 1336 |
+
if isinstance(self.layout, Layout):
|
| 1337 |
+
self.codegen_size_asserts(wrapper)
|
| 1338 |
+
|
| 1339 |
+
@classmethod
|
| 1340 |
+
def create(
|
| 1341 |
+
cls,
|
| 1342 |
+
x: "TensorBox",
|
| 1343 |
+
w: "TensorBox",
|
| 1344 |
+
qGroupSize: "TensorBox",
|
| 1345 |
+
qScalesAndZeros: "TensorBox",
|
| 1346 |
+
):
|
| 1347 |
+
inputs = [x, w, qGroupSize, qScalesAndZeros]
|
| 1348 |
+
*m, _ = x.get_size()
|
| 1349 |
+
n, _ = w.get_size()
|
| 1350 |
+
output_size = list(m) + [n]
|
| 1351 |
+
output_stride = FlexibleLayout.contiguous_strides(output_size)
|
| 1352 |
+
kernel_layout = FixedLayout(
|
| 1353 |
+
x.get_device(), # type: ignore[arg-type]
|
| 1354 |
+
x.get_dtype(),
|
| 1355 |
+
output_size,
|
| 1356 |
+
output_stride,
|
| 1357 |
+
)
|
| 1358 |
+
return WeightInt4PackMatmul(
|
| 1359 |
+
layout=kernel_layout,
|
| 1360 |
+
inputs=inputs,
|
| 1361 |
+
)
|
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/mkldnn_lowerings.py
ADDED
|
@@ -0,0 +1,1399 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# mypy: allow-untyped-defs
|
| 2 |
+
import functools
|
| 3 |
+
|
| 4 |
+
import torch
|
| 5 |
+
import torch.utils._pytree as pytree
|
| 6 |
+
from torch._inductor.kernel.mm_common import mm_args
|
| 7 |
+
|
| 8 |
+
from . import config, ir
|
| 9 |
+
from .codegen.cpp_gemm_template import CppGemmTemplate
|
| 10 |
+
from .codegen.cpp_grouped_gemm_template import CppGroupedGemmTemplate
|
| 11 |
+
from .codegen.cpp_utils import create_epilogue_with_attr
|
| 12 |
+
from .ir import TensorBox
|
| 13 |
+
from .lowering import (
|
| 14 |
+
add,
|
| 15 |
+
add_needs_realized_inputs,
|
| 16 |
+
aten,
|
| 17 |
+
permute,
|
| 18 |
+
register_lowering,
|
| 19 |
+
to_dtype,
|
| 20 |
+
view,
|
| 21 |
+
)
|
| 22 |
+
from .select_algorithm import (
|
| 23 |
+
autotune_select_algorithm,
|
| 24 |
+
ChoiceCaller,
|
| 25 |
+
ExternKernelChoice,
|
| 26 |
+
)
|
| 27 |
+
from .utils import use_aten_gemm_kernels, use_cpp_gemm_template
|
| 28 |
+
from .virtualized import ops, OpsValue, V
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def create_int8_compensation(
|
| 32 |
+
W_tensor: torch.Tensor,
|
| 33 |
+
packed_weight: ir.TensorBox,
|
| 34 |
+
x_scale: ir.TensorBox,
|
| 35 |
+
x_zp: ir.TensorBox,
|
| 36 |
+
w_scale: ir.TensorBox,
|
| 37 |
+
) -> tuple[
|
| 38 |
+
bool,
|
| 39 |
+
ir.TensorBox,
|
| 40 |
+
ir.TensorBox | None,
|
| 41 |
+
]:
|
| 42 |
+
x_w_scale: ir.TensorBox | None = None
|
| 43 |
+
use_int8_fast_compensation_path = all(
|
| 44 |
+
isinstance(item, ir.TensorBox)
|
| 45 |
+
and item.get_name() in V.graph.constants
|
| 46 |
+
and hasattr(item.data, "data")
|
| 47 |
+
and isinstance(item.data.data, ir.ConstantBuffer)
|
| 48 |
+
for item in [x_scale, x_zp, w_scale]
|
| 49 |
+
)
|
| 50 |
+
if use_int8_fast_compensation_path:
|
| 51 |
+
x_w_scale_tensor = (
|
| 52 |
+
V.graph.constants[x_scale.get_name()]
|
| 53 |
+
* V.graph.constants[w_scale.get_name()]
|
| 54 |
+
)
|
| 55 |
+
x_w_scale = V.graph.add_tensor_constant(
|
| 56 |
+
x_w_scale_tensor,
|
| 57 |
+
name=packed_weight.get_name() + "_x_w_compens",
|
| 58 |
+
)
|
| 59 |
+
weight_compens_tensor = torch.sum(W_tensor.to(torch.float), dim=0)
|
| 60 |
+
x_zp_tensor = V.graph.constants[x_zp.get_name()]
|
| 61 |
+
weight_compens_tensor = weight_compens_tensor * x_w_scale_tensor * x_zp_tensor
|
| 62 |
+
weight_compens = V.graph.add_tensor_constant(
|
| 63 |
+
weight_compens_tensor,
|
| 64 |
+
name=packed_weight.get_name() + "_BMatrixCompens",
|
| 65 |
+
)
|
| 66 |
+
else:
|
| 67 |
+
weight_compens_tensor = torch.sum(W_tensor.to(torch.float), dim=0)
|
| 68 |
+
weight_compens = V.graph.add_tensor_constant(
|
| 69 |
+
weight_compens_tensor,
|
| 70 |
+
name=packed_weight.get_name() + "_BMatrixCompens",
|
| 71 |
+
)
|
| 72 |
+
return ( # type: ignore[return-type]
|
| 73 |
+
use_int8_fast_compensation_path,
|
| 74 |
+
weight_compens,
|
| 75 |
+
x_w_scale,
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def codegen_int8_gemm_template_compensation(
|
| 80 |
+
use_int8_fast_compensation_path: bool,
|
| 81 |
+
input: OpsValue,
|
| 82 |
+
_weight_compo: OpsValue,
|
| 83 |
+
_x_scale: OpsValue | None,
|
| 84 |
+
_x_zp: OpsValue | None,
|
| 85 |
+
_w_scale: OpsValue | None,
|
| 86 |
+
_x_w_scale: OpsValue | None,
|
| 87 |
+
) -> OpsValue:
|
| 88 |
+
if use_int8_fast_compensation_path:
|
| 89 |
+
temp = ops.sub(
|
| 90 |
+
ops.mul(
|
| 91 |
+
input,
|
| 92 |
+
_x_w_scale,
|
| 93 |
+
),
|
| 94 |
+
_weight_compo,
|
| 95 |
+
)
|
| 96 |
+
else:
|
| 97 |
+
temp = ops.mul(
|
| 98 |
+
ops.mul(
|
| 99 |
+
input,
|
| 100 |
+
_x_scale,
|
| 101 |
+
),
|
| 102 |
+
_w_scale,
|
| 103 |
+
)
|
| 104 |
+
# NOTE: We will apply compensation even if the x_zp is 0 for int8 quantization.
|
| 105 |
+
# That's because when torch.compile is invoked for dynamic quantization,
|
| 106 |
+
# x might coincidentally have such values that x_zp might be zero despite
|
| 107 |
+
# asymmetric quantization.
|
| 108 |
+
# Besides, if x_zp is dummy for int8 x, or if x is statically quantized,
|
| 109 |
+
# we'd still perform that redundant compute to avoid making the code messy
|
| 110 |
+
# because we discovered that redundant computation of compensation did not
|
| 111 |
+
# lead to performance degradation with the input shapes tested.
|
| 112 |
+
temp = ops.sub(
|
| 113 |
+
temp,
|
| 114 |
+
ops.mul(
|
| 115 |
+
ops.mul(
|
| 116 |
+
ops.mul(
|
| 117 |
+
_x_scale,
|
| 118 |
+
_w_scale,
|
| 119 |
+
),
|
| 120 |
+
_x_zp,
|
| 121 |
+
),
|
| 122 |
+
_weight_compo,
|
| 123 |
+
),
|
| 124 |
+
)
|
| 125 |
+
return temp
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def grouped_gemm_lowering(
|
| 129 |
+
x: TensorBox,
|
| 130 |
+
w: list[TensorBox],
|
| 131 |
+
b: list[TensorBox],
|
| 132 |
+
attr=None,
|
| 133 |
+
scalars=None,
|
| 134 |
+
algorithm=None,
|
| 135 |
+
layout=None,
|
| 136 |
+
):
|
| 137 |
+
x_size = x.get_size()
|
| 138 |
+
if len(x_size) > 2:
|
| 139 |
+
# GEMM template needs 2D input, normalize input shape here
|
| 140 |
+
x = view(x, [-1, x_size[-1]])
|
| 141 |
+
num_gemm = len(w)
|
| 142 |
+
|
| 143 |
+
assert config.max_autotune or config.max_autotune_gemm
|
| 144 |
+
# pyrefly: ignore [bad-assignment]
|
| 145 |
+
b = [bias if bias is None else ir.ExternKernel.realize_input(bias) for bias in b]
|
| 146 |
+
|
| 147 |
+
choices: list[ChoiceCaller] = []
|
| 148 |
+
*_, layout, x, _ = mm_args(x, permute(w[0], [1, 0]), layout=layout)
|
| 149 |
+
|
| 150 |
+
kwargs = {
|
| 151 |
+
"has_bias": [bias is not None for bias in b],
|
| 152 |
+
"trans_w": True,
|
| 153 |
+
"epilogue_creator": None,
|
| 154 |
+
"act_mapping": dict.fromkeys(range(num_gemm), x),
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
input_nodes = [x, *w]
|
| 158 |
+
input_nodes.extend([bias for bias in b if bias is not None])
|
| 159 |
+
|
| 160 |
+
CppGroupedGemmTemplate.add_choices(
|
| 161 |
+
choices,
|
| 162 |
+
layout,
|
| 163 |
+
input_nodes,
|
| 164 |
+
**kwargs, # type: ignore[arg-type]
|
| 165 |
+
)
|
| 166 |
+
|
| 167 |
+
assert len(choices) != 0
|
| 168 |
+
result, _ = autotune_select_algorithm(
|
| 169 |
+
"grouped_gemm",
|
| 170 |
+
choices,
|
| 171 |
+
input_nodes,
|
| 172 |
+
layout,
|
| 173 |
+
)
|
| 174 |
+
template_buf = result.data.data
|
| 175 |
+
return_bufs = [
|
| 176 |
+
ir.MultiOutput(layout, template_buf, [(list, gemm_idx)])
|
| 177 |
+
for gemm_idx in range(num_gemm)
|
| 178 |
+
]
|
| 179 |
+
# pyrefly: ignore [bad-argument-type]
|
| 180 |
+
template_buf.layout = ir.MultiOutputLayout(device=input_nodes[0].get_device())
|
| 181 |
+
template_buf.outputs = return_bufs
|
| 182 |
+
return_tensors = [
|
| 183 |
+
ir.TensorBox.create(return_bufs[gemm_idx]) for gemm_idx in range(num_gemm)
|
| 184 |
+
]
|
| 185 |
+
if len(x_size) > 2:
|
| 186 |
+
for gemm_idx in range(num_gemm):
|
| 187 |
+
return_tensors[gemm_idx] = view(
|
| 188 |
+
return_tensors[gemm_idx], # type: ignore[arg-type]
|
| 189 |
+
(*x_size[:-1], return_tensors[gemm_idx].get_size()[-1]),
|
| 190 |
+
)
|
| 191 |
+
return return_tensors
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
grouped_gemm_lowering._inductor_lowering_function = True # type: ignore[attr-defined]
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
def register_onednn_fusion_ops():
|
| 198 |
+
if torch._C._has_mkldnn:
|
| 199 |
+
from . import mkldnn_ir
|
| 200 |
+
|
| 201 |
+
aten_mkldnn_linear_unary = ExternKernelChoice(
|
| 202 |
+
torch.ops.mkldnn._linear_pointwise,
|
| 203 |
+
"mkldnn::_linear_pointwise",
|
| 204 |
+
has_out_variant=False,
|
| 205 |
+
kernel_creator=mkldnn_ir.LinearUnary.create,
|
| 206 |
+
)
|
| 207 |
+
aten_mkldnn_linear_binary = ExternKernelChoice(
|
| 208 |
+
torch.ops.mkldnn._linear_pointwise.binary,
|
| 209 |
+
"mkldnn::_linear_pointwise",
|
| 210 |
+
has_out_variant=False,
|
| 211 |
+
kernel_creator=mkldnn_ir.LinearBinary.create,
|
| 212 |
+
)
|
| 213 |
+
aten_mkldnn_qlinear_unary = ExternKernelChoice(
|
| 214 |
+
torch.ops.onednn.qlinear_pointwise,
|
| 215 |
+
"onednn::qlinear_pointwise",
|
| 216 |
+
has_out_variant=False,
|
| 217 |
+
kernel_creator=mkldnn_ir.QLinearPointwisePT2E.create,
|
| 218 |
+
)
|
| 219 |
+
aten_mkldnn_qlinear_binary = ExternKernelChoice(
|
| 220 |
+
torch.ops.onednn.qlinear_pointwise.binary,
|
| 221 |
+
"onednn::qlinear_pointwise",
|
| 222 |
+
has_out_variant=False,
|
| 223 |
+
kernel_creator=mkldnn_ir.QLinearPointwiseBinaryPT2E.create,
|
| 224 |
+
)
|
| 225 |
+
cpu_needs_realized_inputs: list[
|
| 226 |
+
torch._ops.OpOverload | torch._ops.OpOverloadPacket
|
| 227 |
+
] = [
|
| 228 |
+
torch.ops.mkldnn._convolution_pointwise,
|
| 229 |
+
torch.ops.mkldnn._convolution_pointwise_,
|
| 230 |
+
torch.ops.mkldnn._convolution_transpose_pointwise,
|
| 231 |
+
torch.ops.mkldnn._linear_pointwise,
|
| 232 |
+
aten.mkldnn_rnn_layer.default,
|
| 233 |
+
torch.ops.onednn.qconv_pointwise,
|
| 234 |
+
]
|
| 235 |
+
|
| 236 |
+
@register_lowering(torch.ops.mkldnn._convolution_pointwise)
|
| 237 |
+
def convolution_unary(
|
| 238 |
+
x: TensorBox,
|
| 239 |
+
weight: TensorBox,
|
| 240 |
+
bias: TensorBox,
|
| 241 |
+
padding,
|
| 242 |
+
stride,
|
| 243 |
+
dilation,
|
| 244 |
+
groups,
|
| 245 |
+
attr,
|
| 246 |
+
scalars,
|
| 247 |
+
algorithm,
|
| 248 |
+
):
|
| 249 |
+
return TensorBox.create(
|
| 250 |
+
mkldnn_ir.ConvolutionUnary.create(
|
| 251 |
+
x,
|
| 252 |
+
weight,
|
| 253 |
+
bias,
|
| 254 |
+
padding,
|
| 255 |
+
stride,
|
| 256 |
+
dilation,
|
| 257 |
+
groups,
|
| 258 |
+
attr,
|
| 259 |
+
scalars,
|
| 260 |
+
algorithm,
|
| 261 |
+
)
|
| 262 |
+
)
|
| 263 |
+
|
| 264 |
+
@register_lowering(torch.ops.mkldnn._convolution_pointwise.binary)
|
| 265 |
+
def convolution_binary(
|
| 266 |
+
x: TensorBox,
|
| 267 |
+
other: TensorBox,
|
| 268 |
+
weight: TensorBox,
|
| 269 |
+
bias: TensorBox,
|
| 270 |
+
padding,
|
| 271 |
+
stride,
|
| 272 |
+
dilation,
|
| 273 |
+
groups,
|
| 274 |
+
binary_attr,
|
| 275 |
+
binary_alpha,
|
| 276 |
+
unary_attr,
|
| 277 |
+
unary_scalars,
|
| 278 |
+
unary_algorithm,
|
| 279 |
+
):
|
| 280 |
+
return TensorBox.create(
|
| 281 |
+
mkldnn_ir.ConvolutionBinary.create(
|
| 282 |
+
x,
|
| 283 |
+
other,
|
| 284 |
+
weight,
|
| 285 |
+
bias,
|
| 286 |
+
padding,
|
| 287 |
+
stride,
|
| 288 |
+
dilation,
|
| 289 |
+
groups,
|
| 290 |
+
binary_attr,
|
| 291 |
+
binary_alpha,
|
| 292 |
+
unary_attr,
|
| 293 |
+
unary_scalars,
|
| 294 |
+
unary_algorithm,
|
| 295 |
+
)
|
| 296 |
+
)
|
| 297 |
+
|
| 298 |
+
@register_lowering(torch.ops.mkldnn._convolution_pointwise_.binary)
|
| 299 |
+
def convolution_binary_inplace(
|
| 300 |
+
x: TensorBox,
|
| 301 |
+
other: TensorBox,
|
| 302 |
+
weight: TensorBox,
|
| 303 |
+
bias: TensorBox,
|
| 304 |
+
padding,
|
| 305 |
+
stride,
|
| 306 |
+
dilation,
|
| 307 |
+
groups,
|
| 308 |
+
binary_attr,
|
| 309 |
+
binary_alpha,
|
| 310 |
+
unary_attr,
|
| 311 |
+
unary_scalars,
|
| 312 |
+
unary_algorithm,
|
| 313 |
+
):
|
| 314 |
+
return TensorBox.create(
|
| 315 |
+
mkldnn_ir.ConvolutionBinaryInplace.create(
|
| 316 |
+
x,
|
| 317 |
+
other,
|
| 318 |
+
weight,
|
| 319 |
+
bias,
|
| 320 |
+
padding,
|
| 321 |
+
stride,
|
| 322 |
+
dilation,
|
| 323 |
+
groups,
|
| 324 |
+
binary_attr,
|
| 325 |
+
binary_alpha,
|
| 326 |
+
unary_attr,
|
| 327 |
+
unary_scalars,
|
| 328 |
+
unary_algorithm,
|
| 329 |
+
)
|
| 330 |
+
)
|
| 331 |
+
|
| 332 |
+
@register_lowering(torch.ops.mkldnn._linear_pointwise)
|
| 333 |
+
def linear_unary(
|
| 334 |
+
x: TensorBox,
|
| 335 |
+
w: TensorBox,
|
| 336 |
+
b: TensorBox,
|
| 337 |
+
attr,
|
| 338 |
+
scalars,
|
| 339 |
+
algorithm,
|
| 340 |
+
layout=None,
|
| 341 |
+
):
|
| 342 |
+
x_size = x.get_size()
|
| 343 |
+
if len(x_size) > 2:
|
| 344 |
+
# GEMM template needs 2D input, normalize input shape here
|
| 345 |
+
x = view(x, [-1, x_size[-1]])
|
| 346 |
+
if b is not None:
|
| 347 |
+
b = ir.ExternKernel.realize_input(b) # type: ignore[assignment]
|
| 348 |
+
choices: list[ChoiceCaller] = []
|
| 349 |
+
if config.max_autotune or config.max_autotune_gemm:
|
| 350 |
+
transposed_w = permute(w, [1, 0])
|
| 351 |
+
*_, layout, x, transposed_w = mm_args(x, transposed_w, layout=layout)
|
| 352 |
+
if use_cpp_gemm_template(layout, x, transposed_w):
|
| 353 |
+
|
| 354 |
+
def epilogue_creator(buf):
|
| 355 |
+
return create_epilogue_with_attr(
|
| 356 |
+
buf, attr, scalars=scalars, algorithm=algorithm
|
| 357 |
+
)
|
| 358 |
+
|
| 359 |
+
kwargs = {
|
| 360 |
+
"has_bias": b is not None,
|
| 361 |
+
"trans_w": True,
|
| 362 |
+
"epilogue_creator": (
|
| 363 |
+
None if attr == "none" else epilogue_creator
|
| 364 |
+
),
|
| 365 |
+
}
|
| 366 |
+
if b is not None:
|
| 367 |
+
kwargs["input_indices"] = [2, 0, 1] # type: ignore[assignment]
|
| 368 |
+
CppGemmTemplate.add_choices(
|
| 369 |
+
choices,
|
| 370 |
+
layout,
|
| 371 |
+
[x, w] if b is None else [x, w, b],
|
| 372 |
+
**kwargs, # type: ignore[arg-type]
|
| 373 |
+
)
|
| 374 |
+
if len(choices) == 0 or use_aten_gemm_kernels():
|
| 375 |
+
kwargs = dict(attr=attr, scalars=scalars, algorithm=algorithm)
|
| 376 |
+
if b is None:
|
| 377 |
+
kwargs["B"] = None
|
| 378 |
+
choices.append(
|
| 379 |
+
aten_mkldnn_linear_unary.bind(
|
| 380 |
+
[x, w] if b is None else [x, w, b],
|
| 381 |
+
layout,
|
| 382 |
+
**kwargs,
|
| 383 |
+
)
|
| 384 |
+
)
|
| 385 |
+
assert w.get_name() in V.graph.constants
|
| 386 |
+
input_gen_fns = {
|
| 387 |
+
1: lambda x: V.graph.constants[x.get_name()],
|
| 388 |
+
}
|
| 389 |
+
result, _ = autotune_select_algorithm(
|
| 390 |
+
"linear_unary",
|
| 391 |
+
choices,
|
| 392 |
+
[x, w] if b is None else [x, w, b],
|
| 393 |
+
layout,
|
| 394 |
+
input_gen_fns=input_gen_fns,
|
| 395 |
+
)
|
| 396 |
+
if len(x_size) > 2:
|
| 397 |
+
result = view(result, (*x_size[:-1], result.get_size()[-1]))
|
| 398 |
+
return result
|
| 399 |
+
|
| 400 |
+
@register_lowering(torch.ops.mkldnn._linear_pointwise.binary)
|
| 401 |
+
def linear_binary(
|
| 402 |
+
x: TensorBox, y: TensorBox, w: TensorBox, b: TensorBox, attr, layout=None
|
| 403 |
+
):
|
| 404 |
+
x_size = x.get_size()
|
| 405 |
+
if len(x_size) > 2:
|
| 406 |
+
# GEMM template needs 2D input, normalize input shape here
|
| 407 |
+
x = view(x, [-1, x_size[-1]])
|
| 408 |
+
y_size = y.get_size()
|
| 409 |
+
if len(y_size) > 2:
|
| 410 |
+
y = view(y, [-1, y_size[-1]])
|
| 411 |
+
if b is not None:
|
| 412 |
+
b = ir.ExternKernel.realize_input(b) # type: ignore[assignment]
|
| 413 |
+
choices: list[ChoiceCaller] = []
|
| 414 |
+
if config.max_autotune or config.max_autotune_gemm:
|
| 415 |
+
transposed_w = permute(w, [1, 0])
|
| 416 |
+
*_, layout, x, transposed_w, y = mm_args(
|
| 417 |
+
x, transposed_w, y, layout=layout
|
| 418 |
+
)
|
| 419 |
+
if use_cpp_gemm_template(layout, x, transposed_w):
|
| 420 |
+
|
| 421 |
+
def epilogue_creator(buf):
|
| 422 |
+
return create_epilogue_with_attr(buf, attr, other=y)
|
| 423 |
+
|
| 424 |
+
kwargs = {
|
| 425 |
+
"has_bias": b is not None,
|
| 426 |
+
"trans_w": True,
|
| 427 |
+
"epilogue_creator": epilogue_creator,
|
| 428 |
+
}
|
| 429 |
+
|
| 430 |
+
# pyrefly: ignore [bad-typed-dict-key, unsupported-operation]
|
| 431 |
+
kwargs["input_indices"] = [0, 2, 1] if b is None else [3, 0, 2, 1]
|
| 432 |
+
CppGemmTemplate.add_choices(
|
| 433 |
+
choices,
|
| 434 |
+
layout,
|
| 435 |
+
[x, y, w] if b is None else [x, y, w, b],
|
| 436 |
+
**kwargs, # type: ignore[arg-type]
|
| 437 |
+
)
|
| 438 |
+
if len(choices) == 0 or use_aten_gemm_kernels():
|
| 439 |
+
kwargs = dict(attr=attr)
|
| 440 |
+
if b is None:
|
| 441 |
+
kwargs["B"] = None
|
| 442 |
+
choices.append(
|
| 443 |
+
aten_mkldnn_linear_binary.bind(
|
| 444 |
+
[x, y, w] if b is None else [x, y, w, b],
|
| 445 |
+
layout,
|
| 446 |
+
**kwargs,
|
| 447 |
+
)
|
| 448 |
+
)
|
| 449 |
+
assert w.get_name() in V.graph.constants
|
| 450 |
+
input_gen_fns = {
|
| 451 |
+
2: lambda x: V.graph.constants[x.get_name()],
|
| 452 |
+
}
|
| 453 |
+
result, _ = autotune_select_algorithm(
|
| 454 |
+
"linear_binary",
|
| 455 |
+
choices,
|
| 456 |
+
[x, y, w] if b is None else [x, y, w, b],
|
| 457 |
+
layout,
|
| 458 |
+
input_gen_fns=input_gen_fns,
|
| 459 |
+
)
|
| 460 |
+
if len(x_size) > 2:
|
| 461 |
+
result = view(result, (*x_size[:-1], result.get_size()[-1]))
|
| 462 |
+
return result
|
| 463 |
+
|
| 464 |
+
@register_lowering(torch.ops.mkldnn._convolution_transpose_pointwise)
|
| 465 |
+
def convolution_transpose_unary(
|
| 466 |
+
x: TensorBox,
|
| 467 |
+
weight: TensorBox,
|
| 468 |
+
bias: TensorBox,
|
| 469 |
+
padding,
|
| 470 |
+
output_padding,
|
| 471 |
+
stride,
|
| 472 |
+
dilation,
|
| 473 |
+
groups,
|
| 474 |
+
attr,
|
| 475 |
+
scalars,
|
| 476 |
+
algorithm,
|
| 477 |
+
):
|
| 478 |
+
return TensorBox.create(
|
| 479 |
+
mkldnn_ir.ConvolutionTransposeUnary.create(
|
| 480 |
+
x,
|
| 481 |
+
weight,
|
| 482 |
+
bias,
|
| 483 |
+
padding,
|
| 484 |
+
output_padding,
|
| 485 |
+
stride,
|
| 486 |
+
dilation,
|
| 487 |
+
groups,
|
| 488 |
+
attr,
|
| 489 |
+
scalars,
|
| 490 |
+
algorithm,
|
| 491 |
+
)
|
| 492 |
+
)
|
| 493 |
+
|
| 494 |
+
@register_lowering(aten.mkldnn_rnn_layer.default)
|
| 495 |
+
def mkldnn_rnn_layer(
|
| 496 |
+
x: TensorBox,
|
| 497 |
+
w0: TensorBox,
|
| 498 |
+
w1: TensorBox,
|
| 499 |
+
w2: TensorBox,
|
| 500 |
+
w3: TensorBox,
|
| 501 |
+
hx: TensorBox,
|
| 502 |
+
cx: TensorBox,
|
| 503 |
+
reverse: bool,
|
| 504 |
+
batch_sizes: list[int],
|
| 505 |
+
mode: int,
|
| 506 |
+
hidden_size: int,
|
| 507 |
+
num_layers: int,
|
| 508 |
+
has_biases: bool,
|
| 509 |
+
bidirectional: bool,
|
| 510 |
+
batch_first: bool,
|
| 511 |
+
train: bool,
|
| 512 |
+
):
|
| 513 |
+
return pytree.tree_map(
|
| 514 |
+
TensorBox.create,
|
| 515 |
+
mkldnn_ir.MkldnnRnnLayer.create(
|
| 516 |
+
x,
|
| 517 |
+
w0,
|
| 518 |
+
w1,
|
| 519 |
+
w2,
|
| 520 |
+
w3,
|
| 521 |
+
hx,
|
| 522 |
+
cx,
|
| 523 |
+
reverse,
|
| 524 |
+
batch_sizes,
|
| 525 |
+
mode,
|
| 526 |
+
hidden_size,
|
| 527 |
+
num_layers,
|
| 528 |
+
has_biases,
|
| 529 |
+
bidirectional,
|
| 530 |
+
batch_first,
|
| 531 |
+
train,
|
| 532 |
+
),
|
| 533 |
+
)
|
| 534 |
+
|
| 535 |
+
@register_lowering(torch.ops.onednn.qconv_pointwise, type_promotion_kind=None)
|
| 536 |
+
def qconvolution_unary(
|
| 537 |
+
x: TensorBox,
|
| 538 |
+
x_scale,
|
| 539 |
+
x_zp,
|
| 540 |
+
packed_weight: TensorBox,
|
| 541 |
+
w_scale: TensorBox,
|
| 542 |
+
w_zp,
|
| 543 |
+
bias: TensorBox,
|
| 544 |
+
stride,
|
| 545 |
+
padding,
|
| 546 |
+
dilation,
|
| 547 |
+
groups,
|
| 548 |
+
o_inv_scale,
|
| 549 |
+
o_zero_point,
|
| 550 |
+
output_dtype,
|
| 551 |
+
attr,
|
| 552 |
+
scalars,
|
| 553 |
+
algorithm,
|
| 554 |
+
):
|
| 555 |
+
if not isinstance(x_scale, ir.TensorBox):
|
| 556 |
+
assert type(x_scale) is float
|
| 557 |
+
x_scale = V.graph.add_tensor_constant(
|
| 558 |
+
torch.tensor(x_scale, dtype=torch.float32), name="x_scale"
|
| 559 |
+
)
|
| 560 |
+
|
| 561 |
+
if x_zp is None:
|
| 562 |
+
x_zp = V.graph.add_tensor_constant(
|
| 563 |
+
torch.tensor(0, dtype=torch.int32), name="x_zp"
|
| 564 |
+
)
|
| 565 |
+
if not isinstance(x_zp, ir.TensorBox):
|
| 566 |
+
assert type(x_zp) is int
|
| 567 |
+
x_zp = V.graph.add_tensor_constant(
|
| 568 |
+
torch.tensor(x_zp, dtype=torch.int32), name="x_zp"
|
| 569 |
+
)
|
| 570 |
+
|
| 571 |
+
if w_zp is None:
|
| 572 |
+
w_zp = V.graph.add_tensor_constant(
|
| 573 |
+
torch.tensor(0, dtype=torch.int32), name="w_zp"
|
| 574 |
+
)
|
| 575 |
+
|
| 576 |
+
return TensorBox.create(
|
| 577 |
+
mkldnn_ir.QConvPointWisePT2E.create(
|
| 578 |
+
x,
|
| 579 |
+
x_scale,
|
| 580 |
+
x_zp,
|
| 581 |
+
packed_weight,
|
| 582 |
+
w_scale,
|
| 583 |
+
w_zp,
|
| 584 |
+
bias,
|
| 585 |
+
stride,
|
| 586 |
+
padding,
|
| 587 |
+
dilation,
|
| 588 |
+
groups,
|
| 589 |
+
o_inv_scale,
|
| 590 |
+
o_zero_point,
|
| 591 |
+
output_dtype,
|
| 592 |
+
attr,
|
| 593 |
+
scalars,
|
| 594 |
+
algorithm,
|
| 595 |
+
)
|
| 596 |
+
)
|
| 597 |
+
|
| 598 |
+
@register_lowering(
|
| 599 |
+
torch.ops.onednn.qconv2d_pointwise.binary, type_promotion_kind=None
|
| 600 |
+
)
|
| 601 |
+
@register_lowering(
|
| 602 |
+
torch.ops.onednn.qconv2d_pointwise.binary_tensor, type_promotion_kind=None
|
| 603 |
+
)
|
| 604 |
+
def qconvolution_binary(
|
| 605 |
+
x: TensorBox,
|
| 606 |
+
x_scale,
|
| 607 |
+
x_zp,
|
| 608 |
+
packed_weight: TensorBox,
|
| 609 |
+
w_scale: TensorBox,
|
| 610 |
+
w_zp,
|
| 611 |
+
accum: TensorBox,
|
| 612 |
+
bias: TensorBox,
|
| 613 |
+
stride,
|
| 614 |
+
padding,
|
| 615 |
+
dilation,
|
| 616 |
+
groups,
|
| 617 |
+
o_inv_scale,
|
| 618 |
+
o_zero_point,
|
| 619 |
+
output_dtype,
|
| 620 |
+
accum_scale,
|
| 621 |
+
accum_zp,
|
| 622 |
+
binary_attr,
|
| 623 |
+
alpha,
|
| 624 |
+
unary_attr,
|
| 625 |
+
unary_scalars,
|
| 626 |
+
unary_algorithm,
|
| 627 |
+
):
|
| 628 |
+
if not isinstance(x_scale, ir.TensorBox):
|
| 629 |
+
assert type(x_scale) is float
|
| 630 |
+
x_scale = V.graph.add_tensor_constant(
|
| 631 |
+
torch.tensor(x_scale, dtype=torch.float32), name="x_scale"
|
| 632 |
+
)
|
| 633 |
+
|
| 634 |
+
if x_zp is None:
|
| 635 |
+
x_zp = V.graph.add_tensor_constant(
|
| 636 |
+
torch.tensor(0, dtype=torch.int32), name="x_zp"
|
| 637 |
+
)
|
| 638 |
+
if not isinstance(x_zp, ir.TensorBox):
|
| 639 |
+
assert type(x_zp) is int
|
| 640 |
+
x_zp = V.graph.add_tensor_constant(
|
| 641 |
+
torch.tensor(x_zp, dtype=torch.int32), name="x_zp"
|
| 642 |
+
)
|
| 643 |
+
|
| 644 |
+
if w_zp is None:
|
| 645 |
+
w_zp = V.graph.add_tensor_constant(
|
| 646 |
+
torch.tensor(0, dtype=torch.int32), name="w_zp"
|
| 647 |
+
)
|
| 648 |
+
|
| 649 |
+
if (
|
| 650 |
+
binary_attr == "sum"
|
| 651 |
+
and output_dtype in [torch.float32, torch.bfloat16]
|
| 652 |
+
and accum.get_dtype() in [torch.float32, torch.bfloat16]
|
| 653 |
+
and accum.get_dtype() != output_dtype
|
| 654 |
+
):
|
| 655 |
+
# For int8-mixed-bf16 quantization and inplace add,
|
| 656 |
+
# there is case when accum dtype is float32 but output dtype is bfloat16.
|
| 657 |
+
# Since the accum will be inplaced changed with post op sum,
|
| 658 |
+
# we will do accum dtype conversion here.
|
| 659 |
+
accum = to_dtype(accum, output_dtype)
|
| 660 |
+
return TensorBox.create(
|
| 661 |
+
mkldnn_ir.QConvPointWiseBinaryPT2E.create(
|
| 662 |
+
x,
|
| 663 |
+
x_scale, # type: ignore[arg-type]
|
| 664 |
+
x_zp, # type: ignore[arg-type]
|
| 665 |
+
packed_weight,
|
| 666 |
+
w_scale,
|
| 667 |
+
w_zp,
|
| 668 |
+
accum,
|
| 669 |
+
bias,
|
| 670 |
+
stride,
|
| 671 |
+
padding,
|
| 672 |
+
dilation,
|
| 673 |
+
groups,
|
| 674 |
+
o_inv_scale,
|
| 675 |
+
o_zero_point,
|
| 676 |
+
output_dtype,
|
| 677 |
+
accum_scale,
|
| 678 |
+
accum_zp,
|
| 679 |
+
binary_attr,
|
| 680 |
+
alpha,
|
| 681 |
+
unary_attr,
|
| 682 |
+
unary_scalars,
|
| 683 |
+
unary_algorithm,
|
| 684 |
+
)
|
| 685 |
+
)
|
| 686 |
+
|
| 687 |
+
@register_lowering(torch.ops.onednn.qlinear_pointwise, type_promotion_kind=None)
|
| 688 |
+
def qlinear_unary(
|
| 689 |
+
x: TensorBox,
|
| 690 |
+
x_scale,
|
| 691 |
+
x_zp,
|
| 692 |
+
packed_weight: TensorBox,
|
| 693 |
+
w_scale: TensorBox,
|
| 694 |
+
w_zp: TensorBox,
|
| 695 |
+
bias: TensorBox,
|
| 696 |
+
o_scale,
|
| 697 |
+
o_zero_point,
|
| 698 |
+
output_dtype,
|
| 699 |
+
attr,
|
| 700 |
+
scalars,
|
| 701 |
+
algorithm,
|
| 702 |
+
layout=None,
|
| 703 |
+
):
|
| 704 |
+
assert packed_weight.get_dtype() in [torch.int8, torch.float8_e4m3fn], (
|
| 705 |
+
"Only int8 and e4m3fn weights are supported by oneDNN qlinear."
|
| 706 |
+
)
|
| 707 |
+
x_size = x.get_size()
|
| 708 |
+
if len(x_size) > 2:
|
| 709 |
+
# GEMM template needs 2D input, normalize input shape here
|
| 710 |
+
x = view(x, [-1, x_size[-1]])
|
| 711 |
+
if not isinstance(x_scale, ir.TensorBox):
|
| 712 |
+
assert type(x_scale) is float
|
| 713 |
+
x_scale = V.graph.add_tensor_constant(
|
| 714 |
+
torch.tensor(x_scale, dtype=torch.float32), name="x_scale"
|
| 715 |
+
)
|
| 716 |
+
else:
|
| 717 |
+
x_scale.realize()
|
| 718 |
+
if all(dim == 1 for dim in x_scale.get_size()):
|
| 719 |
+
# Corner-case discovered with LLaMA series.
|
| 720 |
+
# If all outer dims of x_scale are 1, make it a 0D tensor.
|
| 721 |
+
# Otherwise, epilogue creator will run into indexing issues.
|
| 722 |
+
x_scale = view(x_scale, [])
|
| 723 |
+
assert len(x_scale.get_size()) in [0, 1], "x_scale must be 0D or 1D"
|
| 724 |
+
|
| 725 |
+
if x_zp is None:
|
| 726 |
+
# If x_zp is None, x is int8 quantized per-tensor and its scale is not reshaped,
|
| 727 |
+
# then the codegened code would segfault if we don't create a tensor for x_zp.
|
| 728 |
+
# It's safe to do so since x is a symmetrically quantized int8 tensor.
|
| 729 |
+
# Moreover, oneDNN qlinear API doesn't accept None value for zp
|
| 730 |
+
x_zp = V.graph.add_tensor_constant(
|
| 731 |
+
torch.tensor(0, dtype=torch.int32), name="x_zp"
|
| 732 |
+
)
|
| 733 |
+
if not isinstance(x_zp, ir.TensorBox):
|
| 734 |
+
assert type(x_zp) is int
|
| 735 |
+
x_zp = V.graph.add_tensor_constant(
|
| 736 |
+
torch.tensor(x_zp, dtype=torch.int32), name="x_zp"
|
| 737 |
+
)
|
| 738 |
+
else:
|
| 739 |
+
x_zp.realize()
|
| 740 |
+
|
| 741 |
+
assert x_zp.get_numel() == 1, "x_zp is incompatible with oneDNN qlinear"
|
| 742 |
+
|
| 743 |
+
# When channels less than 8, w_scale/w_zp is Pointwise instead of ConstantBuffer
|
| 744 |
+
# Refer to
|
| 745 |
+
# https://github.com/pytorch/pytorch/blob/f353d17755ed23b02924c962a86ff99a3405fe10/torch/_inductor/graph.py#L570-L577 # noqa: B950
|
| 746 |
+
if w_zp is None:
|
| 747 |
+
# If w_zp is None, then it's a dummy tensor created to denote the
|
| 748 |
+
# absence of a zero point, and thus w is int8 symmetrically quantized.
|
| 749 |
+
# Moreover, oneDNN qlinear API doesn't accept None value for zp
|
| 750 |
+
|
| 751 |
+
w_zp = V.graph.add_tensor_constant(
|
| 752 |
+
torch.tensor(0, dtype=torch.int32), name="w_zp"
|
| 753 |
+
)
|
| 754 |
+
w_scale.realize()
|
| 755 |
+
w_zp.realize()
|
| 756 |
+
if w_zp.get_dtype() != torch.int32 and isinstance(
|
| 757 |
+
ir.InputsKernel.unwrap_storage_for_input(w_zp),
|
| 758 |
+
ir.ConstantBuffer,
|
| 759 |
+
):
|
| 760 |
+
# W_zp might be a ConstantBuffer with int64, convert it to int32
|
| 761 |
+
w_zp_tensor = V.graph.constants[w_zp.get_name()].to(torch.int32)
|
| 762 |
+
w_zp = V.graph.add_tensor_constant( # type: ignore[assignment]
|
| 763 |
+
torch.tensor(w_zp_tensor, dtype=torch.int32), name=w_zp.get_name()
|
| 764 |
+
)
|
| 765 |
+
|
| 766 |
+
bias_dtype = None if bias is None else bias.get_dtype()
|
| 767 |
+
choices: list[ChoiceCaller] = []
|
| 768 |
+
|
| 769 |
+
if config.max_autotune or config.max_autotune_gemm:
|
| 770 |
+
*_, layout, x, packed_weight = mm_args(
|
| 771 |
+
x, packed_weight, layout=layout, out_dtype=output_dtype
|
| 772 |
+
)
|
| 773 |
+
|
| 774 |
+
if (
|
| 775 |
+
# GEMM template currently only supports symmetrically quantized weights
|
| 776 |
+
isinstance(
|
| 777 |
+
ir.InputsKernel.unwrap_storage_for_input(w_zp),
|
| 778 |
+
ir.ConstantBuffer,
|
| 779 |
+
)
|
| 780 |
+
and torch.equal(
|
| 781 |
+
torch.zeros_like(V.graph.constants[w_zp.get_name()]),
|
| 782 |
+
V.graph.constants[w_zp.get_name()],
|
| 783 |
+
)
|
| 784 |
+
) and use_cpp_gemm_template(layout, x, packed_weight):
|
| 785 |
+
W_tensor = V.graph.constants[packed_weight.get_name()].to_dense()
|
| 786 |
+
|
| 787 |
+
(
|
| 788 |
+
use_int8_fast_compensation_path,
|
| 789 |
+
weight_compens,
|
| 790 |
+
x_w_scale,
|
| 791 |
+
) = create_int8_compensation(
|
| 792 |
+
W_tensor,
|
| 793 |
+
packed_weight,
|
| 794 |
+
x_scale,
|
| 795 |
+
x_zp,
|
| 796 |
+
w_scale,
|
| 797 |
+
)
|
| 798 |
+
|
| 799 |
+
def epilogue_creator(input_buffer):
|
| 800 |
+
# Epilogue to convert from s32 to f32 for u8s8f32
|
| 801 |
+
assert output_dtype in [
|
| 802 |
+
torch.float32,
|
| 803 |
+
torch.bfloat16,
|
| 804 |
+
torch.uint8,
|
| 805 |
+
torch.int8,
|
| 806 |
+
]
|
| 807 |
+
input_loader = input_buffer.make_loader()
|
| 808 |
+
weight_compens_loader = weight_compens.make_loader()
|
| 809 |
+
x_w_scale_loader = None
|
| 810 |
+
if use_int8_fast_compensation_path:
|
| 811 |
+
assert x_w_scale is not None
|
| 812 |
+
x_w_scale_loader = x_w_scale.make_loader()
|
| 813 |
+
x_scale_loader = x_scale.make_loader()
|
| 814 |
+
w_scale_loader = w_scale.make_loader()
|
| 815 |
+
x_zp_loader = x_zp.make_loader()
|
| 816 |
+
nonlocal bias
|
| 817 |
+
bias_loader = None
|
| 818 |
+
if bias is not None:
|
| 819 |
+
bias_loader = bias.make_loader()
|
| 820 |
+
|
| 821 |
+
def inner_fn(index):
|
| 822 |
+
nonlocal bias
|
| 823 |
+
input = input_loader(index)
|
| 824 |
+
# MicroKernel Output is with int32
|
| 825 |
+
# cvt to FP32 before doing compensation
|
| 826 |
+
input = ops.to_dtype(input, torch.float32)
|
| 827 |
+
weight_compens_index = (index[-1],)
|
| 828 |
+
|
| 829 |
+
_x_scale = None
|
| 830 |
+
_x_zp = None
|
| 831 |
+
_w_scale = None
|
| 832 |
+
if not use_int8_fast_compensation_path:
|
| 833 |
+
_x_scale = x_scale_loader(())
|
| 834 |
+
_x_zp = x_zp_loader(())
|
| 835 |
+
_w_scale = w_scale_loader(weight_compens_index)
|
| 836 |
+
_weight_compo = weight_compens_loader(weight_compens_index)
|
| 837 |
+
_x_w_scale = None
|
| 838 |
+
if use_int8_fast_compensation_path:
|
| 839 |
+
assert x_w_scale_loader is not None
|
| 840 |
+
_x_w_scale = x_w_scale_loader(weight_compens_index)
|
| 841 |
+
# Step 1: Compute s8s8->s32 or u8s8->s32 GEMM & then apply compensation
|
| 842 |
+
temp = codegen_int8_gemm_template_compensation(
|
| 843 |
+
use_int8_fast_compensation_path,
|
| 844 |
+
input,
|
| 845 |
+
_weight_compo,
|
| 846 |
+
_x_scale,
|
| 847 |
+
_x_zp,
|
| 848 |
+
_w_scale,
|
| 849 |
+
_x_w_scale,
|
| 850 |
+
)
|
| 851 |
+
# Step 2: add Bias if applicable
|
| 852 |
+
if bias is not None:
|
| 853 |
+
# pyrefly: ignore [not-callable]
|
| 854 |
+
_bias = bias_loader(weight_compens_index)
|
| 855 |
+
nonlocal bias_dtype
|
| 856 |
+
assert bias_dtype in [torch.float32, torch.bfloat16]
|
| 857 |
+
if bias_dtype == torch.bfloat16:
|
| 858 |
+
_bias = ops.to_dtype(_bias, torch.float32)
|
| 859 |
+
temp = ops.add(temp, _bias)
|
| 860 |
+
|
| 861 |
+
return temp
|
| 862 |
+
|
| 863 |
+
output_buf = ir.Pointwise(
|
| 864 |
+
device=input_buffer.get_device(),
|
| 865 |
+
dtype=torch.float32, # Hardcode to FP32 for u8s8f32 & s8s8f32
|
| 866 |
+
inner_fn=inner_fn,
|
| 867 |
+
ranges=input_buffer.get_size(),
|
| 868 |
+
)
|
| 869 |
+
|
| 870 |
+
# Step 3: Doing the unary post op fusion
|
| 871 |
+
if attr != "none":
|
| 872 |
+
output_buf = create_epilogue_with_attr(
|
| 873 |
+
output_buf, attr, scalars=scalars, algorithm=algorithm
|
| 874 |
+
)
|
| 875 |
+
|
| 876 |
+
# Step 4: Cast output to Target Dtype
|
| 877 |
+
if output_dtype == torch.bfloat16:
|
| 878 |
+
output_cast_loader = output_buf.make_loader()
|
| 879 |
+
|
| 880 |
+
def inner_fn_cast_output_to_bf16(index):
|
| 881 |
+
input = output_cast_loader(index)
|
| 882 |
+
return ops.to_dtype(input, output_dtype)
|
| 883 |
+
|
| 884 |
+
output_buf = ir.Pointwise(
|
| 885 |
+
device=output_buf.get_device_or_error(),
|
| 886 |
+
dtype=output_dtype,
|
| 887 |
+
inner_fn=inner_fn_cast_output_to_bf16,
|
| 888 |
+
ranges=output_buf.get_size(),
|
| 889 |
+
)
|
| 890 |
+
elif output_dtype in [torch.uint8, torch.int8]:
|
| 891 |
+
from .lowering import _create_constants
|
| 892 |
+
|
| 893 |
+
requant_input_loader = output_buf.make_loader()
|
| 894 |
+
|
| 895 |
+
def inner_fn_requant(index, scale, zero_point):
|
| 896 |
+
input = requant_input_loader(index)
|
| 897 |
+
inv_scale, zero_point = _create_constants(
|
| 898 |
+
1.0 / scale, zero_point, dtype=torch.float32
|
| 899 |
+
)
|
| 900 |
+
val = ops.round(input * inv_scale) + zero_point
|
| 901 |
+
if output_dtype == torch.uint8:
|
| 902 |
+
qmin, qmax = _create_constants(
|
| 903 |
+
0, 255, dtype=torch.float32
|
| 904 |
+
)
|
| 905 |
+
else:
|
| 906 |
+
qmin, qmax = _create_constants(
|
| 907 |
+
-128, 127, dtype=torch.float32
|
| 908 |
+
)
|
| 909 |
+
clamped = ops.minimum(ops.maximum(val, qmin), qmax)
|
| 910 |
+
return ops.to_dtype(clamped, output_dtype)
|
| 911 |
+
|
| 912 |
+
output_buf = ir.Pointwise(
|
| 913 |
+
device=output_buf.get_device_or_error(),
|
| 914 |
+
dtype=output_dtype,
|
| 915 |
+
inner_fn=functools.partial(
|
| 916 |
+
inner_fn_requant,
|
| 917 |
+
scale=float(o_scale),
|
| 918 |
+
zero_point=int(o_zero_point),
|
| 919 |
+
),
|
| 920 |
+
ranges=output_buf.get_size(),
|
| 921 |
+
)
|
| 922 |
+
|
| 923 |
+
return output_buf
|
| 924 |
+
|
| 925 |
+
assert x.get_dtype() in [torch.uint8, torch.int8]
|
| 926 |
+
CppGemmTemplate.add_choices(
|
| 927 |
+
choices,
|
| 928 |
+
layout,
|
| 929 |
+
[x, x_scale, x_zp, packed_weight, w_scale, w_zp]
|
| 930 |
+
if bias is None
|
| 931 |
+
else [x, x_scale, x_zp, packed_weight, w_scale, w_zp, bias],
|
| 932 |
+
has_bias=bias is not None,
|
| 933 |
+
epilogue_creator=epilogue_creator,
|
| 934 |
+
input_indices=[0, 3, 1, 2, 4, 5]
|
| 935 |
+
if bias is None
|
| 936 |
+
else [6, 0, 3, 1, 2, 4, 5],
|
| 937 |
+
)
|
| 938 |
+
if len(choices) == 0 or use_aten_gemm_kernels():
|
| 939 |
+
kwargs = dict(
|
| 940 |
+
output_scale=o_scale,
|
| 941 |
+
output_zero_point=o_zero_point,
|
| 942 |
+
output_dtype=output_dtype,
|
| 943 |
+
post_op_name=attr,
|
| 944 |
+
post_op_args=scalars,
|
| 945 |
+
post_op_algorithm=algorithm,
|
| 946 |
+
)
|
| 947 |
+
if bias is None:
|
| 948 |
+
kwargs["bias"] = None
|
| 949 |
+
choices.append(
|
| 950 |
+
aten_mkldnn_qlinear_unary.bind(
|
| 951 |
+
(x, x_scale, x_zp, packed_weight, w_scale, w_zp)
|
| 952 |
+
if bias is None
|
| 953 |
+
else (x, x_scale, x_zp, packed_weight, w_scale, w_zp, bias),
|
| 954 |
+
layout,
|
| 955 |
+
**kwargs,
|
| 956 |
+
)
|
| 957 |
+
)
|
| 958 |
+
assert packed_weight.get_name() in V.graph.constants
|
| 959 |
+
input_gen_fns = {
|
| 960 |
+
3: lambda x: V.graph.constants[x.get_name()], # packed weight
|
| 961 |
+
4: lambda x: V.graph.constants[x.get_name()], # weight scale
|
| 962 |
+
5: lambda x: V.graph.constants[x.get_name()], # weight zp
|
| 963 |
+
6: lambda x: V.graph.constants[x.get_name()], # bias
|
| 964 |
+
}
|
| 965 |
+
if isinstance(
|
| 966 |
+
ir.InputsKernel.unwrap_storage_for_input(x_scale),
|
| 967 |
+
ir.ConstantBuffer,
|
| 968 |
+
):
|
| 969 |
+
# x is statically quantized
|
| 970 |
+
input_gen_fns[1] = lambda x: V.graph.constants[x.get_name()]
|
| 971 |
+
if isinstance(
|
| 972 |
+
ir.InputsKernel.unwrap_storage_for_input(x_zp),
|
| 973 |
+
ir.ConstantBuffer,
|
| 974 |
+
):
|
| 975 |
+
input_gen_fns[2] = lambda x: V.graph.constants[x.get_name()]
|
| 976 |
+
|
| 977 |
+
result, _ = autotune_select_algorithm(
|
| 978 |
+
"qlinear_unary",
|
| 979 |
+
choices,
|
| 980 |
+
[x, x_scale, x_zp, packed_weight, w_scale, w_zp]
|
| 981 |
+
if bias is None
|
| 982 |
+
else [x, x_scale, x_zp, packed_weight, w_scale, w_zp, bias],
|
| 983 |
+
layout,
|
| 984 |
+
input_gen_fns=input_gen_fns,
|
| 985 |
+
)
|
| 986 |
+
if len(x_size) > 2:
|
| 987 |
+
result = view(result, (*x_size[:-1], result.get_size()[-1]))
|
| 988 |
+
return result
|
| 989 |
+
|
| 990 |
+
@register_lowering(
|
| 991 |
+
torch.ops.onednn.qlinear_pointwise.binary, type_promotion_kind=None
|
| 992 |
+
)
|
| 993 |
+
@register_lowering(
|
| 994 |
+
torch.ops.onednn.qlinear_pointwise.binary_tensor, type_promotion_kind=None
|
| 995 |
+
)
|
| 996 |
+
def qlinear_binary(
|
| 997 |
+
x: TensorBox,
|
| 998 |
+
x_scale,
|
| 999 |
+
x_zp,
|
| 1000 |
+
packed_weight: TensorBox,
|
| 1001 |
+
w_scale: TensorBox,
|
| 1002 |
+
w_zp: TensorBox,
|
| 1003 |
+
x2: TensorBox,
|
| 1004 |
+
bias: TensorBox,
|
| 1005 |
+
o_scale,
|
| 1006 |
+
o_zero_point,
|
| 1007 |
+
output_dtype,
|
| 1008 |
+
x2_scale,
|
| 1009 |
+
x2_zp,
|
| 1010 |
+
binary_attr,
|
| 1011 |
+
alpha,
|
| 1012 |
+
unary_attr,
|
| 1013 |
+
unary_scalars,
|
| 1014 |
+
unary_algorithm,
|
| 1015 |
+
layout=None,
|
| 1016 |
+
):
|
| 1017 |
+
x_size = x.get_size()
|
| 1018 |
+
x2_size = x2.get_size()
|
| 1019 |
+
assert len(x_size) == len(x2_size)
|
| 1020 |
+
if len(x_size) > 2 and binary_attr in ["add", "sum"]:
|
| 1021 |
+
# GEMM template needs 2D input, normalize input shape here
|
| 1022 |
+
x = view(x, [-1, x_size[-1]])
|
| 1023 |
+
x2 = view(x2, [-1, x2_size[-1]])
|
| 1024 |
+
if not isinstance(x_scale, ir.TensorBox):
|
| 1025 |
+
assert type(x_scale) is float
|
| 1026 |
+
x_scale = V.graph.add_tensor_constant(
|
| 1027 |
+
torch.tensor(x_scale, dtype=torch.float32), name="x_scale"
|
| 1028 |
+
)
|
| 1029 |
+
else:
|
| 1030 |
+
x_scale.realize()
|
| 1031 |
+
if all(dim == 1 for dim in x_scale.get_size()):
|
| 1032 |
+
# Corner-case discovered with LLaMA series.
|
| 1033 |
+
# If all outer dims of x_scale are 1, make it a 0D tensor.
|
| 1034 |
+
# Otherwise, epilogue creator will run into indexing issues.
|
| 1035 |
+
x_scale = view(x_scale, [])
|
| 1036 |
+
assert len(x_scale.get_size()) in [0, 1], "x_scale must be 0D or 1D"
|
| 1037 |
+
|
| 1038 |
+
if x_zp is None:
|
| 1039 |
+
x_zp = V.graph.add_tensor_constant(
|
| 1040 |
+
torch.tensor(0, dtype=torch.int32), name="x_zp"
|
| 1041 |
+
)
|
| 1042 |
+
|
| 1043 |
+
if w_zp is None:
|
| 1044 |
+
w_zp = V.graph.add_tensor_constant(
|
| 1045 |
+
torch.tensor(0, dtype=torch.int32), name="w_zp"
|
| 1046 |
+
)
|
| 1047 |
+
|
| 1048 |
+
if not isinstance(x_zp, ir.TensorBox):
|
| 1049 |
+
assert type(x_zp) is int
|
| 1050 |
+
x_zp = V.graph.add_tensor_constant(
|
| 1051 |
+
torch.tensor(x_zp, dtype=torch.int32), name="x_zp"
|
| 1052 |
+
)
|
| 1053 |
+
else:
|
| 1054 |
+
x_zp.realize()
|
| 1055 |
+
|
| 1056 |
+
# When channels less than 8, w_scale/w_zp is Pointwise instead of ConstantBuffer
|
| 1057 |
+
# Refer to
|
| 1058 |
+
# https://github.com/pytorch/pytorch/blob/f353d17755ed23b02924c962a86ff99a3405fe10/torch/_inductor/graph.py#L570-L577 # noqa: B950
|
| 1059 |
+
w_scale.realize()
|
| 1060 |
+
w_zp.realize()
|
| 1061 |
+
if w_zp.get_dtype() != torch.int32 and isinstance(
|
| 1062 |
+
ir.InputsKernel.unwrap_storage_for_input(w_zp),
|
| 1063 |
+
ir.ConstantBuffer,
|
| 1064 |
+
):
|
| 1065 |
+
w_zp_tensor = V.graph.constants[w_zp.get_name()].to(torch.int32)
|
| 1066 |
+
w_zp = V.graph.add_tensor_constant( # type: ignore[assignment]
|
| 1067 |
+
torch.tensor(w_zp_tensor, dtype=torch.int32), name=w_zp.get_name()
|
| 1068 |
+
)
|
| 1069 |
+
if binary_attr == "sum":
|
| 1070 |
+
if output_dtype in [
|
| 1071 |
+
torch.float32,
|
| 1072 |
+
torch.bfloat16,
|
| 1073 |
+
] and x2.get_dtype() in [torch.float32, torch.bfloat16]:
|
| 1074 |
+
if x2.get_dtype() != output_dtype:
|
| 1075 |
+
# For int8-mixed-bf16 quantization and inplace add,
|
| 1076 |
+
# there is case when accum dtype is float32 but output dtype is bfloat16.
|
| 1077 |
+
# Since the accum will be inplaced changed with post op sum,
|
| 1078 |
+
# we will do accum dtype conversion here.
|
| 1079 |
+
x2 = to_dtype(x2, output_dtype)
|
| 1080 |
+
else:
|
| 1081 |
+
assert x2.get_dtype() == output_dtype, (
|
| 1082 |
+
"dtype of accum for qlinear post op sum should be the same as output"
|
| 1083 |
+
)
|
| 1084 |
+
x2_dtype = x2.get_dtype()
|
| 1085 |
+
bias_dtype = bias.get_dtype() if bias is not None else None
|
| 1086 |
+
choices: list[ChoiceCaller] = []
|
| 1087 |
+
if (config.max_autotune or config.max_autotune_gemm) and binary_attr in [
|
| 1088 |
+
"add",
|
| 1089 |
+
"sum",
|
| 1090 |
+
]:
|
| 1091 |
+
*_, layout, x, packed_weight, x2 = mm_args(
|
| 1092 |
+
x, packed_weight, x2, layout=layout, out_dtype=output_dtype
|
| 1093 |
+
)
|
| 1094 |
+
if (
|
| 1095 |
+
isinstance(
|
| 1096 |
+
ir.InputsKernel.unwrap_storage_for_input(x_zp),
|
| 1097 |
+
ir.ConstantBuffer,
|
| 1098 |
+
)
|
| 1099 |
+
and len(x_zp.get_layout().size) == 0 # Per tensor quant of act
|
| 1100 |
+
and isinstance(
|
| 1101 |
+
ir.InputsKernel.unwrap_storage_for_input(w_zp),
|
| 1102 |
+
ir.ConstantBuffer,
|
| 1103 |
+
)
|
| 1104 |
+
and torch.equal(
|
| 1105 |
+
torch.zeros_like(V.graph.constants[w_zp.get_name()]),
|
| 1106 |
+
V.graph.constants[w_zp.get_name()],
|
| 1107 |
+
) # We only compensate MatrixB and assume B_zp is 0 to avoid the compensation of MatrixA
|
| 1108 |
+
and use_cpp_gemm_template(layout, x, packed_weight)
|
| 1109 |
+
):
|
| 1110 |
+
W_tensor = V.graph.constants[packed_weight.get_name()]
|
| 1111 |
+
W_tensor = W_tensor.to_dense()
|
| 1112 |
+
(
|
| 1113 |
+
use_int8_fast_compensation_path,
|
| 1114 |
+
weight_compens,
|
| 1115 |
+
x_w_scale,
|
| 1116 |
+
) = create_int8_compensation(
|
| 1117 |
+
W_tensor,
|
| 1118 |
+
packed_weight,
|
| 1119 |
+
x_scale,
|
| 1120 |
+
x_zp,
|
| 1121 |
+
w_scale,
|
| 1122 |
+
)
|
| 1123 |
+
|
| 1124 |
+
def epilogue_creator(input_buffer):
|
| 1125 |
+
# Epilogue to convert from s32 to f32 for u8s8f32
|
| 1126 |
+
assert output_dtype in [
|
| 1127 |
+
torch.float32,
|
| 1128 |
+
torch.bfloat16,
|
| 1129 |
+
torch.uint8,
|
| 1130 |
+
torch.int8,
|
| 1131 |
+
]
|
| 1132 |
+
|
| 1133 |
+
input_loader = input_buffer.make_loader()
|
| 1134 |
+
x2_loader = x2.make_loader()
|
| 1135 |
+
weight_compens_loader = weight_compens.make_loader()
|
| 1136 |
+
x_w_scale_loader = None
|
| 1137 |
+
if use_int8_fast_compensation_path:
|
| 1138 |
+
assert x_w_scale is not None
|
| 1139 |
+
x_w_scale_loader = x_w_scale.make_loader()
|
| 1140 |
+
x_scale_loader = x_scale.make_loader()
|
| 1141 |
+
w_scale_loader = w_scale.make_loader()
|
| 1142 |
+
x_zp_loader = x_zp.make_loader()
|
| 1143 |
+
nonlocal bias
|
| 1144 |
+
bias_loader = None
|
| 1145 |
+
if bias is not None:
|
| 1146 |
+
bias_loader = bias.make_loader()
|
| 1147 |
+
|
| 1148 |
+
def inner_fn(index):
|
| 1149 |
+
nonlocal bias
|
| 1150 |
+
input = input_loader(index)
|
| 1151 |
+
_x2 = x2_loader(index)
|
| 1152 |
+
_x_scale = None
|
| 1153 |
+
_x_zp = None
|
| 1154 |
+
_w_scale = None
|
| 1155 |
+
weight_compens_index = (index[-1],)
|
| 1156 |
+
if not use_int8_fast_compensation_path:
|
| 1157 |
+
_x_scale = x_scale_loader(())
|
| 1158 |
+
_x_zp = x_zp_loader(())
|
| 1159 |
+
_w_scale = w_scale_loader(weight_compens_index)
|
| 1160 |
+
# MicroKernel Output is with int32: cvt to FP32 before doing compensation
|
| 1161 |
+
input = ops.to_dtype(input, torch.float32)
|
| 1162 |
+
_weight_compo = weight_compens_loader(weight_compens_index)
|
| 1163 |
+
_x_w_scale = None
|
| 1164 |
+
if use_int8_fast_compensation_path:
|
| 1165 |
+
assert x_w_scale_loader is not None
|
| 1166 |
+
_x_w_scale = x_w_scale_loader(weight_compens_index)
|
| 1167 |
+
# Step 1: Doing compensation to cvt fp32
|
| 1168 |
+
temp = codegen_int8_gemm_template_compensation(
|
| 1169 |
+
use_int8_fast_compensation_path,
|
| 1170 |
+
input,
|
| 1171 |
+
_weight_compo,
|
| 1172 |
+
_x_scale,
|
| 1173 |
+
_x_zp,
|
| 1174 |
+
_w_scale,
|
| 1175 |
+
_x_w_scale,
|
| 1176 |
+
)
|
| 1177 |
+
# Step 2: add Bias if applicable
|
| 1178 |
+
if bias is not None:
|
| 1179 |
+
# pyrefly: ignore [not-callable]
|
| 1180 |
+
_bias = bias_loader(weight_compens_index)
|
| 1181 |
+
nonlocal bias_dtype
|
| 1182 |
+
assert bias_dtype in [torch.float32, torch.bfloat16]
|
| 1183 |
+
if bias_dtype == torch.bfloat16:
|
| 1184 |
+
_bias = ops.to_dtype(_bias, torch.float32)
|
| 1185 |
+
temp = ops.add(temp, _bias)
|
| 1186 |
+
|
| 1187 |
+
# Step 3: Binary add
|
| 1188 |
+
nonlocal x2_dtype
|
| 1189 |
+
assert x2_dtype in [torch.float32, torch.bfloat16]
|
| 1190 |
+
if x2_dtype == torch.bfloat16:
|
| 1191 |
+
_x2 = ops.to_dtype(_x2, torch.float32)
|
| 1192 |
+
temp = ops.add(temp, _x2)
|
| 1193 |
+
|
| 1194 |
+
return temp
|
| 1195 |
+
|
| 1196 |
+
output_buf = ir.Pointwise(
|
| 1197 |
+
device=input_buffer.get_device(),
|
| 1198 |
+
dtype=torch.float32, # Hardcode to FP32 for u8s8f32
|
| 1199 |
+
inner_fn=inner_fn,
|
| 1200 |
+
ranges=input_buffer.get_size(),
|
| 1201 |
+
)
|
| 1202 |
+
|
| 1203 |
+
# Step 4: Unary post op if has
|
| 1204 |
+
if unary_attr != "none":
|
| 1205 |
+
output_buf = create_epilogue_with_attr(
|
| 1206 |
+
output_buf,
|
| 1207 |
+
unary_attr,
|
| 1208 |
+
scalars=unary_scalars,
|
| 1209 |
+
algorithm=unary_algorithm,
|
| 1210 |
+
)
|
| 1211 |
+
|
| 1212 |
+
# Step 5: Cast output to Target Dtype
|
| 1213 |
+
if output_dtype == torch.bfloat16:
|
| 1214 |
+
output_cast_loader = output_buf.make_loader()
|
| 1215 |
+
|
| 1216 |
+
def inner_fn_cast_output_to_bf16(index):
|
| 1217 |
+
input = output_cast_loader(index)
|
| 1218 |
+
return ops.to_dtype(input, output_dtype)
|
| 1219 |
+
|
| 1220 |
+
output_buf = ir.Pointwise(
|
| 1221 |
+
device=output_buf.get_device_or_error(),
|
| 1222 |
+
dtype=output_dtype,
|
| 1223 |
+
inner_fn=inner_fn_cast_output_to_bf16,
|
| 1224 |
+
ranges=output_buf.get_size(),
|
| 1225 |
+
)
|
| 1226 |
+
elif output_dtype in [torch.uint8, torch.int8]:
|
| 1227 |
+
from .lowering import _create_constants
|
| 1228 |
+
|
| 1229 |
+
requant_input_loader = output_buf.make_loader()
|
| 1230 |
+
|
| 1231 |
+
def inner_fn_requant(index, scale, zero_point):
|
| 1232 |
+
input = requant_input_loader(index)
|
| 1233 |
+
inv_scale, zero_point = _create_constants(
|
| 1234 |
+
1.0 / scale, zero_point, dtype=torch.float32
|
| 1235 |
+
)
|
| 1236 |
+
val = ops.round(input * inv_scale) + zero_point
|
| 1237 |
+
if output_dtype == torch.uint8:
|
| 1238 |
+
qmin, qmax = _create_constants(
|
| 1239 |
+
0, 255, dtype=torch.float32
|
| 1240 |
+
)
|
| 1241 |
+
else:
|
| 1242 |
+
qmin, qmax = _create_constants(
|
| 1243 |
+
-128, 127, dtype=torch.float32
|
| 1244 |
+
)
|
| 1245 |
+
clamped = ops.minimum(ops.maximum(val, qmin), qmax)
|
| 1246 |
+
return ops.to_dtype(clamped, torch.uint8)
|
| 1247 |
+
|
| 1248 |
+
output_buf = ir.Pointwise(
|
| 1249 |
+
device=output_buf.get_device_or_error(),
|
| 1250 |
+
dtype=torch.uint8,
|
| 1251 |
+
inner_fn=functools.partial(
|
| 1252 |
+
inner_fn_requant,
|
| 1253 |
+
scale=float(o_scale),
|
| 1254 |
+
zero_point=int(o_zero_point),
|
| 1255 |
+
),
|
| 1256 |
+
ranges=output_buf.get_size(),
|
| 1257 |
+
)
|
| 1258 |
+
|
| 1259 |
+
return output_buf
|
| 1260 |
+
|
| 1261 |
+
CppGemmTemplate.add_choices(
|
| 1262 |
+
choices,
|
| 1263 |
+
layout,
|
| 1264 |
+
[x, x_scale, x_zp, packed_weight, w_scale, w_zp, x2]
|
| 1265 |
+
if bias is None
|
| 1266 |
+
else [x, x_scale, x_zp, packed_weight, w_scale, w_zp, x2, bias],
|
| 1267 |
+
has_bias=bias is not None,
|
| 1268 |
+
epilogue_creator=epilogue_creator,
|
| 1269 |
+
# Reorder bias and x2
|
| 1270 |
+
input_indices=[0, 3, 1, 2, 4, 5, 6]
|
| 1271 |
+
if bias is None
|
| 1272 |
+
else [7, 0, 3, 1, 2, 4, 5, 6],
|
| 1273 |
+
)
|
| 1274 |
+
|
| 1275 |
+
if len(choices) == 0 or use_aten_gemm_kernels():
|
| 1276 |
+
kwargs = dict(
|
| 1277 |
+
output_scale=o_scale,
|
| 1278 |
+
output_zero_point=o_zero_point,
|
| 1279 |
+
output_dtype=output_dtype,
|
| 1280 |
+
other_scale=x2_scale,
|
| 1281 |
+
other_zp=x2_zp,
|
| 1282 |
+
binary_post_op=binary_attr,
|
| 1283 |
+
binary_alpha=alpha,
|
| 1284 |
+
unary_post_op=unary_attr,
|
| 1285 |
+
unary_post_op_args=unary_scalars,
|
| 1286 |
+
unary_post_op_algorithm=unary_algorithm,
|
| 1287 |
+
)
|
| 1288 |
+
if bias is None:
|
| 1289 |
+
kwargs["bias"] = None
|
| 1290 |
+
choices.append(
|
| 1291 |
+
aten_mkldnn_qlinear_binary.bind(
|
| 1292 |
+
(x, x_scale, x_zp, packed_weight, w_scale, w_zp, x2)
|
| 1293 |
+
if bias is None
|
| 1294 |
+
else (x, x_scale, x_zp, packed_weight, w_scale, w_zp, x2, bias),
|
| 1295 |
+
layout,
|
| 1296 |
+
**kwargs,
|
| 1297 |
+
)
|
| 1298 |
+
)
|
| 1299 |
+
assert packed_weight.get_name() in V.graph.constants
|
| 1300 |
+
input_gen_fns = {
|
| 1301 |
+
3: lambda x: V.graph.constants[x.get_name()],
|
| 1302 |
+
4: lambda x: V.graph.constants[x.get_name()],
|
| 1303 |
+
5: lambda x: V.graph.constants[x.get_name()],
|
| 1304 |
+
}
|
| 1305 |
+
if bias is not None:
|
| 1306 |
+
input_gen_fns[7] = lambda x: V.graph.constants[x.get_name()] # For bias
|
| 1307 |
+
result, _ = autotune_select_algorithm(
|
| 1308 |
+
"qlinear_binary",
|
| 1309 |
+
choices,
|
| 1310 |
+
[x, x_scale, x_zp, packed_weight, w_scale, w_zp, x2]
|
| 1311 |
+
if bias is None
|
| 1312 |
+
else [x, x_scale, x_zp, packed_weight, w_scale, w_zp, x2, bias],
|
| 1313 |
+
layout,
|
| 1314 |
+
input_gen_fns=input_gen_fns,
|
| 1315 |
+
)
|
| 1316 |
+
if (
|
| 1317 |
+
isinstance(result.data.data, ir.CppTemplateBuffer)
|
| 1318 |
+
and binary_attr == "sum"
|
| 1319 |
+
and result.data.data.layout == x2.get_layout()
|
| 1320 |
+
):
|
| 1321 |
+
# In this case, since x2 is inplace updated when binary_attr is "sum"
|
| 1322 |
+
# we update the layout of result to view of x2
|
| 1323 |
+
result = ir.TensorBox.create(
|
| 1324 |
+
ir.CppTemplateBuffer(
|
| 1325 |
+
layout=ir.NonOwningLayout(
|
| 1326 |
+
ir.ReinterpretView(data=x2, layout=x2.get_layout())
|
| 1327 |
+
),
|
| 1328 |
+
inputs=result.data.data.inputs, # type: ignore[arg-type]
|
| 1329 |
+
make_kernel_render=result.data.data.make_kernel_render, # type: ignore[arg-type]
|
| 1330 |
+
template=result.data.data.template,
|
| 1331 |
+
choice=result.data.data.choice,
|
| 1332 |
+
)
|
| 1333 |
+
)
|
| 1334 |
+
if len(x_size) > 2 and binary_attr in ["add", "sum"]:
|
| 1335 |
+
result = view(result, (*x_size[:-1], result.get_size()[-1])) # type: ignore[arg-type]
|
| 1336 |
+
return result
|
| 1337 |
+
|
| 1338 |
+
if torch._C.has_mkl:
|
| 1339 |
+
aten_mkl_linear = ExternKernelChoice(
|
| 1340 |
+
torch.ops.mkl._mkl_linear,
|
| 1341 |
+
"mkl::_mkl_linear",
|
| 1342 |
+
has_out_variant=False,
|
| 1343 |
+
kernel_creator=mkldnn_ir.MKLPackedLinear.create,
|
| 1344 |
+
)
|
| 1345 |
+
cpu_needs_realized_inputs.append(torch.ops.mkl._mkl_linear)
|
| 1346 |
+
|
| 1347 |
+
@register_lowering(torch.ops.mkl._mkl_linear)
|
| 1348 |
+
def mkl_packed_linear(
|
| 1349 |
+
x: TensorBox,
|
| 1350 |
+
packed_w: TensorBox,
|
| 1351 |
+
orig_w: TensorBox,
|
| 1352 |
+
b: TensorBox | None,
|
| 1353 |
+
batch_size,
|
| 1354 |
+
*,
|
| 1355 |
+
layout=None,
|
| 1356 |
+
):
|
| 1357 |
+
choices: list[ChoiceCaller] = []
|
| 1358 |
+
if config.max_autotune or config.max_autotune_gemm:
|
| 1359 |
+
transposed_w = permute(orig_w, [1, 0])
|
| 1360 |
+
*_, layout, x, transposed_w = mm_args(
|
| 1361 |
+
x, transposed_w, layout=layout
|
| 1362 |
+
)
|
| 1363 |
+
if use_cpp_gemm_template(layout, x, transposed_w):
|
| 1364 |
+
CppGemmTemplate.add_choices(
|
| 1365 |
+
choices,
|
| 1366 |
+
layout,
|
| 1367 |
+
[x, packed_w, orig_w],
|
| 1368 |
+
trans_w=True,
|
| 1369 |
+
input_indices=[0, 2],
|
| 1370 |
+
)
|
| 1371 |
+
|
| 1372 |
+
if len(choices) == 0 or use_aten_gemm_kernels():
|
| 1373 |
+
choices.append(
|
| 1374 |
+
aten_mkl_linear.bind(
|
| 1375 |
+
(x, packed_w, orig_w), layout, B=None, batch_size=batch_size
|
| 1376 |
+
)
|
| 1377 |
+
)
|
| 1378 |
+
|
| 1379 |
+
assert packed_w.get_name() in V.graph.constants
|
| 1380 |
+
assert orig_w.get_name() in V.graph.constants
|
| 1381 |
+
# packed_w is a mkldnn tensor which we can't generate directly
|
| 1382 |
+
# so we use the weights from the original tensor in autotune.
|
| 1383 |
+
input_gen_fns = {
|
| 1384 |
+
1: lambda x: V.graph.constants[x.get_name()],
|
| 1385 |
+
2: lambda x: V.graph.constants[x.get_name()],
|
| 1386 |
+
}
|
| 1387 |
+
result: TensorBox # annotation on separate line since tuple unpacking doesn't support inline annotation
|
| 1388 |
+
result, _ = autotune_select_algorithm(
|
| 1389 |
+
"packed_linear",
|
| 1390 |
+
choices,
|
| 1391 |
+
[x, packed_w, orig_w],
|
| 1392 |
+
layout,
|
| 1393 |
+
input_gen_fns=input_gen_fns,
|
| 1394 |
+
)
|
| 1395 |
+
if b is not None:
|
| 1396 |
+
result = add(result, b)
|
| 1397 |
+
return result
|
| 1398 |
+
|
| 1399 |
+
add_needs_realized_inputs(cpu_needs_realized_inputs)
|
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/mock_cache.py
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# mypy: ignore-errors
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import contextlib
|
| 6 |
+
import dataclasses
|
| 7 |
+
import sys
|
| 8 |
+
import threading
|
| 9 |
+
from typing import Any, TYPE_CHECKING
|
| 10 |
+
from typing_extensions import override, Self
|
| 11 |
+
from unittest.mock import patch
|
| 12 |
+
|
| 13 |
+
from torch._inductor import config
|
| 14 |
+
from torch._inductor.remote_cache import RemoteCacheBackend
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
if TYPE_CHECKING:
|
| 18 |
+
from collections.abc import Callable
|
| 19 |
+
from types import TracebackType
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
@dataclasses.dataclass
|
| 23 |
+
class Stats:
|
| 24 |
+
num_put: int = 0
|
| 25 |
+
num_get_hit: int = 0
|
| 26 |
+
num_get_miss: int = 0
|
| 27 |
+
|
| 28 |
+
def __iadd__(self, other: Stats) -> Self:
|
| 29 |
+
self.num_put += other.num_put
|
| 30 |
+
self.num_get_hit += other.num_get_hit
|
| 31 |
+
self.num_get_miss += other.num_get_miss
|
| 32 |
+
return self
|
| 33 |
+
|
| 34 |
+
def reset(self) -> None:
|
| 35 |
+
self.num_put = 0
|
| 36 |
+
self.num_get_hit = 0
|
| 37 |
+
self.num_get_miss = 0
|
| 38 |
+
|
| 39 |
+
def __str__(self) -> str:
|
| 40 |
+
return "".join(
|
| 41 |
+
(
|
| 42 |
+
f"puts: {self.num_put}, ",
|
| 43 |
+
f"misses: {self.num_get_miss}, ",
|
| 44 |
+
f"hits: {self.num_get_hit}, ",
|
| 45 |
+
)
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
def __eq__(self, other: object) -> bool:
|
| 49 |
+
# Dataclass's default __eq__ checks that the types are the same so can't
|
| 50 |
+
# be used with _GlobalItemStats.
|
| 51 |
+
return (
|
| 52 |
+
isinstance(other, (Stats, _GlobalItemStats))
|
| 53 |
+
and self.num_put == other.num_put
|
| 54 |
+
and self.num_get_hit == other.num_get_hit
|
| 55 |
+
and self.num_get_miss == other.num_get_miss
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
class _GlobalItemStats(Stats):
|
| 60 |
+
cache: dict[str, object]
|
| 61 |
+
|
| 62 |
+
def __init__(self) -> None:
|
| 63 |
+
super().__init__()
|
| 64 |
+
self.cache = {}
|
| 65 |
+
|
| 66 |
+
def reset(self) -> None:
|
| 67 |
+
super().reset()
|
| 68 |
+
self.cache = {}
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
# The cache states are thread-local so if we're running multiple tests at once
|
| 72 |
+
# they won't cross contaminate. However - it needs to be "global" because we
|
| 73 |
+
# allow code to create new cache clients which refer to the same cache (because
|
| 74 |
+
# it's a remote cache).
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
class _GlobalStats(threading.local):
|
| 78 |
+
def __init__(self) -> None:
|
| 79 |
+
self.autotune_local = _GlobalItemStats()
|
| 80 |
+
self.autotune_remote = _GlobalItemStats()
|
| 81 |
+
self.bundled_autotune = _GlobalItemStats()
|
| 82 |
+
self.fx_graph = _GlobalItemStats()
|
| 83 |
+
self.triton = _GlobalItemStats()
|
| 84 |
+
self.aot_autograd = _GlobalItemStats()
|
| 85 |
+
self.dynamo_pgo = _GlobalItemStats()
|
| 86 |
+
|
| 87 |
+
def reset(self) -> None:
|
| 88 |
+
self.autotune_local.reset()
|
| 89 |
+
self.autotune_remote.reset()
|
| 90 |
+
self.bundled_autotune.reset()
|
| 91 |
+
self.fx_graph.reset()
|
| 92 |
+
self.triton.reset()
|
| 93 |
+
self.aot_autograd.reset()
|
| 94 |
+
self.dynamo_pgo.reset()
|
| 95 |
+
|
| 96 |
+
def get_stat(self, name: str) -> _GlobalItemStats:
|
| 97 |
+
return getattr(self, name)
|
| 98 |
+
|
| 99 |
+
def report(self):
|
| 100 |
+
subs = (
|
| 101 |
+
("autotune_local", self.autotune_local),
|
| 102 |
+
("autotune_remote", self.autotune_remote),
|
| 103 |
+
("bundled_autotune", self.bundled_autotune),
|
| 104 |
+
("fx_graph", self.fx_graph),
|
| 105 |
+
("triton", self.triton),
|
| 106 |
+
("aot_autograd", self.aot_autograd),
|
| 107 |
+
("dynamo_pgo", self.dynamo_pgo),
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
print("Cache Stats:", file=sys.stderr)
|
| 111 |
+
for name, sub in subs:
|
| 112 |
+
print(f" {name}: {sub}", file=sys.stderr)
|
| 113 |
+
|
| 114 |
+
print("Cache Entries:", file=sys.stderr)
|
| 115 |
+
for name, sub in subs:
|
| 116 |
+
if sub.cache:
|
| 117 |
+
print(f" {name}:", file=sys.stderr)
|
| 118 |
+
for k, v in sorted(sub.cache.items()):
|
| 119 |
+
v = repr(v)
|
| 120 |
+
if len(v) > 100:
|
| 121 |
+
v = v[:100] + "..."
|
| 122 |
+
print(f" {k!r}: {v}", file=sys.stderr)
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
global_stats = _GlobalStats()
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
class MockBackend(RemoteCacheBackend[Any]):
|
| 129 |
+
def __init__(self, name: str) -> None:
|
| 130 |
+
self._name = name
|
| 131 |
+
|
| 132 |
+
@staticmethod
|
| 133 |
+
def with_name(name: str) -> Callable[[], MockBackend]:
|
| 134 |
+
def wrapper() -> MockBackend:
|
| 135 |
+
return MockBackend(name)
|
| 136 |
+
|
| 137 |
+
return wrapper
|
| 138 |
+
|
| 139 |
+
@override
|
| 140 |
+
def _get(self, key: str) -> Any | None:
|
| 141 |
+
stat = global_stats.get_stat(self._name)
|
| 142 |
+
if key in stat.cache:
|
| 143 |
+
stat += Stats(num_get_hit=1)
|
| 144 |
+
return stat.cache.get(key)
|
| 145 |
+
else:
|
| 146 |
+
stat += Stats(num_get_miss=1)
|
| 147 |
+
return None
|
| 148 |
+
|
| 149 |
+
@override
|
| 150 |
+
def _put(self, key: str, data: Any) -> None:
|
| 151 |
+
stat = global_stats.get_stat(self._name)
|
| 152 |
+
stat += Stats(num_put=1)
|
| 153 |
+
stat.cache[key] = data
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
# List of configs for each cache
|
| 157 |
+
_CACHE_CONFIG_EN = (
|
| 158 |
+
"fx_graph_cache",
|
| 159 |
+
"fx_graph_remote_cache",
|
| 160 |
+
"autotune_local_cache",
|
| 161 |
+
"autotune_remote_cache",
|
| 162 |
+
"bundled_autotune_remote_cache",
|
| 163 |
+
)
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
class PatchCaches(contextlib.AbstractContextManager):
|
| 167 |
+
@classmethod
|
| 168 |
+
def setUp(cls):
|
| 169 |
+
# If this test is using PatchCaches then disable all the caches by
|
| 170 |
+
# default, letting the tests turn them on explicitly. This is because
|
| 171 |
+
# tests using PatchCaches will often want to check stats explicitly.
|
| 172 |
+
cls._savedCacheState = {}
|
| 173 |
+
for name in _CACHE_CONFIG_EN:
|
| 174 |
+
if hasattr(config, name):
|
| 175 |
+
cls._savedCacheState[name] = getattr(config, name)
|
| 176 |
+
setattr(config, name, False)
|
| 177 |
+
|
| 178 |
+
@classmethod
|
| 179 |
+
def tearDown(cls):
|
| 180 |
+
# Restore cache defaults
|
| 181 |
+
for name in _CACHE_CONFIG_EN:
|
| 182 |
+
delattr(config, name)
|
| 183 |
+
if name in cls._savedCacheState:
|
| 184 |
+
setattr(config, name, cls._savedCacheState[name])
|
| 185 |
+
|
| 186 |
+
def __init__(self) -> None:
|
| 187 |
+
self._stack = contextlib.ExitStack()
|
| 188 |
+
|
| 189 |
+
def __enter__(self) -> Self:
|
| 190 |
+
global_stats.reset()
|
| 191 |
+
self._stack.__enter__()
|
| 192 |
+
|
| 193 |
+
ctx = patch(
|
| 194 |
+
"torch._inductor.runtime.autotune_cache.LocalAutotuneCache.backend_override_cls",
|
| 195 |
+
MockBackend.with_name("autotune_local"),
|
| 196 |
+
)
|
| 197 |
+
self._stack.enter_context(ctx)
|
| 198 |
+
|
| 199 |
+
ctx = patch(
|
| 200 |
+
"torch._inductor.remote_cache.RemoteAutotuneCache.backend_override_cls",
|
| 201 |
+
MockBackend.with_name("autotune_remote"),
|
| 202 |
+
)
|
| 203 |
+
self._stack.enter_context(ctx)
|
| 204 |
+
|
| 205 |
+
ctx = patch(
|
| 206 |
+
"torch._inductor.remote_cache.RemoteBundledAutotuneCache.backend_override_cls",
|
| 207 |
+
MockBackend.with_name("bundled_autotune"),
|
| 208 |
+
)
|
| 209 |
+
self._stack.enter_context(ctx)
|
| 210 |
+
|
| 211 |
+
ctx = patch(
|
| 212 |
+
"torch._inductor.remote_cache.RemoteFxGraphCache.backend_override_cls",
|
| 213 |
+
MockBackend.with_name("fx_graph"),
|
| 214 |
+
)
|
| 215 |
+
self._stack.enter_context(ctx)
|
| 216 |
+
|
| 217 |
+
ctx = patch(
|
| 218 |
+
"torch._inductor.remote_cache.RemoteAOTAutogradCache.backend_override_cls",
|
| 219 |
+
MockBackend.with_name("aot_autograd"),
|
| 220 |
+
)
|
| 221 |
+
self._stack.enter_context(ctx)
|
| 222 |
+
|
| 223 |
+
ctx = patch(
|
| 224 |
+
"torch._inductor.remote_cache.RemoteDynamoPGOCache.backend_override_cls",
|
| 225 |
+
MockBackend.with_name("dynamo_pgo"),
|
| 226 |
+
)
|
| 227 |
+
self._stack.enter_context(ctx)
|
| 228 |
+
|
| 229 |
+
if config.is_fbcode():
|
| 230 |
+
ctx = patch(
|
| 231 |
+
"torch._inductor.fb.remote_cache.FbRemoteAutotuneCache.backend_override_cls",
|
| 232 |
+
MockBackend.with_name("autotune_remote"),
|
| 233 |
+
)
|
| 234 |
+
self._stack.enter_context(ctx)
|
| 235 |
+
|
| 236 |
+
ctx = patch(
|
| 237 |
+
"torch._inductor.fb.remote_cache.FbRemoteBundledAutotuneCache.backend_override_cls",
|
| 238 |
+
MockBackend.with_name("bundled_autotune"),
|
| 239 |
+
)
|
| 240 |
+
self._stack.enter_context(ctx)
|
| 241 |
+
|
| 242 |
+
ctx = patch(
|
| 243 |
+
"torch._inductor.fb.remote_cache.FbRemoteFxGraphCache.backend_override_cls",
|
| 244 |
+
MockBackend.with_name("fx_graph"),
|
| 245 |
+
)
|
| 246 |
+
self._stack.enter_context(ctx)
|
| 247 |
+
|
| 248 |
+
ctx = patch(
|
| 249 |
+
"triton.fb.fb_memcache.FbMemcacheRemoteKernelCache.backend_override_cls",
|
| 250 |
+
MockBackend.with_name("triton"),
|
| 251 |
+
)
|
| 252 |
+
self._stack.enter_context(ctx)
|
| 253 |
+
|
| 254 |
+
ctx = patch(
|
| 255 |
+
"torch._inductor.fb.remote_cache.FbRemoteAOTAutogradCache.backend_override_cls",
|
| 256 |
+
MockBackend.with_name("aot_autograd"),
|
| 257 |
+
)
|
| 258 |
+
self._stack.enter_context(ctx)
|
| 259 |
+
|
| 260 |
+
ctx = patch(
|
| 261 |
+
"torch._inductor.fb.remote_cache.FbRemoteDynamoPGOCache.backend_override_cls",
|
| 262 |
+
MockBackend.with_name("dynamo_pgo"),
|
| 263 |
+
)
|
| 264 |
+
self._stack.enter_context(ctx)
|
| 265 |
+
|
| 266 |
+
return self
|
| 267 |
+
|
| 268 |
+
def __exit__(
|
| 269 |
+
self,
|
| 270 |
+
exc_type: type[BaseException] | None,
|
| 271 |
+
exc_value: BaseException | None,
|
| 272 |
+
traceback: TracebackType | None,
|
| 273 |
+
) -> None:
|
| 274 |
+
self._stack.__exit__(exc_type, exc_value, traceback)
|
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/ops_handler.py
ADDED
|
@@ -0,0 +1,1207 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# mypy: allow-untyped-defs
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import inspect
|
| 5 |
+
import itertools
|
| 6 |
+
import re
|
| 7 |
+
import warnings
|
| 8 |
+
from io import StringIO
|
| 9 |
+
from typing import Any, Generic, Literal, NamedTuple, TYPE_CHECKING, TypeVar
|
| 10 |
+
from unittest.mock import patch
|
| 11 |
+
|
| 12 |
+
import sympy
|
| 13 |
+
|
| 14 |
+
import torch
|
| 15 |
+
import torch.utils._pytree as pytree
|
| 16 |
+
|
| 17 |
+
from ..utils._ordered_set import OrderedSet
|
| 18 |
+
from .utils import IndentedBuffer, reduction_num_outputs, sympy_index_symbol, sympy_str
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
if TYPE_CHECKING:
|
| 22 |
+
from collections.abc import Callable
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
T = TypeVar("T")
|
| 26 |
+
AtomicMode = Literal[
|
| 27 |
+
"atomic_add",
|
| 28 |
+
"atomic_max",
|
| 29 |
+
"atomic_min",
|
| 30 |
+
"atomic_and",
|
| 31 |
+
"atomic_or",
|
| 32 |
+
"atomic_xor",
|
| 33 |
+
"atomic_cas",
|
| 34 |
+
"atomic_xchg",
|
| 35 |
+
]
|
| 36 |
+
StoreMode = AtomicMode | Literal["tma"] | None
|
| 37 |
+
ReductionType = Literal[
|
| 38 |
+
"argmax",
|
| 39 |
+
"argmin",
|
| 40 |
+
"welford_reduce",
|
| 41 |
+
"welford_combine",
|
| 42 |
+
"any",
|
| 43 |
+
"max",
|
| 44 |
+
"min",
|
| 45 |
+
"prod",
|
| 46 |
+
"sum",
|
| 47 |
+
"dot",
|
| 48 |
+
"xor_sum",
|
| 49 |
+
"online_softmax_reduce",
|
| 50 |
+
]
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def _arg_str(a: object) -> str:
|
| 54 |
+
if isinstance(a, sympy.Expr):
|
| 55 |
+
return sympy_str(a)
|
| 56 |
+
return str(a)
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
# See OpDecompositions for superclass that desugars operations like reciprocal/square.
|
| 60 |
+
class OpsHandler(Generic[T]):
|
| 61 |
+
"""
|
| 62 |
+
Protocol describing the set of valid operations on ``torch._inductor.virtualized.ops``,
|
| 63 |
+
as well as the contract for op handlers. The type T signifies the domain
|
| 64 |
+
of the abstract analysis AKA what all the functions return / take as arguments
|
| 65 |
+
anywhere compute occurs.
|
| 66 |
+
|
| 67 |
+
While these operators are typically dtype polymorphic (e.g., you can use mul
|
| 68 |
+
on both integers and floats), they do NOT do promotion and usually return the
|
| 69 |
+
same dtype as the input. You are expected to have handled type promotion
|
| 70 |
+
during ATen decompositions. Most operators correspond exactly to pointwise
|
| 71 |
+
operations as defined by torch, so when in doubt about semantics, check the
|
| 72 |
+
corresponding torch documentation. These are all scalar operations (so they
|
| 73 |
+
are defined to operate on a single element at a time.)
|
| 74 |
+
|
| 75 |
+
For convenience, many operators take a src_dtype which indicates what the dtype
|
| 76 |
+
of the input argument is. Although in principle this can be derived by an
|
| 77 |
+
analysis, providing this for ops where it is useful helps avoid having to repeatedly
|
| 78 |
+
recompute dtype in code generation.
|
| 79 |
+
|
| 80 |
+
Note that this often describes a class of static methods, for stateless
|
| 81 |
+
ops handlers.
|
| 82 |
+
|
| 83 |
+
Handlers are often defined using metaprogramming (e.g. _initialize_pointwise_overrides),
|
| 84 |
+
which means you will not get type errors for those methods. We have tests in
|
| 85 |
+
test/inductor/test_op_completeness.py which check that all operators are implemented after
|
| 86 |
+
all the metaprogramming has run.
|
| 87 |
+
"""
|
| 88 |
+
|
| 89 |
+
def constant(self, value: bool | float | int, dtype: torch.dtype) -> T:
|
| 90 |
+
"""Produces a scalar constant of type dtype."""
|
| 91 |
+
raise NotImplementedError
|
| 92 |
+
|
| 93 |
+
def load_seed(self, name: str, offset: T) -> T:
|
| 94 |
+
"""Computes inductor_prims.lookup_seed."""
|
| 95 |
+
raise NotImplementedError
|
| 96 |
+
|
| 97 |
+
def rand(self, seed: T, offset: T) -> T:
|
| 98 |
+
"""Computes inductor_prims.random with mode="rand". offset has dtype int32."""
|
| 99 |
+
raise NotImplementedError
|
| 100 |
+
|
| 101 |
+
def rand_eager(
|
| 102 |
+
self, seed: T, base_offset: T, threads_per_round: T, tid: T, vec: T
|
| 103 |
+
) -> T:
|
| 104 |
+
"""Computes inductor_prims.random with mode="rand_eager". offset has dtype int32."""
|
| 105 |
+
raise NotImplementedError
|
| 106 |
+
|
| 107 |
+
def randn(self, seed: T, offset: T) -> T:
|
| 108 |
+
"""Computes inductor_prims.random with mode="randn". offset has dtype int32."""
|
| 109 |
+
raise NotImplementedError
|
| 110 |
+
|
| 111 |
+
def randint64(self, seed: T, offset: T, low: T, high: T) -> T:
|
| 112 |
+
"""Computes inductor_prims.randint. offset has dtype int32."""
|
| 113 |
+
raise NotImplementedError
|
| 114 |
+
|
| 115 |
+
def masked(self, mask: T, body: Callable[[], T], other: T) -> T:
|
| 116 |
+
"""
|
| 117 |
+
Computes body, but only perform loads/stores if the boolean mask
|
| 118 |
+
evaluates to true. For example, you would use this if you needed to
|
| 119 |
+
perform an indirect load that may not be valid on some elements;
|
| 120 |
+
without masking, invalid accesses can cause IMAs. When mask is true,
|
| 121 |
+
the result is the result of body; otherwise it is other. Here, `other`
|
| 122 |
+
needs to be a constant.
|
| 123 |
+
|
| 124 |
+
Contrast this with ops.where, which can multiplex between two values
|
| 125 |
+
that have been unconditionally computed.
|
| 126 |
+
"""
|
| 127 |
+
raise NotImplementedError
|
| 128 |
+
|
| 129 |
+
def where(self, condition: T, input: T, other: T) -> T:
|
| 130 |
+
"""
|
| 131 |
+
Computes torch.where: when condition is true, return input; otherwise return other.
|
| 132 |
+
"""
|
| 133 |
+
raise NotImplementedError
|
| 134 |
+
|
| 135 |
+
def index_expr(self, expr: sympy.Expr, dtype: torch.dtype) -> T:
|
| 136 |
+
"""
|
| 137 |
+
Converts a sympy expression into a scalar of type dtype. expr is typically
|
| 138 |
+
an indexing expression, thus the name; however, it can also be used in
|
| 139 |
+
non-indexing situations.
|
| 140 |
+
"""
|
| 141 |
+
raise NotImplementedError
|
| 142 |
+
|
| 143 |
+
def to_dtype(
|
| 144 |
+
self,
|
| 145 |
+
x: T,
|
| 146 |
+
dtype: torch.dtype,
|
| 147 |
+
src_dtype: torch.dtype | None = None,
|
| 148 |
+
use_compute_types: bool = True,
|
| 149 |
+
) -> T:
|
| 150 |
+
"""
|
| 151 |
+
Convert x to dtype. src_dtype can be optionally set to specify what the original
|
| 152 |
+
dtype of x was, which can improve code generation (used by torch to(dtype=dtype)).
|
| 153 |
+
"""
|
| 154 |
+
raise NotImplementedError
|
| 155 |
+
|
| 156 |
+
def trunc_to_int(self, x: T, dtype: torch.dtype) -> T:
|
| 157 |
+
"""
|
| 158 |
+
Convert x to dtype with truncation semantics (similar to how the int
|
| 159 |
+
constructor works in Python). In Inductor codegen, this just decays
|
| 160 |
+
to trunc and then to_dtype, but this composite operation helps
|
| 161 |
+
roundtrips for Sympy evaluation.
|
| 162 |
+
|
| 163 |
+
dtype is taken as an explicit parameter because the desired output
|
| 164 |
+
dtype is typically the index dtype, which may vary between int32 and
|
| 165 |
+
int64 depending on if we've shown that all the indexing operations can
|
| 166 |
+
be done in int32.
|
| 167 |
+
"""
|
| 168 |
+
raise NotImplementedError
|
| 169 |
+
|
| 170 |
+
def ceil_to_int(self, x: T, dtype: torch.dtype) -> T:
|
| 171 |
+
"""
|
| 172 |
+
Convert x to dtype with ceiling semantics. See also trunc_to_int.
|
| 173 |
+
"""
|
| 174 |
+
raise NotImplementedError
|
| 175 |
+
|
| 176 |
+
def floor_to_int(self, x: T, dtype: torch.dtype) -> T:
|
| 177 |
+
"""
|
| 178 |
+
Convert x to dtype with ceiling semantics. See also trunc_to_int.
|
| 179 |
+
"""
|
| 180 |
+
raise NotImplementedError
|
| 181 |
+
|
| 182 |
+
def round_to_int(self, x: T, dtype: torch.dtype) -> T:
|
| 183 |
+
"""
|
| 184 |
+
Convert x to dtype with round-to-even semantics. See also trunc_to_int.
|
| 185 |
+
"""
|
| 186 |
+
raise NotImplementedError
|
| 187 |
+
|
| 188 |
+
def to_dtype_bitcast(self, x: T, dtype: torch.dtype, src_dtype: torch.dtype) -> T:
|
| 189 |
+
"""
|
| 190 |
+
Reinterpret cast x to dtype (reinterpreting the bits in memory as another dtype.)
|
| 191 |
+
src_dtype must be the original type of x.
|
| 192 |
+
"""
|
| 193 |
+
raise NotImplementedError
|
| 194 |
+
|
| 195 |
+
def identity(self, x: T) -> T:
|
| 196 |
+
"""
|
| 197 |
+
Returns x as is. This is used to trigger CSE.
|
| 198 |
+
"""
|
| 199 |
+
raise NotImplementedError
|
| 200 |
+
|
| 201 |
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
| 202 |
+
# These operations are only available in a "kernel" context. Check
|
| 203 |
+
# torch._inductor.codegen.common.CSEProxy for their typical implementation
|
| 204 |
+
# in op handler (routing to their respective implementations in the kernel
|
| 205 |
+
# handler)
|
| 206 |
+
#
|
| 207 |
+
# Importantly, inside a kernel, indexing and mask variables are available
|
| 208 |
+
# in scope, which are typically used by sympy.Expr indexing.
|
| 209 |
+
|
| 210 |
+
def indirect_indexing(
|
| 211 |
+
self, x: T, size: sympy.Expr, check: bool = True, wrap_neg=True
|
| 212 |
+
) -> sympy.Expr:
|
| 213 |
+
"""
|
| 214 |
+
Convert an integral x into a sympy.Expr that can be subsequently used in
|
| 215 |
+
indexing computation. 'size' represents an upper bound on what valid
|
| 216 |
+
indexes can be; when 'check' is True, we check that the x is in bounds.
|
| 217 |
+
|
| 218 |
+
NB: This is typically mandatory to implement for any analysis, because you
|
| 219 |
+
MUST return a valid sympy.Expr of some sort (even if it's a meaningless symbol).
|
| 220 |
+
"""
|
| 221 |
+
raise NotImplementedError
|
| 222 |
+
|
| 223 |
+
def load(self, name: str, index: sympy.Expr) -> T:
|
| 224 |
+
"""
|
| 225 |
+
Load from the memory location 'name', offset by some indexing expression 'index'.
|
| 226 |
+
"""
|
| 227 |
+
raise NotImplementedError
|
| 228 |
+
|
| 229 |
+
def store(
|
| 230 |
+
self,
|
| 231 |
+
name: str,
|
| 232 |
+
index: sympy.Expr,
|
| 233 |
+
value: T,
|
| 234 |
+
mode: StoreMode = None,
|
| 235 |
+
) -> None:
|
| 236 |
+
"""
|
| 237 |
+
Store 'value' to the memory location 'name' offset by 'expr'. If
|
| 238 |
+
specified, 'mode' can require the store to be an atomic addition.
|
| 239 |
+
"""
|
| 240 |
+
raise NotImplementedError
|
| 241 |
+
|
| 242 |
+
# TODO: Better explain how the "collective" semantics of these ops;
|
| 243 |
+
# remember that the input value is a scalar, you can't reduce on it in the
|
| 244 |
+
# traditional sense!
|
| 245 |
+
def reduction(
|
| 246 |
+
self,
|
| 247 |
+
dtype: torch.dtype,
|
| 248 |
+
src_dtype: torch.dtype,
|
| 249 |
+
reduction_type: ReductionType,
|
| 250 |
+
value: T,
|
| 251 |
+
) -> T | tuple[T, ...]:
|
| 252 |
+
"""
|
| 253 |
+
Perform a 'reduction_type' reduction on 'value' of dtype 'src_dtype',
|
| 254 |
+
using 'dtype' as the accumulation dtype for the reduction. The result
|
| 255 |
+
is an intermediate computation which should be stored to the final
|
| 256 |
+
location using 'ops.store_reduction'.
|
| 257 |
+
|
| 258 |
+
Valid reduction types are . For Welford reduction types, this
|
| 259 |
+
function returns multiple outputs; consult reduction_num_outputs to
|
| 260 |
+
determine the amount in metaprogramming applications.
|
| 261 |
+
"""
|
| 262 |
+
raise NotImplementedError
|
| 263 |
+
|
| 264 |
+
# TODO: in practice, this seems to actually return None, but not returning
|
| 265 |
+
# a T makes common __getattr__ idioms not type correctly. Figure out if
|
| 266 |
+
# this should be returning something.
|
| 267 |
+
def store_reduction(self, name: str, index: sympy.Expr, value: T) -> None:
|
| 268 |
+
"""
|
| 269 |
+
Store the fully accumulated result of 'reduction' to the memory
|
| 270 |
+
location 'name' offset by 'expr'.
|
| 271 |
+
"""
|
| 272 |
+
raise NotImplementedError
|
| 273 |
+
|
| 274 |
+
def scan(
|
| 275 |
+
self,
|
| 276 |
+
dtypes: tuple[torch.dtype, ...],
|
| 277 |
+
combine_fn: Callable[[tuple[T, ...], tuple[T, ...]], tuple[T, ...]],
|
| 278 |
+
values: tuple[T, ...],
|
| 279 |
+
) -> tuple[T, ...]:
|
| 280 |
+
"""
|
| 281 |
+
Perform an associative scan on 'value'.
|
| 282 |
+
"""
|
| 283 |
+
# TODO: Improve the description with some pseudocode
|
| 284 |
+
raise NotImplementedError
|
| 285 |
+
|
| 286 |
+
def sort(
|
| 287 |
+
self,
|
| 288 |
+
dtypes: tuple[torch.dtype, ...],
|
| 289 |
+
values: tuple[T, ...],
|
| 290 |
+
stable: bool,
|
| 291 |
+
descending: bool,
|
| 292 |
+
) -> tuple[T, ...]:
|
| 293 |
+
"""
|
| 294 |
+
Sort values along the reduction dimension.
|
| 295 |
+
"""
|
| 296 |
+
raise NotImplementedError
|
| 297 |
+
|
| 298 |
+
def bucketize(
|
| 299 |
+
self,
|
| 300 |
+
values: T,
|
| 301 |
+
boundaries: tuple[str, sympy.Expr, sympy.Expr, sympy.Expr],
|
| 302 |
+
boundary_indices: T,
|
| 303 |
+
indexing_dtype: torch.dtype,
|
| 304 |
+
right: bool,
|
| 305 |
+
sorter: tuple[str, sympy.Expr] | None = None,
|
| 306 |
+
sorter_indices: T | None = None,
|
| 307 |
+
) -> T:
|
| 308 |
+
# See [Note: Inductor bucketize op]
|
| 309 |
+
raise NotImplementedError
|
| 310 |
+
|
| 311 |
+
def partial_accumulate(
|
| 312 |
+
self,
|
| 313 |
+
name: str,
|
| 314 |
+
reduction_type: ReductionType,
|
| 315 |
+
value: T,
|
| 316 |
+
extra_meta: dict[str, Any],
|
| 317 |
+
) -> None:
|
| 318 |
+
raise NotImplementedError
|
| 319 |
+
|
| 320 |
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
| 321 |
+
# The following ops have semantics that correspond exactly to the torch
|
| 322 |
+
# operation with the same corresponding name.
|
| 323 |
+
|
| 324 |
+
def abs(self, x0: T) -> T:
|
| 325 |
+
raise NotImplementedError
|
| 326 |
+
|
| 327 |
+
def exp(self, x0: T) -> T:
|
| 328 |
+
raise NotImplementedError
|
| 329 |
+
|
| 330 |
+
def exp2(self, x0: T) -> T:
|
| 331 |
+
raise NotImplementedError
|
| 332 |
+
|
| 333 |
+
def expm1(self, x0: T) -> T:
|
| 334 |
+
raise NotImplementedError
|
| 335 |
+
|
| 336 |
+
def sqrt(self, x0: T) -> T:
|
| 337 |
+
raise NotImplementedError
|
| 338 |
+
|
| 339 |
+
def relu(self, x0: T) -> T:
|
| 340 |
+
raise NotImplementedError
|
| 341 |
+
|
| 342 |
+
def minimum(self, x0: T, x1: T) -> T:
|
| 343 |
+
raise NotImplementedError
|
| 344 |
+
|
| 345 |
+
def maximum(self, x0: T, x1: T) -> T:
|
| 346 |
+
raise NotImplementedError
|
| 347 |
+
|
| 348 |
+
def cos(self, x0: T) -> T:
|
| 349 |
+
raise NotImplementedError
|
| 350 |
+
|
| 351 |
+
def sin(self, x0: T) -> T:
|
| 352 |
+
raise NotImplementedError
|
| 353 |
+
|
| 354 |
+
def lgamma(self, x0: T) -> T:
|
| 355 |
+
raise NotImplementedError
|
| 356 |
+
|
| 357 |
+
def erf(self, x0: T) -> T:
|
| 358 |
+
raise NotImplementedError
|
| 359 |
+
|
| 360 |
+
def cosh(self, x0: T) -> T:
|
| 361 |
+
raise NotImplementedError
|
| 362 |
+
|
| 363 |
+
def sinh(self, x0: T) -> T:
|
| 364 |
+
raise NotImplementedError
|
| 365 |
+
|
| 366 |
+
def acos(self, x0: T) -> T:
|
| 367 |
+
raise NotImplementedError
|
| 368 |
+
|
| 369 |
+
def acosh(self, x0: T) -> T:
|
| 370 |
+
raise NotImplementedError
|
| 371 |
+
|
| 372 |
+
def asin(self, x0: T) -> T:
|
| 373 |
+
raise NotImplementedError
|
| 374 |
+
|
| 375 |
+
def asinh(self, x0: T) -> T:
|
| 376 |
+
raise NotImplementedError
|
| 377 |
+
|
| 378 |
+
def atan2(self, x0: T, x1: T) -> T:
|
| 379 |
+
raise NotImplementedError
|
| 380 |
+
|
| 381 |
+
def atan(self, x0: T) -> T:
|
| 382 |
+
raise NotImplementedError
|
| 383 |
+
|
| 384 |
+
def atanh(self, x0: T) -> T:
|
| 385 |
+
raise NotImplementedError
|
| 386 |
+
|
| 387 |
+
def copysign(self, x0: T, x1: T) -> T:
|
| 388 |
+
raise NotImplementedError
|
| 389 |
+
|
| 390 |
+
def erfc(self, x0: T) -> T:
|
| 391 |
+
raise NotImplementedError
|
| 392 |
+
|
| 393 |
+
def erfinv(self, x0: T) -> T:
|
| 394 |
+
raise NotImplementedError
|
| 395 |
+
|
| 396 |
+
def frexp(self, x0: T):
|
| 397 |
+
raise NotImplementedError
|
| 398 |
+
|
| 399 |
+
def hypot(self, x0: T, x1: T) -> T:
|
| 400 |
+
raise NotImplementedError
|
| 401 |
+
|
| 402 |
+
def log10(self, x0: T) -> T:
|
| 403 |
+
raise NotImplementedError
|
| 404 |
+
|
| 405 |
+
def log2(self, x0: T) -> T:
|
| 406 |
+
raise NotImplementedError
|
| 407 |
+
|
| 408 |
+
def ldexp(self, x0: T, n: T) -> T:
|
| 409 |
+
raise NotImplementedError
|
| 410 |
+
|
| 411 |
+
def nextafter(self, x0: T, x1: T) -> T:
|
| 412 |
+
raise NotImplementedError
|
| 413 |
+
|
| 414 |
+
def logical_and(self, x0: T, x1: T) -> T:
|
| 415 |
+
raise NotImplementedError
|
| 416 |
+
|
| 417 |
+
def logical_not(self, x0: T) -> T:
|
| 418 |
+
raise NotImplementedError
|
| 419 |
+
|
| 420 |
+
def logical_or(self, x0: T, x1: T) -> T:
|
| 421 |
+
raise NotImplementedError
|
| 422 |
+
|
| 423 |
+
def logical_xor(self, x0: T, x1: T) -> T:
|
| 424 |
+
raise NotImplementedError
|
| 425 |
+
|
| 426 |
+
def bitwise_and(self, x0: T, x1: T) -> T:
|
| 427 |
+
raise NotImplementedError
|
| 428 |
+
|
| 429 |
+
def bitwise_not(self, x0: T) -> T:
|
| 430 |
+
raise NotImplementedError
|
| 431 |
+
|
| 432 |
+
def bitwise_or(self, x0: T, x1: T) -> T:
|
| 433 |
+
raise NotImplementedError
|
| 434 |
+
|
| 435 |
+
def bitwise_xor(self, x0: T, x1: T) -> T:
|
| 436 |
+
raise NotImplementedError
|
| 437 |
+
|
| 438 |
+
def bitwise_left_shift(self, x0: T, x1: T) -> T:
|
| 439 |
+
raise NotImplementedError
|
| 440 |
+
|
| 441 |
+
def bitwise_right_shift(self, x0: T, x1: T) -> T:
|
| 442 |
+
raise NotImplementedError
|
| 443 |
+
|
| 444 |
+
def rsqrt(self, x0: T) -> T:
|
| 445 |
+
raise NotImplementedError
|
| 446 |
+
|
| 447 |
+
def log1p(self, x0: T) -> T:
|
| 448 |
+
raise NotImplementedError
|
| 449 |
+
|
| 450 |
+
def tan(self, x0: T) -> T:
|
| 451 |
+
raise NotImplementedError
|
| 452 |
+
|
| 453 |
+
def tanh(self, x0: T) -> T:
|
| 454 |
+
raise NotImplementedError
|
| 455 |
+
|
| 456 |
+
def sigmoid(self, x0: T) -> T:
|
| 457 |
+
raise NotImplementedError
|
| 458 |
+
|
| 459 |
+
def signbit(self, x0: T) -> T:
|
| 460 |
+
raise NotImplementedError
|
| 461 |
+
|
| 462 |
+
def fmod(self, x0: T, x1: T) -> T:
|
| 463 |
+
raise NotImplementedError
|
| 464 |
+
|
| 465 |
+
def log(self, x0: T) -> T:
|
| 466 |
+
raise NotImplementedError
|
| 467 |
+
|
| 468 |
+
def isinf(self, x0: T) -> T:
|
| 469 |
+
raise NotImplementedError
|
| 470 |
+
|
| 471 |
+
def isnan(self, x0: T) -> T:
|
| 472 |
+
raise NotImplementedError
|
| 473 |
+
|
| 474 |
+
# NB: this returns a float, like the torch operation
|
| 475 |
+
# This rounds half to even to break ties
|
| 476 |
+
def round(self, x0: T) -> T:
|
| 477 |
+
raise NotImplementedError
|
| 478 |
+
|
| 479 |
+
# NB: this returns a float, like the torch operation
|
| 480 |
+
def floor(self, x0: T) -> T:
|
| 481 |
+
raise NotImplementedError
|
| 482 |
+
|
| 483 |
+
def sign(self, x0: T) -> T:
|
| 484 |
+
raise NotImplementedError
|
| 485 |
+
|
| 486 |
+
# NB: this returns a float, like the torch operation
|
| 487 |
+
def trunc(self, x0: T) -> T:
|
| 488 |
+
raise NotImplementedError
|
| 489 |
+
|
| 490 |
+
# NB: this returns a float, like the torch operation
|
| 491 |
+
def ceil(self, x0: T) -> T:
|
| 492 |
+
raise NotImplementedError
|
| 493 |
+
|
| 494 |
+
def neg(self, x0: T) -> T:
|
| 495 |
+
raise NotImplementedError
|
| 496 |
+
|
| 497 |
+
def reciprocal(self, x0: T) -> T:
|
| 498 |
+
raise NotImplementedError
|
| 499 |
+
|
| 500 |
+
def eq(self, x0: T, x1: T) -> T:
|
| 501 |
+
raise NotImplementedError
|
| 502 |
+
|
| 503 |
+
def ne(self, x0: T, x1: T) -> T:
|
| 504 |
+
raise NotImplementedError
|
| 505 |
+
|
| 506 |
+
def lt(self, x0: T, x1: T) -> T:
|
| 507 |
+
raise NotImplementedError
|
| 508 |
+
|
| 509 |
+
def gt(self, x0: T, x1: T) -> T:
|
| 510 |
+
raise NotImplementedError
|
| 511 |
+
|
| 512 |
+
def le(self, x0: T, x1: T) -> T:
|
| 513 |
+
raise NotImplementedError
|
| 514 |
+
|
| 515 |
+
def ge(self, x0: T, x1: T) -> T:
|
| 516 |
+
raise NotImplementedError
|
| 517 |
+
|
| 518 |
+
def add(self, x0: T, x1: T) -> T:
|
| 519 |
+
raise NotImplementedError
|
| 520 |
+
|
| 521 |
+
def sub(self, x0: T, x1: T) -> T:
|
| 522 |
+
raise NotImplementedError
|
| 523 |
+
|
| 524 |
+
def mul(self, x0: T, x1: T) -> T:
|
| 525 |
+
raise NotImplementedError
|
| 526 |
+
|
| 527 |
+
# NB: this returns a float, like the torch operation
|
| 528 |
+
def pow(self, x0: T, x1: T) -> T:
|
| 529 |
+
raise NotImplementedError
|
| 530 |
+
|
| 531 |
+
def and_(self, x0: T, x1: T) -> T:
|
| 532 |
+
raise NotImplementedError
|
| 533 |
+
|
| 534 |
+
def or_(self, x0: T, x1: T) -> T:
|
| 535 |
+
raise NotImplementedError
|
| 536 |
+
|
| 537 |
+
def xor(self, x0: T, x1: T) -> T:
|
| 538 |
+
raise NotImplementedError
|
| 539 |
+
|
| 540 |
+
# These are metaprogrammed by MockHandler._init_cls
|
| 541 |
+
def lshift(self, x0: T, x1: T) -> T:
|
| 542 |
+
raise NotImplementedError
|
| 543 |
+
|
| 544 |
+
def rshift(self, x0: T, x1: T) -> T:
|
| 545 |
+
raise NotImplementedError
|
| 546 |
+
|
| 547 |
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
| 548 |
+
# These are "special" operators. These only exist if the target
|
| 549 |
+
# language actually supports the operator. Keep this in sync with
|
| 550 |
+
# pointwise_overrides_data.
|
| 551 |
+
|
| 552 |
+
def airy_ai(self, x: T) -> T:
|
| 553 |
+
raise NotImplementedError
|
| 554 |
+
|
| 555 |
+
def bessel_j0(self, x: T) -> T:
|
| 556 |
+
raise NotImplementedError
|
| 557 |
+
|
| 558 |
+
def bessel_j1(self, x: T) -> T:
|
| 559 |
+
raise NotImplementedError
|
| 560 |
+
|
| 561 |
+
def bessel_y0(self, x: T) -> T:
|
| 562 |
+
raise NotImplementedError
|
| 563 |
+
|
| 564 |
+
def bessel_y1(self, x: T) -> T:
|
| 565 |
+
raise NotImplementedError
|
| 566 |
+
|
| 567 |
+
def digamma(self, x: T) -> T:
|
| 568 |
+
raise NotImplementedError
|
| 569 |
+
|
| 570 |
+
def erfcx(self, x: T) -> T:
|
| 571 |
+
raise NotImplementedError
|
| 572 |
+
|
| 573 |
+
def fma(self, x: T, y: T, z: T) -> T:
|
| 574 |
+
raise NotImplementedError
|
| 575 |
+
|
| 576 |
+
def mul_rn(self, x: T, y: T) -> T:
|
| 577 |
+
"""Multiplication with round-to-nearest, preventing fusion with subsequent ops."""
|
| 578 |
+
raise NotImplementedError
|
| 579 |
+
|
| 580 |
+
def igamma(self, x: T, y: T) -> T:
|
| 581 |
+
raise NotImplementedError
|
| 582 |
+
|
| 583 |
+
def igammac(self, x: T, y: T) -> T:
|
| 584 |
+
raise NotImplementedError
|
| 585 |
+
|
| 586 |
+
def gammainc(self, x: T, y: T) -> T:
|
| 587 |
+
raise NotImplementedError
|
| 588 |
+
|
| 589 |
+
def gammaincc(self, x: T, y: T) -> T:
|
| 590 |
+
raise NotImplementedError
|
| 591 |
+
|
| 592 |
+
def i0(self, x: T) -> T:
|
| 593 |
+
raise NotImplementedError
|
| 594 |
+
|
| 595 |
+
def i0e(self, x: T) -> T:
|
| 596 |
+
raise NotImplementedError
|
| 597 |
+
|
| 598 |
+
def i1(self, x: T) -> T:
|
| 599 |
+
raise NotImplementedError
|
| 600 |
+
|
| 601 |
+
def i1e(self, x: T) -> T:
|
| 602 |
+
raise NotImplementedError
|
| 603 |
+
|
| 604 |
+
def log_ndtr(self, x: T) -> T:
|
| 605 |
+
raise NotImplementedError
|
| 606 |
+
|
| 607 |
+
def modified_bessel_i0(self, x: T) -> T:
|
| 608 |
+
raise NotImplementedError
|
| 609 |
+
|
| 610 |
+
def modified_bessel_i1(self, x: T) -> T:
|
| 611 |
+
raise NotImplementedError
|
| 612 |
+
|
| 613 |
+
def modified_bessel_k0(self, x: T) -> T:
|
| 614 |
+
raise NotImplementedError
|
| 615 |
+
|
| 616 |
+
def modified_bessel_k1(self, x: T) -> T:
|
| 617 |
+
raise NotImplementedError
|
| 618 |
+
|
| 619 |
+
def ndtr(self, x: T) -> T:
|
| 620 |
+
raise NotImplementedError
|
| 621 |
+
|
| 622 |
+
def ndtri(self, x: T) -> T:
|
| 623 |
+
raise NotImplementedError
|
| 624 |
+
|
| 625 |
+
def polygamma(self, x: T, y: T) -> T:
|
| 626 |
+
raise NotImplementedError
|
| 627 |
+
|
| 628 |
+
def scaled_modified_bessel_k0(self, x: T) -> T:
|
| 629 |
+
raise NotImplementedError
|
| 630 |
+
|
| 631 |
+
def scaled_modified_bessel_k1(self, x: T) -> T:
|
| 632 |
+
raise NotImplementedError
|
| 633 |
+
|
| 634 |
+
def spherical_bessel_j0(self, x: T) -> T:
|
| 635 |
+
raise NotImplementedError
|
| 636 |
+
|
| 637 |
+
def zeta(self, x: T, y: T) -> T:
|
| 638 |
+
raise NotImplementedError
|
| 639 |
+
|
| 640 |
+
def chebyshev_polynomial_t(self, x: T, y: T) -> T:
|
| 641 |
+
raise NotImplementedError
|
| 642 |
+
|
| 643 |
+
def chebyshev_polynomial_u(self, x: T, y: T) -> T:
|
| 644 |
+
raise NotImplementedError
|
| 645 |
+
|
| 646 |
+
def chebyshev_polynomial_v(self, x: T, y: T) -> T:
|
| 647 |
+
raise NotImplementedError
|
| 648 |
+
|
| 649 |
+
def chebyshev_polynomial_w(self, x: T, y: T) -> T:
|
| 650 |
+
raise NotImplementedError
|
| 651 |
+
|
| 652 |
+
def legendre_polynomial_p(self, x: T, y: T) -> T:
|
| 653 |
+
raise NotImplementedError
|
| 654 |
+
|
| 655 |
+
def shifted_chebyshev_polynomial_t(self, x: T, y: T) -> T:
|
| 656 |
+
raise NotImplementedError
|
| 657 |
+
|
| 658 |
+
def shifted_chebyshev_polynomial_u(self, x: T, y: T) -> T:
|
| 659 |
+
raise NotImplementedError
|
| 660 |
+
|
| 661 |
+
def shifted_chebyshev_polynomial_v(self, x: T, y: T) -> T:
|
| 662 |
+
raise NotImplementedError
|
| 663 |
+
|
| 664 |
+
def shifted_chebyshev_polynomial_w(self, x: T, y: T) -> T:
|
| 665 |
+
raise NotImplementedError
|
| 666 |
+
|
| 667 |
+
def hermite_polynomial_h(self, x: T, y: T) -> T:
|
| 668 |
+
raise NotImplementedError
|
| 669 |
+
|
| 670 |
+
def hermite_polynomial_he(self, x: T, y: T) -> T:
|
| 671 |
+
raise NotImplementedError
|
| 672 |
+
|
| 673 |
+
def laguerre_polynomial_l(self, x: T, y: T) -> T:
|
| 674 |
+
raise NotImplementedError
|
| 675 |
+
|
| 676 |
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
| 677 |
+
# These operators are a bit special, because they are conventionally
|
| 678 |
+
# natively supported in both Python and C, but the semantics differ so
|
| 679 |
+
# care must be taken
|
| 680 |
+
|
| 681 |
+
def truncdiv(self, x0: T, x1: T) -> T:
|
| 682 |
+
"""C-style trunc division between integers only. Computes the true
|
| 683 |
+
division of two numbers and rounds the result to zero.
|
| 684 |
+
"""
|
| 685 |
+
raise NotImplementedError
|
| 686 |
+
|
| 687 |
+
def floordiv(self, x0: T, x1: T) -> T:
|
| 688 |
+
"""Python-style floor division between integers only. Computes the
|
| 689 |
+
true division of two numbers and floors the result. If you want
|
| 690 |
+
floor division for floats, do regular truediv and floor the result.
|
| 691 |
+
"""
|
| 692 |
+
raise NotImplementedError
|
| 693 |
+
|
| 694 |
+
def truediv(self, x0: T, x1: T) -> T:
|
| 695 |
+
"""True division between floats. Integer inputs are NOT valid. To
|
| 696 |
+
do Python-style (int, int) -> float division, use int_truediv"""
|
| 697 |
+
raise NotImplementedError
|
| 698 |
+
|
| 699 |
+
def div_rn(self, x0: T, x1: T) -> T:
|
| 700 |
+
"""Division with round-to-nearest rounding mode. Used for matching
|
| 701 |
+
eager CUDA semantics where division uses IEEE round-to-nearest."""
|
| 702 |
+
raise NotImplementedError
|
| 703 |
+
|
| 704 |
+
def int_truediv(self, x0: T, x1: T) -> T:
|
| 705 |
+
"""True division between integers. This is NOT the same as promoting
|
| 706 |
+
to float and doing integer division, there is a bespoke algorithm for
|
| 707 |
+
doing the division in higher precision than the above.
|
| 708 |
+
"""
|
| 709 |
+
raise NotImplementedError
|
| 710 |
+
|
| 711 |
+
def mod(self, x0: T, x1: T) -> T:
|
| 712 |
+
"""C-style modulus, take sign from LHS (x0)."""
|
| 713 |
+
raise NotImplementedError
|
| 714 |
+
|
| 715 |
+
def remainder(self, x0: T, x1: T) -> T:
|
| 716 |
+
"""Python-style modulus, take sign from RHS (x1)."""
|
| 717 |
+
raise NotImplementedError
|
| 718 |
+
|
| 719 |
+
def square(self, x0: T) -> T:
|
| 720 |
+
raise NotImplementedError
|
| 721 |
+
|
| 722 |
+
def check_bounds(
|
| 723 |
+
self, expr: sympy.Expr, size: sympy.Expr, lower: bool, upper: bool
|
| 724 |
+
) -> None:
|
| 725 |
+
raise NotImplementedError
|
| 726 |
+
|
| 727 |
+
# halide-only
|
| 728 |
+
def halide_clamp(self, value: T, size: sympy.Expr, check: bool) -> T:
|
| 729 |
+
raise NotImplementedError
|
| 730 |
+
|
| 731 |
+
# triton-only
|
| 732 |
+
def dot(self, x: T, y: T) -> T:
|
| 733 |
+
raise NotImplementedError
|
| 734 |
+
|
| 735 |
+
# triton-only
|
| 736 |
+
def inline_asm_elementwise(
|
| 737 |
+
self,
|
| 738 |
+
*inputs: T,
|
| 739 |
+
asm: str,
|
| 740 |
+
constraints: str | None = None,
|
| 741 |
+
dtype: torch.dtype = torch.float32,
|
| 742 |
+
is_pure: bool = True,
|
| 743 |
+
pack: int = 1,
|
| 744 |
+
input_dtypes: tuple[torch.dtype, ...] | None = None,
|
| 745 |
+
) -> T:
|
| 746 |
+
raise NotImplementedError
|
| 747 |
+
|
| 748 |
+
def output(self, *args: T) -> None:
|
| 749 |
+
"""This is a fake op used in analysis but not codegen"""
|
| 750 |
+
raise NotImplementedError
|
| 751 |
+
|
| 752 |
+
def placeholder(self, index: int) -> T:
|
| 753 |
+
"""This is a fake op used in analysis but not codegen"""
|
| 754 |
+
raise NotImplementedError
|
| 755 |
+
|
| 756 |
+
def device_assert_async(self, cond: T, msg: str) -> T:
|
| 757 |
+
raise NotImplementedError
|
| 758 |
+
|
| 759 |
+
|
| 760 |
+
_ignore_op_re = re.compile(r"_.*|paren").fullmatch
|
| 761 |
+
|
| 762 |
+
|
| 763 |
+
def list_ops(cls: type[Any]):
|
| 764 |
+
return OrderedSet([x for x in dir(cls) if not _ignore_op_re(x)])
|
| 765 |
+
|
| 766 |
+
|
| 767 |
+
OP_NAMES = list_ops(OpsHandler)
|
| 768 |
+
|
| 769 |
+
|
| 770 |
+
class DefaultHandler(OpsHandler[Any]):
|
| 771 |
+
def _default(self, name: str, args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any:
|
| 772 |
+
"""
|
| 773 |
+
Default implementation for all ops. Override in a subclass to
|
| 774 |
+
provide generic op behavior.
|
| 775 |
+
|
| 776 |
+
Args:
|
| 777 |
+
name: name of the op, see OpHandler.{name}
|
| 778 |
+
args: positional args passed to the op
|
| 779 |
+
kwargs: keyword args passed to the op
|
| 780 |
+
|
| 781 |
+
Returns:
|
| 782 |
+
return value of the op
|
| 783 |
+
|
| 784 |
+
"""
|
| 785 |
+
raise NotImplementedError
|
| 786 |
+
|
| 787 |
+
def __getattr__(self, name: str) -> Any:
|
| 788 |
+
def fallback(*args: Any, **kwargs: Any) -> Any:
|
| 789 |
+
return self._default(name, args, kwargs)
|
| 790 |
+
|
| 791 |
+
# would like to remove this function entirely, but it's used in MTIA backend
|
| 792 |
+
warnings.warn(f"undefined OpHandler.{name}, please add missing op schema")
|
| 793 |
+
return fallback
|
| 794 |
+
|
| 795 |
+
@staticmethod
|
| 796 |
+
def _call_default(target: str):
|
| 797 |
+
def call_default(self, *args, **kwargs):
|
| 798 |
+
return self._default(target, args, kwargs)
|
| 799 |
+
|
| 800 |
+
call_default.__name__ = target
|
| 801 |
+
return call_default
|
| 802 |
+
|
| 803 |
+
@classmethod
|
| 804 |
+
def _init_cls(cls):
|
| 805 |
+
"""
|
| 806 |
+
Here we codegen many functions of the form:
|
| 807 |
+
|
| 808 |
+
def add(self, a, b):
|
| 809 |
+
return self._default('add', (a, b), {})
|
| 810 |
+
|
| 811 |
+
and install them in cls. This is the same as _call_default above,
|
| 812 |
+
but is about 1.2x faster since CPython varargs parsing is slow.
|
| 813 |
+
"""
|
| 814 |
+
code = StringIO()
|
| 815 |
+
for target in OP_NAMES:
|
| 816 |
+
sig = inspect.signature(getattr(OpsHandler, target))
|
| 817 |
+
if all(
|
| 818 |
+
p.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD
|
| 819 |
+
and p.default is inspect.Parameter.empty
|
| 820 |
+
for p in sig.parameters.values()
|
| 821 |
+
):
|
| 822 |
+
self_arg, *args = sig.parameters.keys()
|
| 823 |
+
assert self_arg == "self"
|
| 824 |
+
code.write(
|
| 825 |
+
f"""
|
| 826 |
+
def {target}(self, {", ".join(args)}):
|
| 827 |
+
return self._default({target!r}, ({", ".join(args)}, ), {{}})
|
| 828 |
+
""".strip()
|
| 829 |
+
)
|
| 830 |
+
code.write("\n\n")
|
| 831 |
+
else:
|
| 832 |
+
# slower fallback for ops with default or variadic arguments
|
| 833 |
+
setattr(cls, target, cls._call_default(target))
|
| 834 |
+
|
| 835 |
+
ctx: dict[str, Any] = {}
|
| 836 |
+
exec(code.getvalue(), ctx)
|
| 837 |
+
for target, impl in ctx.items():
|
| 838 |
+
if target in OP_NAMES:
|
| 839 |
+
setattr(cls, target, impl)
|
| 840 |
+
|
| 841 |
+
|
| 842 |
+
DefaultHandler._init_cls()
|
| 843 |
+
|
| 844 |
+
|
| 845 |
+
class NoopHandler(DefaultHandler):
|
| 846 |
+
name = "NoopHandler"
|
| 847 |
+
|
| 848 |
+
def _default(self, name: str, args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any:
|
| 849 |
+
return None
|
| 850 |
+
|
| 851 |
+
@staticmethod
|
| 852 |
+
def masked(mask, body, other) -> None:
|
| 853 |
+
return None
|
| 854 |
+
|
| 855 |
+
@staticmethod
|
| 856 |
+
# pyrefly: ignore [bad-override]
|
| 857 |
+
def frexp(x) -> tuple[None, None]:
|
| 858 |
+
return (None, None)
|
| 859 |
+
|
| 860 |
+
@staticmethod
|
| 861 |
+
def scan(dtypes, combine_fn, values) -> tuple[None, ...]:
|
| 862 |
+
return (None,) * len(values)
|
| 863 |
+
|
| 864 |
+
@staticmethod
|
| 865 |
+
def sort(dtypes, values, stable, descending) -> tuple[None, ...]:
|
| 866 |
+
return (None,) * len(values)
|
| 867 |
+
|
| 868 |
+
@staticmethod
|
| 869 |
+
# pyrefly: ignore [bad-override]
|
| 870 |
+
def indirect_indexing(index_var, size, check=True, wrap_neg=True) -> sympy.Symbol:
|
| 871 |
+
return sympy.S.Zero
|
| 872 |
+
|
| 873 |
+
|
| 874 |
+
class BasicMathOpsMixin:
|
| 875 |
+
@staticmethod
|
| 876 |
+
def add(a, b):
|
| 877 |
+
return f"{a} + {b}"
|
| 878 |
+
|
| 879 |
+
@staticmethod
|
| 880 |
+
def sub(a, b):
|
| 881 |
+
return f"{a} - {b}"
|
| 882 |
+
|
| 883 |
+
@staticmethod
|
| 884 |
+
def mul(a, b):
|
| 885 |
+
return f"{a} * {b}"
|
| 886 |
+
|
| 887 |
+
@staticmethod
|
| 888 |
+
def floordiv(a, b):
|
| 889 |
+
return f"{a} // {b}"
|
| 890 |
+
|
| 891 |
+
@staticmethod
|
| 892 |
+
def truediv(a, b):
|
| 893 |
+
return f"{a} / {b}"
|
| 894 |
+
|
| 895 |
+
@staticmethod
|
| 896 |
+
def mod(a, b):
|
| 897 |
+
# careful, depending on target semantics varies
|
| 898 |
+
return f"{a} % {b}"
|
| 899 |
+
|
| 900 |
+
@staticmethod
|
| 901 |
+
def pow(a, b):
|
| 902 |
+
return f"{a} ** {b}"
|
| 903 |
+
|
| 904 |
+
@staticmethod
|
| 905 |
+
def lshift(a, b):
|
| 906 |
+
return f"{a} << {b}"
|
| 907 |
+
|
| 908 |
+
@staticmethod
|
| 909 |
+
def rshift(a, b):
|
| 910 |
+
return f"{a} >> {b}"
|
| 911 |
+
|
| 912 |
+
@staticmethod
|
| 913 |
+
def and_(a, b):
|
| 914 |
+
return f"{a} & {b}"
|
| 915 |
+
|
| 916 |
+
@staticmethod
|
| 917 |
+
def or_(a, b):
|
| 918 |
+
return f"{a} | {b}"
|
| 919 |
+
|
| 920 |
+
@staticmethod
|
| 921 |
+
def xor(a, b):
|
| 922 |
+
return f"{a} ^ {b}"
|
| 923 |
+
|
| 924 |
+
@staticmethod
|
| 925 |
+
def eq(a, b):
|
| 926 |
+
return f"{a} == {b}"
|
| 927 |
+
|
| 928 |
+
@staticmethod
|
| 929 |
+
def ne(a, b):
|
| 930 |
+
return f"{a} != {b}"
|
| 931 |
+
|
| 932 |
+
@staticmethod
|
| 933 |
+
def lt(a, b):
|
| 934 |
+
return f"{a} < {b}"
|
| 935 |
+
|
| 936 |
+
@staticmethod
|
| 937 |
+
def gt(a, b):
|
| 938 |
+
return f"{a} > {b}"
|
| 939 |
+
|
| 940 |
+
@staticmethod
|
| 941 |
+
def le(a, b):
|
| 942 |
+
return f"{a} <= {b}"
|
| 943 |
+
|
| 944 |
+
@staticmethod
|
| 945 |
+
def ge(a, b):
|
| 946 |
+
return f"{a} >= {b}"
|
| 947 |
+
|
| 948 |
+
@staticmethod
|
| 949 |
+
def neg(a):
|
| 950 |
+
return f"-{a}"
|
| 951 |
+
|
| 952 |
+
|
| 953 |
+
class MockHandler(BasicMathOpsMixin, DefaultHandler):
|
| 954 |
+
name = "MockHandler"
|
| 955 |
+
|
| 956 |
+
def _default(self, name: str, args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any:
|
| 957 |
+
fargs = [*map(_arg_str, args)]
|
| 958 |
+
for k, v in kwargs.items():
|
| 959 |
+
fargs.append(f"{k}={_arg_str(v)}")
|
| 960 |
+
return f"ops.{name}({', '.join(fargs)})"
|
| 961 |
+
|
| 962 |
+
@staticmethod
|
| 963 |
+
def masked(mask, body, other) -> str:
|
| 964 |
+
return f"ops.masked({mask}, {body()}, {other})"
|
| 965 |
+
|
| 966 |
+
@staticmethod
|
| 967 |
+
# pyrefly: ignore [bad-override]
|
| 968 |
+
def frexp(x):
|
| 969 |
+
return (f"ops.frexp({x})[0]", f"ops.frexp({x})[1]")
|
| 970 |
+
|
| 971 |
+
@staticmethod
|
| 972 |
+
def scan(dtypes, combine_fn, values):
|
| 973 |
+
return tuple(
|
| 974 |
+
f"ops.scan({dtypes}, {combine_fn}, {values})[{i}]"
|
| 975 |
+
for i in range(len(values))
|
| 976 |
+
)
|
| 977 |
+
|
| 978 |
+
@staticmethod
|
| 979 |
+
def sort(dtypes, values, stable, descending):
|
| 980 |
+
return tuple(
|
| 981 |
+
f"ops.sort({dtypes}, {values}, stable={stable}, descending={descending})[{i}]"
|
| 982 |
+
for i in range(len(values))
|
| 983 |
+
)
|
| 984 |
+
|
| 985 |
+
@staticmethod
|
| 986 |
+
# pyrefly: ignore [bad-override]
|
| 987 |
+
def indirect_indexing(index_var, size, check=True, wrap_neg=True) -> sympy.Symbol:
|
| 988 |
+
return sympy_index_symbol(str(index_var))
|
| 989 |
+
|
| 990 |
+
|
| 991 |
+
class KernelFormatterHandler(DefaultHandler):
|
| 992 |
+
def __init__(self, parent_handler: OpsHandler[Any]):
|
| 993 |
+
self.parent_handler = parent_handler
|
| 994 |
+
self._output = IndentedBuffer(1)
|
| 995 |
+
self.var_counter = itertools.count()
|
| 996 |
+
|
| 997 |
+
@staticmethod
|
| 998 |
+
def ir_to_string(ir_fn, index, rindex=None) -> str:
|
| 999 |
+
from .ir import FlexibleLayout
|
| 1000 |
+
from .virtualized import V
|
| 1001 |
+
|
| 1002 |
+
args = [index, rindex] if rindex is not None else [index]
|
| 1003 |
+
names = ["index", "rindex"] if rindex is not None else ["index"]
|
| 1004 |
+
formatter = KernelFormatterHandler(MockHandler())
|
| 1005 |
+
|
| 1006 |
+
with formatter._output.indent(-1):
|
| 1007 |
+
formatter._output.writeline(f"def inner_fn({', '.join(names)}):")
|
| 1008 |
+
for name, arg in zip(names, args):
|
| 1009 |
+
if arg:
|
| 1010 |
+
lhs = ", ".join(
|
| 1011 |
+
[
|
| 1012 |
+
str("_" if isinstance(v, (int, sympy.Integer)) else v)
|
| 1013 |
+
for v in arg
|
| 1014 |
+
]
|
| 1015 |
+
)
|
| 1016 |
+
formatter._output.writeline(f"{lhs} = {name}")
|
| 1017 |
+
|
| 1018 |
+
with (
|
| 1019 |
+
V.set_ops_handler(formatter),
|
| 1020 |
+
patch.object(FlexibleLayout, "allow_indexing", True),
|
| 1021 |
+
):
|
| 1022 |
+
result = ir_fn(*args)
|
| 1023 |
+
return formatter.getvalue(result)
|
| 1024 |
+
|
| 1025 |
+
def indirect_indexing(self, *args, **kwargs) -> sympy.Symbol:
|
| 1026 |
+
return self.parent_handler.indirect_indexing(*args, **kwargs)
|
| 1027 |
+
|
| 1028 |
+
def _write(self, line):
|
| 1029 |
+
# replace line with a new variable name
|
| 1030 |
+
varname = f"tmp{next(self.var_counter)}"
|
| 1031 |
+
self._output.writeline(f"{varname} = {line}")
|
| 1032 |
+
return varname
|
| 1033 |
+
|
| 1034 |
+
def _default(self, name: str, args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any:
|
| 1035 |
+
return pytree.tree_map(
|
| 1036 |
+
self._write, getattr(self.parent_handler, name)(*args, **kwargs)
|
| 1037 |
+
)
|
| 1038 |
+
|
| 1039 |
+
def reduction(
|
| 1040 |
+
self,
|
| 1041 |
+
dtype: torch.dtype,
|
| 1042 |
+
src_dtype: torch.dtype,
|
| 1043 |
+
reduction_type: ReductionType,
|
| 1044 |
+
value: str | tuple[str, ...],
|
| 1045 |
+
) -> str | tuple[str, ...]:
|
| 1046 |
+
line = self.parent_handler.reduction(dtype, src_dtype, reduction_type, value)
|
| 1047 |
+
num_values = reduction_num_outputs(reduction_type)
|
| 1048 |
+
varnames = [f"tmp{next(self.var_counter)}" for _ in range(num_values)]
|
| 1049 |
+
self._output.writeline(f"{','.join(varnames)} = {line}")
|
| 1050 |
+
return tuple(varnames) if num_values > 1 else varnames[0]
|
| 1051 |
+
|
| 1052 |
+
def getvalue(self, result):
|
| 1053 |
+
self._output.writeline(f"return {result}")
|
| 1054 |
+
return self._output.getvalue()
|
| 1055 |
+
|
| 1056 |
+
|
| 1057 |
+
class WrapperHandler(DefaultHandler):
|
| 1058 |
+
def __init__(self, inner: OpsHandler[Any]):
|
| 1059 |
+
self._inner = inner
|
| 1060 |
+
|
| 1061 |
+
def _default(self, name: str, args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any:
|
| 1062 |
+
return getattr(self._inner, name)(*args, **kwargs)
|
| 1063 |
+
|
| 1064 |
+
|
| 1065 |
+
class AddParenHandler(WrapperHandler):
|
| 1066 |
+
def _default(self, name: str, args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any:
|
| 1067 |
+
val = getattr(self._inner, name)(*args, **kwargs)
|
| 1068 |
+
if not val or isinstance(val, (sympy.Expr, tuple, list)):
|
| 1069 |
+
return val
|
| 1070 |
+
return f"({val})"
|
| 1071 |
+
|
| 1072 |
+
|
| 1073 |
+
class OpCountResult(NamedTuple):
|
| 1074 |
+
num_ops: int
|
| 1075 |
+
used_ops: OrderedSet[str]
|
| 1076 |
+
read_buffers: list[str]
|
| 1077 |
+
nontrivial_read_count: int
|
| 1078 |
+
|
| 1079 |
+
|
| 1080 |
+
class OpCounterCSE(DefaultHandler):
|
| 1081 |
+
"""Shim to count how many ops are used"""
|
| 1082 |
+
|
| 1083 |
+
def __init__(self, inner: OpsHandler[Any]):
|
| 1084 |
+
super().__init__()
|
| 1085 |
+
self.parent_handler = inner
|
| 1086 |
+
self.op_count = 0
|
| 1087 |
+
self.var_names: dict[str, str] = {}
|
| 1088 |
+
self._used_ops: OrderedSet[str] = OrderedSet()
|
| 1089 |
+
self._read_names: list[str] = []
|
| 1090 |
+
self._nontrivial_read_count = 0
|
| 1091 |
+
|
| 1092 |
+
def _default(self, name: str, args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any:
|
| 1093 |
+
self._used_ops.add(name)
|
| 1094 |
+
return pytree.tree_map(
|
| 1095 |
+
self._update_count, getattr(self.parent_handler, name)(*args, **kwargs)
|
| 1096 |
+
)
|
| 1097 |
+
|
| 1098 |
+
def _update_count(self, val):
|
| 1099 |
+
varname = self.var_names.get(val)
|
| 1100 |
+
if not varname:
|
| 1101 |
+
varname = f"tmp{self.op_count}"
|
| 1102 |
+
self.op_count += 1
|
| 1103 |
+
self.var_names[val] = varname
|
| 1104 |
+
return varname
|
| 1105 |
+
|
| 1106 |
+
def indirect_indexing(self, *args, **kwargs):
|
| 1107 |
+
self._used_ops.add("indirect_indexing")
|
| 1108 |
+
return self.parent_handler.indirect_indexing(*args, **kwargs)
|
| 1109 |
+
|
| 1110 |
+
def load(self, name: str, index: sympy.Expr) -> str:
|
| 1111 |
+
val = self.parent_handler.load(name, index)
|
| 1112 |
+
if val not in self.var_names:
|
| 1113 |
+
self._used_ops.add("load")
|
| 1114 |
+
self._read_names.append(name)
|
| 1115 |
+
if not isinstance(index, (sympy.Integer, int)):
|
| 1116 |
+
self._nontrivial_read_count += 1
|
| 1117 |
+
return self._update_count(val)
|
| 1118 |
+
|
| 1119 |
+
def load_seed(self, name: str, offset: T):
|
| 1120 |
+
val = self.parent_handler.load_seed(name, offset)
|
| 1121 |
+
if val not in self.var_names:
|
| 1122 |
+
self._used_ops.add("load_seed")
|
| 1123 |
+
self._read_names.append(name)
|
| 1124 |
+
return self._update_count(val)
|
| 1125 |
+
|
| 1126 |
+
def bucketize(
|
| 1127 |
+
self,
|
| 1128 |
+
values: T,
|
| 1129 |
+
boundaries: tuple[str, sympy.Expr, sympy.Expr, sympy.Expr],
|
| 1130 |
+
boundary_indices: T,
|
| 1131 |
+
indexing_dtype: torch.dtype,
|
| 1132 |
+
right: bool,
|
| 1133 |
+
sorter: tuple[str, sympy.Expr] | None = None,
|
| 1134 |
+
sorter_indices: T | None = None,
|
| 1135 |
+
) -> T:
|
| 1136 |
+
"""
|
| 1137 |
+
See [Note: Inductor bucketize op]
|
| 1138 |
+
"""
|
| 1139 |
+
val = self.parent_handler.bucketize(
|
| 1140 |
+
values,
|
| 1141 |
+
boundaries,
|
| 1142 |
+
boundary_indices,
|
| 1143 |
+
indexing_dtype,
|
| 1144 |
+
right,
|
| 1145 |
+
sorter,
|
| 1146 |
+
sorter_indices,
|
| 1147 |
+
)
|
| 1148 |
+
if val not in self.var_names:
|
| 1149 |
+
self._used_ops.add("bucketize")
|
| 1150 |
+
self._read_names.append(boundaries[0])
|
| 1151 |
+
if sorter is not None:
|
| 1152 |
+
self._read_names.append(sorter[0])
|
| 1153 |
+
return self._update_count(val)
|
| 1154 |
+
|
| 1155 |
+
def getvalue(self):
|
| 1156 |
+
return OpCountResult(
|
| 1157 |
+
self.op_count, self._used_ops, self._read_names, self._nontrivial_read_count
|
| 1158 |
+
)
|
| 1159 |
+
|
| 1160 |
+
|
| 1161 |
+
class ExtractConstantsHandler(NoopHandler):
|
| 1162 |
+
def __init__(self, device: torch.device | None):
|
| 1163 |
+
self.device = device
|
| 1164 |
+
|
| 1165 |
+
def constant(self, value: Any, dtype: torch.dtype) -> torch._inductor.ir.Constant:
|
| 1166 |
+
from torch._inductor import ir
|
| 1167 |
+
|
| 1168 |
+
return ir.Constant(
|
| 1169 |
+
value=value, dtype=dtype, device=self.device or torch.get_default_device()
|
| 1170 |
+
)
|
| 1171 |
+
|
| 1172 |
+
|
| 1173 |
+
class SimpleCSEHandler(WrapperHandler):
|
| 1174 |
+
"""Wraps the underlying handler with a CSE pass
|
| 1175 |
+
|
| 1176 |
+
NOTE: Compared to codegen level CSE this is simplified as it
|
| 1177 |
+
doesn't support stores which require load cache invalidation.
|
| 1178 |
+
"""
|
| 1179 |
+
|
| 1180 |
+
def __init__(self, inner: Any):
|
| 1181 |
+
super().__init__(inner)
|
| 1182 |
+
self.cse_cache: dict[str, Any | tuple[Any, ...]] = {}
|
| 1183 |
+
self.mock = MockHandler()
|
| 1184 |
+
|
| 1185 |
+
def indirect_indexing(self, *args, **kwargs) -> sympy.Expr:
|
| 1186 |
+
return super().indirect_indexing(*args, **kwargs) # type: ignore[misc]
|
| 1187 |
+
|
| 1188 |
+
def store(self, *args, **kwargs) -> None:
|
| 1189 |
+
raise NotImplementedError("store not implemented")
|
| 1190 |
+
|
| 1191 |
+
def store_reduction(self, *args, **kwargs) -> None:
|
| 1192 |
+
raise NotImplementedError("store not implemented")
|
| 1193 |
+
|
| 1194 |
+
def _default(self, name: str, args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any:
|
| 1195 |
+
key = getattr(self.mock, name)(*args, **kwargs)
|
| 1196 |
+
val = self.cse_cache.get(key)
|
| 1197 |
+
if val is not None:
|
| 1198 |
+
return val
|
| 1199 |
+
|
| 1200 |
+
val = getattr(self._inner, name)(*args, **kwargs)
|
| 1201 |
+
self.cse_cache[key] = val
|
| 1202 |
+
return val
|
| 1203 |
+
|
| 1204 |
+
def device_assert_async(self, *args, **kwargs) -> None:
|
| 1205 |
+
raise NotImplementedError(
|
| 1206 |
+
f"{type(self).__name__}: device_assert_async should be handled by CSEProxy"
|
| 1207 |
+
)
|
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/optimize_indexing.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
from typing import Any
|
| 3 |
+
|
| 4 |
+
import sympy
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
from torch.utils._sympy.value_ranges import ValueRanges
|
| 8 |
+
|
| 9 |
+
from .loop_body import LoopBody
|
| 10 |
+
from .utils import dominated_nodes
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def val_expressable_in_32_bits(val: Any) -> bool:
|
| 14 |
+
if getattr(val, "is_Boolean", False):
|
| 15 |
+
return True
|
| 16 |
+
|
| 17 |
+
if isinstance(val, sympy.Expr):
|
| 18 |
+
assert val.is_number
|
| 19 |
+
if val.is_Integer or val.is_Boolean:
|
| 20 |
+
val = int(val)
|
| 21 |
+
else:
|
| 22 |
+
val = float(val)
|
| 23 |
+
|
| 24 |
+
# bound within mantissa
|
| 25 |
+
if isinstance(val, float):
|
| 26 |
+
return val <= (2**24) and val >= -(2**24)
|
| 27 |
+
|
| 28 |
+
if isinstance(val, int):
|
| 29 |
+
iinfo = torch.iinfo(torch.int32)
|
| 30 |
+
return val <= iinfo.max and val >= iinfo.min
|
| 31 |
+
|
| 32 |
+
raise TypeError(f"Unexpected value {val}")
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def range_expressable_in_32_bits(range: ValueRanges[sympy.Expr]) -> bool:
|
| 36 |
+
return val_expressable_in_32_bits(range.lower) and val_expressable_in_32_bits(
|
| 37 |
+
range.upper
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def try_to_reduce_precision(
|
| 42 |
+
node: Any,
|
| 43 |
+
bounds: dict[Any, Any],
|
| 44 |
+
indirect_vars: list[Any],
|
| 45 |
+
indices: dict[Any, sympy.Expr],
|
| 46 |
+
replacement_vals: dict[Any, ValueRanges[sympy.Expr]],
|
| 47 |
+
) -> None:
|
| 48 |
+
# if a downstream use of a node explicitly converts to int32, or float16/float32/float64,
|
| 49 |
+
# then it's precision is set for that chain of uses, and we don't need to consider those
|
| 50 |
+
# dominated values
|
| 51 |
+
def skip_filter(node: Any) -> bool:
|
| 52 |
+
return node.target == "to_dtype" and node.args[2] in (
|
| 53 |
+
torch.int32,
|
| 54 |
+
torch.float32,
|
| 55 |
+
torch.float64,
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
# TODO - there are dominated uses whose dtype does not depend on whether
|
| 59 |
+
# we reduce the precision here, e.g. add(int64, int64) one of the args can be reduced to
|
| 60 |
+
# int32 without changing the output precision of the node. this case hasn't shown up
|
| 61 |
+
for dominated in dominated_nodes([node], skip_filter):
|
| 62 |
+
if dominated.target in ["store", "output"]:
|
| 63 |
+
continue
|
| 64 |
+
|
| 65 |
+
if isinstance(dominated.target, str) and "set_indirect" in dominated.target:
|
| 66 |
+
idx = int(dominated.target[len("set_indirect") :])
|
| 67 |
+
indirect_var = indirect_vars[idx]
|
| 68 |
+
|
| 69 |
+
# We check that we can compute all the indices it's involved in with int32
|
| 70 |
+
for index, expr in indices.items():
|
| 71 |
+
if indirect_var in expr.free_symbols:
|
| 72 |
+
index_val = replacement_vals[index]
|
| 73 |
+
|
| 74 |
+
if math.isinf(index_val.lower) or math.isinf(index_val.upper):
|
| 75 |
+
return
|
| 76 |
+
|
| 77 |
+
# all indices are integers, so make sure that we
|
| 78 |
+
# use the bounds of integers instead of floats.
|
| 79 |
+
# TODO - not sure if we should be doing int/float casts while tracing,
|
| 80 |
+
# might interfere with sympy.
|
| 81 |
+
|
| 82 |
+
index_val_int = ValueRanges[sympy.Expr](
|
| 83 |
+
int(index_val.lower), int(index_val.upper)
|
| 84 |
+
)
|
| 85 |
+
if not range_expressable_in_32_bits(index_val_int):
|
| 86 |
+
return
|
| 87 |
+
|
| 88 |
+
if not range_expressable_in_32_bits(bounds[dominated]):
|
| 89 |
+
return
|
| 90 |
+
|
| 91 |
+
args = list(node.args)
|
| 92 |
+
args[2] = torch.int32
|
| 93 |
+
node.args = tuple(args)
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def indexing_dtype_strength_reduction(loop_body: LoopBody) -> None:
|
| 97 |
+
"""
|
| 98 |
+
Performs Value Range Analysis on LoopBody's fx graph to reduce precision of
|
| 99 |
+
intermediaries from int64 to int32
|
| 100 |
+
"""
|
| 101 |
+
bv = loop_body.bounds()
|
| 102 |
+
|
| 103 |
+
int64_dtype_nodes = [
|
| 104 |
+
node
|
| 105 |
+
for node in loop_body.get_nodes()
|
| 106 |
+
if (
|
| 107 |
+
node.target == "to_dtype"
|
| 108 |
+
and node.args[2] == torch.int64
|
| 109 |
+
and node not in bv.unbounded_vars
|
| 110 |
+
)
|
| 111 |
+
]
|
| 112 |
+
if not int64_dtype_nodes:
|
| 113 |
+
return
|
| 114 |
+
|
| 115 |
+
bounds = bv.get_bounds()
|
| 116 |
+
|
| 117 |
+
# TODO - if dominated node of one to_dtype is not expressible in int32,
|
| 118 |
+
# we should short circuit another to_dtype node if that node also dominates
|
| 119 |
+
for node in int64_dtype_nodes:
|
| 120 |
+
try_to_reduce_precision(
|
| 121 |
+
node,
|
| 122 |
+
bounds,
|
| 123 |
+
loop_body.indirect_vars,
|
| 124 |
+
loop_body.indexing_exprs,
|
| 125 |
+
bv.replacement_vals,
|
| 126 |
+
)
|
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/output_code.py
ADDED
|
@@ -0,0 +1,1202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
This provides an abstract class which parametrizes over an "output code" concept
|
| 3 |
+
for Inductor. Intuitively, this represents the compiled callable which Inductor
|
| 4 |
+
produces which you can call to get optimized code. However, this callable
|
| 5 |
+
has some other capabilities:
|
| 6 |
+
|
| 7 |
+
- It is serializable, so you can save/load this product from disk without
|
| 8 |
+
having to do compilation again.
|
| 9 |
+
|
| 10 |
+
- (When using remote cache) it is addressable, so you can save just a key
|
| 11 |
+
which you can use to load this product from remote cache later.
|
| 12 |
+
|
| 13 |
+
This class is abstract because we have several different implementations of
|
| 14 |
+
serialized format:
|
| 15 |
+
|
| 16 |
+
- Python wrapper (the default)
|
| 17 |
+
|
| 18 |
+
- AOTInductor (this produces ABI stable binaries which work across PyTorch
|
| 19 |
+
versions)
|
| 20 |
+
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
from __future__ import annotations
|
| 24 |
+
|
| 25 |
+
import dataclasses
|
| 26 |
+
import logging
|
| 27 |
+
import os
|
| 28 |
+
from functools import partial
|
| 29 |
+
from typing import Any, TYPE_CHECKING, TypeAlias
|
| 30 |
+
|
| 31 |
+
import torch
|
| 32 |
+
from torch._dynamo.utils import counters, get_runtime_metrics_context
|
| 33 |
+
from torch._higher_order_ops.wrap import inductor_compiled_code
|
| 34 |
+
from torch._inductor.cudagraph_utils import (
|
| 35 |
+
BoxedDeviceIndex,
|
| 36 |
+
CudagraphCachedInfo,
|
| 37 |
+
CudagraphMetadata,
|
| 38 |
+
get_partition_cudagraph_metadata,
|
| 39 |
+
get_placeholder_info,
|
| 40 |
+
log_cudagraph_skip_and_bump_counter,
|
| 41 |
+
)
|
| 42 |
+
from torch._inductor.freezing_utils import has_frozen_params, is_frozen_param
|
| 43 |
+
from torch._inductor.utils import (
|
| 44 |
+
_unstable_customized_partition_wrapper,
|
| 45 |
+
align_inputs_from_check_idxs,
|
| 46 |
+
BoxedBool,
|
| 47 |
+
CUDAGraphWrapperMetadata,
|
| 48 |
+
GraphPartitionMap,
|
| 49 |
+
InputType,
|
| 50 |
+
output_node,
|
| 51 |
+
set_tracing_context_output_strides,
|
| 52 |
+
)
|
| 53 |
+
from torch._opaque_base import OpaqueBase
|
| 54 |
+
from torch.fx._graph_pickler import _node_metadata_key_filter_safe, _ops_filter_safe
|
| 55 |
+
from torch.utils._ordered_set import OrderedSet
|
| 56 |
+
from torch.utils._python_dispatch import is_in_torch_dispatch_mode
|
| 57 |
+
|
| 58 |
+
from . import config
|
| 59 |
+
from .runtime.autotune_cache import AutotuneCacheBundler
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
if TYPE_CHECKING:
|
| 63 |
+
from collections import Counter
|
| 64 |
+
from collections.abc import Callable, Sequence
|
| 65 |
+
|
| 66 |
+
from torch._inductor import metrics
|
| 67 |
+
from torch._inductor.graph import GraphLowering
|
| 68 |
+
from torch._library.fake_class_registry import FakeScriptObject
|
| 69 |
+
from torch.export.pt2_archive._package_weights import Weights
|
| 70 |
+
|
| 71 |
+
from .compile_fx import _CompileFxKwargs
|
| 72 |
+
from .triton_bundler import TritonBundle
|
| 73 |
+
|
| 74 |
+
log = logging.getLogger(__name__)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
@dataclasses.dataclass
|
| 78 |
+
class OutputCode:
|
| 79 |
+
# TODO: Remove underscores here
|
| 80 |
+
|
| 81 |
+
# None if the output is not remote cacheable
|
| 82 |
+
_fx_graph_cache_key: str | None = dataclasses.field(default=None, init=False)
|
| 83 |
+
_fx_graph_cache_debug_lines: list[str] | None = dataclasses.field(
|
| 84 |
+
default=None, init=False
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
# How long it took to compile this OutputCode, end to end
|
| 88 |
+
_time_taken_ns: int | None = dataclasses.field(default=None, init=False)
|
| 89 |
+
|
| 90 |
+
def __call__(self, inputs: Sequence[Any]) -> Any:
|
| 91 |
+
raise NotImplementedError(type(self))
|
| 92 |
+
|
| 93 |
+
def prepare_for_serialization(self) -> None:
|
| 94 |
+
raise NotImplementedError(type(self))
|
| 95 |
+
|
| 96 |
+
def post_compile(
|
| 97 |
+
self,
|
| 98 |
+
example_inputs: Sequence[InputType],
|
| 99 |
+
constants: CompiledFxGraphConstants,
|
| 100 |
+
graph_kwargs: _CompileFxKwargs,
|
| 101 |
+
) -> None:
|
| 102 |
+
raise NotImplementedError(type(self))
|
| 103 |
+
|
| 104 |
+
# TODO: Get rid of this
|
| 105 |
+
def set_triton_bundle(self, triton_bundle: Any) -> None:
|
| 106 |
+
raise NotImplementedError(type(self))
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
_StrideExprStr: TypeAlias = str
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
# copy_ fails when trying to write to tensors with memory overlap,
|
| 113 |
+
# for expanded dimensions (a dimension which used to have size 1 -> ?)
|
| 114 |
+
# we can select one element from that dimension and write to it
|
| 115 |
+
# to achieve writing to all values of that dimension of the input tensor
|
| 116 |
+
def get_expanded_dims(t: torch.Tensor) -> list[int]:
|
| 117 |
+
if not isinstance(t, torch.Tensor):
|
| 118 |
+
# pyrefly: ignore [bad-return]
|
| 119 |
+
return None
|
| 120 |
+
return [i for i in range(t.ndim) if t.stride(i) == 0 and t.size(i) != 1]
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def index_expanded_dims(t: torch.Tensor, expanded_dims: list[int]) -> torch.Tensor:
|
| 124 |
+
for expanded_dim in expanded_dims:
|
| 125 |
+
t = torch.ops.aten.slice(t, expanded_dim, 0, 1)
|
| 126 |
+
return t
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def complex_memory_overlap(t: torch.Tensor) -> bool:
|
| 130 |
+
if config.always_complex_memory_overlap_TESTING_ONLY:
|
| 131 |
+
return True
|
| 132 |
+
|
| 133 |
+
# if torch._debug_has_internal_overlap thinks this tensor potentially has
|
| 134 |
+
# memory overlap internally, let's dig deeper to find out whether it's true.
|
| 135 |
+
#
|
| 136 |
+
# Call squeeze() so that dimension with size 1 does not cause false positive.
|
| 137 |
+
t = index_expanded_dims(t, get_expanded_dims(t)).squeeze()
|
| 138 |
+
if torch._debug_has_internal_overlap(t) != 0:
|
| 139 |
+
strides = t.stride()
|
| 140 |
+
sizes = t.shape
|
| 141 |
+
indices = list(range(len(strides)))
|
| 142 |
+
indices = [x for _, x in sorted(zip(strides, indices))]
|
| 143 |
+
for i in range(len(strides)):
|
| 144 |
+
prev_stride = 1 if i == 0 else strides[indices[i - 1]]
|
| 145 |
+
prev_size = 1 if i == 0 else sizes[indices[i - 1]]
|
| 146 |
+
if strides[indices[i]] < prev_stride * prev_size:
|
| 147 |
+
return True
|
| 148 |
+
return False
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def maybe_handle_backward_generation(
|
| 152 |
+
compiled_graph: CompiledFxGraph,
|
| 153 |
+
boxed_forward_device_index: BoxedDeviceIndex | None,
|
| 154 |
+
) -> None:
|
| 155 |
+
assert compiled_graph.current_callable is not None
|
| 156 |
+
is_backward = compiled_graph.fx_kwargs["is_backward"]
|
| 157 |
+
|
| 158 |
+
# See [Backward Generation Handling]
|
| 159 |
+
# if cudagraph'd the forward and set the device, we need to let the cudagraph manager
|
| 160 |
+
# know we are we running the backward even if we will not run it in cudagraphs
|
| 161 |
+
if is_backward and config.triton.cudagraph_trees:
|
| 162 |
+
assert boxed_forward_device_index is not None
|
| 163 |
+
assert boxed_forward_device_index.value is not None
|
| 164 |
+
compiled_graph_callable = compiled_graph.current_callable
|
| 165 |
+
|
| 166 |
+
manager = torch._inductor.cudagraph_trees.get_manager(
|
| 167 |
+
boxed_forward_device_index.value, create_if_none_exists=False
|
| 168 |
+
)
|
| 169 |
+
# should already exist from forward
|
| 170 |
+
assert manager is not None
|
| 171 |
+
|
| 172 |
+
def compiled_artifact(new_inputs: list[Any]) -> Callable[..., Any]:
|
| 173 |
+
manager.set_to_running_backward() # type: ignore[union-attr]
|
| 174 |
+
return compiled_graph_callable(new_inputs)
|
| 175 |
+
|
| 176 |
+
compiled_graph.current_callable = compiled_artifact
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
def prepare_cudagraph_post_compile(
|
| 180 |
+
compiled_graph: CompiledFxGraph,
|
| 181 |
+
example_inputs: Sequence[InputType],
|
| 182 |
+
boxed_forward_device_index: BoxedDeviceIndex | None,
|
| 183 |
+
) -> None:
|
| 184 |
+
if not config.triton.cudagraph_trees:
|
| 185 |
+
# Force specialize all inputs so that CUDA graphs will work
|
| 186 |
+
for t in example_inputs:
|
| 187 |
+
if isinstance(t, torch.SymInt):
|
| 188 |
+
int(t) # guard
|
| 189 |
+
|
| 190 |
+
is_inference = compiled_graph.fx_kwargs["is_inference"]
|
| 191 |
+
is_backward = compiled_graph.fx_kwargs["is_backward"]
|
| 192 |
+
if boxed_forward_device_index is not None and not is_inference and not is_backward:
|
| 193 |
+
boxed_forward_device_index.set(next(iter(compiled_graph.device_idxs)))
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
def cudagraph_post_compile(
|
| 197 |
+
example_inputs: Sequence[InputType],
|
| 198 |
+
compiled_graph: CompiledFxGraph,
|
| 199 |
+
cudagraphs: BoxedBool,
|
| 200 |
+
constants: dict[str, torch.Tensor | type],
|
| 201 |
+
boxed_forward_device_index: BoxedDeviceIndex | None,
|
| 202 |
+
) -> None:
|
| 203 |
+
"""
|
| 204 |
+
Checks for any reasons not to run cudagraphs and then
|
| 205 |
+
runs it on compiled_graph.
|
| 206 |
+
Mutates the `compiled_graph.current_callable` and `cudagraphs`
|
| 207 |
+
"""
|
| 208 |
+
from torch._inductor.compiler_bisector import CompilerBisector
|
| 209 |
+
|
| 210 |
+
assert compiled_graph.current_callable is not None
|
| 211 |
+
assert compiled_graph.cudagraph_info is not None
|
| 212 |
+
cached_info = compiled_graph.cudagraph_info
|
| 213 |
+
cudagraph_fail_reasons = cached_info.cudagraph_fail_reasons
|
| 214 |
+
is_inference = compiled_graph.fx_kwargs["is_inference"]
|
| 215 |
+
is_backward = compiled_graph.fx_kwargs["is_backward"]
|
| 216 |
+
|
| 217 |
+
# Check if bisector wants to disable cudagraphs for this graph
|
| 218 |
+
if CompilerBisector.disable_subsystem("inductor", "cudagraphs"):
|
| 219 |
+
BoxedBool.disable(cudagraphs)
|
| 220 |
+
maybe_handle_backward_generation(compiled_graph, boxed_forward_device_index)
|
| 221 |
+
log_cudagraph_skip_and_bump_counter("skipping cudagraphs due to bisector")
|
| 222 |
+
return
|
| 223 |
+
|
| 224 |
+
if not cudagraph_fail_reasons:
|
| 225 |
+
fx_kwargs = compiled_graph.fx_kwargs
|
| 226 |
+
static_input_idxs = fx_kwargs["static_input_idxs"]
|
| 227 |
+
|
| 228 |
+
placeholders = cached_info.placeholders
|
| 229 |
+
stack_traces = cached_info.stack_traces
|
| 230 |
+
assert stack_traces is not None, (
|
| 231 |
+
"stack_traces should not be None in cudagraph_post_compile"
|
| 232 |
+
)
|
| 233 |
+
|
| 234 |
+
prepare_cudagraph_post_compile(
|
| 235 |
+
compiled_graph, example_inputs, boxed_forward_device_index
|
| 236 |
+
)
|
| 237 |
+
|
| 238 |
+
current_callable = compiled_graph.current_callable
|
| 239 |
+
assert current_callable is not None
|
| 240 |
+
# Filter to only tensor constants (exclude opaque value type classes)
|
| 241 |
+
tensor_constants = {
|
| 242 |
+
k: v for k, v in constants.items() if isinstance(v, torch.Tensor)
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
device_index = next(iter(compiled_graph.device_idxs))
|
| 246 |
+
cudagraphify_kwargs = dict(
|
| 247 |
+
device_index=device_index,
|
| 248 |
+
stack_traces=stack_traces,
|
| 249 |
+
is_backward=is_backward,
|
| 250 |
+
is_inference=is_inference,
|
| 251 |
+
constants=tuple(tensor_constants.values()),
|
| 252 |
+
placeholders=placeholders,
|
| 253 |
+
mutated_input_idxs=tuple(compiled_graph.mutated_input_idxs),
|
| 254 |
+
)
|
| 255 |
+
|
| 256 |
+
policy = config.cudagraph_policy
|
| 257 |
+
if policy is not None:
|
| 258 |
+
compiled_graph.current_callable = policy.cudagraphify(
|
| 259 |
+
current_callable,
|
| 260 |
+
example_inputs,
|
| 261 |
+
static_input_idxs or (),
|
| 262 |
+
**cudagraphify_kwargs,
|
| 263 |
+
)
|
| 264 |
+
else:
|
| 265 |
+
from .compile_fx import cudagraphify
|
| 266 |
+
|
| 267 |
+
compiled_graph.current_callable = cudagraphify(
|
| 268 |
+
current_callable,
|
| 269 |
+
static_input_idxs=static_input_idxs or (),
|
| 270 |
+
**cudagraphify_kwargs,
|
| 271 |
+
)
|
| 272 |
+
|
| 273 |
+
else:
|
| 274 |
+
BoxedBool.disable(cudagraphs)
|
| 275 |
+
maybe_handle_backward_generation(compiled_graph, boxed_forward_device_index)
|
| 276 |
+
|
| 277 |
+
if "cuda" in compiled_graph.device_types:
|
| 278 |
+
# prefer better disable_cudagraphs_reason bc stack trace
|
| 279 |
+
# TODO: migrate all disable reasons to stack trace, refactor
|
| 280 |
+
if compiled_graph.disabled_cudagraphs_reason:
|
| 281 |
+
log_cudagraph_skip_and_bump_counter(
|
| 282 |
+
compiled_graph.disabled_cudagraphs_reason
|
| 283 |
+
)
|
| 284 |
+
else:
|
| 285 |
+
log_cudagraph_skip_and_bump_counter(
|
| 286 |
+
f"skipping cudagraphs due to {cudagraph_fail_reasons}"
|
| 287 |
+
)
|
| 288 |
+
|
| 289 |
+
|
| 290 |
+
def cudagraph_partition_post_compile(
|
| 291 |
+
example_inputs: Sequence[InputType],
|
| 292 |
+
compiled_graph: CompiledFxGraph,
|
| 293 |
+
cudagraphs: BoxedBool,
|
| 294 |
+
constants: dict[str, torch.Tensor | type],
|
| 295 |
+
boxed_forward_device_index: BoxedDeviceIndex | None,
|
| 296 |
+
) -> None:
|
| 297 |
+
"""
|
| 298 |
+
Cudagraphify each partition functions, which first prepares the necessary
|
| 299 |
+
metadata and then applies the cudagraphify function to each partition.
|
| 300 |
+
|
| 301 |
+
Assuming all partition functions are cudagraphified and share the same order
|
| 302 |
+
as `compiled_graph.partition_maps`. See [Note: Graph Partition Map for CUDAGraph].
|
| 303 |
+
"""
|
| 304 |
+
from torch._inductor.compiler_bisector import CompilerBisector
|
| 305 |
+
|
| 306 |
+
if CompilerBisector.disable_subsystem("inductor", "cudagraphs"):
|
| 307 |
+
BoxedBool.disable(cudagraphs)
|
| 308 |
+
maybe_handle_backward_generation(compiled_graph, boxed_forward_device_index)
|
| 309 |
+
log_cudagraph_skip_and_bump_counter("skipping cudagraphs due to bisector")
|
| 310 |
+
return
|
| 311 |
+
|
| 312 |
+
assert compiled_graph.cudagraph_info is not None
|
| 313 |
+
cudagraph_fail_reasons = compiled_graph.cudagraph_info.cudagraph_fail_reasons
|
| 314 |
+
|
| 315 |
+
if (
|
| 316 |
+
cudagraph_fail_reasons
|
| 317 |
+
or compiled_graph.partition_maps is None
|
| 318 |
+
or len(compiled_graph.partition_maps) == 0
|
| 319 |
+
):
|
| 320 |
+
# cudagraphify is not called if there are no partitions
|
| 321 |
+
BoxedBool.disable(cudagraphs)
|
| 322 |
+
maybe_handle_backward_generation(compiled_graph, boxed_forward_device_index)
|
| 323 |
+
return
|
| 324 |
+
|
| 325 |
+
assert compiled_graph.current_callable is not None
|
| 326 |
+
assert compiled_graph.recursively_apply_fns is not None
|
| 327 |
+
is_inference = compiled_graph.fx_kwargs["is_inference"]
|
| 328 |
+
is_backward = compiled_graph.fx_kwargs["is_backward"]
|
| 329 |
+
static_input_idxs = OrderedSet(compiled_graph.fx_kwargs["static_input_idxs"] or ())
|
| 330 |
+
mutated_input_idxs = compiled_graph.mutated_input_idxs
|
| 331 |
+
device_index = next(iter(compiled_graph.device_idxs))
|
| 332 |
+
|
| 333 |
+
# Filter to only tensor constants (exclude opaque value type classes)
|
| 334 |
+
tensor_constants = {
|
| 335 |
+
k: v for k, v in constants.items() if isinstance(v, torch.Tensor)
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
assert compiled_graph.cudagraph_info.stack_traces is not None, (
|
| 339 |
+
"stack_traces should not be None in cudagraph_partition_post_compile"
|
| 340 |
+
)
|
| 341 |
+
graph_metadata = CudagraphMetadata(
|
| 342 |
+
compiled_graph.cudagraph_info.placeholders,
|
| 343 |
+
static_input_idxs,
|
| 344 |
+
mutated_input_idxs,
|
| 345 |
+
compiled_graph.cudagraph_info.stack_traces,
|
| 346 |
+
tensor_constants,
|
| 347 |
+
)
|
| 348 |
+
|
| 349 |
+
prepare_cudagraph_post_compile(
|
| 350 |
+
compiled_graph, example_inputs, boxed_forward_device_index
|
| 351 |
+
)
|
| 352 |
+
|
| 353 |
+
from .compile_fx import cudagraphify
|
| 354 |
+
|
| 355 |
+
# cudagraphify each partition function, assuming every graph partition function
|
| 356 |
+
# is cudagraphable. Non-cudagraphable ops (e.g., cpu ops) are inlined into
|
| 357 |
+
# `call` function and not included in partition functions.
|
| 358 |
+
cudagraphify_fns = []
|
| 359 |
+
for partition_map in compiled_graph.partition_maps:
|
| 360 |
+
partition_metadata = get_partition_cudagraph_metadata(
|
| 361 |
+
partition_map,
|
| 362 |
+
graph_metadata,
|
| 363 |
+
)
|
| 364 |
+
|
| 365 |
+
cudagraphify_fn = partial(
|
| 366 |
+
cudagraphify,
|
| 367 |
+
static_input_idxs=tuple(partition_metadata.static_input_idxs),
|
| 368 |
+
device_index=device_index,
|
| 369 |
+
stack_traces=partition_metadata.stack_traces,
|
| 370 |
+
is_backward=is_backward,
|
| 371 |
+
is_inference=is_inference,
|
| 372 |
+
constants=tuple(partition_metadata.constants.values()),
|
| 373 |
+
placeholders=partition_metadata.placeholders,
|
| 374 |
+
mutated_input_idxs=tuple(partition_metadata.mutated_input_idxs),
|
| 375 |
+
)
|
| 376 |
+
cudagraphify_fns.append(cudagraphify_fn)
|
| 377 |
+
|
| 378 |
+
compiled_graph.recursively_apply_fns(cudagraphify_fns)
|
| 379 |
+
|
| 380 |
+
|
| 381 |
+
def maybe_realign_inputs(
|
| 382 |
+
ran_cudagraphs: BoxedBool,
|
| 383 |
+
compiled_graph: CompiledFxGraph,
|
| 384 |
+
inputs_to_check: Sequence[int],
|
| 385 |
+
mutated_inputs_idxs: OrderedSet[int],
|
| 386 |
+
) -> None:
|
| 387 |
+
"""
|
| 388 |
+
Realigns input strides from inputs_to_check if
|
| 389 |
+
we didn't end up running cudagraphs. Mutates
|
| 390 |
+
`compiled_graph.current_callable` if cudagraphs
|
| 391 |
+
was run. Otherwise, does nothing.
|
| 392 |
+
|
| 393 |
+
Non-mutated inputs are handled by deferred alignment copies
|
| 394 |
+
in the generated code. Only mutated inputs need the wrapper
|
| 395 |
+
for writeback.
|
| 396 |
+
"""
|
| 397 |
+
if not ran_cudagraphs:
|
| 398 |
+
check_idxs = inputs_to_check
|
| 399 |
+
if compiled_graph._defers_input_alignment:
|
| 400 |
+
# Non-mutated inputs are handled by deferred alignment copies
|
| 401 |
+
# in the generated Python code. Only mutated inputs need the wrapper
|
| 402 |
+
# for writeback. Backends that don't emit deferred copies (cpp_wrapper,
|
| 403 |
+
# FXIR) need the full wrapper.
|
| 404 |
+
check_idxs = [i for i in inputs_to_check if i in mutated_inputs_idxs]
|
| 405 |
+
if check_idxs:
|
| 406 |
+
assert compiled_graph.current_callable is not None
|
| 407 |
+
new_callable = align_inputs_from_check_idxs(
|
| 408 |
+
compiled_graph.current_callable,
|
| 409 |
+
check_idxs,
|
| 410 |
+
mutated_inputs_idxs,
|
| 411 |
+
)
|
| 412 |
+
if new_callable is not compiled_graph.current_callable:
|
| 413 |
+
compiled_graph.current_callable = new_callable
|
| 414 |
+
|
| 415 |
+
|
| 416 |
+
class CompiledFxGraphConstants:
|
| 417 |
+
"""Wrapper class that unwraps constants from a compiled fx graph. This
|
| 418 |
+
version of the class only supports directly grabbing the saved constants off of
|
| 419 |
+
a CompiledFxGraph.
|
| 420 |
+
|
| 421 |
+
With freezing, FxGraphCache doesn't store the constants of the input
|
| 422 |
+
GraphModule it gets from AOTAutograd. Instead, it saves just the **names**
|
| 423 |
+
of those constants, and grabs the constant values directly from the graph module
|
| 424 |
+
passed in at runtime.
|
| 425 |
+
|
| 426 |
+
Thing is, we don't always *have* the graph module available at runtime, hence
|
| 427 |
+
the existence of this class and its CompiledFxGraphConstantsWithGm counterpart.
|
| 428 |
+
|
| 429 |
+
To support freezing, FXGraphCache gets passed a CompiledFxGraphConstantsWithGm during
|
| 430 |
+
post compile. Otherwise, CompiledFxGraphConstants supports the basic case of loading
|
| 431 |
+
the value of constants directly off of the original saved object.
|
| 432 |
+
"""
|
| 433 |
+
|
| 434 |
+
def unwrap(self, g: CompiledFxGraph) -> dict[str, torch.Tensor | type]:
|
| 435 |
+
assert g.constants is not None
|
| 436 |
+
return {**g.constants, **g.opaque_value_type_classes}
|
| 437 |
+
|
| 438 |
+
|
| 439 |
+
class CompiledFxGraphConstantsWithGm(CompiledFxGraphConstants):
|
| 440 |
+
"""
|
| 441 |
+
This version of CompiledFxGraphConstants, instead of grabbing constants
|
| 442 |
+
directly saved on CompiledFxGraphs, will just grab their names. Then, it takes
|
| 443 |
+
a second GraphModule to grab the corresponding constant values out of.
|
| 444 |
+
|
| 445 |
+
This is necessary for supporting freezing in FxGraphCache.
|
| 446 |
+
"""
|
| 447 |
+
|
| 448 |
+
def __init__(self, gm: torch.fx.GraphModule) -> None:
|
| 449 |
+
self.gm = gm
|
| 450 |
+
|
| 451 |
+
def unwrap(self, g: CompiledFxGraph) -> dict[str, torch.Tensor | type]:
|
| 452 |
+
frozen_params = {
|
| 453 |
+
name: getattr(self.gm, orig_name)
|
| 454 |
+
for name, orig_name in g.frozen_param_names.items()
|
| 455 |
+
}
|
| 456 |
+
constants = g.constants or {}
|
| 457 |
+
return {**constants, **frozen_params, **g.opaque_value_type_classes}
|
| 458 |
+
|
| 459 |
+
|
| 460 |
+
@dataclasses.dataclass
|
| 461 |
+
class CompiledFxGraph(OutputCode):
|
| 462 |
+
"""
|
| 463 |
+
Class holding a compiled FX graph. This is the object serialized on disk
|
| 464 |
+
to support FxGraph caching.
|
| 465 |
+
"""
|
| 466 |
+
|
| 467 |
+
current_callable: Callable[..., Any] | None
|
| 468 |
+
recursively_apply_fns: Callable[..., Any] | None
|
| 469 |
+
compiled_fn_runner: Any | None
|
| 470 |
+
cache_key: str
|
| 471 |
+
source_code: str = dataclasses.field(repr=False) # Do not display source_code
|
| 472 |
+
runnable_graph_str: str = dataclasses.field(repr=False) # Do not display graph
|
| 473 |
+
inductor_post_grad_graph_str: str = dataclasses.field(
|
| 474 |
+
repr=False
|
| 475 |
+
) # Do not display graph
|
| 476 |
+
cache_linemap: list[tuple[int, str]] | None
|
| 477 |
+
device_types: OrderedSet[str]
|
| 478 |
+
device_idxs: OrderedSet[int]
|
| 479 |
+
mutated_inputs: OrderedSet[str]
|
| 480 |
+
mutated_input_idxs: OrderedSet[int]
|
| 481 |
+
constants: dict[str, torch.Tensor] | None
|
| 482 |
+
frozen_param_names: dict[str, str]
|
| 483 |
+
torchbind_constants: dict[str, torch._C.ScriptObject | FakeScriptObject]
|
| 484 |
+
opaque_value_type_classes: dict[str, type]
|
| 485 |
+
output_strides: list[tuple[_StrideExprStr, ...] | None] | None
|
| 486 |
+
disabled_cudagraphs_reason: str | None
|
| 487 |
+
metrics_deltas: metrics.CachedMetricsDeltas
|
| 488 |
+
counter_deltas: Counter[str]
|
| 489 |
+
# This is a string representation of an expression we serialize
|
| 490 |
+
# with the object so the guards can be evaluated in a different
|
| 491 |
+
# context in order to verify the validity of serving a cached
|
| 492 |
+
# fx graph. The expression must be generated by:
|
| 493 |
+
# ShapeEnv.produce_guards_expression()
|
| 494 |
+
guards_expr: str | None
|
| 495 |
+
extern_libs_key: str | None
|
| 496 |
+
inductor_provenance_mapping_str: str | None
|
| 497 |
+
inductor_provenance_stack_traces_str: str | None
|
| 498 |
+
|
| 499 |
+
cudagraph_info: CudagraphCachedInfo | None
|
| 500 |
+
partition_maps: list[GraphPartitionMap] | None
|
| 501 |
+
compile_region_name: str | None
|
| 502 |
+
fx_kwargs: _CompileFxKwargs
|
| 503 |
+
inputs_to_check: Sequence[int]
|
| 504 |
+
|
| 505 |
+
_boxed_call: bool | None = None
|
| 506 |
+
_triton_bundle: TritonBundle | None = None
|
| 507 |
+
_wrap_compiled_regions: bool = False
|
| 508 |
+
_defers_input_alignment: bool = False
|
| 509 |
+
# Metadata-stripped copy of the FX graph for fake tensor propagation.
|
| 510 |
+
# Running this graph under FakeTensorMode re-derives output shapes
|
| 511 |
+
# (including aliasing) from the input shapes.
|
| 512 |
+
_original_gm: torch.fx.GraphModule | None = None
|
| 513 |
+
|
| 514 |
+
def __init__(
|
| 515 |
+
self,
|
| 516 |
+
current_callable: Callable[..., Any] | None,
|
| 517 |
+
graph: GraphLowering,
|
| 518 |
+
gm: torch.fx.GraphModule,
|
| 519 |
+
output_strides: list[tuple[_StrideExprStr, ...] | None],
|
| 520 |
+
disabled_cudagraphs_reason: str | None,
|
| 521 |
+
metrics_deltas: metrics.CachedMetricsDeltas,
|
| 522 |
+
counter_deltas: Counter[str],
|
| 523 |
+
cudagraphs: BoxedBool,
|
| 524 |
+
example_inputs: Sequence[InputType],
|
| 525 |
+
static_input_idxs: Sequence[int],
|
| 526 |
+
compile_region_name: str | None,
|
| 527 |
+
fx_kwargs: _CompileFxKwargs,
|
| 528 |
+
inputs_to_check: Sequence[int],
|
| 529 |
+
runnable_graph_str: str,
|
| 530 |
+
inductor_post_grad_graph_str: str,
|
| 531 |
+
compiled_fn_runner: Any | None = None,
|
| 532 |
+
inductor_provenance_mapping_str: str | None = None,
|
| 533 |
+
inductor_provenance_stack_traces_str: str | None = None,
|
| 534 |
+
) -> None:
|
| 535 |
+
self.current_callable = current_callable
|
| 536 |
+
self.compiled_fn_runner = compiled_fn_runner
|
| 537 |
+
self.recursively_apply_fns = (
|
| 538 |
+
compiled_fn_runner.recursively_apply_fns
|
| 539 |
+
if compiled_fn_runner is not None
|
| 540 |
+
else None
|
| 541 |
+
)
|
| 542 |
+
self.cache_key = graph.cache_key
|
| 543 |
+
if graph.cache_path:
|
| 544 |
+
with open(graph.cache_path) as f:
|
| 545 |
+
self.source_code = f.read()
|
| 546 |
+
self.runnable_graph_str = runnable_graph_str
|
| 547 |
+
self.inductor_post_grad_graph_str = inductor_post_grad_graph_str
|
| 548 |
+
self.inductor_provenance_mapping_str = inductor_provenance_mapping_str
|
| 549 |
+
self.inductor_provenance_stack_traces_str = inductor_provenance_stack_traces_str
|
| 550 |
+
self.cache_linemap = graph.cache_linemap
|
| 551 |
+
# TODO - ordered set
|
| 552 |
+
self.device_types = OrderedSet(graph.device_types)
|
| 553 |
+
self.device_idxs = OrderedSet(graph.device_idxs)
|
| 554 |
+
self.mutated_inputs = OrderedSet(graph.mutated_inputs)
|
| 555 |
+
self.mutated_input_idxs = OrderedSet(graph.mutated_input_idxs)
|
| 556 |
+
|
| 557 |
+
# We store the constant attributes in the cache entry and re-attach them
|
| 558 |
+
# to the module created in PyCodeCache.load_by_key_path. In the case that
|
| 559 |
+
# the graph has frozen parameters, we save the mapping from the attribute
|
| 560 |
+
# names in the GraphLowering to the original name of the attribute in the
|
| 561 |
+
# GraphModule. When we create the module from the cache entry, we then
|
| 562 |
+
# look up the constants from the current GraphModule. This scheme allows
|
| 563 |
+
# us to support caching with freezing.
|
| 564 |
+
if not has_frozen_params(gm):
|
| 565 |
+
self.constants = graph.constants
|
| 566 |
+
self.frozen_param_names = {}
|
| 567 |
+
else:
|
| 568 |
+
self.constants = {}
|
| 569 |
+
self.frozen_param_names = {}
|
| 570 |
+
for k, v in graph.constants.items():
|
| 571 |
+
if is_frozen_param(v):
|
| 572 |
+
self.frozen_param_names[k] = graph.allocated_constant_name[k]
|
| 573 |
+
else:
|
| 574 |
+
self.constants[k] = v
|
| 575 |
+
|
| 576 |
+
self.torchbind_constants = graph.torchbind_constants
|
| 577 |
+
self.opaque_value_type_classes = graph.opaque_value_type_classes
|
| 578 |
+
self.output_strides = output_strides
|
| 579 |
+
self.disabled_cudagraphs_reason = disabled_cudagraphs_reason
|
| 580 |
+
self.metrics_deltas = metrics_deltas
|
| 581 |
+
self.counter_deltas = counter_deltas
|
| 582 |
+
self.guards_expr = None
|
| 583 |
+
self.extern_libs_key = None
|
| 584 |
+
self.cudagraph_info = None
|
| 585 |
+
self.partition_maps = graph.partition_maps
|
| 586 |
+
self._defers_input_alignment = getattr(graph, "_defers_input_alignment", False)
|
| 587 |
+
self.fx_kwargs = {}
|
| 588 |
+
self.inputs_to_check = ()
|
| 589 |
+
|
| 590 |
+
cudagraph_info = None
|
| 591 |
+
if cudagraphs:
|
| 592 |
+
# check cudagraph disabling reasons from inductor lowering
|
| 593 |
+
if self.disabled_cudagraphs_reason:
|
| 594 |
+
if "cuda" in self.device_types:
|
| 595 |
+
log_cudagraph_skip_and_bump_counter(
|
| 596 |
+
f"skipping cudagraphs due to {self.disabled_cudagraphs_reason}"
|
| 597 |
+
)
|
| 598 |
+
else:
|
| 599 |
+
counters["inductor"]["cudagraph_skips"] += 1
|
| 600 |
+
BoxedBool.disable(cudagraphs)
|
| 601 |
+
else:
|
| 602 |
+
complex_memory_overlap_inputs = any(
|
| 603 |
+
complex_memory_overlap(t)
|
| 604 |
+
for t in example_inputs
|
| 605 |
+
if isinstance(t, torch.Tensor)
|
| 606 |
+
)
|
| 607 |
+
|
| 608 |
+
if not config.triton.cudagraph_support_input_mutation:
|
| 609 |
+
# Skip supports for cudagraph-managed tensors
|
| 610 |
+
from torch._inductor.cudagraph_utils import (
|
| 611 |
+
check_for_mutation_ignore_cuda_graph_managed_tensor,
|
| 612 |
+
)
|
| 613 |
+
|
| 614 |
+
has_mutation_str = (
|
| 615 |
+
check_for_mutation_ignore_cuda_graph_managed_tensor(
|
| 616 |
+
gm,
|
| 617 |
+
self.mutated_inputs,
|
| 618 |
+
self.mutated_input_idxs,
|
| 619 |
+
static_input_idxs,
|
| 620 |
+
)
|
| 621 |
+
)
|
| 622 |
+
has_mutation = has_mutation_str is not None
|
| 623 |
+
|
| 624 |
+
if has_mutation:
|
| 625 |
+
self.disabled_cudagraphs_reason = has_mutation_str
|
| 626 |
+
else:
|
| 627 |
+
# Check mutation later to support cudagraph-managed tensors
|
| 628 |
+
has_mutation = None
|
| 629 |
+
|
| 630 |
+
cudagraph_tests = [
|
| 631 |
+
(not has_mutation, "mutated inputs"),
|
| 632 |
+
(not complex_memory_overlap_inputs, "complex memory overlap"),
|
| 633 |
+
(
|
| 634 |
+
all(
|
| 635 |
+
isinstance(
|
| 636 |
+
t,
|
| 637 |
+
(
|
| 638 |
+
torch.Tensor,
|
| 639 |
+
torch.SymInt,
|
| 640 |
+
torch.Generator,
|
| 641 |
+
OpaqueBase,
|
| 642 |
+
),
|
| 643 |
+
)
|
| 644 |
+
for t in example_inputs
|
| 645 |
+
),
|
| 646 |
+
"non-Tensor inputs",
|
| 647 |
+
),
|
| 648 |
+
]
|
| 649 |
+
output = output_node(gm)
|
| 650 |
+
# output args are tuple of first argument
|
| 651 |
+
assert len(output.args) == 1
|
| 652 |
+
# Use stack traces captured on the output node before
|
| 653 |
+
# post-grad passes, which may strip stack_trace from
|
| 654 |
+
# individual arg nodes.
|
| 655 |
+
stack_traces = output.meta.get("output_stack_traces") or [
|
| 656 |
+
(arg.stack_trace if isinstance(arg, torch.fx.node.Node) else None)
|
| 657 |
+
for arg in output.args[0] # type: ignore[union-attr]
|
| 658 |
+
]
|
| 659 |
+
cudagraph_fail_reasons = [s for b, s in cudagraph_tests if not b]
|
| 660 |
+
placeholders = tuple(get_placeholder_info(gm.graph))
|
| 661 |
+
cudagraph_info = CudagraphCachedInfo(
|
| 662 |
+
placeholders, stack_traces, cudagraph_fail_reasons
|
| 663 |
+
)
|
| 664 |
+
|
| 665 |
+
self.cudagraph_info = cudagraph_info
|
| 666 |
+
self.compile_region_name = compile_region_name
|
| 667 |
+
self.inputs_to_check = inputs_to_check
|
| 668 |
+
self.fx_kwargs = fx_kwargs
|
| 669 |
+
|
| 670 |
+
# aot autograd needs to know to pass in inputs as a list
|
| 671 |
+
self._boxed_call = True
|
| 672 |
+
|
| 673 |
+
# Store whether to wrap compiled regions in inductor_compiled_code HOP
|
| 674 |
+
# This is set at compile time to avoid runtime overhead
|
| 675 |
+
self._wrap_compiled_regions = config.wrap_inductor_compiled_regions
|
| 676 |
+
|
| 677 |
+
if self._wrap_compiled_regions:
|
| 678 |
+
# Store a metadata-stripped copy of the FX graph. Running this
|
| 679 |
+
# under FakeTensorMode re-derives output shapes and aliasing
|
| 680 |
+
# from the input fake tensors.
|
| 681 |
+
import copy
|
| 682 |
+
|
| 683 |
+
gm_copy = copy.deepcopy(gm)
|
| 684 |
+
for node in gm_copy.graph.nodes:
|
| 685 |
+
node.meta.clear()
|
| 686 |
+
self._original_gm = gm_copy
|
| 687 |
+
|
| 688 |
+
def __del__(self) -> None:
|
| 689 |
+
if self.compiled_fn_runner is not None:
|
| 690 |
+
# For torch._inductor.config.graph_partition = True,
|
| 691 |
+
# self.compiled_fn_runner.partitions hold cudagraphified functions
|
| 692 |
+
# which prevents deallocation. When CompiledFxGraph is deleted,
|
| 693 |
+
# self.compiled_fn_runner will not be called in the future so we
|
| 694 |
+
# should also delete these partitions.
|
| 695 |
+
del self.compiled_fn_runner.partitions
|
| 696 |
+
|
| 697 |
+
def __call__(self, inputs: Sequence[Any]) -> Any:
|
| 698 |
+
assert self.current_callable is not None
|
| 699 |
+
|
| 700 |
+
if (
|
| 701 |
+
torch._inductor.debug.RECORD_GRAPH_EXECUTION
|
| 702 |
+
and torch._inductor.debug.GRAPH_EXECUTION_ORDER is not None
|
| 703 |
+
):
|
| 704 |
+
graph_id = self.fx_kwargs.get("graph_id")
|
| 705 |
+
compile_id = (
|
| 706 |
+
torch._inductor.debug.GRAPH_COMPILE_IDS.get(graph_id)
|
| 707 |
+
if graph_id is not None
|
| 708 |
+
and torch._inductor.debug.GRAPH_COMPILE_IDS is not None
|
| 709 |
+
else None
|
| 710 |
+
)
|
| 711 |
+
torch._inductor.debug.GRAPH_EXECUTION_ORDER.append(
|
| 712 |
+
{
|
| 713 |
+
"compile_id": compile_id,
|
| 714 |
+
}
|
| 715 |
+
)
|
| 716 |
+
try:
|
| 717 |
+
# Checking the profiler directly is faster than nullcontext
|
| 718 |
+
if torch.autograd.profiler._is_profiler_enabled:
|
| 719 |
+
with torch._C._profiler._RecordFunctionFast(
|
| 720 |
+
f"## Call CompiledFxGraph {self._fx_graph_cache_key} ##",
|
| 721 |
+
keyword_values={"scope": "user_scope"},
|
| 722 |
+
):
|
| 723 |
+
return self.current_callable(inputs)
|
| 724 |
+
else:
|
| 725 |
+
return self.current_callable(inputs)
|
| 726 |
+
finally:
|
| 727 |
+
get_runtime_metrics_context().finish()
|
| 728 |
+
AutotuneCacheBundler.end_compile()
|
| 729 |
+
|
| 730 |
+
def post_compile(
|
| 731 |
+
self,
|
| 732 |
+
example_inputs: Sequence[InputType],
|
| 733 |
+
constants: CompiledFxGraphConstants,
|
| 734 |
+
graph_kwargs: _CompileFxKwargs,
|
| 735 |
+
) -> None:
|
| 736 |
+
"""
|
| 737 |
+
Run a set of post processing steps after loading from the cache. These involve:
|
| 738 |
+
- Setting the tracing context output strides
|
| 739 |
+
- Running cudagraphs if enabled
|
| 740 |
+
- Realigning inputs
|
| 741 |
+
|
| 742 |
+
This runs whether or not we have a cache hit, and always runs directly after we get a CompiledFxGraph.
|
| 743 |
+
The results of this function are *not* saved in the cache itself.
|
| 744 |
+
"""
|
| 745 |
+
if config.graph_partition and _unstable_customized_partition_wrapper.wrapper:
|
| 746 |
+
# Mechanically apply user-specified cudagraph wrappers without modification
|
| 747 |
+
assert self.recursively_apply_fns is not None
|
| 748 |
+
assert self.compiled_fn_runner is not None
|
| 749 |
+
num_partitions = len(self.compiled_fn_runner.partitions)
|
| 750 |
+
wrapper_metadatas = [
|
| 751 |
+
CUDAGraphWrapperMetadata(num_partitions, i)
|
| 752 |
+
for i in range(num_partitions)
|
| 753 |
+
]
|
| 754 |
+
customized_wrapper = _unstable_customized_partition_wrapper.wrapper
|
| 755 |
+
customized_wrappers_with_metadata = [
|
| 756 |
+
lambda f, m=metadata: customized_wrapper(f, m)
|
| 757 |
+
for metadata in wrapper_metadatas
|
| 758 |
+
]
|
| 759 |
+
self.recursively_apply_fns(customized_wrappers_with_metadata)
|
| 760 |
+
return
|
| 761 |
+
|
| 762 |
+
set_tracing_context_output_strides(example_inputs, self)
|
| 763 |
+
assert graph_kwargs["cudagraphs"] is not None
|
| 764 |
+
assert graph_kwargs["is_backward"] is not None
|
| 765 |
+
is_backward = graph_kwargs["is_backward"]
|
| 766 |
+
cudagraphs: BoxedBool = graph_kwargs["cudagraphs"]
|
| 767 |
+
|
| 768 |
+
# When a CUDAGraphPolicy is set and it says not to wrap this
|
| 769 |
+
# inner CompiledFxGraph (e.g. because wrapping happens at the
|
| 770 |
+
# outer level via policy.wrap_output), disable cudagraphs for
|
| 771 |
+
# this graph so the rest of post_compile (input realignment,
|
| 772 |
+
# _wrap_compiled_regions) still runs normally.
|
| 773 |
+
policy = config.cudagraph_policy
|
| 774 |
+
if policy is not None and not policy.should_wrap(self):
|
| 775 |
+
counters["inductor"]["cudagraph_skips"] += 1
|
| 776 |
+
BoxedBool.disable(cudagraphs)
|
| 777 |
+
|
| 778 |
+
if cudagraphs:
|
| 779 |
+
# It's possible that cudagraphs is enabled, but was disabled
|
| 780 |
+
# during a previous compilation we're loading from the cache.
|
| 781 |
+
# If so, we need to disable it on this new process too.
|
| 782 |
+
if self.disabled_cudagraphs_reason:
|
| 783 |
+
if "cuda" in self.device_types:
|
| 784 |
+
log_cudagraph_skip_and_bump_counter(
|
| 785 |
+
f"skipping cudagraphs due to {self.disabled_cudagraphs_reason}"
|
| 786 |
+
)
|
| 787 |
+
else:
|
| 788 |
+
counters["inductor"]["cudagraph_skips"] += 1
|
| 789 |
+
BoxedBool.disable(cudagraphs)
|
| 790 |
+
else:
|
| 791 |
+
if is_backward:
|
| 792 |
+
assert "boxed_forward_device_index" in graph_kwargs
|
| 793 |
+
boxed_forward_device_index = graph_kwargs[
|
| 794 |
+
"boxed_forward_device_index"
|
| 795 |
+
]
|
| 796 |
+
else:
|
| 797 |
+
# On the forward we don't know whether or not
|
| 798 |
+
# boxed_forward_device_index is set yet
|
| 799 |
+
boxed_forward_device_index = graph_kwargs.get(
|
| 800 |
+
"boxed_forward_device_index", None
|
| 801 |
+
)
|
| 802 |
+
|
| 803 |
+
if config.graph_partition and policy is None:
|
| 804 |
+
# With graph_partition=True, we skip some cudagraph checks
|
| 805 |
+
# if it's supported with partition, so we use
|
| 806 |
+
# cudagraph_partition_post_compile. When a CUDAGraphPolicy
|
| 807 |
+
# is active, we use cudagraph_post_compile instead so the
|
| 808 |
+
# policy controls wrapping via policy.cudagraphify().
|
| 809 |
+
cudagraph_partition_post_compile(
|
| 810 |
+
example_inputs,
|
| 811 |
+
self,
|
| 812 |
+
cudagraphs,
|
| 813 |
+
constants.unwrap(self),
|
| 814 |
+
boxed_forward_device_index,
|
| 815 |
+
)
|
| 816 |
+
else:
|
| 817 |
+
cudagraph_post_compile(
|
| 818 |
+
example_inputs,
|
| 819 |
+
self,
|
| 820 |
+
cudagraphs,
|
| 821 |
+
constants.unwrap(self),
|
| 822 |
+
boxed_forward_device_index,
|
| 823 |
+
)
|
| 824 |
+
inputs_to_check = self.inputs_to_check
|
| 825 |
+
# cudagraphs could have been disabled from the earlier conditions
|
| 826 |
+
# so we still need to realign inputs if that happens
|
| 827 |
+
maybe_realign_inputs(
|
| 828 |
+
cudagraphs,
|
| 829 |
+
self,
|
| 830 |
+
inputs_to_check,
|
| 831 |
+
self.mutated_input_idxs,
|
| 832 |
+
)
|
| 833 |
+
|
| 834 |
+
# Apply inductor_compiled_code HOP wrapper if configured
|
| 835 |
+
# This is done in post_compile to ensure it works with cached artifacts
|
| 836 |
+
if self._wrap_compiled_regions and self.current_callable is not None:
|
| 837 |
+
from torch._higher_order_ops.wrap import InductorCompiledCallable
|
| 838 |
+
|
| 839 |
+
original_callable = self.current_callable
|
| 840 |
+
|
| 841 |
+
inductor_callable = InductorCompiledCallable(
|
| 842 |
+
original_callable,
|
| 843 |
+
self._original_gm,
|
| 844 |
+
compile_region_name=self.compile_region_name,
|
| 845 |
+
)
|
| 846 |
+
|
| 847 |
+
def wrapped_callable(inputs):
|
| 848 |
+
if is_in_torch_dispatch_mode():
|
| 849 |
+
kwargs = (
|
| 850 |
+
{"name": self.compile_region_name}
|
| 851 |
+
if self.compile_region_name is not None
|
| 852 |
+
else {}
|
| 853 |
+
)
|
| 854 |
+
return inductor_compiled_code(inductor_callable, inputs, **kwargs)
|
| 855 |
+
else:
|
| 856 |
+
return original_callable(inputs)
|
| 857 |
+
|
| 858 |
+
self.current_callable = wrapped_callable
|
| 859 |
+
|
| 860 |
+
def set_triton_bundle(self, triton_bundle: Any) -> None:
|
| 861 |
+
self._triton_bundle = triton_bundle
|
| 862 |
+
|
| 863 |
+
def prepare_for_serialization(self) -> None:
|
| 864 |
+
# We can't really serialize callables that may be C++/Triton/etc.,
|
| 865 |
+
# so we serialize their PyCodeCache disk cache location instead.
|
| 866 |
+
# TODO: This could be better if we're ever able to serialize compiled
|
| 867 |
+
# models to disk.
|
| 868 |
+
self.current_callable = None
|
| 869 |
+
self.recursively_apply_fns = None
|
| 870 |
+
self.compiled_fn_runner = None
|
| 871 |
+
# Note: _original_gm is already picklable (metadata stripped at creation)
|
| 872 |
+
# Note: _serialized_fx_graph is already in serializable form (SerializedGraphModule)
|
| 873 |
+
# so it doesn't need to be cleared
|
| 874 |
+
|
| 875 |
+
def write_to_disk(self) -> str:
|
| 876 |
+
from torch._dynamo.utils import counters
|
| 877 |
+
from torch._inductor.codecache import get_path, write_atomic
|
| 878 |
+
|
| 879 |
+
# See _save_graph(); we don't store the callable in the cache entry so
|
| 880 |
+
# recreate it here from the PyCodeCache disk cache.
|
| 881 |
+
artifact_path = get_path(self.cache_key, "py")[2]
|
| 882 |
+
code = self.source_code
|
| 883 |
+
if not os.path.exists(artifact_path):
|
| 884 |
+
counters["inductor"]["fxgraph_lookup_write_file"] += 1
|
| 885 |
+
write_atomic(artifact_path, code, make_dirs=True)
|
| 886 |
+
return artifact_path
|
| 887 |
+
|
| 888 |
+
def after_deserialization(self, constants: CompiledFxGraphConstants) -> str:
|
| 889 |
+
from torch._dynamo.utils import dynamo_timed
|
| 890 |
+
from torch._inductor.codecache import PyCodeCache
|
| 891 |
+
|
| 892 |
+
artifact_path = self.write_to_disk()
|
| 893 |
+
|
| 894 |
+
try:
|
| 895 |
+
with dynamo_timed(
|
| 896 |
+
"PyCodeCache.load_by_key_path",
|
| 897 |
+
log_pt2_compile_event=True,
|
| 898 |
+
):
|
| 899 |
+
code_cache = PyCodeCache.load_by_key_path(
|
| 900 |
+
self.cache_key,
|
| 901 |
+
artifact_path,
|
| 902 |
+
self.cache_linemap,
|
| 903 |
+
constants.unwrap(self),
|
| 904 |
+
)
|
| 905 |
+
self.current_callable = code_cache.call
|
| 906 |
+
self.recursively_apply_fns = getattr(
|
| 907 |
+
code_cache, "recursively_apply_fns", None
|
| 908 |
+
)
|
| 909 |
+
self.compiled_fn_runner = getattr(code_cache, "runner", None)
|
| 910 |
+
except OSError:
|
| 911 |
+
log.error("Failed to load artifact: %s", artifact_path)
|
| 912 |
+
raise
|
| 913 |
+
|
| 914 |
+
return artifact_path
|
| 915 |
+
|
| 916 |
+
|
| 917 |
+
@dataclasses.dataclass
|
| 918 |
+
class CompiledAOTI(OutputCode):
|
| 919 |
+
"""
|
| 920 |
+
Class holding an AOTInductor compiled so.
|
| 921 |
+
"""
|
| 922 |
+
|
| 923 |
+
filename: str | list[str | Weights] | torch.fx.GraphModule
|
| 924 |
+
device_type: str
|
| 925 |
+
current_callable: Callable[..., Any] | None = None
|
| 926 |
+
_cached_files: dict[str, bytes] = dataclasses.field(default_factory=dict)
|
| 927 |
+
|
| 928 |
+
def __post_init__(self):
|
| 929 |
+
if not config.aot_inductor.link_libtorch:
|
| 930 |
+
return
|
| 931 |
+
|
| 932 |
+
if (
|
| 933 |
+
torch._inductor.cpp_builder._IS_MACOS
|
| 934 |
+
or torch._inductor.cpp_builder._IS_WINDOWS
|
| 935 |
+
):
|
| 936 |
+
return
|
| 937 |
+
|
| 938 |
+
if config.aot_inductor.cross_target_platform == "windows":
|
| 939 |
+
return
|
| 940 |
+
|
| 941 |
+
if config.aot_inductor.package_cpp_only:
|
| 942 |
+
return
|
| 943 |
+
|
| 944 |
+
if not config.enable_autograd_for_aot:
|
| 945 |
+
return
|
| 946 |
+
|
| 947 |
+
if isinstance(self.filename, list):
|
| 948 |
+
current_callable = next(
|
| 949 |
+
fn for fn in self.filename if isinstance(fn, str) and fn.endswith(".so")
|
| 950 |
+
)
|
| 951 |
+
else:
|
| 952 |
+
current_callable = self.filename
|
| 953 |
+
|
| 954 |
+
if isinstance(current_callable, torch.fx.GraphModule):
|
| 955 |
+
# pyrefly: ignore [bad-assignment]
|
| 956 |
+
self.current_callable = current_callable
|
| 957 |
+
return
|
| 958 |
+
|
| 959 |
+
if self.device_type.startswith("cuda"):
|
| 960 |
+
current_callable = (
|
| 961 |
+
torch._C._aoti.AOTIModelContainerRunnerCuda( # type: ignore[call-arg]
|
| 962 |
+
current_callable,
|
| 963 |
+
1,
|
| 964 |
+
self.device_type,
|
| 965 |
+
"",
|
| 966 |
+
True,
|
| 967 |
+
).run # type: ignore[attr-defined]
|
| 968 |
+
) # type: ignore[attr-defined]
|
| 969 |
+
elif self.device_type.startswith("xpu"):
|
| 970 |
+
current_callable = (
|
| 971 |
+
torch._C._aoti.AOTIModelContainerRunnerXpu( # type: ignore[call-arg]
|
| 972 |
+
current_callable,
|
| 973 |
+
1,
|
| 974 |
+
self.device_type,
|
| 975 |
+
"",
|
| 976 |
+
).run # type: ignore[attr-defined]
|
| 977 |
+
) # type: ignore[attr-defined]
|
| 978 |
+
elif self.device_type == "cpu":
|
| 979 |
+
current_callable = (
|
| 980 |
+
torch._C._aoti.AOTIModelContainerRunnerCpu( # type: ignore[call-arg]
|
| 981 |
+
current_callable, 1
|
| 982 |
+
).run # type: ignore[attr-defined]
|
| 983 |
+
) # type: ignore[attr-defined]
|
| 984 |
+
else:
|
| 985 |
+
raise RuntimeError(f"unsupported device type {self.device_type}")
|
| 986 |
+
self.current_callable = current_callable
|
| 987 |
+
self._boxed_call = True
|
| 988 |
+
for file in self._cached_files:
|
| 989 |
+
if not os.path.exists(file):
|
| 990 |
+
with open(file, "wb") as f:
|
| 991 |
+
f.write(self._cached_files[file])
|
| 992 |
+
|
| 993 |
+
def __call__(self, inputs: Sequence[Any]) -> Any:
|
| 994 |
+
if self.current_callable is None:
|
| 995 |
+
raise RuntimeError("AOTInductor compiled so is not loaded")
|
| 996 |
+
return self.current_callable(inputs)
|
| 997 |
+
|
| 998 |
+
def prepare_for_serialization(self) -> None:
|
| 999 |
+
self.current_callable = None
|
| 1000 |
+
self._cached_files = {}
|
| 1001 |
+
filenames: list[str] = []
|
| 1002 |
+
if isinstance(self.filename, list):
|
| 1003 |
+
filenames = self.filename # type: ignore[assignment]
|
| 1004 |
+
elif isinstance(self.filename, str):
|
| 1005 |
+
filenames = [self.filename]
|
| 1006 |
+
for name in filenames:
|
| 1007 |
+
with open(name, "rb") as f:
|
| 1008 |
+
self._cached_files[name] = f.read()
|
| 1009 |
+
|
| 1010 |
+
def __getstate__(self):
|
| 1011 |
+
state = self.__dict__.copy()
|
| 1012 |
+
state["current_callable"] = None
|
| 1013 |
+
return state
|
| 1014 |
+
|
| 1015 |
+
def post_compile(
|
| 1016 |
+
self,
|
| 1017 |
+
example_inputs: Sequence[InputType],
|
| 1018 |
+
constants: CompiledFxGraphConstants,
|
| 1019 |
+
graph_kwargs: _CompileFxKwargs,
|
| 1020 |
+
) -> None:
|
| 1021 |
+
if self.current_callable is None:
|
| 1022 |
+
self.__post_init__()
|
| 1023 |
+
|
| 1024 |
+
def set_triton_bundle(self, triton_bundle: Any) -> None:
|
| 1025 |
+
pass
|
| 1026 |
+
|
| 1027 |
+
|
| 1028 |
+
@dataclasses.dataclass
|
| 1029 |
+
class MockFXGraphCacheOutput(OutputCode):
|
| 1030 |
+
gm: Any = None
|
| 1031 |
+
|
| 1032 |
+
def __post_init__(self) -> None:
|
| 1033 |
+
self._boxed_call = True
|
| 1034 |
+
|
| 1035 |
+
def post_compile(
|
| 1036 |
+
self,
|
| 1037 |
+
example_inputs: Sequence[InputType],
|
| 1038 |
+
constants: CompiledFxGraphConstants,
|
| 1039 |
+
graph_kwargs: _CompileFxKwargs,
|
| 1040 |
+
) -> None:
|
| 1041 |
+
pass
|
| 1042 |
+
|
| 1043 |
+
def __call__(self, inputs: Sequence[Any]) -> Any:
|
| 1044 |
+
return self.gm(inputs)
|
| 1045 |
+
|
| 1046 |
+
def set_triton_bundle(self, triton_bundle: Any) -> None:
|
| 1047 |
+
pass
|
| 1048 |
+
|
| 1049 |
+
|
| 1050 |
+
@dataclasses.dataclass
|
| 1051 |
+
class RegionalOutputCode(OutputCode):
|
| 1052 |
+
"""
|
| 1053 |
+
OutputCode for regional inductor compilation results.
|
| 1054 |
+
|
| 1055 |
+
Regional inductor returns a torch.fx.GraphModule that contains both
|
| 1056 |
+
compiled regions (via standalone_compile) and eager regions. This needs
|
| 1057 |
+
special serialization using GraphPickler instead of standard pickle.
|
| 1058 |
+
|
| 1059 |
+
The serialization strategy stores the GraphModule as bytes using
|
| 1060 |
+
GraphPickler.dumps(), which handles FakeTensors, AOTCompiledArtifacts,
|
| 1061 |
+
and other special objects that standard pickle cannot handle.
|
| 1062 |
+
"""
|
| 1063 |
+
|
| 1064 |
+
# The serialized graph module as bytes (using GraphPickler)
|
| 1065 |
+
_serialized_graph_module: bytes | None = None
|
| 1066 |
+
|
| 1067 |
+
# The actual graph module (cleared during serialization)
|
| 1068 |
+
_graph_module: torch.nn.Module | None = None
|
| 1069 |
+
|
| 1070 |
+
# Optional filter for ops during serialization
|
| 1071 |
+
_ops_filter: Callable[[str], bool] | None = None
|
| 1072 |
+
|
| 1073 |
+
def __init__(
|
| 1074 |
+
self,
|
| 1075 |
+
graph_module: torch.fx.GraphModule,
|
| 1076 |
+
ops_filter: Callable[[str], bool] = _ops_filter_safe,
|
| 1077 |
+
node_metadata_key_filter: Callable[[str], bool] | None = (
|
| 1078 |
+
_node_metadata_key_filter_safe
|
| 1079 |
+
),
|
| 1080 |
+
):
|
| 1081 |
+
"""
|
| 1082 |
+
Args:
|
| 1083 |
+
graph_module: The torch.fx.GraphModule returned by regional_inductor
|
| 1084 |
+
ops_filter: Optional filter function for op names during serialization.
|
| 1085 |
+
If provided, only ops whose name passes the filter will be serialized.
|
| 1086 |
+
"""
|
| 1087 |
+
super().__init__()
|
| 1088 |
+
self._graph_module = graph_module
|
| 1089 |
+
self._serialized_graph_module = None
|
| 1090 |
+
self._serialized_wrappers = []
|
| 1091 |
+
self._boxed_call = True
|
| 1092 |
+
_, module = self._unwrap_graph_module()
|
| 1093 |
+
self._inner_boxed_call = isinstance(
|
| 1094 |
+
module.graph._codegen, torch.fx.graph._BoxedCodeGen
|
| 1095 |
+
)
|
| 1096 |
+
self._ops_filter = ops_filter
|
| 1097 |
+
self._node_metadata_key_filter = node_metadata_key_filter
|
| 1098 |
+
|
| 1099 |
+
def __call__(self, inputs: list[Any]) -> Any:
|
| 1100 |
+
"""
|
| 1101 |
+
Execute the regional compiled graph.
|
| 1102 |
+
|
| 1103 |
+
Args:
|
| 1104 |
+
inputs: A mutable list of inputs. Must be a list (not an arbitrary
|
| 1105 |
+
Sequence) because the boxed calling convention allows the callee
|
| 1106 |
+
to mutably clear the list when inputs become dead, enabling early
|
| 1107 |
+
memory deallocation.
|
| 1108 |
+
"""
|
| 1109 |
+
if self._graph_module is None:
|
| 1110 |
+
raise RuntimeError(
|
| 1111 |
+
"RegionalOutputCode has no graph module loaded. "
|
| 1112 |
+
"Did you forget to call post_compile()?"
|
| 1113 |
+
)
|
| 1114 |
+
|
| 1115 |
+
if self._inner_boxed_call:
|
| 1116 |
+
return self._graph_module(inputs)
|
| 1117 |
+
return self._graph_module(*inputs)
|
| 1118 |
+
|
| 1119 |
+
@property
|
| 1120 |
+
def graph(self):
|
| 1121 |
+
_, module = self._unwrap_graph_module()
|
| 1122 |
+
return module.graph
|
| 1123 |
+
|
| 1124 |
+
def _unwrap_graph_module(
|
| 1125 |
+
self,
|
| 1126 |
+
) -> tuple[list[tuple[Callable, dict[str, Any]]], torch.fx.GraphModule]:
|
| 1127 |
+
module = self._graph_module
|
| 1128 |
+
serialized_wrappers = []
|
| 1129 |
+
if isinstance(module, torch._dynamo.OptimizedModule):
|
| 1130 |
+
dynamo_ctx = module.dynamo_ctx
|
| 1131 |
+
assert isinstance(dynamo_ctx, torch._dynamo.eval_frame.DisableContext)
|
| 1132 |
+
serialized_wrappers.append(
|
| 1133 |
+
(
|
| 1134 |
+
torch._dynamo.disable,
|
| 1135 |
+
{"reason": dynamo_ctx.msg, "wrapping": dynamo_ctx.wrapping},
|
| 1136 |
+
)
|
| 1137 |
+
)
|
| 1138 |
+
module = module._orig_mod
|
| 1139 |
+
assert isinstance(module, torch.fx.GraphModule)
|
| 1140 |
+
return serialized_wrappers, module
|
| 1141 |
+
|
| 1142 |
+
def post_compile(
|
| 1143 |
+
self,
|
| 1144 |
+
example_inputs: Sequence[InputType],
|
| 1145 |
+
constants: CompiledFxGraphConstants,
|
| 1146 |
+
graph_kwargs: _CompileFxKwargs,
|
| 1147 |
+
) -> None:
|
| 1148 |
+
"""
|
| 1149 |
+
Post-compile processing for regional inductor.
|
| 1150 |
+
|
| 1151 |
+
This deserializes the GraphModule from bytes using GraphPickler,
|
| 1152 |
+
extracting the fake_mode from example_inputs.
|
| 1153 |
+
"""
|
| 1154 |
+
if self._graph_module is not None:
|
| 1155 |
+
return
|
| 1156 |
+
assert self._serialized_graph_module is not None
|
| 1157 |
+
# Get fake mode from example inputs
|
| 1158 |
+
from torch._guards import detect_fake_mode
|
| 1159 |
+
|
| 1160 |
+
fake_mode = detect_fake_mode(example_inputs)
|
| 1161 |
+
if fake_mode is None:
|
| 1162 |
+
raise RuntimeError(
|
| 1163 |
+
"Could not detect fake mode from example inputs. "
|
| 1164 |
+
"Regional inductor requires fake mode for deserialization."
|
| 1165 |
+
)
|
| 1166 |
+
|
| 1167 |
+
# Deserialize the graph module
|
| 1168 |
+
from torch.fx._graph_pickler import GraphPickler
|
| 1169 |
+
|
| 1170 |
+
gm = GraphPickler.loads(self._serialized_graph_module, fake_mode)
|
| 1171 |
+
assert isinstance(gm, torch.fx.GraphModule)
|
| 1172 |
+
gm.recompile()
|
| 1173 |
+
for fn, kwargs in reversed(self._serialized_wrappers):
|
| 1174 |
+
gm = fn(gm, **kwargs)
|
| 1175 |
+
self._graph_module = gm
|
| 1176 |
+
|
| 1177 |
+
def set_triton_bundle(self, triton_bundle: Any) -> None:
|
| 1178 |
+
"""Regional inductor doesn't use triton bundles directly."""
|
| 1179 |
+
|
| 1180 |
+
def prepare_for_serialization(self) -> None:
|
| 1181 |
+
"""
|
| 1182 |
+
Prepare for serialization by converting the GraphModule to bytes.
|
| 1183 |
+
|
| 1184 |
+
This uses GraphPickler to serialize the graph module since it contains
|
| 1185 |
+
special objects like FakeTensors and AOTCompiledArtifacts that need
|
| 1186 |
+
custom pickling.
|
| 1187 |
+
"""
|
| 1188 |
+
if self._graph_module is not None:
|
| 1189 |
+
from torch.fx._graph_pickler import GraphPickler, Options
|
| 1190 |
+
|
| 1191 |
+
self._serialized_graph_module = None
|
| 1192 |
+
self._serialized_wrappers, graph_module = self._unwrap_graph_module()
|
| 1193 |
+
|
| 1194 |
+
self._serialized_graph_module = GraphPickler.dumps(
|
| 1195 |
+
graph_module,
|
| 1196 |
+
options=Options(
|
| 1197 |
+
ops_filter=self._ops_filter,
|
| 1198 |
+
node_metadata_key_filter=self._node_metadata_key_filter,
|
| 1199 |
+
),
|
| 1200 |
+
)
|
| 1201 |
+
# Clear the graph module to avoid pickling it with standard pickle
|
| 1202 |
+
self._graph_module = None
|
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/package/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
from .package import AOTICompiledModel, load_package, package_aoti
|
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/package/build_package.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
build_package_contents = """
|
| 2 |
+
import os
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
|
| 5 |
+
from torch._inductor.package.package import compile_so
|
| 6 |
+
|
| 7 |
+
curr_dir = Path(__file__).parent
|
| 8 |
+
aoti_files = [
|
| 9 |
+
os.path.join(root, file)
|
| 10 |
+
for root, dirs, files in os.walk(curr_dir)
|
| 11 |
+
for file in files
|
| 12 |
+
]
|
| 13 |
+
|
| 14 |
+
output_so = compile_so(curr_dir, aoti_files, curr_dir)
|
| 15 |
+
"""
|
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/package/package.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import io
|
| 2 |
+
import json
|
| 3 |
+
import logging
|
| 4 |
+
import os
|
| 5 |
+
import tempfile
|
| 6 |
+
from typing import IO
|
| 7 |
+
|
| 8 |
+
import torch
|
| 9 |
+
from torch._inductor import config
|
| 10 |
+
from torch._inductor.cpp_builder import BuildOptionsBase, CppBuilder
|
| 11 |
+
from torch.export.pt2_archive._package import (
|
| 12 |
+
AOTI_FILES,
|
| 13 |
+
AOTICompiledModel,
|
| 14 |
+
load_pt2,
|
| 15 |
+
package_pt2,
|
| 16 |
+
)
|
| 17 |
+
from torch.types import FileLike
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
log = logging.getLogger(__name__)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def compile_so(aoti_dir: str, aoti_files: list[str], so_path: str) -> str:
|
| 24 |
+
def get_aoti_file_with_suffix(suffix: str) -> str:
|
| 25 |
+
for file in aoti_files:
|
| 26 |
+
if file.endswith(suffix):
|
| 27 |
+
return file
|
| 28 |
+
raise RuntimeError(f"Unable to find file with suffix {suffix}")
|
| 29 |
+
|
| 30 |
+
# Compile all the files into a .so
|
| 31 |
+
cpp_file = os.path.join(aoti_dir, get_aoti_file_with_suffix(".cpp"))
|
| 32 |
+
consts_o = os.path.join(aoti_dir, get_aoti_file_with_suffix(".o"))
|
| 33 |
+
|
| 34 |
+
file_name = os.path.splitext(cpp_file)[0]
|
| 35 |
+
|
| 36 |
+
# Parse compile flags and build the .o file
|
| 37 |
+
with open(file_name + "_compile_flags.json") as f:
|
| 38 |
+
compile_flags = json.load(f)
|
| 39 |
+
|
| 40 |
+
compile_options = BuildOptionsBase(
|
| 41 |
+
**compile_flags, use_relative_path=config.is_fbcode()
|
| 42 |
+
)
|
| 43 |
+
object_builder = CppBuilder(
|
| 44 |
+
name=file_name,
|
| 45 |
+
sources=cpp_file,
|
| 46 |
+
BuildOption=compile_options,
|
| 47 |
+
)
|
| 48 |
+
output_o = object_builder.get_target_file_path()
|
| 49 |
+
object_builder.build()
|
| 50 |
+
|
| 51 |
+
# Parse linker flags and build the .so file
|
| 52 |
+
with open(file_name + "_linker_flags.json") as f:
|
| 53 |
+
linker_flags = json.load(f)
|
| 54 |
+
|
| 55 |
+
linker_options = BuildOptionsBase(
|
| 56 |
+
**linker_flags, use_relative_path=config.is_fbcode()
|
| 57 |
+
)
|
| 58 |
+
so_builder = CppBuilder(
|
| 59 |
+
name=os.path.split(so_path)[-1],
|
| 60 |
+
sources=[output_o, consts_o],
|
| 61 |
+
BuildOption=linker_options,
|
| 62 |
+
output_dir=so_path,
|
| 63 |
+
)
|
| 64 |
+
output_so = so_builder.get_target_file_path()
|
| 65 |
+
so_builder.build()
|
| 66 |
+
|
| 67 |
+
# mmapped weights
|
| 68 |
+
serialized_weights_filename = file_name + "_serialized_weights.bin"
|
| 69 |
+
if serialized_weights_filename in aoti_files:
|
| 70 |
+
with open(serialized_weights_filename, "rb") as f_weights:
|
| 71 |
+
serialized_weights = f_weights.read()
|
| 72 |
+
|
| 73 |
+
with open(output_so, "a+b") as f_so:
|
| 74 |
+
so_size = f_so.tell()
|
| 75 |
+
# Page align the weights
|
| 76 |
+
f_so.write(b" " * (16384 - so_size % 16384))
|
| 77 |
+
f_so.write(serialized_weights)
|
| 78 |
+
|
| 79 |
+
return output_so
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def package_aoti(
|
| 83 |
+
archive_file: FileLike,
|
| 84 |
+
aoti_files: AOTI_FILES,
|
| 85 |
+
) -> FileLike:
|
| 86 |
+
"""
|
| 87 |
+
Saves the AOTInductor generated files to the PT2Archive format.
|
| 88 |
+
|
| 89 |
+
Args:
|
| 90 |
+
archive_file: The file name to save the package to.
|
| 91 |
+
aoti_files: This can either be a singular path to a directory containing
|
| 92 |
+
the AOTInductor files, or a dictionary mapping the model name to the
|
| 93 |
+
path to its AOTInductor generated files.
|
| 94 |
+
"""
|
| 95 |
+
|
| 96 |
+
return package_pt2(
|
| 97 |
+
archive_file,
|
| 98 |
+
aoti_files=aoti_files,
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def load_package(
|
| 103 |
+
path: FileLike,
|
| 104 |
+
model_name: str = "model",
|
| 105 |
+
run_single_threaded: bool = False,
|
| 106 |
+
num_runners: int = 1,
|
| 107 |
+
device_index: int = -1,
|
| 108 |
+
) -> AOTICompiledModel:
|
| 109 |
+
try:
|
| 110 |
+
pt2_contents = load_pt2(
|
| 111 |
+
path,
|
| 112 |
+
run_single_threaded=run_single_threaded,
|
| 113 |
+
num_runners=num_runners,
|
| 114 |
+
device_index=device_index,
|
| 115 |
+
)
|
| 116 |
+
if model_name not in pt2_contents.aoti_runners:
|
| 117 |
+
raise RuntimeError(f"Model {model_name} not found in package")
|
| 118 |
+
return pt2_contents.aoti_runners[model_name]
|
| 119 |
+
except RuntimeError:
|
| 120 |
+
log.warning("Loading outdated pt2 file. Please regenerate your package.")
|
| 121 |
+
|
| 122 |
+
if isinstance(path, (io.IOBase, IO)):
|
| 123 |
+
with tempfile.NamedTemporaryFile(suffix=".pt2") as f:
|
| 124 |
+
# TODO(angelayi): We shouldn't need to do this -- miniz should
|
| 125 |
+
# handle reading the buffer. This is just a temporary workaround
|
| 126 |
+
path.seek(0)
|
| 127 |
+
f.write(path.read())
|
| 128 |
+
log.debug("Writing buffer to tmp file located at %s.", f.name)
|
| 129 |
+
loader = torch._C._aoti.AOTIModelPackageLoader(
|
| 130 |
+
f.name, model_name, run_single_threaded, num_runners, device_index
|
| 131 |
+
)
|
| 132 |
+
return AOTICompiledModel(loader)
|
| 133 |
+
|
| 134 |
+
path = os.fspath(path) # AOTIModelPackageLoader expects (str, str)
|
| 135 |
+
loader = torch._C._aoti.AOTIModelPackageLoader(
|
| 136 |
+
path, model_name, run_single_threaded, num_runners, device_index
|
| 137 |
+
)
|
| 138 |
+
return AOTICompiledModel(loader)
|
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/pattern_matcher.py
ADDED
|
@@ -0,0 +1,2495 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
# Inductor Pattern Matcher
|
| 3 |
+
|
| 4 |
+
The pattern matcher enables search/replace within an FX graph.
|
| 5 |
+
|
| 6 |
+
The main entrypoint to the pattern matcher is register_replacement(). Given a
|
| 7 |
+
search function and a replacement function this will register a replacement with
|
| 8 |
+
a pass (such as torch._inductor.fx_passes.joint_graph.patterns).
|
| 9 |
+
|
| 10 |
+
Internally the pattern matcher represents patterns as a graph (a DAG). Creating
|
| 11 |
+
new patterns manually as a graph is cumbersome and error-prone so the standard
|
| 12 |
+
way to create patterns (using register_replacement()) is to provide a search
|
| 13 |
+
function and a replacement function which is traced and converted into a graph.
|
| 14 |
+
|
| 15 |
+
Because the search functions are built somewhat generic (they tend to ignore
|
| 16 |
+
tensor sizes, for example) register_replacement() allows you to specify an
|
| 17 |
+
`extra_check` function which performs additional checks to verify that the
|
| 18 |
+
matched pattern fully matches before returning it.
|
| 19 |
+
|
| 20 |
+
## Precompiled Patterns
|
| 21 |
+
|
| 22 |
+
New patterns are added using register_replacement(). Patterns added in this way
|
| 23 |
+
can have a compile-time overhead because they need to be traced before
|
| 24 |
+
use. Patterns can be precompiled and added using gen_register_replacement()
|
| 25 |
+
instead. To do this you call gen_register_replacement() instead of
|
| 26 |
+
register_replacement(). The arguments are the same except for an additional
|
| 27 |
+
unique name which is used as a lookup key.
|
| 28 |
+
|
| 29 |
+
## Internals
|
| 30 |
+
|
| 31 |
+
The match DAG is represented by a graph of `PatternExpr` nodes. Each PatternExpr
|
| 32 |
+
implements a `_match` method which returns either a `Match` object for a
|
| 33 |
+
successful match or a `FailedMatch` object for a failure to match.
|
| 34 |
+
"""
|
| 35 |
+
|
| 36 |
+
from __future__ import annotations
|
| 37 |
+
|
| 38 |
+
import contextlib
|
| 39 |
+
import dataclasses
|
| 40 |
+
import functools
|
| 41 |
+
import importlib
|
| 42 |
+
import inspect
|
| 43 |
+
import itertools
|
| 44 |
+
import logging
|
| 45 |
+
import operator
|
| 46 |
+
import os
|
| 47 |
+
import re
|
| 48 |
+
import textwrap
|
| 49 |
+
import typing
|
| 50 |
+
from abc import ABC, abstractmethod
|
| 51 |
+
from collections import defaultdict
|
| 52 |
+
from collections.abc import Callable, Collection, Generator, Iterable, Mapping, Sequence
|
| 53 |
+
from pathlib import Path
|
| 54 |
+
from typing import Any, NoReturn, Protocol, TypeVar
|
| 55 |
+
from typing_extensions import Self, TypeIs
|
| 56 |
+
|
| 57 |
+
import torch
|
| 58 |
+
import torch._guards
|
| 59 |
+
import torch.fx
|
| 60 |
+
import torch.utils._pytree as pytree
|
| 61 |
+
from torch._dispatch.python import enable_python_dispatcher
|
| 62 |
+
from torch._dynamo.utils import counters
|
| 63 |
+
from torch._prims_common import is_integer_dtype
|
| 64 |
+
from torch._subclasses.fake_tensor import unset_fake_temporarily
|
| 65 |
+
from torch.fx.experimental.proxy_tensor import make_fx
|
| 66 |
+
from torch.fx.experimental.symbolic_shapes import guard_or_false, statically_known_true
|
| 67 |
+
from torch.fx.graph_module import _get_attr
|
| 68 |
+
from torch.fx.immutable_collections import immutable_dict, immutable_list
|
| 69 |
+
from torch.fx.passes.graph_transform_observer import GraphTransformObserver
|
| 70 |
+
from torch.fx.traceback import preserve_node_meta
|
| 71 |
+
from torch.utils._ordered_set import OrderedSet
|
| 72 |
+
|
| 73 |
+
from .._functorch import config as functorch_config
|
| 74 |
+
from .._functorch.aot_autograd import aot_function, make_boxed_func
|
| 75 |
+
from .._functorch.partitioners import default_partition
|
| 76 |
+
from .._subclasses import FakeTensor, FakeTensorMode
|
| 77 |
+
from ..fx import Transformer
|
| 78 |
+
from . import config
|
| 79 |
+
from .decomposition import select_decomp_table
|
| 80 |
+
from .lowering import fallback_node_due_to_unsupported_type
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
log = logging.getLogger(__name__)
|
| 84 |
+
aten = torch.ops.aten
|
| 85 |
+
prims = torch.ops.prims
|
| 86 |
+
|
| 87 |
+
Constant = Any
|
| 88 |
+
NodeOrConstant = Constant | torch.fx.Node
|
| 89 |
+
|
| 90 |
+
backend = os.environ.get("TORCHINDUCTOR_PATTERN_MATCH_BACKEND", "inductor")
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
_debug_nodes_cache: bool | OrderedSet[str] | None = None
|
| 94 |
+
_debug_nodes_env_value_cache: str | None = None
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def _should_debug_node(node_name: str) -> bool:
|
| 98 |
+
def _get_debug_nodes() -> bool | OrderedSet[str]:
|
| 99 |
+
global _debug_nodes_cache, _debug_nodes_env_value_cache
|
| 100 |
+
|
| 101 |
+
def parse_debug_env(env_value: str | None) -> bool | OrderedSet[str]:
|
| 102 |
+
if not env_value:
|
| 103 |
+
return False
|
| 104 |
+
if env_value == "all":
|
| 105 |
+
return True
|
| 106 |
+
return OrderedSet(env_value.split(","))
|
| 107 |
+
|
| 108 |
+
current_env = os.environ.get("TORCHINDUCTOR_PATTERN_MATCH_DEBUG")
|
| 109 |
+
|
| 110 |
+
# Recompute only if env changed
|
| 111 |
+
if current_env != _debug_nodes_env_value_cache or _debug_nodes_cache is None:
|
| 112 |
+
_debug_nodes_cache = parse_debug_env(current_env)
|
| 113 |
+
_debug_nodes_env_value_cache = current_env
|
| 114 |
+
|
| 115 |
+
return _debug_nodes_cache
|
| 116 |
+
|
| 117 |
+
debug_nodes = _get_debug_nodes()
|
| 118 |
+
if isinstance(debug_nodes, bool):
|
| 119 |
+
return debug_nodes
|
| 120 |
+
return node_name in debug_nodes
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
class SearchFn(Protocol):
|
| 124 |
+
__name__: str
|
| 125 |
+
|
| 126 |
+
def __call__(self, *args: Any, **kwargs: Any) -> Any: ...
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
class ReplaceFn(Protocol):
|
| 130 |
+
def __call__(self, *args: Any, **kwargs: Any) -> Any: ...
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
class TraceFn(Protocol):
|
| 134 |
+
def __call__(
|
| 135 |
+
self, fn: SearchFn | ReplaceFn, *args: Any, **kwargs: Any
|
| 136 |
+
) -> torch.fx.GraphModule: ...
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
T = TypeVar("T")
|
| 140 |
+
|
| 141 |
+
# What's a better name for this?
|
| 142 |
+
FnsType = torch.fx.node.Target | str
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
class Multiple:
|
| 146 |
+
def __init__(self) -> None:
|
| 147 |
+
# Ensure we're really a singleton.
|
| 148 |
+
assert "MULTIPLE" not in globals() or self is MULTIPLE
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
# Sentinel indicating multiple quantities can be matched
|
| 152 |
+
MULTIPLE = Multiple()
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
def _transfer_meta(
|
| 156 |
+
new_meta: dict[str, Any], old_node: torch.fx.Node, pass_name: str = ""
|
| 157 |
+
) -> None:
|
| 158 |
+
from torch.fx.traceback import NodeSource, NodeSourceAction
|
| 159 |
+
|
| 160 |
+
# Transfer metadata after pattern matching occurs.
|
| 161 |
+
# Copies _COPY_META_FIELDS, stack_trace, and (if missing) val/tensor_meta.
|
| 162 |
+
if config.trace.provenance_tracking_level == 1:
|
| 163 |
+
new_from_node = new_meta.get("from_node", []).copy()
|
| 164 |
+
new_from_node.append(NodeSource(old_node, pass_name, NodeSourceAction.REPLACE))
|
| 165 |
+
new_meta.update(
|
| 166 |
+
(k, v)
|
| 167 |
+
for k, v in old_node.meta.items()
|
| 168 |
+
if k in torch.fx.proxy._COPY_META_FIELDS
|
| 169 |
+
)
|
| 170 |
+
new_meta["from_node"] = new_from_node
|
| 171 |
+
else:
|
| 172 |
+
new_meta.update(
|
| 173 |
+
(k, v)
|
| 174 |
+
for k, v in old_node.meta.items()
|
| 175 |
+
if k in torch.fx.proxy._COPY_META_FIELDS
|
| 176 |
+
)
|
| 177 |
+
if "stack_trace" in old_node.meta:
|
| 178 |
+
new_meta["stack_trace"] = old_node.meta["stack_trace"]
|
| 179 |
+
# Copy val/tensor_meta only when the new node doesn't already have them
|
| 180 |
+
# (e.g. from tracing the replacement graph). Don't overwrite if present
|
| 181 |
+
# since the replacement's own val is more accurate.
|
| 182 |
+
if "val" not in new_meta and "val" in old_node.meta:
|
| 183 |
+
new_meta["val"] = old_node.meta["val"]
|
| 184 |
+
if "tensor_meta" not in new_meta and "tensor_meta" in old_node.meta:
|
| 185 |
+
new_meta["tensor_meta"] = old_node.meta["tensor_meta"]
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
class Match:
|
| 189 |
+
"""
|
| 190 |
+
Represents a successfully matched pattern.
|
| 191 |
+
|
| 192 |
+
The `Match` object is returned to represent a successfully matched
|
| 193 |
+
pattern. Included in the Match are the pattern that was matched, the graph
|
| 194 |
+
nodes matched, and any args that were used during the matching.
|
| 195 |
+
|
| 196 |
+
The args and kwargs are specific to the type of pattern that was matched and
|
| 197 |
+
provide hints about what was matched.
|
| 198 |
+
"""
|
| 199 |
+
|
| 200 |
+
pattern: PatternExpr
|
| 201 |
+
args: list[Any]
|
| 202 |
+
kwargs: dict[str, Any]
|
| 203 |
+
nodes: list[torch.fx.Node]
|
| 204 |
+
targets: dict[_TargetExpr, torch.fx.node.Target]
|
| 205 |
+
ctx: MatchContext
|
| 206 |
+
replacement_graph: torch.fx.GraphModule | None
|
| 207 |
+
|
| 208 |
+
def __init__(
|
| 209 |
+
self,
|
| 210 |
+
ctx: MatchContext,
|
| 211 |
+
pattern: PatternExpr,
|
| 212 |
+
args: Sequence[Any] | None = None,
|
| 213 |
+
kwargs: dict[str, Any] | None = None,
|
| 214 |
+
) -> None:
|
| 215 |
+
super().__init__()
|
| 216 |
+
self.pattern = pattern
|
| 217 |
+
# The input nodes that must be passed in to the result
|
| 218 |
+
self.args = list(args or [])
|
| 219 |
+
self.kwargs = kwargs or {}
|
| 220 |
+
# The nodes matched in this expression
|
| 221 |
+
self.nodes = []
|
| 222 |
+
# Mapping CallFunction to the node.target
|
| 223 |
+
self.targets = {}
|
| 224 |
+
self.ctx = ctx
|
| 225 |
+
self.replacement_graph = None
|
| 226 |
+
|
| 227 |
+
@property
|
| 228 |
+
def graph(self) -> torch.fx.Graph:
|
| 229 |
+
return self.ctx.graph
|
| 230 |
+
|
| 231 |
+
def extend(self, other: Match) -> None:
|
| 232 |
+
if self.kwargs:
|
| 233 |
+
for key in OrderedSet(self.kwargs.keys()) & OrderedSet(other.kwargs.keys()):
|
| 234 |
+
if self.kwargs[key] != other.kwargs[key]:
|
| 235 |
+
raise FailedMatch("kwarg mismatch: {}", key)
|
| 236 |
+
self.args.extend(other.args)
|
| 237 |
+
self.nodes.extend(other.nodes)
|
| 238 |
+
self.kwargs.update(other.kwargs)
|
| 239 |
+
self.targets.update(other.targets)
|
| 240 |
+
|
| 241 |
+
def bundle(self) -> Match:
|
| 242 |
+
# Wrap args in an extra list
|
| 243 |
+
self.args = [tuple(self.args)] if self.args else []
|
| 244 |
+
return self
|
| 245 |
+
|
| 246 |
+
def __repr__(self) -> str:
|
| 247 |
+
return f"Match(..., {self.args}, {self.kwargs})"
|
| 248 |
+
|
| 249 |
+
def erase_nodes(self) -> None:
|
| 250 |
+
graph = self.graph
|
| 251 |
+
for n in reversed(self.nodes):
|
| 252 |
+
if not n._erased and not n.users:
|
| 253 |
+
graph.erase_node(n)
|
| 254 |
+
|
| 255 |
+
def output_nodes(self) -> list[torch.fx.Node | None]:
|
| 256 |
+
return [
|
| 257 |
+
(self.ctx.pattern_to_node[p] if p is not None else None)
|
| 258 |
+
for p in self.ctx.outputs
|
| 259 |
+
]
|
| 260 |
+
|
| 261 |
+
def output_node(self) -> torch.fx.Node:
|
| 262 |
+
return next(p for p in self.output_nodes() if p)
|
| 263 |
+
|
| 264 |
+
def replace_with_graph(
|
| 265 |
+
self, replacement_graph: torch.fx.Graph, args: Sequence[Any]
|
| 266 |
+
) -> None:
|
| 267 |
+
ReplacementPatternEntry.replace_with_graph(
|
| 268 |
+
self, self.ctx.graph, replacement_graph, args
|
| 269 |
+
)
|
| 270 |
+
|
| 271 |
+
def replace_by_example(
|
| 272 |
+
self,
|
| 273 |
+
replacement_fn: ReplaceFn,
|
| 274 |
+
args: Sequence[Any],
|
| 275 |
+
trace_fn: TraceFn | None = None,
|
| 276 |
+
run_functional_passes: bool = True,
|
| 277 |
+
) -> None:
|
| 278 |
+
"""Replace with a graph generated by tracing the replacement_fn.
|
| 279 |
+
|
| 280 |
+
Args:
|
| 281 |
+
run_functional_passes (bool). If we should run passes that
|
| 282 |
+
assume functional IR (like DCE, remove_noop_ops), on the
|
| 283 |
+
replacement graph.
|
| 284 |
+
|
| 285 |
+
"""
|
| 286 |
+
from torch._inductor.virtualized import NullHandler, V
|
| 287 |
+
|
| 288 |
+
context = (
|
| 289 |
+
V.fake_mode
|
| 290 |
+
if (not isinstance(V.fake_mode, NullHandler) or (V.fake_mode is None))
|
| 291 |
+
else contextlib.nullcontext()
|
| 292 |
+
)
|
| 293 |
+
|
| 294 |
+
def should_propagate_eager_input_vals(nodes: list[torch.fx.Node]) -> bool:
|
| 295 |
+
if len(nodes) != 1:
|
| 296 |
+
return False
|
| 297 |
+
node = nodes[0]
|
| 298 |
+
if "eager_input_vals" not in node.meta:
|
| 299 |
+
return False
|
| 300 |
+
return node.target in OrderedSet(
|
| 301 |
+
[
|
| 302 |
+
torch.ops.higher_order.triton_kernel_wrapper_functional,
|
| 303 |
+
torch.ops.higher_order.auto_functionalized,
|
| 304 |
+
torch.ops.higher_order.auto_functionalized_v2,
|
| 305 |
+
]
|
| 306 |
+
)
|
| 307 |
+
|
| 308 |
+
# pyrefly: ignore [bad-context-manager]
|
| 309 |
+
with context:
|
| 310 |
+
if trace_fn is None:
|
| 311 |
+
trace_fn = functools.partial(
|
| 312 |
+
fwd_only, run_functional_passes=run_functional_passes
|
| 313 |
+
)
|
| 314 |
+
|
| 315 |
+
if should_propagate_eager_input_vals(self.nodes):
|
| 316 |
+
# Our strategy is:
|
| 317 |
+
# 1) trace out the graph with eager_input_vals (which have accurate eager-mode metadata)
|
| 318 |
+
# 2) trace out the graph with vals (which have the accurate Inductor metadata)
|
| 319 |
+
# 3) Propagate the eager_input_vals from the first graph to the second.
|
| 320 |
+
# 4) Use the second graph as the replacement graph.
|
| 321 |
+
|
| 322 |
+
# Construct a map of node -> FakeTensor val in eager_input_vals
|
| 323 |
+
node_to_val = {}
|
| 324 |
+
|
| 325 |
+
fake_args, fake_kwargs = self.nodes[0].meta["eager_input_vals"]
|
| 326 |
+
fake_kwargs = {**fake_kwargs}
|
| 327 |
+
match_args, match_kwargs = tuple(self.args), self.kwargs
|
| 328 |
+
|
| 329 |
+
def record(node: torch.fx.Node, val: Any) -> None:
|
| 330 |
+
if isinstance(node, torch.fx.Node):
|
| 331 |
+
node_to_val[node] = val
|
| 332 |
+
|
| 333 |
+
torch.utils._pytree.tree_map(
|
| 334 |
+
record, (match_args, match_kwargs), (fake_args, fake_kwargs)
|
| 335 |
+
)
|
| 336 |
+
# map args to their FakeTensor val in eager_input_vals
|
| 337 |
+
example_vals = torch.fx.map_arg(args, lambda arg: node_to_val[arg])
|
| 338 |
+
|
| 339 |
+
# first graph
|
| 340 |
+
graph_with_eager_vals = trace_fn(replacement_fn, example_vals)
|
| 341 |
+
|
| 342 |
+
# second graph
|
| 343 |
+
example_vals = torch.fx.map_arg(args, lambda arg: arg.meta["val"])
|
| 344 |
+
# pyrefly: ignore [bad-argument-type]
|
| 345 |
+
replacement = trace_fn(graph_with_eager_vals, example_vals)
|
| 346 |
+
|
| 347 |
+
# propagate metadata from first graph to second
|
| 348 |
+
# NB: This assertion might not be true in general, but it is true for
|
| 349 |
+
# the two use cases we have
|
| 350 |
+
# (triton_kernel_wrapper_functional, auto_functionalized)
|
| 351 |
+
assert len(graph_with_eager_vals.graph.nodes) == len(
|
| 352 |
+
replacement.graph.nodes
|
| 353 |
+
)
|
| 354 |
+
for old_node, new_node in zip(
|
| 355 |
+
graph_with_eager_vals.graph.nodes, replacement.graph.nodes
|
| 356 |
+
):
|
| 357 |
+
if "eager_input_vals" in old_node.meta:
|
| 358 |
+
new_node.meta["eager_input_vals"] = old_node.meta[
|
| 359 |
+
"eager_input_vals"
|
| 360 |
+
]
|
| 361 |
+
|
| 362 |
+
else:
|
| 363 |
+
example_vals = torch.fx.map_arg(
|
| 364 |
+
args,
|
| 365 |
+
lambda arg: arg.meta["val"]
|
| 366 |
+
if "val" in arg.meta
|
| 367 |
+
else arg.meta["example_value"],
|
| 368 |
+
)
|
| 369 |
+
fake_mode = torch._dynamo.utils.detect_fake_mode(example_vals)
|
| 370 |
+
if fake_mode is not None:
|
| 371 |
+
|
| 372 |
+
def _convert_to_fake_mode(it):
|
| 373 |
+
if isinstance(it, FakeTensor) and fake_mode is not None:
|
| 374 |
+
return fake_mode.from_tensor(it)
|
| 375 |
+
return it
|
| 376 |
+
|
| 377 |
+
example_vals = torch.fx.node.map_aggregate(
|
| 378 |
+
example_vals, _convert_to_fake_mode
|
| 379 |
+
)
|
| 380 |
+
replacement = trace_fn(replacement_fn, example_vals)
|
| 381 |
+
if len(self.nodes) == 1:
|
| 382 |
+
for n in replacement.graph.nodes:
|
| 383 |
+
_transfer_meta(
|
| 384 |
+
new_meta=n.meta,
|
| 385 |
+
old_node=self.nodes[0],
|
| 386 |
+
pass_name="replace_by_example",
|
| 387 |
+
)
|
| 388 |
+
|
| 389 |
+
ReplacementPatternEntry.replace_with_graph(
|
| 390 |
+
self,
|
| 391 |
+
self.ctx.graph,
|
| 392 |
+
replacement,
|
| 393 |
+
args,
|
| 394 |
+
)
|
| 395 |
+
|
| 396 |
+
|
| 397 |
+
class FailedMatch(RuntimeError):
|
| 398 |
+
"""
|
| 399 |
+
Represents a unsuccessful match.
|
| 400 |
+
|
| 401 |
+
The `FailedMatch` object is returned to represent a failure to match a
|
| 402 |
+
pattern.
|
| 403 |
+
"""
|
| 404 |
+
|
| 405 |
+
format_string: str
|
| 406 |
+
|
| 407 |
+
def __init__(self, format_string: str, *args: Any, **kwargs: Any) -> None:
|
| 408 |
+
self.format_string = format_string
|
| 409 |
+
# We want to construct error messages lazily instead of eagerly, as
|
| 410 |
+
# constructing them eagerly can significantly worsen compile times.
|
| 411 |
+
if len(format_string) > 200:
|
| 412 |
+
raise RuntimeError(
|
| 413 |
+
f"Format string too long - use lazy construction of strings instead. Format string is\n {format_string}"
|
| 414 |
+
)
|
| 415 |
+
self.args = args
|
| 416 |
+
self.kwargs = kwargs
|
| 417 |
+
|
| 418 |
+
def __str__(self) -> str:
|
| 419 |
+
return self.format_string.format(*self.args, **self.kwargs)
|
| 420 |
+
|
| 421 |
+
def __bool__(self) -> bool:
|
| 422 |
+
return False
|
| 423 |
+
|
| 424 |
+
|
| 425 |
+
MatchResult = Match | FailedMatch
|
| 426 |
+
|
| 427 |
+
|
| 428 |
+
def is_match(m: MatchResult) -> TypeIs[Match]:
|
| 429 |
+
"""
|
| 430 |
+
TypeIs cannot act on `self`. Thus this function exists to let mypy
|
| 431 |
+
recognize FailedMatch.__bool__ as a TypeIs.
|
| 432 |
+
"""
|
| 433 |
+
return bool(m)
|
| 434 |
+
|
| 435 |
+
|
| 436 |
+
class MatchContext:
|
| 437 |
+
"""
|
| 438 |
+
Internal state needed while running PatternExpr._match().
|
| 439 |
+
"""
|
| 440 |
+
|
| 441 |
+
outputs: list[PatternExpr | None]
|
| 442 |
+
pattern_to_node: dict[PatternExpr, torch.fx.Node | None]
|
| 443 |
+
graph: torch.fx.Graph
|
| 444 |
+
exclusive_node_set: list[NodeOrConstant]
|
| 445 |
+
|
| 446 |
+
def __init__(
|
| 447 |
+
self,
|
| 448 |
+
outputs: list[PatternExpr | None],
|
| 449 |
+
pattern_to_node: dict[PatternExpr, torch.fx.Node] | None = None,
|
| 450 |
+
*,
|
| 451 |
+
graph: torch.fx.Graph,
|
| 452 |
+
) -> None:
|
| 453 |
+
self.outputs = outputs
|
| 454 |
+
self.pattern_to_node = {} if pattern_to_node is None else dict(pattern_to_node)
|
| 455 |
+
self.graph = graph
|
| 456 |
+
self.exclusive_node_set = []
|
| 457 |
+
|
| 458 |
+
def match(self, pattern: PatternExpr, node: NodeOrConstant) -> MatchResult:
|
| 459 |
+
"""wrapper to check reused nodes in patterns"""
|
| 460 |
+
if pattern in self.pattern_to_node:
|
| 461 |
+
if self.pattern_to_node[pattern] == node:
|
| 462 |
+
return Match(self, pattern) # already checked this node
|
| 463 |
+
else:
|
| 464 |
+
return FailedMatch("repeated pattern differs")
|
| 465 |
+
m = pattern._match(node, self)
|
| 466 |
+
assert pattern not in self.pattern_to_node
|
| 467 |
+
self.pattern_to_node[pattern] = node if m else None
|
| 468 |
+
return m
|
| 469 |
+
|
| 470 |
+
def filter_multi_user_patterns(self) -> dict[PatternExpr, torch.fx.Node]:
|
| 471 |
+
return {
|
| 472 |
+
pattern: node
|
| 473 |
+
for pattern, node in self.pattern_to_node.items()
|
| 474 |
+
if pattern.has_multiple_users() and node is not None
|
| 475 |
+
}
|
| 476 |
+
|
| 477 |
+
|
| 478 |
+
class PatternExpr(ABC):
|
| 479 |
+
"""
|
| 480 |
+
Base class for types of patterns.
|
| 481 |
+
"""
|
| 482 |
+
|
| 483 |
+
@abstractmethod
|
| 484 |
+
def _match(self, node: torch.fx.Node, ctx: MatchContext) -> MatchResult: ...
|
| 485 |
+
|
| 486 |
+
def match(self, node: torch.fx.Node) -> MatchResult:
|
| 487 |
+
try:
|
| 488 |
+
return MatchContext([self], graph=node.graph).match(self, node)
|
| 489 |
+
except FailedMatch as e:
|
| 490 |
+
return e
|
| 491 |
+
|
| 492 |
+
def has_multiple_users(self) -> bool:
|
| 493 |
+
return False
|
| 494 |
+
|
| 495 |
+
def __repr__(self) -> str:
|
| 496 |
+
return self.__class__.__name__ + "()"
|
| 497 |
+
|
| 498 |
+
def find_anchor_nodes(
|
| 499 |
+
self, ctx: MatchContext, searched: OrderedSet[torch.fx.Node]
|
| 500 |
+
) -> Generator[torch.fx.Node | None, None, None]:
|
| 501 |
+
if self in ctx.pattern_to_node:
|
| 502 |
+
yield ctx.pattern_to_node[self]
|
| 503 |
+
|
| 504 |
+
def pattern_eq(self, other: Any) -> bool:
|
| 505 |
+
"""
|
| 506 |
+
Compare two `PatternExpr`s and return true if they are the
|
| 507 |
+
same. Note this is NOT matching a pattern - it is comparing the pattern
|
| 508 |
+
structures (for debugging).
|
| 509 |
+
"""
|
| 510 |
+
return isinstance(other, self.__class__)
|
| 511 |
+
|
| 512 |
+
|
| 513 |
+
class Arg(PatternExpr):
|
| 514 |
+
"""
|
| 515 |
+
Capture an arg which will become an input to the handler. Args are
|
| 516 |
+
passed in depth first order.
|
| 517 |
+
"""
|
| 518 |
+
|
| 519 |
+
def _match(self, node: NodeOrConstant, ctx: MatchContext) -> MatchResult:
|
| 520 |
+
return Match(ctx, self, args=[node]) # matches anything
|
| 521 |
+
|
| 522 |
+
|
| 523 |
+
class Ignored(PatternExpr):
|
| 524 |
+
"""
|
| 525 |
+
Match an arg, but don't pass it to handler
|
| 526 |
+
"""
|
| 527 |
+
|
| 528 |
+
def _match(self, node: NodeOrConstant, ctx: MatchContext) -> MatchResult:
|
| 529 |
+
return Match(ctx, self) # matches anything
|
| 530 |
+
|
| 531 |
+
def __repr__(self) -> str:
|
| 532 |
+
return "*"
|
| 533 |
+
|
| 534 |
+
def pretty_print(self, pp: PatternPrettyPrinter) -> str:
|
| 535 |
+
return "Ignored()"
|
| 536 |
+
|
| 537 |
+
|
| 538 |
+
class KeywordArg(PatternExpr):
|
| 539 |
+
"""
|
| 540 |
+
Capture a kwarg which will become an input to the handler.
|
| 541 |
+
"""
|
| 542 |
+
|
| 543 |
+
def __init__(self, name: str) -> None:
|
| 544 |
+
super().__init__()
|
| 545 |
+
self.name = name
|
| 546 |
+
|
| 547 |
+
def __repr__(self) -> str:
|
| 548 |
+
return f"KeywordArg({self.name!r})"
|
| 549 |
+
|
| 550 |
+
def _match(self, node: NodeOrConstant, ctx: MatchContext) -> MatchResult:
|
| 551 |
+
return Match(ctx, self, kwargs={self.name: node}) # matches anything
|
| 552 |
+
|
| 553 |
+
def pattern_eq(self, other: Any) -> bool:
|
| 554 |
+
other = typing.cast(Self, other) # super makes sure this is true
|
| 555 |
+
return super().pattern_eq(other) and self.name == other.name
|
| 556 |
+
|
| 557 |
+
|
| 558 |
+
class ExclusiveKeywordArg(PatternExpr):
|
| 559 |
+
"""
|
| 560 |
+
Capture a kwarg which will become an input to the handler.
|
| 561 |
+
"""
|
| 562 |
+
|
| 563 |
+
name: str
|
| 564 |
+
|
| 565 |
+
def __init__(self, name: str) -> None:
|
| 566 |
+
super().__init__()
|
| 567 |
+
self.name = name
|
| 568 |
+
|
| 569 |
+
def __repr__(self) -> str:
|
| 570 |
+
return f"ExclusiveKeywordArg({self.name!r})"
|
| 571 |
+
|
| 572 |
+
def _match(self, node: NodeOrConstant, ctx: MatchContext) -> MatchResult:
|
| 573 |
+
if node in ctx.exclusive_node_set:
|
| 574 |
+
return FailedMatch("exclusive arg appears twice")
|
| 575 |
+
|
| 576 |
+
ctx.exclusive_node_set.append(node)
|
| 577 |
+
return Match(ctx, self, kwargs={self.name: node}) # matches anything
|
| 578 |
+
|
| 579 |
+
def pattern_eq(self, other: Any) -> bool:
|
| 580 |
+
other = typing.cast(Self, other) # super makes sure this is true
|
| 581 |
+
return super().pattern_eq(other) and self.name == other.name
|
| 582 |
+
|
| 583 |
+
|
| 584 |
+
class _TargetExpr(PatternExpr):
|
| 585 |
+
"""
|
| 586 |
+
Base class for filtering match by node.target
|
| 587 |
+
"""
|
| 588 |
+
|
| 589 |
+
fns: list[FnsType]
|
| 590 |
+
fns_set: OrderedSet[FnsType]
|
| 591 |
+
|
| 592 |
+
def __init__(
|
| 593 |
+
self, fns: FnsType | Sequence[FnsType], users: Multiple | int = 1
|
| 594 |
+
) -> None:
|
| 595 |
+
super().__init__()
|
| 596 |
+
fns = [fns] if callable(fns) or isinstance(fns, str) else list(fns)
|
| 597 |
+
for fn in fns:
|
| 598 |
+
if isinstance(fn, torch._ops.OpOverloadPacket):
|
| 599 |
+
fns.extend(getattr(fn, overload) for overload in fn.overloads()) # noqa: B909
|
| 600 |
+
|
| 601 |
+
self.fns = fns
|
| 602 |
+
self.fns_set = OrderedSet(fns)
|
| 603 |
+
self.users = users
|
| 604 |
+
|
| 605 |
+
@property
|
| 606 |
+
@abstractmethod
|
| 607 |
+
def op(self) -> str: ...
|
| 608 |
+
|
| 609 |
+
def fns_repr(self) -> str:
|
| 610 |
+
first_repr = self.fns[0]
|
| 611 |
+
if not isinstance(first_repr, str):
|
| 612 |
+
first_repr = first_repr.__name__
|
| 613 |
+
|
| 614 |
+
if len(self.fns) > 1:
|
| 615 |
+
return f"[{first_repr}, ...]"
|
| 616 |
+
elif self.fns[0] is getattr(torch, first_repr, None):
|
| 617 |
+
return f"torch.{first_repr}"
|
| 618 |
+
elif self.fns[0] is getattr(operator, first_repr, None):
|
| 619 |
+
return f"operator.{first_repr}"
|
| 620 |
+
elif isinstance(self.fns[0], torch._ops.OpOverload):
|
| 621 |
+
return str(self.fns[0])
|
| 622 |
+
else:
|
| 623 |
+
return first_repr
|
| 624 |
+
|
| 625 |
+
def __repr__(self) -> str:
|
| 626 |
+
if self.users is MULTIPLE:
|
| 627 |
+
comma_users = ", MULTIPLE"
|
| 628 |
+
elif self.users != 1:
|
| 629 |
+
comma_users = f", {self.users})"
|
| 630 |
+
else:
|
| 631 |
+
comma_users = ""
|
| 632 |
+
return f"{self.__class__.__name__}({self.fns_repr()}{comma_users})"
|
| 633 |
+
|
| 634 |
+
def has_multiple_users(self) -> bool:
|
| 635 |
+
return isinstance(self.users, Multiple) or self.users > 1
|
| 636 |
+
|
| 637 |
+
def find_anchor_nodes(
|
| 638 |
+
self, ctx: MatchContext, searched: OrderedSet[torch.fx.Node]
|
| 639 |
+
) -> Generator[torch.fx.Node | None, None, None]:
|
| 640 |
+
raise NotImplementedError
|
| 641 |
+
|
| 642 |
+
def _match_fns(self, node: torch.fx.Node) -> bool:
|
| 643 |
+
return (
|
| 644 |
+
isinstance(node, torch.fx.Node)
|
| 645 |
+
and node.op == self.op
|
| 646 |
+
and extract_target(node) in self.fns_set
|
| 647 |
+
)
|
| 648 |
+
|
| 649 |
+
def _match_users(self, node: torch.fx.Node, ctx: MatchContext) -> bool:
|
| 650 |
+
return (
|
| 651 |
+
self in ctx.outputs
|
| 652 |
+
or self.users is MULTIPLE
|
| 653 |
+
or len(node.users) == self.users
|
| 654 |
+
)
|
| 655 |
+
|
| 656 |
+
def pattern_eq(self, other: Any) -> bool:
|
| 657 |
+
other = typing.cast(Self, other) # super makes sure this is true
|
| 658 |
+
return (
|
| 659 |
+
super().pattern_eq(other)
|
| 660 |
+
and self.op == other.op
|
| 661 |
+
and self.fns == other.fns
|
| 662 |
+
and self.users == other.users
|
| 663 |
+
)
|
| 664 |
+
|
| 665 |
+
|
| 666 |
+
_SimpleSpec = tuple[Any, ...]
|
| 667 |
+
|
| 668 |
+
|
| 669 |
+
class _TargetArgsExpr(_TargetExpr):
|
| 670 |
+
"""
|
| 671 |
+
Base class for filtering match by node.{target,args,kwargs}
|
| 672 |
+
"""
|
| 673 |
+
|
| 674 |
+
def __init__(
|
| 675 |
+
self,
|
| 676 |
+
fns: torch.fx.node.Target | str | Sequence[Any],
|
| 677 |
+
*args: Any,
|
| 678 |
+
_users: int | Multiple = 1,
|
| 679 |
+
**kwargs: Any,
|
| 680 |
+
) -> None:
|
| 681 |
+
super().__init__(fns, _users)
|
| 682 |
+
self.args = tuple(args)
|
| 683 |
+
self.kwargs = dict(kwargs)
|
| 684 |
+
if any(
|
| 685 |
+
isinstance(x, (dict, list, tuple))
|
| 686 |
+
for x in itertools.chain(args, kwargs.values())
|
| 687 |
+
):
|
| 688 |
+
self.flatten = self.pytree_flatten
|
| 689 |
+
else:
|
| 690 |
+
self.flatten = self.simple_flatten
|
| 691 |
+
self.flat_args_kwargs = self.flatten(self.args, self.kwargs)
|
| 692 |
+
|
| 693 |
+
@staticmethod
|
| 694 |
+
def simple_flatten(
|
| 695 |
+
args: Sequence[Any], kwargs: Mapping[Any, Any]
|
| 696 |
+
) -> tuple[Sequence[Any], _SimpleSpec | pytree.TreeSpec]:
|
| 697 |
+
values = (*args, *kwargs.values())
|
| 698 |
+
spec = (len(args), *kwargs.keys())
|
| 699 |
+
return values, spec
|
| 700 |
+
|
| 701 |
+
@staticmethod
|
| 702 |
+
def pytree_flatten(
|
| 703 |
+
args: Sequence[Any], kwargs: Mapping[Any, Any]
|
| 704 |
+
) -> tuple[Sequence[Any], _SimpleSpec | pytree.TreeSpec]:
|
| 705 |
+
type_mapping: dict[type, type] = {
|
| 706 |
+
immutable_list: tuple,
|
| 707 |
+
list: tuple,
|
| 708 |
+
immutable_dict: dict,
|
| 709 |
+
}
|
| 710 |
+
|
| 711 |
+
def convert_type(x: Any) -> Any:
|
| 712 |
+
cls = type(x)
|
| 713 |
+
convert_fn = type_mapping.get(cls)
|
| 714 |
+
if convert_fn is not None:
|
| 715 |
+
return pytree.tree_map(
|
| 716 |
+
convert_type,
|
| 717 |
+
convert_fn(x),
|
| 718 |
+
is_leaf=lambda x: type(x) in type_mapping,
|
| 719 |
+
)
|
| 720 |
+
return x
|
| 721 |
+
|
| 722 |
+
normalized_args_tree = pytree.tree_map(
|
| 723 |
+
convert_type,
|
| 724 |
+
(args, kwargs),
|
| 725 |
+
is_leaf=lambda x: type(x) in type_mapping,
|
| 726 |
+
)
|
| 727 |
+
flat, spec = pytree.tree_flatten(normalized_args_tree)
|
| 728 |
+
return flat, spec
|
| 729 |
+
|
| 730 |
+
def __repr__(self) -> str:
|
| 731 |
+
args = [
|
| 732 |
+
self.fns_repr(),
|
| 733 |
+
*map(repr, self.args),
|
| 734 |
+
*[f"{k}={v}" for k, v in self.kwargs.items()],
|
| 735 |
+
]
|
| 736 |
+
if self.users is MULTIPLE:
|
| 737 |
+
args.append("_users=MULTIPLE")
|
| 738 |
+
elif self.users != 1:
|
| 739 |
+
args.append(f"_users={self.users}")
|
| 740 |
+
return f"{self.__class__.__name__}({', '.join(args)})"
|
| 741 |
+
|
| 742 |
+
def pretty_print(self, pp: PatternPrettyPrinter) -> str:
|
| 743 |
+
args = [
|
| 744 |
+
self.fns_repr(),
|
| 745 |
+
*(pp.pretty_print(x) for x in self.args),
|
| 746 |
+
*[f"{k}={pp.pretty_print(v)}" for k, v in self.kwargs.items()],
|
| 747 |
+
]
|
| 748 |
+
if self.users is MULTIPLE:
|
| 749 |
+
args.append("_users=MULTIPLE")
|
| 750 |
+
elif self.users != 1:
|
| 751 |
+
args.append(f"_users={self.users}")
|
| 752 |
+
|
| 753 |
+
joiner_str = ", "
|
| 754 |
+
return f"{self.__class__.__name__}({joiner_str.join(args)})"
|
| 755 |
+
|
| 756 |
+
def _match(self, node: torch.fx.Node, ctx: MatchContext) -> MatchResult:
|
| 757 |
+
if not self._match_fns(node) or len(node.args) != len(self.args):
|
| 758 |
+
return FailedMatch("function_mismatch: node={}, pattern={}", node, self)
|
| 759 |
+
|
| 760 |
+
if not self._match_users(node, ctx):
|
| 761 |
+
return FailedMatch("multiple_users {}", self)
|
| 762 |
+
|
| 763 |
+
_args = node.args
|
| 764 |
+
_kwargs = node.kwargs
|
| 765 |
+
if len(_kwargs) < len(self.kwargs):
|
| 766 |
+
from torch.fx.operator_schemas import normalize_function
|
| 767 |
+
|
| 768 |
+
assert callable(node.target)
|
| 769 |
+
normalized_args_and_kwargs = normalize_function(
|
| 770 |
+
node.target, node.args, node.kwargs
|
| 771 |
+
)
|
| 772 |
+
|
| 773 |
+
if normalized_args_and_kwargs is None:
|
| 774 |
+
return FailedMatch("function_mismatch: node={}, pattern={}", node, self)
|
| 775 |
+
else:
|
| 776 |
+
_args, _kwargs = normalized_args_and_kwargs
|
| 777 |
+
if len(_args) == len(self.args) and len(_kwargs) >= len(self.kwargs):
|
| 778 |
+
_kwargs = {i: _kwargs[i] for i in _kwargs if i in self.kwargs}
|
| 779 |
+
else:
|
| 780 |
+
return FailedMatch(
|
| 781 |
+
"function_mismatch: node={}, pattern={}", node, self
|
| 782 |
+
)
|
| 783 |
+
else:
|
| 784 |
+
_kwargs = {i: _kwargs[i] for i in _kwargs if i in self.kwargs}
|
| 785 |
+
|
| 786 |
+
node_items, node_spec = self.flatten(_args, _kwargs)
|
| 787 |
+
self_items, self_spec = self.flat_args_kwargs
|
| 788 |
+
if node_spec != self_spec:
|
| 789 |
+
return FailedMatch("args_structure {} {}", node_spec, self_spec)
|
| 790 |
+
assert len(node_items) == len(self_items)
|
| 791 |
+
|
| 792 |
+
m = Match(ctx, self)
|
| 793 |
+
for pattern, child_node in zip(self_items, node_items):
|
| 794 |
+
if isinstance(pattern, PatternExpr):
|
| 795 |
+
child_match = ctx.match(pattern, child_node)
|
| 796 |
+
if not is_match(child_match):
|
| 797 |
+
return child_match
|
| 798 |
+
m.extend(child_match)
|
| 799 |
+
elif isinstance(child_node, torch.fx.Node) or child_node != pattern:
|
| 800 |
+
return FailedMatch(
|
| 801 |
+
"constant_args: {} {!r}!={pattern!r}",
|
| 802 |
+
node,
|
| 803 |
+
child_node,
|
| 804 |
+
pattern=pattern,
|
| 805 |
+
)
|
| 806 |
+
m.nodes.append(node)
|
| 807 |
+
m.targets[self] = node.target
|
| 808 |
+
return m
|
| 809 |
+
|
| 810 |
+
def find_anchor_nodes(
|
| 811 |
+
self, ctx: MatchContext, searched: OrderedSet[torch.fx.Node]
|
| 812 |
+
) -> Generator[torch.fx.Node | None, None, None]:
|
| 813 |
+
"""
|
| 814 |
+
This is used when we are matching a pattern with multiple outputs.
|
| 815 |
+
There is a partial match (stored in ctx) and we want to walk
|
| 816 |
+
this pattern to find a connection to an already-matched node.
|
| 817 |
+
|
| 818 |
+
Yields candidate nodes that `self._match` might like.
|
| 819 |
+
"""
|
| 820 |
+
if self in ctx.pattern_to_node:
|
| 821 |
+
yield ctx.pattern_to_node[self]
|
| 822 |
+
return
|
| 823 |
+
|
| 824 |
+
for pattern in self.flat_args_kwargs[0]:
|
| 825 |
+
if isinstance(pattern, PatternExpr):
|
| 826 |
+
for other_node in pattern.find_anchor_nodes(ctx, searched):
|
| 827 |
+
if not isinstance(other_node, torch.fx.Node):
|
| 828 |
+
continue
|
| 829 |
+
for node in other_node.users:
|
| 830 |
+
if node not in searched:
|
| 831 |
+
if self._match_fns(node):
|
| 832 |
+
yield node
|
| 833 |
+
searched.add(node)
|
| 834 |
+
|
| 835 |
+
def pattern_eq(self, other: Any) -> bool:
|
| 836 |
+
other = typing.cast(Self, other) # super makes sure this is true
|
| 837 |
+
return (
|
| 838 |
+
super().pattern_eq(other)
|
| 839 |
+
and self.flat_args_kwargs[1] == other.flat_args_kwargs[1]
|
| 840 |
+
and all(
|
| 841 |
+
a.pattern_eq(b) if isinstance(a, PatternExpr) else a == b
|
| 842 |
+
for a, b in zip(self.flat_args_kwargs[0], other.flat_args_kwargs[0])
|
| 843 |
+
)
|
| 844 |
+
)
|
| 845 |
+
|
| 846 |
+
|
| 847 |
+
class CallFunction(_TargetArgsExpr):
|
| 848 |
+
"""
|
| 849 |
+
Matches a call_function node in the FX graphs: `fns[i](*args, **kwargs)`
|
| 850 |
+
"""
|
| 851 |
+
|
| 852 |
+
op = "call_function"
|
| 853 |
+
|
| 854 |
+
|
| 855 |
+
class CallMethod(_TargetArgsExpr):
|
| 856 |
+
"""
|
| 857 |
+
Matches a call_method node in the FX graphs: `fns[i].method(*args, **kwargs)`
|
| 858 |
+
"""
|
| 859 |
+
|
| 860 |
+
op = "call_method"
|
| 861 |
+
|
| 862 |
+
|
| 863 |
+
class CallModule(_TargetArgsExpr):
|
| 864 |
+
"""
|
| 865 |
+
Matches a call_module node in the FX graphs: `module(*args, **kwargs)`
|
| 866 |
+
"""
|
| 867 |
+
|
| 868 |
+
op = "call_module"
|
| 869 |
+
|
| 870 |
+
|
| 871 |
+
class _TargetExprVarArgs(_TargetExpr):
|
| 872 |
+
"""
|
| 873 |
+
Matches a call_function node with any arguments which are passed into the pattern
|
| 874 |
+
"""
|
| 875 |
+
|
| 876 |
+
def _match(self, node: torch.fx.Node, ctx: MatchContext) -> MatchResult:
|
| 877 |
+
if not self._match_fns(node):
|
| 878 |
+
return FailedMatch("function_mismatch")
|
| 879 |
+
|
| 880 |
+
if not self._match_users(node, ctx):
|
| 881 |
+
return FailedMatch("multiple_users")
|
| 882 |
+
|
| 883 |
+
m = Match(ctx, self)
|
| 884 |
+
m.nodes.append(node)
|
| 885 |
+
m.targets[self] = node.target
|
| 886 |
+
m.args.extend(node.args)
|
| 887 |
+
m.kwargs.update(node.kwargs)
|
| 888 |
+
return m
|
| 889 |
+
|
| 890 |
+
|
| 891 |
+
class CallFunctionVarArgs(_TargetExprVarArgs):
|
| 892 |
+
op = "call_function"
|
| 893 |
+
|
| 894 |
+
|
| 895 |
+
class CallMethodVarArgs(_TargetExprVarArgs):
|
| 896 |
+
op = "call_method"
|
| 897 |
+
|
| 898 |
+
|
| 899 |
+
class CallModuleVarArgs(_TargetExprVarArgs):
|
| 900 |
+
op = "call_module"
|
| 901 |
+
|
| 902 |
+
|
| 903 |
+
class ListOf(PatternExpr):
|
| 904 |
+
"""
|
| 905 |
+
Matches a repeated pattern
|
| 906 |
+
"""
|
| 907 |
+
|
| 908 |
+
def __init__(self, pattern: PatternExpr, partial: bool = False) -> None:
|
| 909 |
+
super().__init__()
|
| 910 |
+
assert isinstance(pattern, PatternExpr)
|
| 911 |
+
self.pattern = pattern
|
| 912 |
+
self.partial = partial
|
| 913 |
+
|
| 914 |
+
def __repr__(self) -> str:
|
| 915 |
+
return f"{self.__class__.__name__}({self.pattern})"
|
| 916 |
+
|
| 917 |
+
def _match(self, node: list[torch.fx.Node], ctx: MatchContext) -> MatchResult: # type: ignore[override]
|
| 918 |
+
if not isinstance(node, (list, tuple)) or len(node) == 0:
|
| 919 |
+
return FailedMatch("non_list")
|
| 920 |
+
m = Match(ctx, self)
|
| 921 |
+
# Propagating patterns with multiple users will ensure we don't revisit
|
| 922 |
+
# the same nodes
|
| 923 |
+
pattern_to_node = ctx.filter_multi_user_patterns()
|
| 924 |
+
matched = False
|
| 925 |
+
for i, child_node in enumerate(node):
|
| 926 |
+
child_ctx = MatchContext(
|
| 927 |
+
ctx.outputs, pattern_to_node, graph=child_node.graph
|
| 928 |
+
)
|
| 929 |
+
child_match = child_ctx.match(self.pattern, child_node)
|
| 930 |
+
pattern_to_node = child_ctx.filter_multi_user_patterns()
|
| 931 |
+
if not is_match(child_match):
|
| 932 |
+
if not self.partial:
|
| 933 |
+
return FailedMatch("list[{}]: {}", i, child_match)
|
| 934 |
+
continue
|
| 935 |
+
matched = True
|
| 936 |
+
m.extend(child_match.bundle())
|
| 937 |
+
if not matched:
|
| 938 |
+
return FailedMatch("list: no_match")
|
| 939 |
+
return m.bundle()
|
| 940 |
+
|
| 941 |
+
def pattern_eq(self, other: Any) -> bool:
|
| 942 |
+
other = typing.cast(Self, other) # super makes sure this is true
|
| 943 |
+
return (
|
| 944 |
+
super().pattern_eq(other)
|
| 945 |
+
and self.pattern.pattern_eq(other.pattern)
|
| 946 |
+
and self.partial == other.partial
|
| 947 |
+
)
|
| 948 |
+
|
| 949 |
+
|
| 950 |
+
class MultiOutputPattern(PatternExpr):
|
| 951 |
+
outputs: list[PatternExpr | None]
|
| 952 |
+
|
| 953 |
+
def __init__(self, outputs: Sequence[PatternExpr | None]) -> None:
|
| 954 |
+
super().__init__()
|
| 955 |
+
assert isinstance(outputs[0], _TargetExpr)
|
| 956 |
+
assert all(x is None or isinstance(x, PatternExpr) for x in outputs), outputs
|
| 957 |
+
self.outputs = list(outputs)
|
| 958 |
+
self.op = outputs[0].op
|
| 959 |
+
|
| 960 |
+
@property
|
| 961 |
+
def fns(self) -> Callable[..., Any] | str | Sequence[Any]:
|
| 962 |
+
# This cast is checked above in __init__()
|
| 963 |
+
output = typing.cast(_TargetExpr, self.outputs[0])
|
| 964 |
+
return output.fns
|
| 965 |
+
|
| 966 |
+
def __repr__(self) -> str:
|
| 967 |
+
return f"{self.__class__.__name__}({self.outputs})"
|
| 968 |
+
|
| 969 |
+
def pretty_print(self, pp: PatternPrettyPrinter) -> str:
|
| 970 |
+
args = [pp.pretty_print(x) for x in self.outputs]
|
| 971 |
+
joiner_str = f",\n{' '}"
|
| 972 |
+
str_out = f"{self.__class__.__name__}([{joiner_str.join(args)}"
|
| 973 |
+
str_out = f"{str_out}\n])"
|
| 974 |
+
return str_out
|
| 975 |
+
|
| 976 |
+
def _match(self, node: torch.fx.Node, ctx: MatchContext) -> MatchResult:
|
| 977 |
+
output = typing.cast(_TargetExpr, self.outputs[0])
|
| 978 |
+
m = ctx.match(output, node)
|
| 979 |
+
if not is_match(m):
|
| 980 |
+
return m
|
| 981 |
+
|
| 982 |
+
for pattern in self.outputs[1:]:
|
| 983 |
+
if pattern is None:
|
| 984 |
+
continue
|
| 985 |
+
child_match = self._match_from_anchors(pattern, ctx)
|
| 986 |
+
if not is_match(child_match):
|
| 987 |
+
return child_match
|
| 988 |
+
m.extend(child_match)
|
| 989 |
+
|
| 990 |
+
return m
|
| 991 |
+
|
| 992 |
+
def _match_from_anchors(
|
| 993 |
+
self, pattern: PatternExpr, ctx: MatchContext
|
| 994 |
+
) -> MatchResult:
|
| 995 |
+
prior = dict(ctx.pattern_to_node)
|
| 996 |
+
m: MatchResult = FailedMatch("no anchor found")
|
| 997 |
+
for node in pattern.find_anchor_nodes(ctx, OrderedSet()):
|
| 998 |
+
m = ctx.match(pattern, node)
|
| 999 |
+
if is_match(m):
|
| 1000 |
+
return m
|
| 1001 |
+
# revert any partial matches
|
| 1002 |
+
ctx.pattern_to_node = dict(prior)
|
| 1003 |
+
return m
|
| 1004 |
+
|
| 1005 |
+
def match(self, node: torch.fx.Node) -> MatchResult:
|
| 1006 |
+
try:
|
| 1007 |
+
return MatchContext(self.outputs, graph=node.graph).match(self, node)
|
| 1008 |
+
except FailedMatch as e:
|
| 1009 |
+
return e
|
| 1010 |
+
|
| 1011 |
+
def pattern_eq(self, other: Any) -> bool:
|
| 1012 |
+
other = typing.cast(Self, other) # super makes sure this is true
|
| 1013 |
+
return (
|
| 1014 |
+
super().pattern_eq(other)
|
| 1015 |
+
and len(self.outputs) == len(other.outputs)
|
| 1016 |
+
and all(
|
| 1017 |
+
a.pattern_eq(b) if isinstance(a, PatternExpr) else a == b
|
| 1018 |
+
for a, b in zip(self.outputs, other.outputs)
|
| 1019 |
+
)
|
| 1020 |
+
)
|
| 1021 |
+
|
| 1022 |
+
|
| 1023 |
+
class RepeatedExpr(PatternExpr):
|
| 1024 |
+
"""
|
| 1025 |
+
Checks for a repeated pattern. Useful for repeated operations after a node such as `split` or `unbind`
|
| 1026 |
+
"""
|
| 1027 |
+
|
| 1028 |
+
def __init__(self, inner_pattern: _TargetExpr) -> None:
|
| 1029 |
+
super().__init__()
|
| 1030 |
+
self.inner_pattern = inner_pattern
|
| 1031 |
+
self.op = inner_pattern.op
|
| 1032 |
+
|
| 1033 |
+
@property
|
| 1034 |
+
def fns(self) -> Sequence[FnsType]:
|
| 1035 |
+
return self.inner_pattern.fns
|
| 1036 |
+
|
| 1037 |
+
def _match(self, node: torch.fx.Node, ctx: MatchContext) -> MatchResult:
|
| 1038 |
+
m = ctx.match(self.inner_pattern, node)
|
| 1039 |
+
if not is_match(m):
|
| 1040 |
+
return m
|
| 1041 |
+
ctx.pattern_to_node.pop(
|
| 1042 |
+
self.inner_pattern,
|
| 1043 |
+
)
|
| 1044 |
+
# Check all anchor nodes match the pattern
|
| 1045 |
+
for anchor_node in self.inner_pattern.find_anchor_nodes(
|
| 1046 |
+
ctx, OrderedSet([node])
|
| 1047 |
+
):
|
| 1048 |
+
anchor_m = MatchContext([self], graph=node.graph).match(
|
| 1049 |
+
self.inner_pattern, anchor_node
|
| 1050 |
+
)
|
| 1051 |
+
if not is_match(anchor_m):
|
| 1052 |
+
return anchor_m
|
| 1053 |
+
m.extend(anchor_m)
|
| 1054 |
+
return m
|
| 1055 |
+
|
| 1056 |
+
def pattern_eq(self, other: Any) -> bool:
|
| 1057 |
+
other = typing.cast(Self, other) # super makes sure this is true
|
| 1058 |
+
return super().pattern_eq(other) and self.inner_pattern.pattern_eq(
|
| 1059 |
+
other.inner_pattern
|
| 1060 |
+
)
|
| 1061 |
+
|
| 1062 |
+
|
| 1063 |
+
class PatternPrettyPrinter:
|
| 1064 |
+
"""
|
| 1065 |
+
Serializes Patterns to executable python.
|
| 1066 |
+
XXX: currently only used and tested for fuse attention patterns. May not cover
|
| 1067 |
+
all patterns.
|
| 1068 |
+
"""
|
| 1069 |
+
|
| 1070 |
+
def __init__(self) -> None:
|
| 1071 |
+
self.namespace = torch.fx.graph._Namespace()
|
| 1072 |
+
self.memoized_objs_names: dict[PatternExpr, str] = {}
|
| 1073 |
+
self.memoized_objs_pp: dict[PatternExpr, str] = {}
|
| 1074 |
+
|
| 1075 |
+
@staticmethod
|
| 1076 |
+
@functools.cache
|
| 1077 |
+
def run(obj: PatternExpr, output_name: str = "output") -> str:
|
| 1078 |
+
"""
|
| 1079 |
+
Serializes obj to python code with obj written out to `output_name`
|
| 1080 |
+
"""
|
| 1081 |
+
|
| 1082 |
+
pp = PatternPrettyPrinter()
|
| 1083 |
+
assert hasattr(obj, "pretty_print")
|
| 1084 |
+
out_str = obj.pretty_print(pp=pp)
|
| 1085 |
+
|
| 1086 |
+
output = [
|
| 1087 |
+
f"{pp.memoized_objs_names[key]} = {pp.memoized_objs_pp[key]}"
|
| 1088 |
+
for key in pp.memoized_objs_names
|
| 1089 |
+
]
|
| 1090 |
+
|
| 1091 |
+
output.append(f"{output_name} = {out_str}")
|
| 1092 |
+
|
| 1093 |
+
return "\n".join(output)
|
| 1094 |
+
|
| 1095 |
+
def pretty_print(self, obj: Any) -> str:
|
| 1096 |
+
if isinstance(obj, _TargetArgsExpr):
|
| 1097 |
+
if memoized_name := self.memoized_objs_names.get(obj):
|
| 1098 |
+
return memoized_name
|
| 1099 |
+
else:
|
| 1100 |
+
return self.memoize(obj)
|
| 1101 |
+
if hasattr(obj, "pretty_print"):
|
| 1102 |
+
return obj.pretty_print(self)
|
| 1103 |
+
|
| 1104 |
+
return repr(obj)
|
| 1105 |
+
|
| 1106 |
+
def memoize(self, obj: _TargetArgsExpr) -> str:
|
| 1107 |
+
obj_str = obj.pretty_print(self)
|
| 1108 |
+
obj_name = obj.fns_repr()
|
| 1109 |
+
for prefix in ("aten.", "torch.", "prims."):
|
| 1110 |
+
obj_name = obj_name.replace(prefix, "")
|
| 1111 |
+
|
| 1112 |
+
tmp_name = self.namespace.create_name(obj_name, None)
|
| 1113 |
+
self.memoized_objs_names[obj] = tmp_name
|
| 1114 |
+
self.memoized_objs_pp[obj] = obj_str
|
| 1115 |
+
return tmp_name
|
| 1116 |
+
|
| 1117 |
+
|
| 1118 |
+
class _PassDictsType(Protocol):
|
| 1119 |
+
def __getitem__(
|
| 1120 |
+
self, k: tuple[str, torch.fx.node.Target]
|
| 1121 |
+
) -> list[PatternEntry]: ...
|
| 1122 |
+
|
| 1123 |
+
|
| 1124 |
+
@dataclasses.dataclass
|
| 1125 |
+
class PatternEntry:
|
| 1126 |
+
pattern: PatternExpr
|
| 1127 |
+
extra_check: Callable[[Match], bool]
|
| 1128 |
+
|
| 1129 |
+
def apply(self, match: Match, graph: torch.fx.Graph, node: torch.fx.Node) -> None:
|
| 1130 |
+
raise NotImplementedError
|
| 1131 |
+
|
| 1132 |
+
def register(
|
| 1133 |
+
self,
|
| 1134 |
+
pass_dicts: _PassDictsType | Sequence[_PassDictsType],
|
| 1135 |
+
target: torch.fx.node.Target | None = None,
|
| 1136 |
+
prepend: bool = False,
|
| 1137 |
+
) -> None:
|
| 1138 |
+
if target is None:
|
| 1139 |
+
assert hasattr(self.pattern, "fns")
|
| 1140 |
+
for fn in self.pattern.fns:
|
| 1141 |
+
self.register(pass_dicts, fn, prepend=prepend)
|
| 1142 |
+
elif isinstance(pass_dicts, (dict, PatternMatcherPass)):
|
| 1143 |
+
assert hasattr(self.pattern, "op")
|
| 1144 |
+
if prepend:
|
| 1145 |
+
pass_dicts[(self.pattern.op, target)].insert(0, self)
|
| 1146 |
+
else:
|
| 1147 |
+
pass_dicts[(self.pattern.op, target)].append(self)
|
| 1148 |
+
else:
|
| 1149 |
+
pass_dicts = typing.cast(Sequence[_PassDictsType], pass_dicts)
|
| 1150 |
+
for x in pass_dicts:
|
| 1151 |
+
self.register(x, target, prepend=prepend)
|
| 1152 |
+
|
| 1153 |
+
|
| 1154 |
+
@dataclasses.dataclass
|
| 1155 |
+
class LoweringPatternEntry(PatternEntry):
|
| 1156 |
+
handler: Callable[..., Any]
|
| 1157 |
+
|
| 1158 |
+
def apply(self, match: Match, graph: torch.fx.Graph, node: torch.fx.Node) -> None:
|
| 1159 |
+
handler = functools.wraps(self.handler)(functools.partial(self.handler, match))
|
| 1160 |
+
with graph.inserting_before(node):
|
| 1161 |
+
replacement = graph.call_function(handler, tuple(match.args), match.kwargs)
|
| 1162 |
+
_transfer_meta(replacement.meta, node)
|
| 1163 |
+
node.replace_all_uses_with(replacement)
|
| 1164 |
+
assert match.nodes[-1] is node
|
| 1165 |
+
match.erase_nodes()
|
| 1166 |
+
|
| 1167 |
+
|
| 1168 |
+
@dataclasses.dataclass
|
| 1169 |
+
class GraphPatternEntry(PatternEntry):
|
| 1170 |
+
"""
|
| 1171 |
+
A pattern that runs a function on the FX graph
|
| 1172 |
+
"""
|
| 1173 |
+
|
| 1174 |
+
handler: Callable[..., Any]
|
| 1175 |
+
|
| 1176 |
+
def apply(self, match: Match, graph: torch.fx.Graph, node: torch.fx.Node) -> None:
|
| 1177 |
+
with graph.inserting_before(node):
|
| 1178 |
+
self.handler(match, *match.args, **match.kwargs)
|
| 1179 |
+
|
| 1180 |
+
|
| 1181 |
+
@dataclasses.dataclass
|
| 1182 |
+
class ReplacementPatternEntry(PatternEntry):
|
| 1183 |
+
"""
|
| 1184 |
+
The replacement pattern for the graph
|
| 1185 |
+
"""
|
| 1186 |
+
|
| 1187 |
+
normalize_args: Callable[..., list[Any]]
|
| 1188 |
+
pattern_name: str | None = None # Unique identifier for per-pattern telemetry
|
| 1189 |
+
|
| 1190 |
+
@staticmethod
|
| 1191 |
+
def replace_with_graph(
|
| 1192 |
+
match: Match,
|
| 1193 |
+
graph: torch.fx.Graph,
|
| 1194 |
+
replacement_graph: torch.fx.Graph | torch.fx.GraphModule,
|
| 1195 |
+
args: Sequence[torch.fx.Node],
|
| 1196 |
+
pass_name: str | None = None,
|
| 1197 |
+
) -> None:
|
| 1198 |
+
"""
|
| 1199 |
+
Inserts the replacement graph into the toplevel graph at the match
|
| 1200 |
+
"""
|
| 1201 |
+
|
| 1202 |
+
added_replacement_nodes: list[torch.fx.Node] = []
|
| 1203 |
+
|
| 1204 |
+
class Replacer(torch.fx.Interpreter):
|
| 1205 |
+
call_method = None # type: ignore[assignment]
|
| 1206 |
+
call_module = None # type: ignore[assignment]
|
| 1207 |
+
get_attr = None # type: ignore[assignment]
|
| 1208 |
+
|
| 1209 |
+
def run_node(self, node: torch.fx.Node) -> Any:
|
| 1210 |
+
if node.op in ("placeholder", "output"):
|
| 1211 |
+
return super().run_node(node)
|
| 1212 |
+
target = node.target
|
| 1213 |
+
args, kwargs = self.fetch_args_kwargs_from_env(node)
|
| 1214 |
+
if node.op == "call_function":
|
| 1215 |
+
assert callable(target)
|
| 1216 |
+
result = graph.call_function(target, args, kwargs)
|
| 1217 |
+
added_replacement_nodes.append(result)
|
| 1218 |
+
_transfer_meta(
|
| 1219 |
+
new_meta=result.meta,
|
| 1220 |
+
old_node=node,
|
| 1221 |
+
pass_name=pass_name or "",
|
| 1222 |
+
)
|
| 1223 |
+
# This function copy-pastes the replacement graph into
|
| 1224 |
+
# the graph. If the replacement graph had any eager_input_vals,
|
| 1225 |
+
# we propagate those over (val/tensor_meta are handled by
|
| 1226 |
+
# _transfer_meta above).
|
| 1227 |
+
if "eager_input_vals" in node.meta:
|
| 1228 |
+
result.meta["eager_input_vals"] = node.meta["eager_input_vals"]
|
| 1229 |
+
return result
|
| 1230 |
+
if node.op == "get_attr":
|
| 1231 |
+
# If the replacement graph contains a HOP, the subgraphs of the HOP are "get_attr" nodes.
|
| 1232 |
+
# We need to fetch the subgraph of the HOP then register the subgraph to the replaced graph's root.
|
| 1233 |
+
from torch._higher_order_ops.utils import (
|
| 1234 |
+
unique_graph_name_with_root,
|
| 1235 |
+
)
|
| 1236 |
+
|
| 1237 |
+
sub_gm = super().get_attr(target, args, kwargs)
|
| 1238 |
+
if not isinstance(sub_gm, torch.fx.GraphModule):
|
| 1239 |
+
raise NotImplementedError(
|
| 1240 |
+
f"NYI: replacement_graph.{target} is not a graph module. Got {sub_gm}."
|
| 1241 |
+
)
|
| 1242 |
+
assert graph.owning_module is not None
|
| 1243 |
+
graph_name = None
|
| 1244 |
+
for n, mod in graph.owning_module.named_modules():
|
| 1245 |
+
if sub_gm is mod:
|
| 1246 |
+
graph_name = n
|
| 1247 |
+
break
|
| 1248 |
+
if graph_name is None:
|
| 1249 |
+
assert isinstance(target, str)
|
| 1250 |
+
_, graph_name = unique_graph_name_with_root(
|
| 1251 |
+
# pyrefly: ignore [unbound-name]
|
| 1252 |
+
graph.owning_module,
|
| 1253 |
+
target,
|
| 1254 |
+
)
|
| 1255 |
+
# pyrefly: ignore [unbound-name]
|
| 1256 |
+
graph.owning_module.register_module(graph_name, sub_gm)
|
| 1257 |
+
# pyrefly: ignore [unbound-name]
|
| 1258 |
+
getattr_node = graph.get_attr(graph_name)
|
| 1259 |
+
added_replacement_nodes.append(getattr_node)
|
| 1260 |
+
return getattr_node
|
| 1261 |
+
|
| 1262 |
+
raise NotImplementedError(f"unhandled {node}")
|
| 1263 |
+
|
| 1264 |
+
output_nodes = match.output_nodes()
|
| 1265 |
+
|
| 1266 |
+
if len(output_nodes) == 1:
|
| 1267 |
+
last_node = output_nodes[0]
|
| 1268 |
+
else:
|
| 1269 |
+
assert output_nodes[0]
|
| 1270 |
+
nodes = list(output_nodes[0].graph.nodes)
|
| 1271 |
+
indices = [
|
| 1272 |
+
(nodes.index(n), n)
|
| 1273 |
+
for n in output_nodes
|
| 1274 |
+
if isinstance(n, torch.fx.Node)
|
| 1275 |
+
]
|
| 1276 |
+
last_node = min(indices, key=operator.itemgetter(0))[1]
|
| 1277 |
+
|
| 1278 |
+
def percolate_tags(
|
| 1279 |
+
node: torch.fx.Node,
|
| 1280 |
+
tag_name: str,
|
| 1281 |
+
tag_value: str,
|
| 1282 |
+
input_stops: OrderedSet[torch.fx.Node],
|
| 1283 |
+
) -> None:
|
| 1284 |
+
queue = [node]
|
| 1285 |
+
visited = OrderedSet[torch.fx.Node]()
|
| 1286 |
+
|
| 1287 |
+
while queue:
|
| 1288 |
+
arg = queue.pop()
|
| 1289 |
+
if (
|
| 1290 |
+
arg not in visited
|
| 1291 |
+
and arg not in input_stops
|
| 1292 |
+
and hasattr(arg, "meta")
|
| 1293 |
+
):
|
| 1294 |
+
visited.add(arg)
|
| 1295 |
+
arg.meta[tag_name] = tag_value
|
| 1296 |
+
queue.extend(arg.all_input_nodes)
|
| 1297 |
+
|
| 1298 |
+
with graph.inserting_before(last_node):
|
| 1299 |
+
assert isinstance(replacement_graph, torch.fx.GraphModule)
|
| 1300 |
+
replacement = Replacer(replacement_graph).run(*args)
|
| 1301 |
+
if isinstance(replacement, torch.fx.Node):
|
| 1302 |
+
replacement = [replacement]
|
| 1303 |
+
|
| 1304 |
+
def maybe_getitem(node: torch.fx.Node) -> Any:
|
| 1305 |
+
if node.op != "call_function":
|
| 1306 |
+
return None
|
| 1307 |
+
if node.target != operator.getitem:
|
| 1308 |
+
return None
|
| 1309 |
+
assert len(node.args) == 2
|
| 1310 |
+
return node.args[1]
|
| 1311 |
+
|
| 1312 |
+
def replace(
|
| 1313 |
+
old: torch.fx.Node | None,
|
| 1314 |
+
new: torch.fx.Node | Sequence[torch.fx.Node] | None,
|
| 1315 |
+
) -> None:
|
| 1316 |
+
def filter_nodes_in_newly_added_nodes(node: torch.fx.Node) -> bool:
|
| 1317 |
+
# Do not replace the use of a node if it is being used by
|
| 1318 |
+
# nodes in the replaced graph
|
| 1319 |
+
return node not in added_replacement_nodes
|
| 1320 |
+
|
| 1321 |
+
if old is None:
|
| 1322 |
+
assert new is None
|
| 1323 |
+
return
|
| 1324 |
+
assert isinstance(old, torch.fx.Node)
|
| 1325 |
+
if new is None:
|
| 1326 |
+
old.replace_all_uses_with(
|
| 1327 |
+
None, # type: ignore[arg-type]
|
| 1328 |
+
delete_user_cb=filter_nodes_in_newly_added_nodes,
|
| 1329 |
+
)
|
| 1330 |
+
if len(old.users) == 0:
|
| 1331 |
+
graph.erase_node(old)
|
| 1332 |
+
return
|
| 1333 |
+
if isinstance(new, torch.fx.Node):
|
| 1334 |
+
_transfer_meta(new.meta, old, pass_name=pass_name or "")
|
| 1335 |
+
|
| 1336 |
+
# Preserve the recompute tags in the replacement graph. We
|
| 1337 |
+
# look at the recompute tags of the original output node to
|
| 1338 |
+
# propagate the tag from the output all the way to the input
|
| 1339 |
+
# args (named as args in the replace_with_graph).
|
| 1340 |
+
# Note that this is best effort. Since patterns are from
|
| 1341 |
+
# many to many, there is no easy way to correctly map the
|
| 1342 |
+
# recomputable tags. It is possible in some scenarios that we
|
| 1343 |
+
# incorrectly tag some nodes as recomputables.
|
| 1344 |
+
for tag_name in ["recompute", "ac_graph_id"]:
|
| 1345 |
+
if tag_name in old.meta:
|
| 1346 |
+
percolate_tags(
|
| 1347 |
+
new, tag_name, old.meta[tag_name], OrderedSet(args)
|
| 1348 |
+
)
|
| 1349 |
+
|
| 1350 |
+
old.replace_all_uses_with(
|
| 1351 |
+
new, delete_user_cb=filter_nodes_in_newly_added_nodes
|
| 1352 |
+
)
|
| 1353 |
+
if len(old.users) == 0:
|
| 1354 |
+
graph.erase_node(old)
|
| 1355 |
+
return
|
| 1356 |
+
|
| 1357 |
+
# `new` is not a node: it's a list of nodes.
|
| 1358 |
+
#
|
| 1359 |
+
# This happens when we want to replace a node that has a single
|
| 1360 |
+
# packed return with multiple unpacked returns. We need to do
|
| 1361 |
+
# some graph surgery here.
|
| 1362 |
+
#
|
| 1363 |
+
# Example:
|
| 1364 |
+
# def original_graph(x):
|
| 1365 |
+
# a = op(x)
|
| 1366 |
+
# b = a[0]
|
| 1367 |
+
# c = a[1]
|
| 1368 |
+
# ...
|
| 1369 |
+
#
|
| 1370 |
+
# Assume that we want to replace op(x) with the graph
|
| 1371 |
+
# def new_op(x):
|
| 1372 |
+
# w = x + 1
|
| 1373 |
+
# z = x + 2
|
| 1374 |
+
# return (w, z)
|
| 1375 |
+
#
|
| 1376 |
+
# We need to replace `op` with the contents of `new_op`,
|
| 1377 |
+
# and then rewrite a[0] to be w and a[1] to be z, as so:
|
| 1378 |
+
# def new_graph(x):
|
| 1379 |
+
# w = x + 1
|
| 1380 |
+
# z = x + 2
|
| 1381 |
+
# b = w
|
| 1382 |
+
# c = z
|
| 1383 |
+
# ...
|
| 1384 |
+
old_uses = list(old.users.keys())
|
| 1385 |
+
for user in old_uses:
|
| 1386 |
+
idx = maybe_getitem(user)
|
| 1387 |
+
if idx is None:
|
| 1388 |
+
# Output is used directly
|
| 1389 |
+
# pyrefly: ignore [bad-argument-type]
|
| 1390 |
+
old.replace_all_uses_with(new)
|
| 1391 |
+
else:
|
| 1392 |
+
replace(user, new[idx])
|
| 1393 |
+
graph.erase_node(old)
|
| 1394 |
+
|
| 1395 |
+
if len(output_nodes) == len(replacement):
|
| 1396 |
+
for old, new in zip(output_nodes, replacement):
|
| 1397 |
+
replace(old, new)
|
| 1398 |
+
else:
|
| 1399 |
+
assert len(output_nodes) == 1
|
| 1400 |
+
replace(output_nodes[0], replacement)
|
| 1401 |
+
|
| 1402 |
+
match.erase_nodes()
|
| 1403 |
+
|
| 1404 |
+
# Remove dead replacement nodes so they don't inflate user counts
|
| 1405 |
+
# in later lowering heuristics (e.g. should_realize_on_reuse).
|
| 1406 |
+
for node in reversed(added_replacement_nodes):
|
| 1407 |
+
if (
|
| 1408 |
+
not node.users
|
| 1409 |
+
and not node.is_impure()
|
| 1410 |
+
and not isinstance(node.target, torch._ops.HigherOrderOperator)
|
| 1411 |
+
):
|
| 1412 |
+
graph.erase_node(node)
|
| 1413 |
+
|
| 1414 |
+
def apply(self, match: Match, graph: torch.fx.Graph, node: torch.fx.Node) -> None:
|
| 1415 |
+
assert match.replacement_graph is not None
|
| 1416 |
+
self.replace_with_graph(
|
| 1417 |
+
match,
|
| 1418 |
+
graph,
|
| 1419 |
+
match.replacement_graph,
|
| 1420 |
+
self.normalize_args(*match.args, **match.kwargs),
|
| 1421 |
+
pass_name=self.pattern_name or "replace_with_graph",
|
| 1422 |
+
)
|
| 1423 |
+
|
| 1424 |
+
|
| 1425 |
+
def _return_true(match: Match) -> bool:
|
| 1426 |
+
return True
|
| 1427 |
+
|
| 1428 |
+
|
| 1429 |
+
def log_trace_failure(search_fn: Callable[..., Any], e: RuntimeError) -> None:
|
| 1430 |
+
log.info(
|
| 1431 |
+
"Replacement pattern %s failed to apply due to shape mismatch: %s",
|
| 1432 |
+
search_fn.__name__,
|
| 1433 |
+
e,
|
| 1434 |
+
)
|
| 1435 |
+
|
| 1436 |
+
|
| 1437 |
+
def check_and_add_duplicate_pattern(
|
| 1438 |
+
pattern: PatternExpr,
|
| 1439 |
+
graph: torch.fx.Graph | None,
|
| 1440 |
+
seen_patterns: dict[str, list[str | None]],
|
| 1441 |
+
skip_duplicates: bool = False,
|
| 1442 |
+
) -> bool:
|
| 1443 |
+
"""
|
| 1444 |
+
Check if a pattern is a duplicate. Because we ignore certain types in searching, but not
|
| 1445 |
+
in matching, use the graph to distinguish equivalent search patterns.
|
| 1446 |
+
|
| 1447 |
+
Returns True if a duplicate is found and `skip_duplicates=True` is passed in. Errors if
|
| 1448 |
+
`skip_duplicates` is False and a duplicate is found.
|
| 1449 |
+
"""
|
| 1450 |
+
|
| 1451 |
+
pattern_repr = PatternPrettyPrinter.run(pattern)
|
| 1452 |
+
equiv_pattern_reprs = seen_patterns.get(pattern_repr)
|
| 1453 |
+
if not equiv_pattern_reprs:
|
| 1454 |
+
seen_patterns[pattern_repr].append(str(graph) if graph else None)
|
| 1455 |
+
return False
|
| 1456 |
+
|
| 1457 |
+
if graph is None:
|
| 1458 |
+
if skip_duplicates:
|
| 1459 |
+
return True
|
| 1460 |
+
torch._check(
|
| 1461 |
+
False,
|
| 1462 |
+
lambda: f"Duplicate pattern: {pattern_repr} with no graph",
|
| 1463 |
+
)
|
| 1464 |
+
|
| 1465 |
+
new_graph_str = str(graph)
|
| 1466 |
+
for graph_str in equiv_pattern_reprs:
|
| 1467 |
+
if new_graph_str != graph_str:
|
| 1468 |
+
continue
|
| 1469 |
+
if skip_duplicates:
|
| 1470 |
+
return True
|
| 1471 |
+
torch._check(
|
| 1472 |
+
False,
|
| 1473 |
+
lambda: f"Duplicate pattern: {pattern_repr} with duplicated match graph {graph_str} ",
|
| 1474 |
+
)
|
| 1475 |
+
equiv_pattern_reprs.append(new_graph_str)
|
| 1476 |
+
return False
|
| 1477 |
+
|
| 1478 |
+
|
| 1479 |
+
def register_replacement(
|
| 1480 |
+
search_fn: SearchFn,
|
| 1481 |
+
replace_fn: ReplaceFn,
|
| 1482 |
+
example_inputs: list[Any] | tuple[Any, ...],
|
| 1483 |
+
trace_fn: TraceFn,
|
| 1484 |
+
pass_dicts: _PassDictsType | Sequence[_PassDictsType],
|
| 1485 |
+
extra_check: Callable[[Match], bool] = _return_true,
|
| 1486 |
+
scalar_workaround: dict[str, float | int] | None = None,
|
| 1487 |
+
exclusive_arg_names: Sequence[str] = (),
|
| 1488 |
+
search_fn_pattern: PatternExpr | None = None,
|
| 1489 |
+
skip_duplicates: bool = False,
|
| 1490 |
+
pattern_name: str | None = None,
|
| 1491 |
+
get_decomp_fn: Callable[..., dict[Any, Callable[..., Any]]] = select_decomp_table,
|
| 1492 |
+
) -> bool:
|
| 1493 |
+
"""
|
| 1494 |
+
Create a replacement rule based on example functions that get traced
|
| 1495 |
+
to create patterns. This supports both training and inference when
|
| 1496 |
+
run on a joint forward+backward graph.
|
| 1497 |
+
|
| 1498 |
+
Args:
|
| 1499 |
+
search_fn: traced to give original pattern
|
| 1500 |
+
replace_fn: traced to give replacement graph
|
| 1501 |
+
example_inputs: example inputs for initial trace
|
| 1502 |
+
trace_fn: fwd_only or joint_fwd_bwd
|
| 1503 |
+
pass_dict: dict of passes to register to
|
| 1504 |
+
extra_check: additional check to run on match(using real shapes)
|
| 1505 |
+
"""
|
| 1506 |
+
argnames_static = [*inspect.signature(search_fn).parameters.keys()]
|
| 1507 |
+
|
| 1508 |
+
if inspect.ismethod(search_fn):
|
| 1509 |
+
search_fn = _wrap_bound_method(search_fn, argnames_static)
|
| 1510 |
+
|
| 1511 |
+
if inspect.ismethod(replace_fn):
|
| 1512 |
+
replace_argnames = [*inspect.signature(replace_fn).parameters.keys()]
|
| 1513 |
+
replace_fn = _wrap_bound_method(replace_fn, replace_argnames)
|
| 1514 |
+
|
| 1515 |
+
if not isinstance(example_inputs, (list, tuple)):
|
| 1516 |
+
raise TypeError(
|
| 1517 |
+
f"example_inputs must be a list or tuple, got {type(example_inputs)}"
|
| 1518 |
+
)
|
| 1519 |
+
|
| 1520 |
+
def check_fn(match: Match) -> bool:
|
| 1521 |
+
"""
|
| 1522 |
+
Often shapes get burned into the pattern, so our initial match ran with
|
| 1523 |
+
`ignore_types=(int, ...)`.
|
| 1524 |
+
|
| 1525 |
+
Recheck the match with the correct shapes.
|
| 1526 |
+
"""
|
| 1527 |
+
argnames = list(argnames_static)
|
| 1528 |
+
for name in argnames:
|
| 1529 |
+
if name not in match.kwargs:
|
| 1530 |
+
raise RuntimeError(
|
| 1531 |
+
f"Not all inputs to pattern found in match.kwargs. Perhaps one "
|
| 1532 |
+
f"of the inputs is unused? argnames={argnames}, match.kwargs={match.kwargs}"
|
| 1533 |
+
)
|
| 1534 |
+
|
| 1535 |
+
args = list(
|
| 1536 |
+
torch.fx.map_arg(
|
| 1537 |
+
[match.kwargs[name] for name in argnames], lambda n: n.meta["val"]
|
| 1538 |
+
)
|
| 1539 |
+
)
|
| 1540 |
+
|
| 1541 |
+
sym_args: list[torch.SymInt] = []
|
| 1542 |
+
fake_mode = torch._dynamo.utils.detect_fake_mode(args)
|
| 1543 |
+
assert fake_mode is not None
|
| 1544 |
+
with fake_mode:
|
| 1545 |
+
for i, grad in enumerate(requires_grad):
|
| 1546 |
+
if isinstance(args[i], torch.Tensor):
|
| 1547 |
+
# pyrefly: ignore [missing-attribute]
|
| 1548 |
+
if grad and is_integer_dtype(args[i].dtype):
|
| 1549 |
+
return False
|
| 1550 |
+
|
| 1551 |
+
args[i] = torch.empty_strided(
|
| 1552 |
+
# pyrefly: ignore [missing-attribute]
|
| 1553 |
+
args[i].size(),
|
| 1554 |
+
# pyrefly: ignore [missing-attribute]
|
| 1555 |
+
args[i].stride(),
|
| 1556 |
+
# pyrefly: ignore [missing-attribute]
|
| 1557 |
+
dtype=args[i].dtype,
|
| 1558 |
+
# pyrefly: ignore [missing-attribute]
|
| 1559 |
+
device=args[i].device,
|
| 1560 |
+
requires_grad=grad,
|
| 1561 |
+
)
|
| 1562 |
+
# pyrefly: ignore [missing-attribute]
|
| 1563 |
+
for v in itertools.chain(args[i].shape, args[i].stride()):
|
| 1564 |
+
if isinstance(v, torch.SymInt) and all(
|
| 1565 |
+
statically_known_true(v != a) for a in sym_args
|
| 1566 |
+
):
|
| 1567 |
+
sym_args.append(v)
|
| 1568 |
+
|
| 1569 |
+
# If we were given a pre-traced pattern then use that instead of
|
| 1570 |
+
# retracing. Note that this means the pattern has to be independent
|
| 1571 |
+
# of its args.
|
| 1572 |
+
specific_pattern = search_fn_pattern
|
| 1573 |
+
|
| 1574 |
+
if not specific_pattern:
|
| 1575 |
+
if sym_args:
|
| 1576 |
+
# AOT Autograd and make fx will dedupe symbolic shape size
|
| 1577 |
+
# accesses of sym ints that appear as inputs
|
| 1578 |
+
# We don't want the sym_size uses to interfere with pattern matching
|
| 1579 |
+
# so we provide them as inputs.
|
| 1580 |
+
# Later, when we actually do the replacement, the symbolic shape
|
| 1581 |
+
# sizes will get re-traced and added to the graph.
|
| 1582 |
+
|
| 1583 |
+
def search_fn_new(*args_new: Any, **_: Any) -> Any:
|
| 1584 |
+
return search_fn(*args_new[len(args_new) - len(args) :])
|
| 1585 |
+
|
| 1586 |
+
try:
|
| 1587 |
+
specific_graph = trace_fn(
|
| 1588 |
+
search_fn_new,
|
| 1589 |
+
sym_args + args,
|
| 1590 |
+
get_decomp_fn=get_decomp_fn,
|
| 1591 |
+
)
|
| 1592 |
+
except RuntimeError as e:
|
| 1593 |
+
log_trace_failure(search_fn, e)
|
| 1594 |
+
return False
|
| 1595 |
+
|
| 1596 |
+
# correct argnames in the graph
|
| 1597 |
+
sym_arg_names = []
|
| 1598 |
+
for i, placeholder in zip(
|
| 1599 |
+
range(len(sym_args) + len(args)),
|
| 1600 |
+
specific_graph.graph.nodes,
|
| 1601 |
+
):
|
| 1602 |
+
if i < len(sym_args):
|
| 1603 |
+
sym_arg_names.append(placeholder.target)
|
| 1604 |
+
continue
|
| 1605 |
+
|
| 1606 |
+
with specific_graph.graph.inserting_after(placeholder):
|
| 1607 |
+
new_node = specific_graph.graph.placeholder(
|
| 1608 |
+
argnames[i - len(sym_args)]
|
| 1609 |
+
)
|
| 1610 |
+
new_node.target = new_node.name
|
| 1611 |
+
placeholder.replace_all_uses_with(new_node)
|
| 1612 |
+
specific_graph.graph.erase_node(placeholder)
|
| 1613 |
+
|
| 1614 |
+
argnames = sym_arg_names + argnames
|
| 1615 |
+
else:
|
| 1616 |
+
try:
|
| 1617 |
+
specific_graph = trace_fn(
|
| 1618 |
+
search_fn, args, get_decomp_fn=get_decomp_fn
|
| 1619 |
+
)
|
| 1620 |
+
except RuntimeError as e:
|
| 1621 |
+
log_trace_failure(search_fn, e)
|
| 1622 |
+
return False
|
| 1623 |
+
|
| 1624 |
+
specific_pattern = fx_to_pattern(
|
| 1625 |
+
specific_graph,
|
| 1626 |
+
argnames=argnames,
|
| 1627 |
+
exclusive_arg_names=exclusive_arg_names,
|
| 1628 |
+
scalar_workaround=scalar_workaround,
|
| 1629 |
+
)
|
| 1630 |
+
|
| 1631 |
+
node = match.output_nodes()[0]
|
| 1632 |
+
assert node is not None
|
| 1633 |
+
specific_pattern_match = specific_pattern.match(node)
|
| 1634 |
+
|
| 1635 |
+
if _should_debug_node(node.name):
|
| 1636 |
+
log.warning(
|
| 1637 |
+
"Specific pattern match: %s%s %s %s",
|
| 1638 |
+
node,
|
| 1639 |
+
node.args,
|
| 1640 |
+
specific_pattern_match,
|
| 1641 |
+
specific_pattern,
|
| 1642 |
+
)
|
| 1643 |
+
|
| 1644 |
+
if is_match(specific_pattern_match) and extra_check(specific_pattern_match):
|
| 1645 |
+
# trace the pattern using the shapes from the user program
|
| 1646 |
+
match.replacement_graph = trace_fn(
|
| 1647 |
+
replace_fn, args, get_decomp_fn=get_decomp_fn
|
| 1648 |
+
)
|
| 1649 |
+
if len(match.nodes) == 1:
|
| 1650 |
+
for n in match.replacement_graph.graph.nodes:
|
| 1651 |
+
_transfer_meta(
|
| 1652 |
+
new_meta=n.meta,
|
| 1653 |
+
old_node=match.nodes[0],
|
| 1654 |
+
pass_name="replacement",
|
| 1655 |
+
)
|
| 1656 |
+
return True
|
| 1657 |
+
return False
|
| 1658 |
+
|
| 1659 |
+
def normalize_args(**kwargs: Any) -> list[Any]:
|
| 1660 |
+
args = [kwargs.pop(name) for name in argnames_static]
|
| 1661 |
+
for i in range(1, len(kwargs) + 1):
|
| 1662 |
+
if f"tangents_{i}" not in kwargs:
|
| 1663 |
+
break
|
| 1664 |
+
args.append(kwargs.pop(f"tangents_{i}"))
|
| 1665 |
+
assert not kwargs, f"leftover kwargs: {kwargs!r}"
|
| 1666 |
+
return args
|
| 1667 |
+
|
| 1668 |
+
if trace_fn is joint_fwd_bwd:
|
| 1669 |
+
# If inference mode is enabled during compilation, assume that we don't
|
| 1670 |
+
# want to match on any training graph patterns
|
| 1671 |
+
if torch.is_inference_mode_enabled():
|
| 1672 |
+
return False
|
| 1673 |
+
|
| 1674 |
+
# TODO: Revisit the functionalize_rng_ops for lowmem dropout
|
| 1675 |
+
with functorch_config.patch(functionalize_rng_ops=False):
|
| 1676 |
+
requires_grad: list[bool] = [
|
| 1677 |
+
isinstance(x, torch.Tensor) and x.requires_grad for x in example_inputs
|
| 1678 |
+
]
|
| 1679 |
+
if search_fn_pattern is None:
|
| 1680 |
+
pattern, gm = gen_pattern_and_search_gm(
|
| 1681 |
+
search_fn,
|
| 1682 |
+
example_inputs,
|
| 1683 |
+
trace_fn,
|
| 1684 |
+
scalar_workaround,
|
| 1685 |
+
exclusive_arg_names,
|
| 1686 |
+
get_decomp_fn=get_decomp_fn,
|
| 1687 |
+
)
|
| 1688 |
+
else:
|
| 1689 |
+
pattern = search_fn_pattern
|
| 1690 |
+
gm = None
|
| 1691 |
+
|
| 1692 |
+
for pattern_matcher_pass in (
|
| 1693 |
+
pass_dicts if isinstance(pass_dicts, Sequence) else [pass_dicts]
|
| 1694 |
+
):
|
| 1695 |
+
if isinstance(pattern_matcher_pass, PatternMatcherPass):
|
| 1696 |
+
if check_and_add_duplicate_pattern(
|
| 1697 |
+
pattern,
|
| 1698 |
+
gm.graph if gm else None,
|
| 1699 |
+
pattern_matcher_pass.seen_patterns,
|
| 1700 |
+
skip_duplicates=skip_duplicates,
|
| 1701 |
+
):
|
| 1702 |
+
return False
|
| 1703 |
+
|
| 1704 |
+
pattern = ReplacementPatternEntry(
|
| 1705 |
+
pattern=pattern,
|
| 1706 |
+
extra_check=check_fn,
|
| 1707 |
+
normalize_args=normalize_args,
|
| 1708 |
+
pattern_name=pattern_name,
|
| 1709 |
+
)
|
| 1710 |
+
pattern.register(pass_dicts)
|
| 1711 |
+
return pattern.pattern # type: ignore[return-value]
|
| 1712 |
+
|
| 1713 |
+
|
| 1714 |
+
_serialized_patterns: OrderedSet[str] = OrderedSet()
|
| 1715 |
+
|
| 1716 |
+
|
| 1717 |
+
def _serialize_pattern(
|
| 1718 |
+
unique_name: str,
|
| 1719 |
+
search_fn: SearchFn,
|
| 1720 |
+
example_inputs: Sequence[Any],
|
| 1721 |
+
trace_fn: TraceFn,
|
| 1722 |
+
scalar_workaround: dict[str, float | int] | None,
|
| 1723 |
+
) -> PatternExpr:
|
| 1724 |
+
def get_file_template() -> str:
|
| 1725 |
+
auto_generated_msg = textwrap.dedent(
|
| 1726 |
+
"""\
|
| 1727 |
+
# This is an auto-generated file. Please do not modify it by hand.
|
| 1728 |
+
# To re-generate, run:
|
| 1729 |
+
# cd ~/pytorch && python torchgen/fuse/gen_patterns.py
|
| 1730 |
+
"""
|
| 1731 |
+
)
|
| 1732 |
+
|
| 1733 |
+
file_template = textwrap.dedent(
|
| 1734 |
+
"""\
|
| 1735 |
+
# mypy: ignore-errors
|
| 1736 |
+
|
| 1737 |
+
# noqa: F401, E501
|
| 1738 |
+
{msg}
|
| 1739 |
+
import torch
|
| 1740 |
+
import torch._inductor
|
| 1741 |
+
import operator
|
| 1742 |
+
|
| 1743 |
+
aten = torch.ops.aten
|
| 1744 |
+
prims = torch.ops.prims
|
| 1745 |
+
|
| 1746 |
+
"""
|
| 1747 |
+
).format(msg=auto_generated_msg)
|
| 1748 |
+
|
| 1749 |
+
pattern_matcher_imports = []
|
| 1750 |
+
for name in dir(torch._inductor.pattern_matcher):
|
| 1751 |
+
attr = getattr(torch._inductor.pattern_matcher, name)
|
| 1752 |
+
try:
|
| 1753 |
+
if isinstance(attr, type) and issubclass(
|
| 1754 |
+
attr, (PatternExpr, _TargetExpr)
|
| 1755 |
+
):
|
| 1756 |
+
# pyrefly: ignore [bad-argument-type]
|
| 1757 |
+
pattern_matcher_imports.append(name)
|
| 1758 |
+
except TypeError:
|
| 1759 |
+
pass
|
| 1760 |
+
|
| 1761 |
+
formatted_imports = ",\n ".join(pattern_matcher_imports)
|
| 1762 |
+
formatted_imports = f"from torch._inductor.pattern_matcher import (\n {formatted_imports},\n)\n"
|
| 1763 |
+
return f"{file_template}{formatted_imports}"
|
| 1764 |
+
|
| 1765 |
+
if not SERIALIZED_PATTERN_PATH.is_dir():
|
| 1766 |
+
raise RuntimeError(
|
| 1767 |
+
f"Could not find serialized patterns directory at {SERIALIZED_PATTERN_PATH}"
|
| 1768 |
+
)
|
| 1769 |
+
|
| 1770 |
+
pattern_name = search_fn.__name__
|
| 1771 |
+
|
| 1772 |
+
from torch._functorch import config as functorch_config
|
| 1773 |
+
|
| 1774 |
+
with functorch_config.patch(functionalize_rng_ops=False):
|
| 1775 |
+
pattern = gen_pattern(search_fn, example_inputs, trace_fn, scalar_workaround)
|
| 1776 |
+
|
| 1777 |
+
serialized_pattern = PatternPrettyPrinter.run(pattern, output_name=unique_name)
|
| 1778 |
+
if pattern_name not in _serialized_patterns:
|
| 1779 |
+
write_mode = "w"
|
| 1780 |
+
_serialized_patterns.add(pattern_name)
|
| 1781 |
+
else:
|
| 1782 |
+
write_mode = "a"
|
| 1783 |
+
|
| 1784 |
+
file_template = get_file_template()
|
| 1785 |
+
|
| 1786 |
+
with open(SERIALIZED_PATTERN_PATH / f"{pattern_name}.py", write_mode) as f:
|
| 1787 |
+
if write_mode == "w":
|
| 1788 |
+
f.write(file_template)
|
| 1789 |
+
else:
|
| 1790 |
+
f.write("\n\n")
|
| 1791 |
+
f.write(serialized_pattern)
|
| 1792 |
+
f.write("\n")
|
| 1793 |
+
|
| 1794 |
+
return pattern
|
| 1795 |
+
|
| 1796 |
+
|
| 1797 |
+
SERIALIZED_PATTERN_PATH = Path(__file__).parent / "fx_passes" / "serialized_patterns"
|
| 1798 |
+
|
| 1799 |
+
# This is the set of serialized patterns that we've registered. Used by
|
| 1800 |
+
# test_serialized_patterns_up_to_date() to ensure the patterns are up
|
| 1801 |
+
# to date.
|
| 1802 |
+
_known_precompiled_patterns: list[
|
| 1803 |
+
tuple[
|
| 1804 |
+
Any,
|
| 1805 |
+
Iterable[Any],
|
| 1806 |
+
Callable[[Callable[..., Any], Iterable[Any]], torch.fx.GraphModule],
|
| 1807 |
+
Any,
|
| 1808 |
+
PatternExpr,
|
| 1809 |
+
]
|
| 1810 |
+
] = []
|
| 1811 |
+
|
| 1812 |
+
|
| 1813 |
+
def gen_register_replacement(
|
| 1814 |
+
unique_name: str,
|
| 1815 |
+
search_fn: SearchFn,
|
| 1816 |
+
replace_fn: ReplaceFn,
|
| 1817 |
+
example_inputs: Iterable[Any],
|
| 1818 |
+
trace_fn: TraceFn,
|
| 1819 |
+
pass_dicts: _PassDictsType | Sequence[_PassDictsType],
|
| 1820 |
+
extra_check: Callable[[Match], bool] = _return_true,
|
| 1821 |
+
scalar_workaround: dict[str, float | int] | None = None,
|
| 1822 |
+
exclusive_arg_names: Sequence[str] = (),
|
| 1823 |
+
skip_duplicates: bool = False,
|
| 1824 |
+
get_decomp_fn: Callable[..., dict[Any, Callable[..., Any]]] = select_decomp_table,
|
| 1825 |
+
) -> None:
|
| 1826 |
+
# Make sure the example_inputs is materialized.
|
| 1827 |
+
example_inputs = tuple(example_inputs)
|
| 1828 |
+
|
| 1829 |
+
if "PYTORCH_GEN_PATTERNS" in os.environ:
|
| 1830 |
+
pat = _serialize_pattern(
|
| 1831 |
+
unique_name, search_fn, example_inputs, trace_fn, scalar_workaround
|
| 1832 |
+
)
|
| 1833 |
+
else:
|
| 1834 |
+
pattern_name = search_fn.__name__
|
| 1835 |
+
m = importlib.import_module(
|
| 1836 |
+
f"torch._inductor.fx_passes.serialized_patterns.{pattern_name}"
|
| 1837 |
+
)
|
| 1838 |
+
if not m or not hasattr(m, unique_name):
|
| 1839 |
+
log.warning(
|
| 1840 |
+
"Precompiled pattern %r not found. Run torchgen/fuse/gen_patterns.py.",
|
| 1841 |
+
unique_name,
|
| 1842 |
+
)
|
| 1843 |
+
pat = getattr(m, unique_name)
|
| 1844 |
+
|
| 1845 |
+
for arg in pytree.tree_iter(example_inputs):
|
| 1846 |
+
if isinstance(arg, FakeTensor) and arg.constant is not None:
|
| 1847 |
+
# This can be a problem - small fake tensors (e.g. `tensor(2)`) will
|
| 1848 |
+
# hold onto their original constant value - and by stashing it here
|
| 1849 |
+
# will cause a memory leak if the constant value is on GPU.
|
| 1850 |
+
# Since this is just an optimization we can clear it out.
|
| 1851 |
+
arg.constant = None
|
| 1852 |
+
|
| 1853 |
+
_known_precompiled_patterns.append(
|
| 1854 |
+
(search_fn, example_inputs, trace_fn, scalar_workaround, pat)
|
| 1855 |
+
)
|
| 1856 |
+
register_replacement(
|
| 1857 |
+
search_fn,
|
| 1858 |
+
replace_fn,
|
| 1859 |
+
example_inputs,
|
| 1860 |
+
trace_fn,
|
| 1861 |
+
pass_dicts,
|
| 1862 |
+
extra_check,
|
| 1863 |
+
scalar_workaround,
|
| 1864 |
+
exclusive_arg_names,
|
| 1865 |
+
search_fn_pattern=pat,
|
| 1866 |
+
skip_duplicates=skip_duplicates,
|
| 1867 |
+
pattern_name=unique_name,
|
| 1868 |
+
get_decomp_fn=get_decomp_fn,
|
| 1869 |
+
)
|
| 1870 |
+
|
| 1871 |
+
|
| 1872 |
+
@functorch_config.patch(functionalize_rng_ops=False) # type: ignore[misc]
|
| 1873 |
+
def gen_pattern_and_search_gm(
|
| 1874 |
+
search_fn: SearchFn,
|
| 1875 |
+
example_inputs: Sequence[Any],
|
| 1876 |
+
trace_fn: TraceFn,
|
| 1877 |
+
scalar_workaround: dict[str, float | int] | None = None,
|
| 1878 |
+
exclusive_arg_names: Sequence[str] = (),
|
| 1879 |
+
get_decomp_fn: Callable[..., dict[Any, Callable[..., Any]]] = select_decomp_table,
|
| 1880 |
+
) -> tuple[PatternExpr, torch.fx.GraphModule]:
|
| 1881 |
+
argnames = [*inspect.signature(search_fn).parameters.keys()]
|
| 1882 |
+
|
| 1883 |
+
if scalar_workaround is None:
|
| 1884 |
+
scalar_workaround = {}
|
| 1885 |
+
flat_inputs = []
|
| 1886 |
+
input_idx = 0 # Positional arguments index
|
| 1887 |
+
|
| 1888 |
+
for argname in argnames:
|
| 1889 |
+
if argname in scalar_workaround:
|
| 1890 |
+
flat_inputs.append(scalar_workaround[argname])
|
| 1891 |
+
else:
|
| 1892 |
+
flat_inputs.append(example_inputs[input_idx])
|
| 1893 |
+
input_idx += 1
|
| 1894 |
+
|
| 1895 |
+
search_gm = trace_fn(search_fn, flat_inputs, get_decomp_fn=get_decomp_fn)
|
| 1896 |
+
return (
|
| 1897 |
+
fx_to_pattern(
|
| 1898 |
+
search_gm,
|
| 1899 |
+
ignore_types=(int, float, list, torch.device, torch.dtype),
|
| 1900 |
+
argnames=argnames,
|
| 1901 |
+
scalar_workaround=scalar_workaround,
|
| 1902 |
+
exclusive_arg_names=exclusive_arg_names,
|
| 1903 |
+
),
|
| 1904 |
+
search_gm,
|
| 1905 |
+
)
|
| 1906 |
+
|
| 1907 |
+
|
| 1908 |
+
def gen_pattern(
|
| 1909 |
+
search_fn: SearchFn,
|
| 1910 |
+
example_inputs: Sequence[Any],
|
| 1911 |
+
trace_fn: TraceFn,
|
| 1912 |
+
scalar_workaround: dict[str, float | int] | None = None,
|
| 1913 |
+
exclusive_arg_names: Sequence[str] = (),
|
| 1914 |
+
) -> PatternExpr:
|
| 1915 |
+
return gen_pattern_and_search_gm(
|
| 1916 |
+
search_fn, example_inputs, trace_fn, scalar_workaround, exclusive_arg_names
|
| 1917 |
+
)[0]
|
| 1918 |
+
|
| 1919 |
+
|
| 1920 |
+
def register_lowering_pattern(
|
| 1921 |
+
pattern: PatternExpr,
|
| 1922 |
+
extra_check: Callable[[Match], bool] = _return_true,
|
| 1923 |
+
*,
|
| 1924 |
+
pass_dict: _PassDictsType,
|
| 1925 |
+
prepend: bool = False,
|
| 1926 |
+
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
| 1927 |
+
"""
|
| 1928 |
+
Register an aten to inductor IR replacement pattern. The decorated
|
| 1929 |
+
function is saved and then called a lowering time allowing direct
|
| 1930 |
+
pattern to inductor IR conversion.
|
| 1931 |
+
"""
|
| 1932 |
+
|
| 1933 |
+
def decorator(handler: Callable[..., Any]) -> Callable[..., Any]:
|
| 1934 |
+
assert callable(handler)
|
| 1935 |
+
LoweringPatternEntry(
|
| 1936 |
+
pattern=pattern, extra_check=extra_check, handler=handler
|
| 1937 |
+
).register(pass_dict, prepend=prepend)
|
| 1938 |
+
handler._inductor_lowering_function = True # type: ignore[attr-defined]
|
| 1939 |
+
return handler
|
| 1940 |
+
|
| 1941 |
+
return decorator
|
| 1942 |
+
|
| 1943 |
+
|
| 1944 |
+
def register_graph_pattern(
|
| 1945 |
+
pattern: PatternExpr,
|
| 1946 |
+
extra_check: Callable[[Match], bool] = _return_true,
|
| 1947 |
+
*,
|
| 1948 |
+
pass_dict: _PassDictsType,
|
| 1949 |
+
prepend: bool = False,
|
| 1950 |
+
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
| 1951 |
+
"""
|
| 1952 |
+
Register a pattern that runs a function on the FX graph, allowing
|
| 1953 |
+
custom transformation code.
|
| 1954 |
+
"""
|
| 1955 |
+
|
| 1956 |
+
def decorator(handler: Callable[..., Any]) -> Callable[..., Any]:
|
| 1957 |
+
assert callable(handler)
|
| 1958 |
+
GraphPatternEntry(
|
| 1959 |
+
pattern=pattern, extra_check=extra_check, handler=handler
|
| 1960 |
+
).register(pass_dict, prepend=prepend)
|
| 1961 |
+
return handler
|
| 1962 |
+
|
| 1963 |
+
return decorator
|
| 1964 |
+
|
| 1965 |
+
|
| 1966 |
+
def is_start_of_fx_graph(graph: torch.fx.Graph, node: torch.fx.Node) -> bool:
|
| 1967 |
+
# first node in the graph
|
| 1968 |
+
return node is next(iter(graph.nodes))
|
| 1969 |
+
|
| 1970 |
+
|
| 1971 |
+
# match: copy_, relu_, _set_grad_enabled, manual_seed, _enter_autocast, etc
|
| 1972 |
+
# doesn't match: __rshift__, etc
|
| 1973 |
+
_mutation_op_re = re.compile(r"(?<!_)(_$|_[.]|(\b|_)(set|enter|exit|seed)(\b|_))(?!_)")
|
| 1974 |
+
|
| 1975 |
+
|
| 1976 |
+
def fixme_incorrect_inductor_schema_op(op: torch._ops.OpOverload) -> bool:
|
| 1977 |
+
if op.namespace != "inductor":
|
| 1978 |
+
return False
|
| 1979 |
+
|
| 1980 |
+
# TODO - fix schema
|
| 1981 |
+
# Dont add any more !
|
| 1982 |
+
return op in (
|
| 1983 |
+
torch.ops.inductor.accumulate_grad_.default,
|
| 1984 |
+
torch.ops.inductor.resize_storage_bytes_.default,
|
| 1985 |
+
)
|
| 1986 |
+
|
| 1987 |
+
|
| 1988 |
+
def is_mutation_op(node: torch.fx.Node) -> bool:
|
| 1989 |
+
if isinstance(
|
| 1990 |
+
node.target, torch._ops.OpOverload
|
| 1991 |
+
) and not fixme_incorrect_inductor_schema_op(node.target):
|
| 1992 |
+
return node.target._schema.is_mutable
|
| 1993 |
+
elif isinstance(
|
| 1994 |
+
node.target, torch._higher_order_ops.auto_functionalize.AutoFunctionalized
|
| 1995 |
+
):
|
| 1996 |
+
return False
|
| 1997 |
+
if node.op == "call_function":
|
| 1998 |
+
assert callable(node.target)
|
| 1999 |
+
if _mutation_op_re.search(node.target.__name__):
|
| 2000 |
+
return True
|
| 2001 |
+
elif node.op == "call_method":
|
| 2002 |
+
assert isinstance(node.target, str)
|
| 2003 |
+
if _mutation_op_re.search(node.target):
|
| 2004 |
+
return True
|
| 2005 |
+
return node.kwargs.get("out") is not None
|
| 2006 |
+
|
| 2007 |
+
|
| 2008 |
+
def same_mutation_regions(a: torch.fx.Node, b: torch.fx.Node) -> bool:
|
| 2009 |
+
assert "mutation_region_id" in a.meta
|
| 2010 |
+
assert "mutation_region_id" in b.meta
|
| 2011 |
+
return a.meta["mutation_region_id"] == b.meta["mutation_region_id"]
|
| 2012 |
+
|
| 2013 |
+
|
| 2014 |
+
def get_mutation_region_id(graph: torch.fx.Graph, node: torch.fx.Node) -> int:
|
| 2015 |
+
n = node
|
| 2016 |
+
while "mutation_region_id" not in n.meta and not is_start_of_fx_graph(graph, n):
|
| 2017 |
+
n = n.prev
|
| 2018 |
+
mutation_region_id = n.meta.get("mutation_region_id", 0)
|
| 2019 |
+
while n is not node:
|
| 2020 |
+
n = n.next
|
| 2021 |
+
if is_mutation_op(n):
|
| 2022 |
+
mutation_region_id += 1
|
| 2023 |
+
n.meta["mutation_region_id"] = mutation_region_id
|
| 2024 |
+
return mutation_region_id
|
| 2025 |
+
|
| 2026 |
+
|
| 2027 |
+
def should_compute_mutation_region_ids(graph: torch.fx.Graph) -> bool:
|
| 2028 |
+
return "mutation_region_id" not in next(iter(graph.nodes)).meta
|
| 2029 |
+
|
| 2030 |
+
|
| 2031 |
+
def compute_mutation_region_ids(graph: torch.fx.Graph) -> None:
|
| 2032 |
+
mutation_region_id = 0
|
| 2033 |
+
for nd in graph.nodes:
|
| 2034 |
+
if is_mutation_op(nd):
|
| 2035 |
+
mutation_region_id += 1
|
| 2036 |
+
nd.meta["mutation_region_id"] = mutation_region_id
|
| 2037 |
+
|
| 2038 |
+
|
| 2039 |
+
def _wrap_bound_method(fn: Any, argnames: list[str]) -> Any:
|
| 2040 |
+
"""
|
| 2041 |
+
Wrap a bound method to remove 'self' from its signature for FX tracing.
|
| 2042 |
+
"""
|
| 2043 |
+
|
| 2044 |
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
| 2045 |
+
return fn(*args, **kwargs)
|
| 2046 |
+
|
| 2047 |
+
params = [
|
| 2048 |
+
inspect.Parameter(name, inspect.Parameter.POSITIONAL_OR_KEYWORD)
|
| 2049 |
+
for name in argnames
|
| 2050 |
+
]
|
| 2051 |
+
wrapper.__signature__ = inspect.Signature(params) # type: ignore[attr-defined]
|
| 2052 |
+
return wrapper
|
| 2053 |
+
|
| 2054 |
+
|
| 2055 |
+
class PatternMatcherPass:
|
| 2056 |
+
"""
|
| 2057 |
+
Registry of patterns to match and replace in FX graphs.
|
| 2058 |
+
"""
|
| 2059 |
+
|
| 2060 |
+
def __init__(
|
| 2061 |
+
self,
|
| 2062 |
+
pass_name: str | None = None,
|
| 2063 |
+
subsystem: str | None = None,
|
| 2064 |
+
) -> None:
|
| 2065 |
+
super().__init__()
|
| 2066 |
+
self.patterns: defaultdict[
|
| 2067 |
+
tuple[str, torch.fx.node.Target], list[PatternEntry]
|
| 2068 |
+
] = defaultdict(list)
|
| 2069 |
+
self.pass_name = pass_name
|
| 2070 |
+
self.subsystem = subsystem
|
| 2071 |
+
|
| 2072 |
+
# For a particular generated pattern repr, store all of the str representations
|
| 2073 |
+
# of the graph used to generate them. Because we ignore certain patterns
|
| 2074 |
+
# in searching, but not in matching, use the graph to distinguish if two equivalent
|
| 2075 |
+
# searches are actually different.
|
| 2076 |
+
self.seen_patterns: dict[str, list[str | None]] = defaultdict(list)
|
| 2077 |
+
|
| 2078 |
+
def __getitem__(self, item: tuple[str, torch.fx.node.Target]) -> list[PatternEntry]:
|
| 2079 |
+
return self.patterns[item]
|
| 2080 |
+
|
| 2081 |
+
def apply(self, gm: torch.fx.GraphModule | torch.fx.Graph) -> int:
|
| 2082 |
+
"""Apply all registered patterns to the graph, returning the number of matches."""
|
| 2083 |
+
if not self.patterns:
|
| 2084 |
+
return 0
|
| 2085 |
+
if isinstance(gm, torch.fx.GraphModule):
|
| 2086 |
+
graph = gm.graph
|
| 2087 |
+
elif isinstance(gm, torch.fx.Graph):
|
| 2088 |
+
graph = gm
|
| 2089 |
+
gm = graph.owning_module
|
| 2090 |
+
else:
|
| 2091 |
+
raise RuntimeError(
|
| 2092 |
+
f"The input to PatternMatcherPass must be a GraphModule or a Graph, but got {type(gm)}"
|
| 2093 |
+
)
|
| 2094 |
+
if should_compute_mutation_region_ids(graph):
|
| 2095 |
+
compute_mutation_region_ids(graph)
|
| 2096 |
+
get_mutation_region_id_partial = functools.partial(
|
| 2097 |
+
get_mutation_region_id, graph
|
| 2098 |
+
)
|
| 2099 |
+
count = 0
|
| 2100 |
+
nodes = []
|
| 2101 |
+
has_call_module = False
|
| 2102 |
+
for op, target in self.patterns:
|
| 2103 |
+
if op == "call_module":
|
| 2104 |
+
has_call_module = True
|
| 2105 |
+
else:
|
| 2106 |
+
nodes.append(graph.find_nodes(op=op, target=target, sort=False))
|
| 2107 |
+
if has_call_module:
|
| 2108 |
+
nodes.append(graph.find_nodes(op="call_module", sort=False))
|
| 2109 |
+
pass_name = self.pass_name if self.pass_name is not None else "pattern_matcher"
|
| 2110 |
+
assert isinstance(gm, torch.fx.GraphModule)
|
| 2111 |
+
with GraphTransformObserver(gm, pass_name, self.subsystem):
|
| 2112 |
+
for node in sorted(itertools.chain.from_iterable(nodes), reverse=True):
|
| 2113 |
+
target = extract_target(node)
|
| 2114 |
+
if node.op == "call_module":
|
| 2115 |
+
if (node.op, target) not in self.patterns:
|
| 2116 |
+
continue
|
| 2117 |
+
|
| 2118 |
+
# conservatively not applying pattern for cpu input,
|
| 2119 |
+
# since some of the patterns induce codegen and split nodes.
|
| 2120 |
+
# Note: we will only skip cpu compute if disable_cpp_codegen=True
|
| 2121 |
+
if fallback_node_due_to_unsupported_type(node, allow_cpu_inputs=False):
|
| 2122 |
+
continue
|
| 2123 |
+
|
| 2124 |
+
for entry in self.patterns[(node.op, target)]:
|
| 2125 |
+
if node._erased:
|
| 2126 |
+
break
|
| 2127 |
+
m = entry.pattern.match(node)
|
| 2128 |
+
# pattern match crosses mutation barrier - discard
|
| 2129 |
+
if (
|
| 2130 |
+
is_match(m)
|
| 2131 |
+
and len(
|
| 2132 |
+
OrderedSet(map(get_mutation_region_id_partial, m.nodes))
|
| 2133 |
+
)
|
| 2134 |
+
!= 1
|
| 2135 |
+
):
|
| 2136 |
+
continue
|
| 2137 |
+
# pattern match crosses stream boundary - discard
|
| 2138 |
+
if (
|
| 2139 |
+
is_match(m)
|
| 2140 |
+
and len(
|
| 2141 |
+
OrderedSet(
|
| 2142 |
+
n.meta.get("custom", {}).get("stream", 0)
|
| 2143 |
+
for n in m.nodes
|
| 2144 |
+
)
|
| 2145 |
+
)
|
| 2146 |
+
!= 1
|
| 2147 |
+
):
|
| 2148 |
+
continue
|
| 2149 |
+
if _should_debug_node(node.name):
|
| 2150 |
+
log.warning("%s%s %s %s", node, node.args, m, entry.pattern)
|
| 2151 |
+
|
| 2152 |
+
if is_match(m) and guard_or_false(entry.extra_check(m)):
|
| 2153 |
+
count += 1
|
| 2154 |
+
entry.apply(m, graph, node)
|
| 2155 |
+
counters[backend]["pattern_matcher_count"] += 1
|
| 2156 |
+
counters[backend]["pattern_matcher_nodes"] += len(m.nodes)
|
| 2157 |
+
|
| 2158 |
+
# Track per-pattern counts when debug mode is active
|
| 2159 |
+
if os.environ.get("TORCHINDUCTOR_PATTERN_MATCH_DEBUG"):
|
| 2160 |
+
if getattr(entry, "pattern_name", None):
|
| 2161 |
+
pattern_name = entry.pattern_name
|
| 2162 |
+
else:
|
| 2163 |
+
# Fallback: use pattern class name + operation target
|
| 2164 |
+
pattern_class = entry.pattern.__class__.__name__
|
| 2165 |
+
target = str(node.target)
|
| 2166 |
+
pattern_name = f"{pattern_class}_{target}"
|
| 2167 |
+
|
| 2168 |
+
pattern_key = f"{backend}_pattern_matcher_per_pattern"
|
| 2169 |
+
counters[pattern_key][pattern_name] += 1
|
| 2170 |
+
return count
|
| 2171 |
+
|
| 2172 |
+
def clear(self) -> None:
|
| 2173 |
+
self.patterns.clear()
|
| 2174 |
+
|
| 2175 |
+
|
| 2176 |
+
def _not_implemented(*args: Any, **kwargs: Any) -> NoReturn:
|
| 2177 |
+
raise NotImplementedError
|
| 2178 |
+
|
| 2179 |
+
|
| 2180 |
+
def fx_to_pattern(
|
| 2181 |
+
gm: torch.fx.GraphModule | torch.fx.Graph,
|
| 2182 |
+
ignore_types: Sequence[type[Any]] = (),
|
| 2183 |
+
argnames: Sequence[str] = (),
|
| 2184 |
+
scalar_workaround: dict[str, float | int] | None = None,
|
| 2185 |
+
exclusive_arg_names: Sequence[str] = (),
|
| 2186 |
+
) -> PatternExpr:
|
| 2187 |
+
"""
|
| 2188 |
+
Convert an FX graph into a PatternExpr. This is useful for simple
|
| 2189 |
+
patterns that can only match single functions and fixed-length lists.
|
| 2190 |
+
"""
|
| 2191 |
+
# scalar_workaround is a hack to capture dropout_p
|
| 2192 |
+
# see https://github.com/pytorch/pytorch/issues/97894
|
| 2193 |
+
scalar_workaround = scalar_workaround or {}
|
| 2194 |
+
inv_scalar_workaround = {v: k for k, v in scalar_workaround.items()}
|
| 2195 |
+
assert len(inv_scalar_workaround) == len(scalar_workaround)
|
| 2196 |
+
|
| 2197 |
+
def process_arg(
|
| 2198 |
+
x: T, ignore_types_override: Sequence[type[Any]] | None = None
|
| 2199 |
+
) -> T | KeywordArg | Ignored:
|
| 2200 |
+
current_ignore_types = (
|
| 2201 |
+
ignore_types_override if ignore_types_override is not None else ignore_types
|
| 2202 |
+
)
|
| 2203 |
+
if isinstance(x, (float, int)) and x in inv_scalar_workaround:
|
| 2204 |
+
return KeywordArg(inv_scalar_workaround[x])
|
| 2205 |
+
if type(x) in current_ignore_types:
|
| 2206 |
+
return Ignored()
|
| 2207 |
+
if isinstance(x, list) and all(isinstance(y, Ignored) for y in x) and x:
|
| 2208 |
+
return Ignored()
|
| 2209 |
+
return x
|
| 2210 |
+
|
| 2211 |
+
argnum = itertools.count()
|
| 2212 |
+
|
| 2213 |
+
class Converter(torch.fx.Interpreter):
|
| 2214 |
+
# pyrefly: ignore [bad-override]
|
| 2215 |
+
call_method = _not_implemented
|
| 2216 |
+
# pyrefly: ignore [bad-override]
|
| 2217 |
+
call_module = _not_implemented
|
| 2218 |
+
# pyrefly: ignore [bad-override]
|
| 2219 |
+
get_attr = _not_implemented
|
| 2220 |
+
|
| 2221 |
+
# pyrefly: ignore [bad-override]
|
| 2222 |
+
def placeholder(
|
| 2223 |
+
self,
|
| 2224 |
+
target: str, # type: ignore[override]
|
| 2225 |
+
args: Sequence[Any],
|
| 2226 |
+
kwargs: Mapping[str, Any],
|
| 2227 |
+
) -> ExclusiveKeywordArg | KeywordArg:
|
| 2228 |
+
n = next(argnum)
|
| 2229 |
+
if n < len(argnames):
|
| 2230 |
+
name = argnames[n]
|
| 2231 |
+
elif argnames:
|
| 2232 |
+
assert target.startswith("tangent")
|
| 2233 |
+
name = target
|
| 2234 |
+
else:
|
| 2235 |
+
target = re.sub(r"_\d+$", "", target) # de-mangle arg name
|
| 2236 |
+
name = target
|
| 2237 |
+
if name in exclusive_arg_names:
|
| 2238 |
+
return ExclusiveKeywordArg(name)
|
| 2239 |
+
else:
|
| 2240 |
+
return KeywordArg(name)
|
| 2241 |
+
|
| 2242 |
+
# pyrefly: ignore [bad-override]
|
| 2243 |
+
def call_function(
|
| 2244 |
+
self,
|
| 2245 |
+
target: str, # type: ignore[override]
|
| 2246 |
+
args: Sequence[Any],
|
| 2247 |
+
kwargs: Mapping[str, Any],
|
| 2248 |
+
) -> PatternExpr:
|
| 2249 |
+
process_arg_fn = process_arg
|
| 2250 |
+
# Indexing is critical for matching getitem nodes, so we can't ignore int args here
|
| 2251 |
+
if target is operator.getitem:
|
| 2252 |
+
|
| 2253 |
+
def process_arg_fn_impl(
|
| 2254 |
+
x: T,
|
| 2255 |
+
ignore_types_override: Sequence[type[Any]] | None = tuple(
|
| 2256 |
+
t for t in ignore_types if t is not int
|
| 2257 |
+
),
|
| 2258 |
+
) -> T | KeywordArg | Ignored:
|
| 2259 |
+
return process_arg(x, ignore_types_override)
|
| 2260 |
+
|
| 2261 |
+
process_arg_fn = process_arg_fn_impl
|
| 2262 |
+
|
| 2263 |
+
args, kwargs = pytree.tree_map(process_arg_fn, (args, kwargs))
|
| 2264 |
+
if list in ignore_types:
|
| 2265 |
+
# Handle a burned in tensor size which are now [Ignored(), Ignored(), ...]
|
| 2266 |
+
args = [process_arg_fn(a) for a in args]
|
| 2267 |
+
kwargs = {k: process_arg_fn(a) for k, a in kwargs.items()}
|
| 2268 |
+
return CallFunction(target, *args, **kwargs)
|
| 2269 |
+
|
| 2270 |
+
def run_node(self, n: torch.fx.Node) -> Any:
|
| 2271 |
+
rv = super().run_node(n)
|
| 2272 |
+
if n.op == "output" and isinstance(rv, tuple):
|
| 2273 |
+
args = n.args[0]
|
| 2274 |
+
assert isinstance(args, Collection)
|
| 2275 |
+
assert len(rv) == len(args)
|
| 2276 |
+
for r, arg in zip(rv, args):
|
| 2277 |
+
# pyrefly: ignore [missing-attribute]
|
| 2278 |
+
r.users = len(arg.users)
|
| 2279 |
+
else:
|
| 2280 |
+
rv.users = len(n.users)
|
| 2281 |
+
return rv
|
| 2282 |
+
|
| 2283 |
+
assert isinstance(gm, torch.fx.GraphModule)
|
| 2284 |
+
pattern = Converter(gm).run()
|
| 2285 |
+
if not isinstance(pattern, PatternExpr):
|
| 2286 |
+
return MultiOutputPattern(pytree.tree_leaves(pattern))
|
| 2287 |
+
return pattern
|
| 2288 |
+
|
| 2289 |
+
|
| 2290 |
+
@torch.no_grad()
|
| 2291 |
+
def fwd_only(
|
| 2292 |
+
fn: Callable[..., Any],
|
| 2293 |
+
args: Sequence[Any],
|
| 2294 |
+
*,
|
| 2295 |
+
run_functional_passes: bool = True,
|
| 2296 |
+
get_decomp_fn: Callable[..., Any] = select_decomp_table,
|
| 2297 |
+
) -> torch.fx.GraphModule:
|
| 2298 |
+
"""Build a normalized inference graph, for use with fx_to_pattern"""
|
| 2299 |
+
# TODO - look into using aot autograd, asserting no mutating ops here
|
| 2300 |
+
with enable_python_dispatcher(), preserve_node_meta():
|
| 2301 |
+
gm = make_fx(fn, get_decomp_fn(), tracing_mode="real")(*args)
|
| 2302 |
+
|
| 2303 |
+
from .fx_passes.post_grad import remove_noop_ops
|
| 2304 |
+
|
| 2305 |
+
if run_functional_passes:
|
| 2306 |
+
remove_noop_ops(gm.graph)
|
| 2307 |
+
|
| 2308 |
+
# NOTE: applying early_patterns to user patterns cause
|
| 2309 |
+
# duplicate patterns being found in vllm. Check
|
| 2310 |
+
# https://github.com/pytorch/pytorch/pull/170649#issuecomment-3693427775
|
| 2311 |
+
# for more details.
|
| 2312 |
+
# from .fx_passes.joint_graph import early_patterns
|
| 2313 |
+
# early_patterns.apply(gm.graph)
|
| 2314 |
+
|
| 2315 |
+
gm.graph.eliminate_dead_code()
|
| 2316 |
+
|
| 2317 |
+
gm.recompile()
|
| 2318 |
+
return gm
|
| 2319 |
+
|
| 2320 |
+
|
| 2321 |
+
@torch.enable_grad()
|
| 2322 |
+
def joint_fwd_bwd(
|
| 2323 |
+
fn: Callable[..., Any],
|
| 2324 |
+
args: Sequence[Any],
|
| 2325 |
+
*,
|
| 2326 |
+
get_decomp_fn: Callable[..., Any] = select_decomp_table,
|
| 2327 |
+
) -> torch.fx.GraphModule:
|
| 2328 |
+
"""Build a normalized training graph, for use with fx_to_pattern"""
|
| 2329 |
+
gm: torch.fx.GraphModule | None = None
|
| 2330 |
+
|
| 2331 |
+
def record_joint_graph(
|
| 2332 |
+
joint_graph: torch.fx.GraphModule, inputs: Sequence[Any], **kwargs: Any
|
| 2333 |
+
) -> tuple[torch.fx.GraphModule, torch.fx.GraphModule]:
|
| 2334 |
+
nonlocal gm
|
| 2335 |
+
assert not gm
|
| 2336 |
+
gm = clone_graph(joint_graph)
|
| 2337 |
+
return default_partition(joint_graph, inputs, **kwargs)
|
| 2338 |
+
|
| 2339 |
+
with torch._guards.tracing(None):
|
| 2340 |
+
aot_function(
|
| 2341 |
+
fn,
|
| 2342 |
+
# pyrefly: ignore[bad-argument-type]
|
| 2343 |
+
lambda gm, example_inputs: make_boxed_func(gm),
|
| 2344 |
+
partition_fn=record_joint_graph,
|
| 2345 |
+
decompositions=get_decomp_fn(),
|
| 2346 |
+
keep_inference_input_mutations=True,
|
| 2347 |
+
enable_log=False,
|
| 2348 |
+
)(*args)
|
| 2349 |
+
assert gm
|
| 2350 |
+
|
| 2351 |
+
from .fx_passes.post_grad import remove_noop_ops
|
| 2352 |
+
|
| 2353 |
+
remove_noop_ops(gm.graph)
|
| 2354 |
+
|
| 2355 |
+
from .fx_passes.joint_graph import early_patterns
|
| 2356 |
+
|
| 2357 |
+
early_patterns.apply(gm.graph)
|
| 2358 |
+
|
| 2359 |
+
# remove in/out specs
|
| 2360 |
+
gm.graph._codegen = torch.fx.graph.CodeGen()
|
| 2361 |
+
gm.graph.eliminate_dead_code()
|
| 2362 |
+
gm.recompile()
|
| 2363 |
+
return gm
|
| 2364 |
+
|
| 2365 |
+
|
| 2366 |
+
def _args(n: torch.fx.Node) -> list[torch.fx.node.Argument]:
|
| 2367 |
+
args: list[torch.fx.node.Argument] = []
|
| 2368 |
+
torch.fx.map_arg((n.args, n.kwargs), args.append)
|
| 2369 |
+
return args
|
| 2370 |
+
|
| 2371 |
+
|
| 2372 |
+
def stable_topological_sort(graph: torch.fx.Graph) -> None:
|
| 2373 |
+
# Nodes are in exactly one of these three collections:
|
| 2374 |
+
|
| 2375 |
+
# - Nodes in `pending` are waiting to be processed (in reverse order):
|
| 2376 |
+
pending = list(reversed(graph.nodes))
|
| 2377 |
+
|
| 2378 |
+
# - Nodes in `ready` have been processed and are already in the correct
|
| 2379 |
+
# order.
|
| 2380 |
+
ready = OrderedSet[torch.fx.Node]()
|
| 2381 |
+
|
| 2382 |
+
# - `waiting` is a mapping from a dependency to nodes which depend on that
|
| 2383 |
+
# dependency.
|
| 2384 |
+
waiting = defaultdict(list)
|
| 2385 |
+
|
| 2386 |
+
# The cursor indicates the last processed node so we can add new nodes
|
| 2387 |
+
# after it.
|
| 2388 |
+
cursor = None
|
| 2389 |
+
while pending:
|
| 2390 |
+
node = pending.pop()
|
| 2391 |
+
waiting_for = [x for x in _args(node) if x not in ready]
|
| 2392 |
+
if waiting_for:
|
| 2393 |
+
# We have unprocessed input nodes. Might as well wait for the last
|
| 2394 |
+
# arg so an already sorted list will only recheck this node once.
|
| 2395 |
+
waiting[waiting_for[-1]].append(node)
|
| 2396 |
+
else:
|
| 2397 |
+
ready.add(node)
|
| 2398 |
+
if cursor and cursor.next is not node:
|
| 2399 |
+
cursor.append(node)
|
| 2400 |
+
cursor = node
|
| 2401 |
+
# Mark the nodes that have been waiting for this node to finish as
|
| 2402 |
+
# ready to check again.
|
| 2403 |
+
pending.extend(reversed(waiting.pop(node, ())))
|
| 2404 |
+
|
| 2405 |
+
assert not waiting and len(ready) == len(graph.nodes)
|
| 2406 |
+
|
| 2407 |
+
|
| 2408 |
+
def init_once_fakemode(fn: Callable[..., Any]) -> Callable[..., Any]:
|
| 2409 |
+
"""Wrapper around lazy init functions in fx_passes/"""
|
| 2410 |
+
|
| 2411 |
+
_fn_params = inspect.signature(fn).parameters
|
| 2412 |
+
|
| 2413 |
+
@functools.cache
|
| 2414 |
+
@functools.wraps(fn)
|
| 2415 |
+
def lazy_init(
|
| 2416 |
+
input_device: Any | None = None,
|
| 2417 |
+
get_decomp_fn: Callable[
|
| 2418 |
+
..., dict[Any, Callable[..., Any]]
|
| 2419 |
+
] = select_decomp_table,
|
| 2420 |
+
) -> Any:
|
| 2421 |
+
counters_ref = counters[backend].copy()
|
| 2422 |
+
|
| 2423 |
+
kwargs: dict[str, Any] = {}
|
| 2424 |
+
if "input_device" in _fn_params:
|
| 2425 |
+
kwargs["input_device"] = input_device
|
| 2426 |
+
if "get_decomp_fn" in _fn_params:
|
| 2427 |
+
kwargs["get_decomp_fn"] = get_decomp_fn
|
| 2428 |
+
|
| 2429 |
+
with torch._guards.tracing(None), unset_fake_temporarily(), FakeTensorMode():
|
| 2430 |
+
result = fn(**kwargs)
|
| 2431 |
+
|
| 2432 |
+
# clear view matches encountered during tracing
|
| 2433 |
+
counters[backend] = counters_ref
|
| 2434 |
+
|
| 2435 |
+
return result
|
| 2436 |
+
|
| 2437 |
+
return lazy_init
|
| 2438 |
+
|
| 2439 |
+
|
| 2440 |
+
def config_flag(name: str) -> Callable[[Match], Any]:
|
| 2441 |
+
"""Function for extra_check to put pass behind a flag"""
|
| 2442 |
+
|
| 2443 |
+
def flag_check(match: Match) -> Any:
|
| 2444 |
+
return getattr(config, name)
|
| 2445 |
+
|
| 2446 |
+
return flag_check
|
| 2447 |
+
|
| 2448 |
+
|
| 2449 |
+
def clone_graph(input_graph: torch.fx.GraphModule) -> torch.fx.GraphModule:
|
| 2450 |
+
class CopyGraph(Transformer):
|
| 2451 |
+
def run_node(self, old_node: torch.fx.Node) -> torch.fx.Node:
|
| 2452 |
+
new_node = super().run_node(old_node)
|
| 2453 |
+
if isinstance(new_node, torch.fx.Proxy):
|
| 2454 |
+
new_node.node.meta.update(old_node.meta)
|
| 2455 |
+
new_node.node.name = self.new_graph._graph_namespace.create_name(
|
| 2456 |
+
old_node.name, None
|
| 2457 |
+
)
|
| 2458 |
+
|
| 2459 |
+
return new_node
|
| 2460 |
+
|
| 2461 |
+
return CopyGraph(input_graph).transform()
|
| 2462 |
+
|
| 2463 |
+
|
| 2464 |
+
# TODO: remove in follow up diff, used internally
|
| 2465 |
+
_seen_patterns: OrderedSet[str] = OrderedSet()
|
| 2466 |
+
|
| 2467 |
+
|
| 2468 |
+
def get_arg_value(
|
| 2469 |
+
node: torch.fx.Node, arg_number: int, kwarg_name: str | None = None
|
| 2470 |
+
) -> Any:
|
| 2471 |
+
if len(node.args) > arg_number:
|
| 2472 |
+
return node.args[arg_number]
|
| 2473 |
+
elif kwarg_name is None:
|
| 2474 |
+
return None
|
| 2475 |
+
else:
|
| 2476 |
+
return node.kwargs.get(kwarg_name)
|
| 2477 |
+
|
| 2478 |
+
|
| 2479 |
+
def filter_nodes(nodes: Iterable[torch.fx.Node], fn: Any) -> list[torch.fx.Node]:
|
| 2480 |
+
fns = [fn]
|
| 2481 |
+
if isinstance(fn, torch._ops.OpOverloadPacket):
|
| 2482 |
+
fns.extend([getattr(fn, overload) for overload in fn.overloads()])
|
| 2483 |
+
|
| 2484 |
+
return [node for node in nodes if node.target in fns]
|
| 2485 |
+
|
| 2486 |
+
|
| 2487 |
+
def extract_target(node: torch.fx.Node) -> torch.fx.node.Target:
|
| 2488 |
+
"""For call_function and call_method, we directly use the target function;
|
| 2489 |
+
For call_module, the target is string, and we treat the module class
|
| 2490 |
+
as a function.
|
| 2491 |
+
"""
|
| 2492 |
+
if node.op == "call_module":
|
| 2493 |
+
assert isinstance(node.target, str)
|
| 2494 |
+
return _get_attr(node.graph.owning_module, node.target).__class__
|
| 2495 |
+
return node.target
|
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/quantized_lowerings.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from typing import Any
|
| 3 |
+
|
| 4 |
+
import torch
|
| 5 |
+
from torch._inductor.kernel.mm_common import mm_args
|
| 6 |
+
|
| 7 |
+
from . import config, lowering
|
| 8 |
+
from .codegen.cpp_gemm_template import CppGemmTemplate, CppWoqInt4GemmTemplate
|
| 9 |
+
from .codegen.cpp_utils import create_epilogue_with_attr
|
| 10 |
+
from .lowering import expand, register_lowering
|
| 11 |
+
from .mkldnn_ir import WeightInt4PackMatmul
|
| 12 |
+
from .select_algorithm import (
|
| 13 |
+
autotune_select_algorithm,
|
| 14 |
+
ExternKernelChoice,
|
| 15 |
+
realize_inputs,
|
| 16 |
+
)
|
| 17 |
+
from .utils import use_aten_gemm_kernels, use_cpp_gemm_template
|
| 18 |
+
from .virtualized import V
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
log = logging.getLogger(__name__)
|
| 22 |
+
|
| 23 |
+
aten__weight_int8pack_mm = ExternKernelChoice(
|
| 24 |
+
torch._weight_int8pack_mm, "at::_weight_int8pack_mm", has_out_variant=False
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
aten__weight_int4pack_mm_cpu = ExternKernelChoice(
|
| 28 |
+
torch.ops.quantized.int4mm_packed_weight_cpu,
|
| 29 |
+
"at::native::_weight_int4pack_mm_cpu_tensor",
|
| 30 |
+
has_out_variant=False,
|
| 31 |
+
kernel_creator=WeightInt4PackMatmul.create,
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
quantized = torch.ops.quantized
|
| 35 |
+
_quantized = torch.ops._quantized
|
| 36 |
+
aten = torch.ops.aten
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def register_quantized_ops() -> None:
|
| 40 |
+
lowering.add_needs_realized_inputs(
|
| 41 |
+
[
|
| 42 |
+
quantized.max_pool2d,
|
| 43 |
+
_quantized.wrapped_fbgemm_pack_gemm_matrix_fp16,
|
| 44 |
+
_quantized.wrapped_fbgemm_linear_fp16_weight,
|
| 45 |
+
]
|
| 46 |
+
)
|
| 47 |
+
lowering.make_fallback(quantized.max_pool2d)
|
| 48 |
+
lowering.make_fallback(_quantized.wrapped_fbgemm_pack_gemm_matrix_fp16)
|
| 49 |
+
lowering.make_fallback(_quantized.wrapped_fbgemm_linear_fp16_weight)
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def register_woq_mm_ops() -> None:
|
| 53 |
+
@register_lowering(aten._weight_int8pack_mm, type_promotion_kind=None) # type: ignore[misc]
|
| 54 |
+
def int8pack_mm(
|
| 55 |
+
input: torch.Tensor,
|
| 56 |
+
weight: torch.Tensor,
|
| 57 |
+
scale: torch.Tensor,
|
| 58 |
+
*,
|
| 59 |
+
layout: Any = None,
|
| 60 |
+
) -> Any:
|
| 61 |
+
_, _, _, layout, mat1, mat2 = mm_args(
|
| 62 |
+
input, weight, layout=layout, mat2_transposed=True
|
| 63 |
+
)
|
| 64 |
+
assert (
|
| 65 |
+
mat1.get_dtype() in [torch.bfloat16, torch.float16, torch.float]
|
| 66 |
+
and mat2.get_dtype() == torch.int8
|
| 67 |
+
)
|
| 68 |
+
aten_layout = layout
|
| 69 |
+
|
| 70 |
+
# options to tune from
|
| 71 |
+
choices = (
|
| 72 |
+
[aten__weight_int8pack_mm.bind((mat1, mat2, scale), aten_layout)]
|
| 73 |
+
if use_aten_gemm_kernels()
|
| 74 |
+
else []
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
# scale is applied as an epilogue, and the scale tensor is expanded (with a view op)
|
| 78 |
+
# for broadcasting, as it's 1D.
|
| 79 |
+
def _mul_epilogue(buf: torch.Tensor) -> Any:
|
| 80 |
+
return create_epilogue_with_attr(
|
| 81 |
+
buf, "mul", other=realize_inputs(expand(scale, layout.size))
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
if use_cpp_gemm_template(aten_layout, mat1, mat2, mat2_transposed=True):
|
| 85 |
+
CppGemmTemplate.add_choices(
|
| 86 |
+
choices,
|
| 87 |
+
aten_layout,
|
| 88 |
+
[mat1, mat2, scale],
|
| 89 |
+
trans_w=True,
|
| 90 |
+
epilogue_creator=_mul_epilogue, # type: ignore[arg-type]
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
node, _ = autotune_select_algorithm(
|
| 94 |
+
"_weight_int8pack_mm", choices, [mat1, mat2, scale], aten_layout
|
| 95 |
+
)
|
| 96 |
+
return node
|
| 97 |
+
|
| 98 |
+
@register_lowering(aten._weight_int4pack_mm_for_cpu, type_promotion_kind=None) # type: ignore[misc]
|
| 99 |
+
def int4pack_mm_cpu(
|
| 100 |
+
input: torch.Tensor,
|
| 101 |
+
weight: torch.Tensor,
|
| 102 |
+
qGroupSize: int,
|
| 103 |
+
qScaleAndZeros: torch.Tensor,
|
| 104 |
+
*,
|
| 105 |
+
layout: Any = None,
|
| 106 |
+
) -> Any:
|
| 107 |
+
_, _, _, layout, mat1, mat2 = mm_args(
|
| 108 |
+
input, weight, layout=layout, use_4x2_dim=True, mat2_transposed=True
|
| 109 |
+
)
|
| 110 |
+
assert (
|
| 111 |
+
mat1.get_dtype() in [torch.bfloat16, torch.float16, torch.float]
|
| 112 |
+
and mat2.get_dtype() == torch.uint8
|
| 113 |
+
)
|
| 114 |
+
group_size = V.graph.add_tensor_constant(
|
| 115 |
+
torch.tensor(qGroupSize, dtype=torch.int64), name=None
|
| 116 |
+
)
|
| 117 |
+
aten_layout = layout
|
| 118 |
+
|
| 119 |
+
# options to tune from
|
| 120 |
+
choices = (
|
| 121 |
+
[
|
| 122 |
+
aten__weight_int4pack_mm_cpu.bind(
|
| 123 |
+
(mat1, mat2, group_size, qScaleAndZeros), aten_layout
|
| 124 |
+
)
|
| 125 |
+
]
|
| 126 |
+
if use_aten_gemm_kernels()
|
| 127 |
+
else []
|
| 128 |
+
)
|
| 129 |
+
if (
|
| 130 |
+
(config.max_autotune or config.max_autotune_gemm)
|
| 131 |
+
and use_cpp_gemm_template(
|
| 132 |
+
aten_layout,
|
| 133 |
+
mat1,
|
| 134 |
+
mat2,
|
| 135 |
+
mat2_transposed=True,
|
| 136 |
+
is_woq_int4=True,
|
| 137 |
+
q_group_size=qGroupSize,
|
| 138 |
+
)
|
| 139 |
+
and mat2.get_layout().is_contiguous()
|
| 140 |
+
):
|
| 141 |
+
# pyrefly: ignore [bad-specialization, missing-attribute, not-a-type]
|
| 142 |
+
CppWoqInt4GemmTemplate[qGroupSize].add_choices(
|
| 143 |
+
choices,
|
| 144 |
+
aten_layout,
|
| 145 |
+
[mat1, mat2, group_size, qScaleAndZeros],
|
| 146 |
+
)
|
| 147 |
+
|
| 148 |
+
# define functions to generate example inputs for weight and group size
|
| 149 |
+
# otherwise, autotuner generates example inputs of all zeros for them
|
| 150 |
+
def get_example_weight(x: torch._inductor.ir.IRNode) -> torch.Tensor:
|
| 151 |
+
assert x.get_layout().is_contiguous()
|
| 152 |
+
shape = x.get_size()
|
| 153 |
+
device = x.get_device()
|
| 154 |
+
return torch.randint(0, 255, shape, dtype=torch.uint8, device=device)
|
| 155 |
+
|
| 156 |
+
input_gen_fns = {
|
| 157 |
+
1: get_example_weight, # packed weight
|
| 158 |
+
2: lambda x: V.graph.constants[x.get_name()], # group size
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
node, _ = autotune_select_algorithm(
|
| 162 |
+
"_weight_int4pack_mm_for_cpu",
|
| 163 |
+
choices,
|
| 164 |
+
[mat1, mat2, group_size, qScaleAndZeros],
|
| 165 |
+
aten_layout,
|
| 166 |
+
input_gen_fns=input_gen_fns,
|
| 167 |
+
)
|
| 168 |
+
return node
|
| 169 |
+
|
| 170 |
+
lowering.make_fallback(aten._dyn_quant_matmul_4bit)
|
| 171 |
+
lowering.make_fallback(aten._dyn_quant_pack_4bit_weight)
|
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/remote_cache.py
ADDED
|
@@ -0,0 +1,432 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import atexit
|
| 4 |
+
import collections
|
| 5 |
+
import dataclasses
|
| 6 |
+
import functools
|
| 7 |
+
import json
|
| 8 |
+
import logging
|
| 9 |
+
import os
|
| 10 |
+
import sys
|
| 11 |
+
import typing
|
| 12 |
+
from abc import abstractmethod
|
| 13 |
+
from typing import Any, Generic, TypeAlias, TypeVar, Union
|
| 14 |
+
from typing_extensions import override
|
| 15 |
+
|
| 16 |
+
from torch._dynamo.utils import dynamo_timed
|
| 17 |
+
from torch._inductor import config
|
| 18 |
+
from torch.monitor import _WaitCounter
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
if typing.TYPE_CHECKING:
|
| 22 |
+
from collections.abc import Callable
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
try:
|
| 26 |
+
import redis
|
| 27 |
+
except ImportError:
|
| 28 |
+
redis = None # type: ignore[assignment]
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
log = logging.getLogger(__name__)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
if config.is_fbcode():
|
| 35 |
+
from rfe.scubadata.scubadata_py3 import ( # type: ignore[import-not-found]
|
| 36 |
+
Sample as Sample_,
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
Sample: TypeAlias = Sample_
|
| 40 |
+
else:
|
| 41 |
+
Sample: TypeAlias = type[object] # type: ignore[misc,no-redef]
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
_T = TypeVar("_T")
|
| 45 |
+
_U = TypeVar("_U")
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
remote_fx_cache_get_timed = functools.partial(
|
| 49 |
+
dynamo_timed,
|
| 50 |
+
"FbRemoteFxGraphCache.get",
|
| 51 |
+
phase_name="remote_fx_graph_cache_get",
|
| 52 |
+
log_pt2_compile_event=False,
|
| 53 |
+
dynamo_compile_column_us="remote_fx_graph_cache_get_time_us",
|
| 54 |
+
log_waitcounter=True,
|
| 55 |
+
)
|
| 56 |
+
remote_fx_cache_put_timed = functools.partial(
|
| 57 |
+
dynamo_timed,
|
| 58 |
+
"FbRemoteFxGraphCache.put",
|
| 59 |
+
phase_name="remote_fx_graph_cache_put",
|
| 60 |
+
log_pt2_compile_event=False,
|
| 61 |
+
dynamo_compile_column_us="remote_fx_graph_cache_put_time_us",
|
| 62 |
+
log_waitcounter=True,
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
class RemoteCacheBackend(Generic[_T]):
|
| 67 |
+
"""
|
| 68 |
+
A backend implementation for accessing a remote/distributed cache. Only
|
| 69 |
+
works with bytes in/out. For structured data use a RemoteCache.
|
| 70 |
+
"""
|
| 71 |
+
|
| 72 |
+
def __init__(self) -> None:
|
| 73 |
+
self._name = f"backend:{type(self).__name__}"
|
| 74 |
+
|
| 75 |
+
@abstractmethod
|
| 76 |
+
def _get(self, key: str) -> _T | None:
|
| 77 |
+
pass
|
| 78 |
+
|
| 79 |
+
@abstractmethod
|
| 80 |
+
def _put(self, key: str, data: _T) -> None:
|
| 81 |
+
pass
|
| 82 |
+
|
| 83 |
+
def get(self, key: str) -> _T | None:
|
| 84 |
+
try:
|
| 85 |
+
value = self._get(key)
|
| 86 |
+
cache_stats.get(self._name, value)
|
| 87 |
+
except Exception:
|
| 88 |
+
cache_stats.exception(self._name)
|
| 89 |
+
raise
|
| 90 |
+
return value
|
| 91 |
+
|
| 92 |
+
def put(self, key: str, data: _T) -> None:
|
| 93 |
+
try:
|
| 94 |
+
self._put(key, data)
|
| 95 |
+
cache_stats.put(self._name)
|
| 96 |
+
except Exception:
|
| 97 |
+
cache_stats.exception(self._name)
|
| 98 |
+
raise
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
# Serde that encodes from _T to _U and decodes from _U to _T.
|
| 102 |
+
class RemoteCacheSerde(Generic[_T, _U]):
|
| 103 |
+
@abstractmethod
|
| 104 |
+
def encode(self, data: _T) -> _U:
|
| 105 |
+
pass
|
| 106 |
+
|
| 107 |
+
@abstractmethod
|
| 108 |
+
def decode(self, data: _U) -> _T:
|
| 109 |
+
pass
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
JsonDataTy: TypeAlias = Union[ # noqa: UP007
|
| 113 |
+
int, float, str, bool, dict[str, "JsonDataTy"], list["JsonDataTy"], None
|
| 114 |
+
]
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
class RemoteCacheJsonSerde(RemoteCacheSerde[JsonDataTy, bytes]):
|
| 118 |
+
def encode(self, data: JsonDataTy) -> bytes:
|
| 119 |
+
return bytes(json.dumps(data), "ascii")
|
| 120 |
+
|
| 121 |
+
def decode(self, data: bytes) -> JsonDataTy:
|
| 122 |
+
return json.loads(data)
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
class RemoteCachePassthroughSerde(RemoteCacheSerde[_T, _T]):
|
| 126 |
+
def encode(self, data: _T) -> _T:
|
| 127 |
+
return data
|
| 128 |
+
|
| 129 |
+
def decode(self, data: _T) -> _T:
|
| 130 |
+
return data
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
# This class is the top of a RemoteCache. A RemoteCache is fundamentally made of
|
| 134 |
+
# three parts:
|
| 135 |
+
#
|
| 136 |
+
# 1. The controller (this class).
|
| 137 |
+
# 2. A serializer/deserializer (instance of RemoteCacheSerde).
|
| 138 |
+
# 3. A backend (instance of RemoteCacheBackend).
|
| 139 |
+
#
|
| 140 |
+
# To write (`put`), the RemoteCache takes data, uses the RemoteCacheSerde to
|
| 141 |
+
# convert it for the backend and passes it to the backend.
|
| 142 |
+
#
|
| 143 |
+
# Conversely when reading (`get`), the RemoteCache takes data from the backend,
|
| 144 |
+
# uses the RemoteCacheSerde to convert it and returns it.
|
| 145 |
+
#
|
| 146 |
+
# The RemoteCacheBackend is generic on _U - which is the type of data the
|
| 147 |
+
# backend can directly cache (usually `bytes`).
|
| 148 |
+
#
|
| 149 |
+
# The RemoteCacheSerde is responsible for converting between _T (the type of
|
| 150 |
+
# data the RemoteCache accepts in `put` and returns in `get`) and _U.
|
| 151 |
+
#
|
| 152 |
+
# When instantiating a RemoteCache you should override, not directly create a
|
| 153 |
+
# RemoteCache. The reason is that when logging cache use (`TORCH_LOGS=cache`) we
|
| 154 |
+
# use the concrete type of the RemoteCache as the reported cache. See
|
| 155 |
+
# RemoteFxGraphCache below as an example.
|
| 156 |
+
class RemoteCache(Generic[_T]):
|
| 157 |
+
backend_override_cls: Callable[[], RemoteCacheBackend[Any]] | None = None
|
| 158 |
+
|
| 159 |
+
def __init__(
|
| 160 |
+
self, backend: RemoteCacheBackend[_U], serde: RemoteCacheSerde[_T, _U]
|
| 161 |
+
) -> None:
|
| 162 |
+
# Support for testing to mock out the backend on a class-by-class basis.
|
| 163 |
+
if (override_cls := self.__class__.backend_override_cls) is not None:
|
| 164 |
+
self.backend = override_cls()
|
| 165 |
+
else:
|
| 166 |
+
self.backend = backend
|
| 167 |
+
# pyrefly: ignore [invalid-type-var]
|
| 168 |
+
self.serde = serde
|
| 169 |
+
|
| 170 |
+
# See if the cache contains `key`. Returns `None` if the value is not
|
| 171 |
+
# present in the cache.
|
| 172 |
+
def get(self, key: str) -> _T | None:
|
| 173 |
+
with _WaitCounter("pytorch.remote_cache.get").guard():
|
| 174 |
+
sample = self._create_sample()
|
| 175 |
+
try:
|
| 176 |
+
result = self._get(key, sample)
|
| 177 |
+
cache_stats.get(type(self).__name__, result)
|
| 178 |
+
except Exception as e:
|
| 179 |
+
cache_stats.exception(type(self).__name__)
|
| 180 |
+
if sample:
|
| 181 |
+
sample.fail_reason = str(e)
|
| 182 |
+
raise
|
| 183 |
+
finally:
|
| 184 |
+
self._log_sample(sample)
|
| 185 |
+
return result
|
| 186 |
+
|
| 187 |
+
# Add `value` to the cache with the key `key`. Note that `None` is not a
|
| 188 |
+
# valid value even if _T supports it (because you can't tell the difference
|
| 189 |
+
# between `None` and a missing cache entry).
|
| 190 |
+
def put(self, key: str, value: _T) -> None:
|
| 191 |
+
with _WaitCounter("pytorch.remote_cache.put").guard():
|
| 192 |
+
assert value is not None
|
| 193 |
+
sample = self._create_sample()
|
| 194 |
+
try:
|
| 195 |
+
self._put(key, value, sample)
|
| 196 |
+
cache_stats.put(type(self).__name__)
|
| 197 |
+
except Exception as e:
|
| 198 |
+
cache_stats.exception(type(self).__name__)
|
| 199 |
+
if sample:
|
| 200 |
+
sample.fail_reason = str(e)
|
| 201 |
+
raise
|
| 202 |
+
finally:
|
| 203 |
+
self._log_sample(sample)
|
| 204 |
+
|
| 205 |
+
# Used to convert data from the cache into structured data.
|
| 206 |
+
def _decode(self, data: _U, sample: Sample | None) -> _T: # type: ignore[override]
|
| 207 |
+
return self.serde.decode(data) # type: ignore[arg-type]
|
| 208 |
+
|
| 209 |
+
# Used to convert structured data into data for the cache.
|
| 210 |
+
def _encode(self, value: _T, sample: Sample | None) -> object: # returns _U
|
| 211 |
+
return self.serde.encode(value)
|
| 212 |
+
|
| 213 |
+
# Get structured data from the cache.
|
| 214 |
+
# Separate from `get` so that it can be overridden.
|
| 215 |
+
def _get(self, key: str, sample: Sample | None) -> _T | None:
|
| 216 |
+
if data := self._backend_get(key):
|
| 217 |
+
return self._decode(data, sample)
|
| 218 |
+
return None
|
| 219 |
+
|
| 220 |
+
# Get unstructured data from the cache.
|
| 221 |
+
# Separate from `get` so that it can be overridden.
|
| 222 |
+
# Returns _U - but we aren't actually generic on _U
|
| 223 |
+
def _backend_get(self, key: str) -> object:
|
| 224 |
+
return self.backend.get(key)
|
| 225 |
+
|
| 226 |
+
# Put structured data into the cache.
|
| 227 |
+
# Separate from `put` so that it can be overridden.
|
| 228 |
+
def _put(self, key: str, value: _T, sample: Sample | None) -> None:
|
| 229 |
+
data = self._encode(value, sample)
|
| 230 |
+
self._backend_put(key, data)
|
| 231 |
+
|
| 232 |
+
# Put unstructured data into the cache.
|
| 233 |
+
# Separate from `put` so that it can be overridden.
|
| 234 |
+
# Takes data: _U - but we aren't actually generic on _U
|
| 235 |
+
def _backend_put(self, key: str, data: object) -> None:
|
| 236 |
+
self.backend.put(key, data)
|
| 237 |
+
|
| 238 |
+
# Create a logging Sample - used with internal loggers to monitor cache
|
| 239 |
+
# effectiveness.
|
| 240 |
+
def _create_sample(self) -> Sample | None:
|
| 241 |
+
return None
|
| 242 |
+
|
| 243 |
+
# Write the logging Sample to the logger.
|
| 244 |
+
def _log_sample(self, sample: Sample | None) -> None:
|
| 245 |
+
pass
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
class RedisRemoteCacheBackend(RemoteCacheBackend[bytes]):
|
| 249 |
+
"""
|
| 250 |
+
A Redis implementation of a remote/distributed cache.
|
| 251 |
+
"""
|
| 252 |
+
|
| 253 |
+
# pyrefly: ignore [missing-attribute]
|
| 254 |
+
_redis: redis.Redis | None = None
|
| 255 |
+
|
| 256 |
+
def __init__(self, cache_id: str) -> None:
|
| 257 |
+
super().__init__()
|
| 258 |
+
if not redis:
|
| 259 |
+
raise RuntimeError("redis not available but required for remote cache")
|
| 260 |
+
|
| 261 |
+
if "TORCHINDUCTOR_REDIS_URL" in os.environ:
|
| 262 |
+
self._redis = redis.Redis.from_url(os.environ["TORCHINDUCTOR_REDIS_URL"])
|
| 263 |
+
else:
|
| 264 |
+
self._redis = redis.Redis(
|
| 265 |
+
host=os.environ.get("TORCHINDUCTOR_REDIS_HOST", "localhost"),
|
| 266 |
+
port=int(os.environ.get("TORCHINDUCTOR_REDIS_PORT", 6379)),
|
| 267 |
+
)
|
| 268 |
+
|
| 269 |
+
@override
|
| 270 |
+
def _get(self, key: str) -> bytes | None:
|
| 271 |
+
if not self._redis:
|
| 272 |
+
# Either redis wasn't found or we already had some trouble...
|
| 273 |
+
return None
|
| 274 |
+
|
| 275 |
+
try:
|
| 276 |
+
# pyrefly: ignore [missing-attribute]
|
| 277 |
+
value = self._redis.get(key)
|
| 278 |
+
# pyrefly: ignore [missing-attribute]
|
| 279 |
+
except redis.exceptions.ConnectionError:
|
| 280 |
+
# Redis is lazy and doesn't actually attempt to connect until the
|
| 281 |
+
# first use. Mark is as unavailable now.
|
| 282 |
+
self._redis = None
|
| 283 |
+
return None
|
| 284 |
+
|
| 285 |
+
# In theory redis.get() can return an Awaitable as well...
|
| 286 |
+
assert value is None or isinstance(value, bytes)
|
| 287 |
+
return value
|
| 288 |
+
|
| 289 |
+
@override
|
| 290 |
+
def _put(self, key: str, data: bytes) -> None:
|
| 291 |
+
if not self._redis:
|
| 292 |
+
# Either redis wasn't found or we already had some trouble...
|
| 293 |
+
return
|
| 294 |
+
|
| 295 |
+
try:
|
| 296 |
+
# pyrefly: ignore [missing-attribute]
|
| 297 |
+
self._redis.set(key, data)
|
| 298 |
+
# pyrefly: ignore [missing-attribute]
|
| 299 |
+
except redis.exceptions.ConnectionError:
|
| 300 |
+
# Redis is lazy and doesn't actually attempt to connect until the
|
| 301 |
+
# first use. Mark is as unavailable now.
|
| 302 |
+
self._redis = None
|
| 303 |
+
|
| 304 |
+
|
| 305 |
+
class RedisRemoteCache(RemoteCache[JsonDataTy]):
|
| 306 |
+
def __init__(self, cache_id: str) -> None:
|
| 307 |
+
# Special test handling: If we're just going to override the backend
|
| 308 |
+
# anyway don't require redis
|
| 309 |
+
if self.__class__.backend_override_cls:
|
| 310 |
+
# This is totally bogus but it works for now...
|
| 311 |
+
backend = typing.cast(RemoteCacheBackend[bytes], None)
|
| 312 |
+
else:
|
| 313 |
+
backend = RedisRemoteCacheBackend(cache_id)
|
| 314 |
+
serde = RemoteCacheJsonSerde()
|
| 315 |
+
super().__init__(backend, serde)
|
| 316 |
+
version = 1 # consistency between various types of keys
|
| 317 |
+
self._key_fmt = f"pt2:{cache_id}::{{key}}:c{version}"
|
| 318 |
+
|
| 319 |
+
def _get_key(self, key: str) -> str:
|
| 320 |
+
return self._key_fmt.format(key=key)
|
| 321 |
+
|
| 322 |
+
@override
|
| 323 |
+
def _get(self, key: str, sample: Sample | None) -> JsonDataTy | None:
|
| 324 |
+
key = self._get_key(key)
|
| 325 |
+
return super()._get(key, sample)
|
| 326 |
+
|
| 327 |
+
@override
|
| 328 |
+
def _put(self, key: str, value: JsonDataTy, sample: Sample | None) -> None:
|
| 329 |
+
key = self._get_key(key)
|
| 330 |
+
super()._put(key, value, sample)
|
| 331 |
+
|
| 332 |
+
|
| 333 |
+
class RemoteAutotuneCache(RedisRemoteCache):
|
| 334 |
+
pass
|
| 335 |
+
|
| 336 |
+
|
| 337 |
+
class RemoteBundledAutotuneCache(RedisRemoteCache):
|
| 338 |
+
pass
|
| 339 |
+
|
| 340 |
+
|
| 341 |
+
class RemoteFxGraphCache(RedisRemoteCache):
|
| 342 |
+
pass
|
| 343 |
+
|
| 344 |
+
|
| 345 |
+
class RemoteAOTAutogradCache(RedisRemoteCache):
|
| 346 |
+
pass
|
| 347 |
+
|
| 348 |
+
|
| 349 |
+
class RemoteDynamoPGOCache(RedisRemoteCache):
|
| 350 |
+
pass
|
| 351 |
+
|
| 352 |
+
|
| 353 |
+
def create_cache(
|
| 354 |
+
key: str,
|
| 355 |
+
is_fbcode: bool,
|
| 356 |
+
fb_cache_cls: str,
|
| 357 |
+
oss_cache_cls: str,
|
| 358 |
+
) -> RemoteCache[JsonDataTy] | None:
|
| 359 |
+
try:
|
| 360 |
+
if is_fbcode:
|
| 361 |
+
import torch._inductor.fb.remote_cache
|
| 362 |
+
|
| 363 |
+
cache_cls = getattr(torch._inductor.fb.remote_cache, fb_cache_cls)
|
| 364 |
+
return cache_cls(key)
|
| 365 |
+
else:
|
| 366 |
+
this_module = sys.modules[__name__]
|
| 367 |
+
|
| 368 |
+
cache_cls = getattr(this_module, oss_cache_cls)
|
| 369 |
+
return cache_cls(key)
|
| 370 |
+
|
| 371 |
+
except Exception:
|
| 372 |
+
log.warning("Unable to create a remote cache", exc_info=True)
|
| 373 |
+
return None
|
| 374 |
+
|
| 375 |
+
|
| 376 |
+
# Some simple stat capture
|
| 377 |
+
@dataclasses.dataclass
|
| 378 |
+
class _CacheStat:
|
| 379 |
+
miss: int = 0
|
| 380 |
+
hit: int = 0
|
| 381 |
+
put: int = 0
|
| 382 |
+
exception: int = 0
|
| 383 |
+
|
| 384 |
+
def __str__(self) -> str:
|
| 385 |
+
return f"{{hit: {self.hit}, miss: {self.miss}, put: {self.put}, exception: {self.exception}}}"
|
| 386 |
+
|
| 387 |
+
|
| 388 |
+
class _CacheStats:
|
| 389 |
+
_stats: dict[str, _CacheStat]
|
| 390 |
+
|
| 391 |
+
def __init__(self) -> None:
|
| 392 |
+
self._stats = collections.defaultdict(_CacheStat)
|
| 393 |
+
|
| 394 |
+
def miss(self, name: str, count: int = 1) -> None:
|
| 395 |
+
self._stats[name].miss += count
|
| 396 |
+
|
| 397 |
+
def hit(self, name: str, count: int = 1) -> None:
|
| 398 |
+
self._stats[name].hit += count
|
| 399 |
+
|
| 400 |
+
def get(self, name: str, value: object | None) -> None:
|
| 401 |
+
if value is None:
|
| 402 |
+
self.miss(name)
|
| 403 |
+
else:
|
| 404 |
+
self.hit(name)
|
| 405 |
+
|
| 406 |
+
def put(self, name: str, count: int = 1) -> None:
|
| 407 |
+
self._stats[name].put += count
|
| 408 |
+
|
| 409 |
+
def exception(self, name: str, count: int = 1) -> None:
|
| 410 |
+
self._stats[name].exception += count
|
| 411 |
+
|
| 412 |
+
|
| 413 |
+
cache_stats = _CacheStats()
|
| 414 |
+
|
| 415 |
+
|
| 416 |
+
@atexit.register
|
| 417 |
+
def dump_cache_stats() -> None:
|
| 418 |
+
if not log.isEnabledFor(logging.INFO):
|
| 419 |
+
return
|
| 420 |
+
|
| 421 |
+
import io
|
| 422 |
+
|
| 423 |
+
out = io.StringIO()
|
| 424 |
+
|
| 425 |
+
if not cache_stats._stats:
|
| 426 |
+
print(" None", file=out)
|
| 427 |
+
else:
|
| 428 |
+
print(file=out)
|
| 429 |
+
for k, v in sorted(cache_stats._stats.items()):
|
| 430 |
+
print(f" {k}: {v}", file=out)
|
| 431 |
+
|
| 432 |
+
log.info("Cache Metrics:%s", out.getvalue())
|
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/remote_gemm_autotune_cache.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
from typing import TypeVar
|
| 3 |
+
|
| 4 |
+
import torch._inductor.config as config
|
| 5 |
+
from torch._inductor import ir
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
_T = TypeVar("_T")
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def gen_best_config(mat1: ir.StorageBox, mat2: ir.StorageBox) -> asyncio.Task[_T]:
|
| 12 |
+
"""
|
| 13 |
+
Generate the best GEMM autotune config for the given matrices.
|
| 14 |
+
"""
|
| 15 |
+
if config.is_fbcode():
|
| 16 |
+
from torch._inductor.fb.remote_gemm_autotune_cache import gen_best_config
|
| 17 |
+
|
| 18 |
+
return gen_best_config(mat1, mat2)
|
| 19 |
+
else:
|
| 20 |
+
raise NotImplementedError("Function gen_best_config is not yet implemented")
|
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/rocm_multiarch_utils.py
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
ROCm Multi-Architecture Support Utilities
|
| 3 |
+
Compile LLVM IR to multi-arch bundles that HIP can load automatically.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import logging
|
| 7 |
+
import os
|
| 8 |
+
import re
|
| 9 |
+
import subprocess
|
| 10 |
+
|
| 11 |
+
import torch
|
| 12 |
+
from torch.utils.cpp_extension import _join_rocm_home, ROCM_HOME
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
log = logging.getLogger(__name__)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def get_rocm_compiler() -> str:
|
| 19 |
+
"""
|
| 20 |
+
Get path to ROCm's clang compiler.
|
| 21 |
+
Uses PyTorch's ROCM_HOME detection.
|
| 22 |
+
|
| 23 |
+
Returns:
|
| 24 |
+
Path to clang compiler
|
| 25 |
+
|
| 26 |
+
Raises:
|
| 27 |
+
RuntimeError: If ROCm is not found
|
| 28 |
+
"""
|
| 29 |
+
if ROCM_HOME is None:
|
| 30 |
+
raise RuntimeError(
|
| 31 |
+
"ROCm installation not found. "
|
| 32 |
+
"PyTorch was not built with ROCm support or ROCM_HOME is not set."
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
# ROCm's clang is at <ROCM_HOME>/llvm/bin/clang
|
| 36 |
+
clang_path = _join_rocm_home("llvm", "bin", "clang")
|
| 37 |
+
|
| 38 |
+
if not os.path.exists(clang_path):
|
| 39 |
+
raise RuntimeError(
|
| 40 |
+
f"ROCm clang not found at {clang_path}. ROCM_HOME is set to {ROCM_HOME}"
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
return clang_path
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def get_rocm_bundler() -> str:
|
| 47 |
+
"""
|
| 48 |
+
Get path to clang-offload-bundler.
|
| 49 |
+
Uses PyTorch's ROCM_HOME detection.
|
| 50 |
+
|
| 51 |
+
Returns:
|
| 52 |
+
Path to bundler
|
| 53 |
+
|
| 54 |
+
Raises:
|
| 55 |
+
RuntimeError: If bundler is not found
|
| 56 |
+
"""
|
| 57 |
+
if ROCM_HOME is None:
|
| 58 |
+
raise RuntimeError(
|
| 59 |
+
"ROCm installation not found. "
|
| 60 |
+
"PyTorch was not built with ROCm support or ROCM_HOME is not set."
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
# Bundler is at <ROCM_HOME>/llvm/bin/clang-offload-bundler
|
| 64 |
+
bundler_path = _join_rocm_home("llvm", "bin", "clang-offload-bundler")
|
| 65 |
+
|
| 66 |
+
if not os.path.exists(bundler_path):
|
| 67 |
+
raise RuntimeError(
|
| 68 |
+
f"clang-offload-bundler not found at {bundler_path}. "
|
| 69 |
+
f"ROCM_HOME is set to {ROCM_HOME}"
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
return bundler_path
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def get_rocm_target_archs() -> list[str]:
|
| 76 |
+
env_archs = os.environ.get("PYTORCH_ROCM_ARCH", "").strip()
|
| 77 |
+
if env_archs:
|
| 78 |
+
archs = [arch.strip() for arch in env_archs.replace(";", ",").split(",")]
|
| 79 |
+
archs = [arch for arch in archs if arch]
|
| 80 |
+
if archs:
|
| 81 |
+
# Ensure current device arch is included
|
| 82 |
+
if torch.cuda.is_available():
|
| 83 |
+
for dev_idx in range(torch.cuda.device_count()):
|
| 84 |
+
current_arch = torch.cuda.get_device_properties(
|
| 85 |
+
dev_idx
|
| 86 |
+
).gcnArchName.split(":")[0]
|
| 87 |
+
if current_arch not in archs:
|
| 88 |
+
archs.append(current_arch)
|
| 89 |
+
return archs
|
| 90 |
+
|
| 91 |
+
try:
|
| 92 |
+
from torch._inductor import config
|
| 93 |
+
|
| 94 |
+
if hasattr(config, "rocm") and hasattr(config.rocm, "target_archs"):
|
| 95 |
+
archs = config.rocm.target_archs
|
| 96 |
+
if archs:
|
| 97 |
+
return archs
|
| 98 |
+
|
| 99 |
+
except Exception:
|
| 100 |
+
pass
|
| 101 |
+
|
| 102 |
+
return torch.cuda.get_arch_list()
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def _sanitize_llvm_ir_for_rocm(llvm_ir_path: str) -> str:
|
| 106 |
+
"""
|
| 107 |
+
Sanitize LLVM IR to be compatible with ROCm's clang.
|
| 108 |
+
|
| 109 |
+
Triton's LLVM (upstream) may emit attributes and metadata that ROCm's
|
| 110 |
+
older clang does not yet support. Only strips attributes confirmed to
|
| 111 |
+
cause parse errors — preserves all others to maintain correct codegen.
|
| 112 |
+
|
| 113 |
+
Currently strips:
|
| 114 |
+
- nocreateundeforpoison: function attribute (upstream LLVM, not in ROCm)
|
| 115 |
+
- dwarfAddressSpace: debug metadata field (upstream LLVM, not in ROCm)
|
| 116 |
+
|
| 117 |
+
Returns:
|
| 118 |
+
Path to sanitized .ll file, or original path if no changes needed.
|
| 119 |
+
"""
|
| 120 |
+
with open(llvm_ir_path) as f:
|
| 121 |
+
content = f.read()
|
| 122 |
+
|
| 123 |
+
sanitized = content
|
| 124 |
+
sanitized = re.sub(r"\bnocreateundeforpoison\b\s*", "", sanitized)
|
| 125 |
+
sanitized = re.sub(r",\s*dwarfAddressSpace:\s*\d+", "", sanitized)
|
| 126 |
+
|
| 127 |
+
if sanitized == content:
|
| 128 |
+
return llvm_ir_path
|
| 129 |
+
|
| 130 |
+
sanitized_path = llvm_ir_path + ".sanitized.ll"
|
| 131 |
+
with open(sanitized_path, "w") as f:
|
| 132 |
+
f.write(sanitized)
|
| 133 |
+
|
| 134 |
+
log.debug(
|
| 135 |
+
"Sanitized LLVM IR for ROCm clang compatibility: %s -> %s",
|
| 136 |
+
llvm_ir_path,
|
| 137 |
+
sanitized_path,
|
| 138 |
+
)
|
| 139 |
+
return sanitized_path
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def compile_llvm_ir_to_code_object(
|
| 143 |
+
llvm_ir_path: str, output_path: str, target_arch: str
|
| 144 |
+
) -> bool:
|
| 145 |
+
"""
|
| 146 |
+
Compile unbundled LLVM IR to a single-arch code object.
|
| 147 |
+
|
| 148 |
+
Args:
|
| 149 |
+
llvm_ir_path: Path to .ll file
|
| 150 |
+
output_path: Where to write .hsaco file
|
| 151 |
+
target_arch: Target architecture (e.g., 'gfx90a')
|
| 152 |
+
|
| 153 |
+
Returns:
|
| 154 |
+
True if successful
|
| 155 |
+
"""
|
| 156 |
+
if not os.path.exists(llvm_ir_path):
|
| 157 |
+
return False
|
| 158 |
+
|
| 159 |
+
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
| 160 |
+
|
| 161 |
+
try:
|
| 162 |
+
clang = get_rocm_compiler()
|
| 163 |
+
except RuntimeError:
|
| 164 |
+
return False
|
| 165 |
+
|
| 166 |
+
# Sanitize LLVM IR to remove attributes unsupported by ROCm's clang
|
| 167 |
+
llvm_ir_path = _sanitize_llvm_ir_for_rocm(llvm_ir_path)
|
| 168 |
+
|
| 169 |
+
# Using clang and not hipcc since we are not compiling source code
|
| 170 |
+
# Instead we use the LLVM IR (.ll) provided by triton
|
| 171 |
+
cmd = [
|
| 172 |
+
clang,
|
| 173 |
+
"-target",
|
| 174 |
+
"amdgcn-amd-amdhsa",
|
| 175 |
+
f"-mcpu={target_arch}",
|
| 176 |
+
llvm_ir_path,
|
| 177 |
+
"-o",
|
| 178 |
+
output_path,
|
| 179 |
+
]
|
| 180 |
+
|
| 181 |
+
try:
|
| 182 |
+
subprocess.run(cmd, capture_output=True, text=True, check=True)
|
| 183 |
+
|
| 184 |
+
if not os.path.exists(output_path):
|
| 185 |
+
return False
|
| 186 |
+
|
| 187 |
+
return True
|
| 188 |
+
|
| 189 |
+
except subprocess.CalledProcessError:
|
| 190 |
+
return False
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
def create_multiarch_bundle(code_objects: dict, output_bundle_path: str) -> bool:
|
| 194 |
+
"""
|
| 195 |
+
Bundle multiple architecture code objects into a single multi-arch bundle.
|
| 196 |
+
|
| 197 |
+
Uses clang-offload-bundler to create a fat binary that HIP runtime can load.
|
| 198 |
+
The runtime automatically selects the correct architecture at load time.
|
| 199 |
+
|
| 200 |
+
Args:
|
| 201 |
+
code_objects: Dict mapping architecture to code object path
|
| 202 |
+
output_bundle_path: Path for output bundle
|
| 203 |
+
|
| 204 |
+
Returns:
|
| 205 |
+
True if successful
|
| 206 |
+
"""
|
| 207 |
+
if not code_objects:
|
| 208 |
+
return False
|
| 209 |
+
|
| 210 |
+
os.makedirs(os.path.dirname(output_bundle_path), exist_ok=True)
|
| 211 |
+
|
| 212 |
+
try:
|
| 213 |
+
bundler = get_rocm_bundler()
|
| 214 |
+
except RuntimeError:
|
| 215 |
+
return False
|
| 216 |
+
|
| 217 |
+
# Build targets and inputs lists for clang-offload-bundler
|
| 218 |
+
targets = ["host-x86_64-unknown-linux-gnu"]
|
| 219 |
+
|
| 220 |
+
# We include a dummy host entry to satisfy the bundler format
|
| 221 |
+
inputs = ["/dev/null"]
|
| 222 |
+
|
| 223 |
+
for arch, path in sorted(code_objects.items()):
|
| 224 |
+
if not os.path.exists(path):
|
| 225 |
+
continue
|
| 226 |
+
# hipv4 = HIP version 4 code object format
|
| 227 |
+
# amdgcn-amd-amdhsa = target triple for ROCm/HSA runtime
|
| 228 |
+
# arch = specific GPU (gfx90a, gfx942, etc.)
|
| 229 |
+
targets.append(f"hipv4-amdgcn-amd-amdhsa--{arch}")
|
| 230 |
+
inputs.append(path)
|
| 231 |
+
|
| 232 |
+
if len(inputs) == 1: # Only host, no device code
|
| 233 |
+
return False
|
| 234 |
+
|
| 235 |
+
cmd = [
|
| 236 |
+
bundler,
|
| 237 |
+
"--type=o",
|
| 238 |
+
# CRITICAL: HIP runtime expects 4096-byte alignment for loading bundles
|
| 239 |
+
# Without this, hipModuleLoadData gives segmentation fault
|
| 240 |
+
"-bundle-align=4096", # CRITICAL: Required by HIP runtime!
|
| 241 |
+
f"--targets={','.join(targets)}",
|
| 242 |
+
]
|
| 243 |
+
|
| 244 |
+
for input_file in inputs:
|
| 245 |
+
cmd.append(f"--input={input_file}")
|
| 246 |
+
|
| 247 |
+
cmd.append(f"--output={output_bundle_path}")
|
| 248 |
+
|
| 249 |
+
try:
|
| 250 |
+
subprocess.run(cmd, capture_output=True, text=True, check=True)
|
| 251 |
+
|
| 252 |
+
if not os.path.exists(output_bundle_path):
|
| 253 |
+
return False
|
| 254 |
+
|
| 255 |
+
return True
|
| 256 |
+
|
| 257 |
+
except subprocess.CalledProcessError:
|
| 258 |
+
return False
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
def compile_multiarch_bundle_from_llvm_ir(
|
| 262 |
+
llvm_ir_path: str, output_bundle_path: str, target_archs: list[str] | None = None
|
| 263 |
+
) -> bool:
|
| 264 |
+
"""
|
| 265 |
+
Complete workflow: LLVM IR → multiple code objects → bundle.
|
| 266 |
+
|
| 267 |
+
This is the main entry point for multi-arch compilation.
|
| 268 |
+
|
| 269 |
+
Args:
|
| 270 |
+
llvm_ir_path: Path to .ll file
|
| 271 |
+
output_bundle_path: Where to write bundle
|
| 272 |
+
target_archs: Optional list of architectures
|
| 273 |
+
|
| 274 |
+
Returns:
|
| 275 |
+
True if successful
|
| 276 |
+
"""
|
| 277 |
+
if target_archs is None:
|
| 278 |
+
# Get architectures from environment variable or config
|
| 279 |
+
target_archs = get_rocm_target_archs()
|
| 280 |
+
|
| 281 |
+
# Step 1: Compile LLVM IR to code object for each architecture
|
| 282 |
+
code_objects = {}
|
| 283 |
+
temp_dir = os.path.dirname(output_bundle_path)
|
| 284 |
+
kernel_name = os.path.splitext(os.path.basename(llvm_ir_path))[0]
|
| 285 |
+
|
| 286 |
+
for arch in target_archs:
|
| 287 |
+
# Create temporary single-architecture code object
|
| 288 |
+
# Format: kernel_name_gfx90a.co, kernel_name_gfx942.co, etc.
|
| 289 |
+
co_path = os.path.join(temp_dir, f"{kernel_name}_{arch}.co")
|
| 290 |
+
|
| 291 |
+
# Compile with clang backend: LLVM IR → GPU machine code
|
| 292 |
+
if compile_llvm_ir_to_code_object(llvm_ir_path, co_path, arch):
|
| 293 |
+
code_objects[arch] = co_path
|
| 294 |
+
|
| 295 |
+
if not code_objects:
|
| 296 |
+
return False
|
| 297 |
+
|
| 298 |
+
# Step 2: Bundle all code objects together
|
| 299 |
+
# Uses clang-offload-bundler to create fat binary
|
| 300 |
+
success = create_multiarch_bundle(code_objects, output_bundle_path)
|
| 301 |
+
|
| 302 |
+
# Step 3: Clean up temporary single-arch code objects
|
| 303 |
+
# The bundle contains all the code, so intermediates are no longer needed
|
| 304 |
+
for co_path in code_objects.values():
|
| 305 |
+
try:
|
| 306 |
+
os.remove(co_path)
|
| 307 |
+
except Exception:
|
| 308 |
+
pass
|
| 309 |
+
|
| 310 |
+
return success
|
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/__init__.py
ADDED
|
File without changes
|
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/autotune_cache.py
ADDED
|
@@ -0,0 +1,667 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
PyTorch Inductor Autotuning Cache System
|
| 3 |
+
|
| 4 |
+
This module implements a caching system for autotuning configurations in PyTorch's Inductor compiler.
|
| 5 |
+
It provides mechanisms to store and retrieve optimal kernel configurations both locally and remotely,
|
| 6 |
+
which significantly speeds up compilation by reusing previously discovered optimal parameters.
|
| 7 |
+
|
| 8 |
+
The caching system includes:
|
| 9 |
+
- Local filesystem caching for individual machine reuse
|
| 10 |
+
- Remote caching for sharing optimizations across machines
|
| 11 |
+
- Bundled caching to efficiently store multiple related configurations
|
| 12 |
+
- Cache invalidation based on PyTorch versions and backend changes
|
| 13 |
+
- Serialization/deserialization support for worker processes
|
| 14 |
+
|
| 15 |
+
Key components:
|
| 16 |
+
- AutotuneCache: Main class for managing cache access and storage
|
| 17 |
+
- AutotuneCacheBundler: Bundles multiple cache entries for efficient storage
|
| 18 |
+
- LocalAutotuneCache: Handles filesystem-based caching
|
| 19 |
+
- _LocalAutotuneCacheBackend: Low-level file operations for cache storage
|
| 20 |
+
- AutotuneCacheArtifact: Integration with PyTorch's artifact system
|
| 21 |
+
|
| 22 |
+
This caching system is critical for performance as it eliminates the need to re-run
|
| 23 |
+
expensive autotuning operations when the same kernels are compiled multiple times.
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
from __future__ import annotations
|
| 27 |
+
|
| 28 |
+
import dataclasses
|
| 29 |
+
import hashlib
|
| 30 |
+
import logging
|
| 31 |
+
import os
|
| 32 |
+
import os.path
|
| 33 |
+
import re
|
| 34 |
+
from typing import Any, TYPE_CHECKING
|
| 35 |
+
from typing_extensions import override
|
| 36 |
+
|
| 37 |
+
import torch
|
| 38 |
+
from torch._inductor.runtime.runtime_utils import cache_dir
|
| 39 |
+
from torch.compiler._cache import (
|
| 40 |
+
CacheArtifact,
|
| 41 |
+
CacheArtifactFactory,
|
| 42 |
+
CacheArtifactManager,
|
| 43 |
+
)
|
| 44 |
+
from torch.utils._triton import has_triton
|
| 45 |
+
|
| 46 |
+
from ..remote_cache import (
|
| 47 |
+
create_cache,
|
| 48 |
+
JsonDataTy,
|
| 49 |
+
RemoteCache,
|
| 50 |
+
RemoteCacheBackend,
|
| 51 |
+
RemoteCacheJsonSerde,
|
| 52 |
+
)
|
| 53 |
+
from .triton_compat import Config, HAS_WARP_SPEC
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
if TYPE_CHECKING:
|
| 57 |
+
from ..remote_cache import Sample
|
| 58 |
+
|
| 59 |
+
log = logging.getLogger(__name__)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
_InductorMetaTy = dict[str, object]
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def inductor_meta_from_config() -> _InductorMetaTy:
|
| 66 |
+
from torch._inductor import config
|
| 67 |
+
|
| 68 |
+
backend_hash = None
|
| 69 |
+
if has_triton():
|
| 70 |
+
try:
|
| 71 |
+
backend_hash = torch.utils._triton.triton_hash_with_backend()
|
| 72 |
+
except RuntimeError:
|
| 73 |
+
# This can get the error:
|
| 74 |
+
# RuntimeError: 0 active drivers ([]). There should only be one.
|
| 75 |
+
pass
|
| 76 |
+
|
| 77 |
+
is_hip = None
|
| 78 |
+
if torch.version.hip is not None:
|
| 79 |
+
is_hip = True
|
| 80 |
+
|
| 81 |
+
return {
|
| 82 |
+
"autotune_local_cache": config.autotune_local_cache,
|
| 83 |
+
"autotune_remote_cache": config.autotune_remote_cache,
|
| 84 |
+
"backend_hash": backend_hash,
|
| 85 |
+
"bundled_autotune_remote_cache": config.bundled_autotune_remote_cache,
|
| 86 |
+
"coordinate_descent_tuning": config.coordinate_descent_tuning,
|
| 87 |
+
"is_fbcode": config.is_fbcode(),
|
| 88 |
+
"is_hip": is_hip,
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
@CacheArtifactFactory.register
|
| 93 |
+
class AutotuneCacheArtifact(CacheArtifact):
|
| 94 |
+
@override
|
| 95 |
+
def populate_cache(self) -> None:
|
| 96 |
+
autotune_cache = _LocalAutotuneCacheBackend()
|
| 97 |
+
key = os.path.join(cache_dir(), self.key)
|
| 98 |
+
autotune_cache._put(key, self.content)
|
| 99 |
+
|
| 100 |
+
@override
|
| 101 |
+
@staticmethod
|
| 102 |
+
def type() -> str:
|
| 103 |
+
return "autotune"
|
| 104 |
+
|
| 105 |
+
@override
|
| 106 |
+
@staticmethod
|
| 107 |
+
def encode(content: JsonDataTy) -> bytes:
|
| 108 |
+
assert not isinstance(content, bytes)
|
| 109 |
+
serde = RemoteCacheJsonSerde()
|
| 110 |
+
content_bytes = serde.encode(content)
|
| 111 |
+
assert isinstance(content_bytes, bytes)
|
| 112 |
+
return content_bytes
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
@dataclasses.dataclass
|
| 116 |
+
class AutotuneCache:
|
| 117 |
+
configs_hash: str
|
| 118 |
+
local_cache: tuple[RemoteCache[JsonDataTy], str] | None = None
|
| 119 |
+
remote_cache: tuple[RemoteCache[JsonDataTy], str] | None = None
|
| 120 |
+
|
| 121 |
+
# Create a AutotuneCache. Returns None if none of the caches can be used.
|
| 122 |
+
@staticmethod
|
| 123 |
+
def create(
|
| 124 |
+
inductor_meta: _InductorMetaTy, filename: str, configs_hash: str
|
| 125 |
+
) -> AutotuneCache | None:
|
| 126 |
+
cache = AutotuneCache(configs_hash)
|
| 127 |
+
key = AutotuneCache._prepare_key(filename)
|
| 128 |
+
|
| 129 |
+
cache._setup_local_cache(inductor_meta, os.path.dirname(filename), key)
|
| 130 |
+
cache._setup_remote_autotune_cache(inductor_meta, key)
|
| 131 |
+
if cache.local_cache or cache.remote_cache:
|
| 132 |
+
return cache
|
| 133 |
+
else:
|
| 134 |
+
return None
|
| 135 |
+
|
| 136 |
+
@staticmethod
|
| 137 |
+
def _prepare_key(filename: str) -> str:
|
| 138 |
+
from torch.compiler import config as cconfig
|
| 139 |
+
|
| 140 |
+
# base of filename is already sha256 hash the source contents
|
| 141 |
+
key = f"{os.path.basename(filename)}:{cconfig.cache_key_tag}"
|
| 142 |
+
return hashlib.sha256(key.encode("utf-8")).hexdigest()
|
| 143 |
+
|
| 144 |
+
# Read the best config options from the most local cache and return it.
|
| 145 |
+
def _read(self) -> dict[str, JsonDataTy] | None:
|
| 146 |
+
if local_cache := self.local_cache:
|
| 147 |
+
cache, key = local_cache
|
| 148 |
+
if best_config := cache.get(key):
|
| 149 |
+
if isinstance(best_config, dict):
|
| 150 |
+
return best_config
|
| 151 |
+
|
| 152 |
+
if remote_cache := self.remote_cache:
|
| 153 |
+
cache, key = remote_cache
|
| 154 |
+
if best_config := cache.get(key):
|
| 155 |
+
if isinstance(best_config, dict):
|
| 156 |
+
return best_config
|
| 157 |
+
|
| 158 |
+
return None
|
| 159 |
+
|
| 160 |
+
# Read the best config options from the most local cache and figure out
|
| 161 |
+
# which `configs` represents that option.
|
| 162 |
+
def read_best(
|
| 163 |
+
self, inductor_meta: _InductorMetaTy, configs: list[Config]
|
| 164 |
+
) -> Config | None:
|
| 165 |
+
if best := self._read():
|
| 166 |
+
return _load_cached_autotuning(
|
| 167 |
+
best, self.configs_hash, configs, inductor_meta
|
| 168 |
+
)
|
| 169 |
+
return None
|
| 170 |
+
|
| 171 |
+
# Set up local filesystem caching information
|
| 172 |
+
def _setup_local_cache(
|
| 173 |
+
self, inductor_meta: _InductorMetaTy, dirname: str, cache_key: str
|
| 174 |
+
) -> None:
|
| 175 |
+
if not inductor_meta.get("autotune_local_cache", True):
|
| 176 |
+
return
|
| 177 |
+
|
| 178 |
+
from ..codecache import torch_key
|
| 179 |
+
|
| 180 |
+
"""
|
| 181 |
+
[Note: torch_key in autotune cache key]
|
| 182 |
+
Include torch_key() in the cache key so that different versions
|
| 183 |
+
of torch result in cache invalidation. This is important in case
|
| 184 |
+
of changes to the best_config format or other code changes that
|
| 185 |
+
are not backward compatible w.r.t. the cache.
|
| 186 |
+
"""
|
| 187 |
+
hasher = hashlib.sha256()
|
| 188 |
+
hasher.update(cache_key.encode("utf-8"))
|
| 189 |
+
hasher.update(torch_key())
|
| 190 |
+
updated_cache_key = hasher.hexdigest()
|
| 191 |
+
|
| 192 |
+
cache_filename = f"{dirname}/{updated_cache_key}.best_config"
|
| 193 |
+
local_cache = LocalAutotuneCache()
|
| 194 |
+
self.local_cache = (local_cache, cache_filename)
|
| 195 |
+
|
| 196 |
+
# Set up remote caching information
|
| 197 |
+
def _setup_remote_autotune_cache(
|
| 198 |
+
self, inductor_meta: _InductorMetaTy, cache_key: str
|
| 199 |
+
) -> None:
|
| 200 |
+
if not _should_use_remote_autotune_cache(inductor_meta):
|
| 201 |
+
return
|
| 202 |
+
|
| 203 |
+
if (backend_hash := inductor_meta.get("backend_hash", None)) is None:
|
| 204 |
+
log.debug(
|
| 205 |
+
"backend_hash is not passed on the inductor_meta, unable to use autotune remote cache"
|
| 206 |
+
)
|
| 207 |
+
return
|
| 208 |
+
assert isinstance(backend_hash, str)
|
| 209 |
+
|
| 210 |
+
from ..codecache import torch_key
|
| 211 |
+
|
| 212 |
+
is_fbcode = bool(inductor_meta.get("is_fbcode", False))
|
| 213 |
+
|
| 214 |
+
salt = "autotune-best-config-v2"
|
| 215 |
+
# re: torch_key - see [Note: torch_key in autotune cache key]
|
| 216 |
+
key = torch_key().hex() + backend_hash + self.configs_hash + salt
|
| 217 |
+
key = hashlib.sha256(key.encode("utf-8")).hexdigest()
|
| 218 |
+
|
| 219 |
+
remote_cache = create_cache(
|
| 220 |
+
key,
|
| 221 |
+
is_fbcode,
|
| 222 |
+
"FbRemoteAutotuneCache",
|
| 223 |
+
"RemoteAutotuneCache",
|
| 224 |
+
)
|
| 225 |
+
if not remote_cache:
|
| 226 |
+
return
|
| 227 |
+
|
| 228 |
+
# Save the args passed to create_cache
|
| 229 |
+
# in case AutotuneCache needs to be pickled
|
| 230 |
+
self.remote_cache_full_key = key
|
| 231 |
+
self.is_fbcode = is_fbcode
|
| 232 |
+
self.remote_cache = (remote_cache, cache_key)
|
| 233 |
+
|
| 234 |
+
# The AutotuneCache may be serialized/deserialized if we're using
|
| 235 |
+
# AsyncCompile worker processes to run triton compilation.
|
| 236 |
+
# This is because AutotuneCache instances are created on the worker
|
| 237 |
+
# process, but we need to run AutotuneCache.save on the parent process
|
| 238 |
+
# when actually doing autotuning.
|
| 239 |
+
def __getstate__(self) -> dict[str, Any]:
|
| 240 |
+
# The remote cache handles themselves may not be serializable
|
| 241 |
+
# So clear it and reconstruct it on setstate
|
| 242 |
+
remote_cache = getattr(self, "remote_cache", None)
|
| 243 |
+
return {
|
| 244 |
+
**self.__dict__,
|
| 245 |
+
# Save the cache_key portion
|
| 246 |
+
"remote_cache": remote_cache and remote_cache[1],
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
def __setstate__(self, state: dict[str, Any]) -> None:
|
| 250 |
+
# Reconstruct the remote cache on the parent class
|
| 251 |
+
self.__dict__.update(state)
|
| 252 |
+
if self.remote_cache is not None:
|
| 253 |
+
assert isinstance(self.remote_cache, str)
|
| 254 |
+
assert hasattr(self, "remote_cache_full_key")
|
| 255 |
+
assert hasattr(self, "is_fbcode")
|
| 256 |
+
cache_key = self.remote_cache
|
| 257 |
+
remote_cache = create_cache(
|
| 258 |
+
self.remote_cache_full_key,
|
| 259 |
+
self.is_fbcode,
|
| 260 |
+
"FbRemoteAutotuneCache",
|
| 261 |
+
"RemoteAutotuneCache",
|
| 262 |
+
)
|
| 263 |
+
if remote_cache is not None:
|
| 264 |
+
self.remote_cache = (remote_cache, cache_key)
|
| 265 |
+
else:
|
| 266 |
+
log.warning("Warning, failed to recreate remote cache after pickling")
|
| 267 |
+
self.remote_cache = None
|
| 268 |
+
|
| 269 |
+
# Save the config in the caches
|
| 270 |
+
def save(
|
| 271 |
+
self,
|
| 272 |
+
config: Config,
|
| 273 |
+
time_taken_ns: int,
|
| 274 |
+
found_by_coordesc: bool = False,
|
| 275 |
+
triton_cache_hash: str | None = None,
|
| 276 |
+
) -> None:
|
| 277 |
+
data = {
|
| 278 |
+
# pyrefly: ignore [missing-attribute]
|
| 279 |
+
**config.kwargs,
|
| 280 |
+
# pyrefly: ignore [missing-attribute]
|
| 281 |
+
"num_warps": config.num_warps,
|
| 282 |
+
# pyrefly: ignore [missing-attribute]
|
| 283 |
+
"num_stages": config.num_stages,
|
| 284 |
+
"configs_hash": self.configs_hash,
|
| 285 |
+
"found_by_coordesc": found_by_coordesc,
|
| 286 |
+
"time_taken_ms": time_taken_ns // 1000000, # Convert from NS to MS
|
| 287 |
+
"triton_cache_hash": triton_cache_hash,
|
| 288 |
+
}
|
| 289 |
+
# Save extra_options if present on the config. This allows third-party
|
| 290 |
+
# backends to store custom tuned options alongside the standard config.
|
| 291 |
+
if extra_options := getattr(config, "extra_options", None):
|
| 292 |
+
data["extra_options"] = extra_options
|
| 293 |
+
if HAS_WARP_SPEC:
|
| 294 |
+
data.update(
|
| 295 |
+
{
|
| 296 |
+
"num_consumer_groups": getattr(config, "num_consumer_groups", 0),
|
| 297 |
+
"num_buffers_warp_spec": getattr(
|
| 298 |
+
config, "num_buffers_warp_spec", 0
|
| 299 |
+
),
|
| 300 |
+
}
|
| 301 |
+
)
|
| 302 |
+
|
| 303 |
+
if local_cache := self.local_cache:
|
| 304 |
+
cache, key = local_cache
|
| 305 |
+
# pyrefly: ignore [bad-argument-type]
|
| 306 |
+
cache.put(key, data)
|
| 307 |
+
# pyrefly: ignore [bad-argument-type]
|
| 308 |
+
AutotuneCacheBundler.put(key, data)
|
| 309 |
+
autotune_artifact_key = os.path.join(*key.split(os.sep)[-2:])
|
| 310 |
+
CacheArtifactManager.record_artifact(
|
| 311 |
+
AutotuneCacheArtifact.type(), autotune_artifact_key, data
|
| 312 |
+
)
|
| 313 |
+
|
| 314 |
+
if log.isEnabledFor(logging.DEBUG):
|
| 315 |
+
type_str = "coordesc" if found_by_coordesc else "heuristic"
|
| 316 |
+
log.debug("Save %s tuning result to %s", type_str, key)
|
| 317 |
+
|
| 318 |
+
if remote_cache := self.remote_cache:
|
| 319 |
+
cache, key = remote_cache
|
| 320 |
+
# pyrefly: ignore [bad-argument-type]
|
| 321 |
+
cache.put(key, data)
|
| 322 |
+
|
| 323 |
+
|
| 324 |
+
class _AutotuneCacheBundlerImpl:
|
| 325 |
+
"""
|
| 326 |
+
Caches a set of LocalAutotuneCacheBackend entries together in a single
|
| 327 |
+
cache.
|
| 328 |
+
"""
|
| 329 |
+
|
| 330 |
+
_key: str
|
| 331 |
+
_cache: RemoteCache[JsonDataTy]
|
| 332 |
+
|
| 333 |
+
# All known entries from LocalAutotuneCache.put()
|
| 334 |
+
_entries: dict[str, JsonDataTy]
|
| 335 |
+
|
| 336 |
+
def end_compile(self) -> None:
|
| 337 |
+
# TODO: Do we need to compute time_taken_ms and encode that somehow?
|
| 338 |
+
if self._entries:
|
| 339 |
+
self._cache.put(self._key, self._entries)
|
| 340 |
+
|
| 341 |
+
def put(self, basename: str, data: JsonDataTy) -> None:
|
| 342 |
+
# Do we need to worry about duplicates? We only have a single local fs
|
| 343 |
+
# entry - so probably not.
|
| 344 |
+
self._entries[basename] = data
|
| 345 |
+
|
| 346 |
+
def __init__(self, key: str, cache: RemoteCache[JsonDataTy]) -> None:
|
| 347 |
+
self._key = key
|
| 348 |
+
self._cache = cache
|
| 349 |
+
self._entries = {}
|
| 350 |
+
|
| 351 |
+
def sync(self) -> None:
|
| 352 |
+
# We don't currently use this - but we could async load starting at
|
| 353 |
+
# `begin_compile` and wait for the load to be finished here.
|
| 354 |
+
pass
|
| 355 |
+
|
| 356 |
+
@classmethod
|
| 357 |
+
def _should_use_bundled_autotune_remote_cache(
|
| 358 |
+
cls, inductor_meta: _InductorMetaTy
|
| 359 |
+
) -> bool:
|
| 360 |
+
# The bundled autotune cache is only available if you've also got local
|
| 361 |
+
# caching enabled (because we feed the bundled data to the local cache).
|
| 362 |
+
if not inductor_meta.get("autotune_local_cache", True):
|
| 363 |
+
return False
|
| 364 |
+
|
| 365 |
+
# Check if the we're enabled via config
|
| 366 |
+
if (
|
| 367 |
+
bundled_autotune_remote_cache := inductor_meta.get(
|
| 368 |
+
"bundled_autotune_remote_cache"
|
| 369 |
+
)
|
| 370 |
+
) is not None:
|
| 371 |
+
return bool(bundled_autotune_remote_cache)
|
| 372 |
+
|
| 373 |
+
if not cls._get_is_fbcode(inductor_meta):
|
| 374 |
+
return False
|
| 375 |
+
if torch._utils_internal.is_fb_unit_test():
|
| 376 |
+
return False
|
| 377 |
+
if inductor_meta.get("is_hip"):
|
| 378 |
+
return False
|
| 379 |
+
|
| 380 |
+
try:
|
| 381 |
+
from torch._inductor.fb.remote_cache import REMOTE_CACHE_VERSION
|
| 382 |
+
except ModuleNotFoundError:
|
| 383 |
+
return False
|
| 384 |
+
|
| 385 |
+
jk = torch._utils_internal.justknobs_getval_int(
|
| 386 |
+
"pytorch/remote_cache:bundled_autotune_remote_cache_version"
|
| 387 |
+
)
|
| 388 |
+
return REMOTE_CACHE_VERSION >= jk
|
| 389 |
+
|
| 390 |
+
def _load_cache(self) -> bool:
|
| 391 |
+
from torch._inductor import codecache
|
| 392 |
+
|
| 393 |
+
# The single key is defined on construction of the cache.
|
| 394 |
+
entries = self._cache.get(self._key)
|
| 395 |
+
if entries is None or not isinstance(entries, dict):
|
| 396 |
+
# We couldn't load the cache - so mark _entries as non-None so we
|
| 397 |
+
# store local cache values.
|
| 398 |
+
return False
|
| 399 |
+
|
| 400 |
+
# Go through the entries we got from the cache and save them locally.
|
| 401 |
+
time_saved_ns = 0
|
| 402 |
+
for basename, data in entries.items():
|
| 403 |
+
# Reconstruct the final filename (see put())
|
| 404 |
+
root, ext = _splitext_nodot(basename)
|
| 405 |
+
_, _, filename = codecache.get_path(root, ext)
|
| 406 |
+
if isinstance(data, dict) and (tsns := data.get("time_saved_ns")):
|
| 407 |
+
time_saved_ns += int(tsns) # type: ignore[arg-type]
|
| 408 |
+
local_cache = LocalAutotuneCache()
|
| 409 |
+
local_cache.put(filename, data)
|
| 410 |
+
|
| 411 |
+
codecache.add_ephemeral_timeout_increase_for_distributed(time_saved_ns)
|
| 412 |
+
|
| 413 |
+
return True
|
| 414 |
+
|
| 415 |
+
@staticmethod
|
| 416 |
+
def _get_is_fbcode(inductor_meta: _InductorMetaTy) -> bool:
|
| 417 |
+
return bool(inductor_meta.get("is_fbcode", False))
|
| 418 |
+
|
| 419 |
+
@staticmethod
|
| 420 |
+
def _get_backend_hash(inductor_meta: _InductorMetaTy) -> str:
|
| 421 |
+
backend_hash = inductor_meta["backend_hash"]
|
| 422 |
+
assert isinstance(backend_hash, str)
|
| 423 |
+
return backend_hash
|
| 424 |
+
|
| 425 |
+
|
| 426 |
+
class AutotuneCacheBundler:
|
| 427 |
+
_bundler: _AutotuneCacheBundlerImpl | None = None
|
| 428 |
+
|
| 429 |
+
def __init__(self) -> None:
|
| 430 |
+
pass
|
| 431 |
+
|
| 432 |
+
# Call this before we start any autotune computation for an inductor python
|
| 433 |
+
# file. On a cache hit it copies the individual results into the local
|
| 434 |
+
# autotune caches.
|
| 435 |
+
@classmethod
|
| 436 |
+
def begin_compile(
|
| 437 |
+
cls,
|
| 438 |
+
inductor_meta: _InductorMetaTy,
|
| 439 |
+
*,
|
| 440 |
+
code: str | None = None,
|
| 441 |
+
code_hash: str | None = None,
|
| 442 |
+
) -> None:
|
| 443 |
+
assert cls._bundler is None
|
| 444 |
+
|
| 445 |
+
if code is not None:
|
| 446 |
+
assert code_hash is None, "Cannot specify both code and code_hash"
|
| 447 |
+
code_hash = _comment_stripped_hash(code)
|
| 448 |
+
assert code_hash is not None
|
| 449 |
+
|
| 450 |
+
if not _AutotuneCacheBundlerImpl._should_use_bundled_autotune_remote_cache(
|
| 451 |
+
inductor_meta
|
| 452 |
+
):
|
| 453 |
+
return
|
| 454 |
+
|
| 455 |
+
cache = create_cache(
|
| 456 |
+
"bundled-autotune-v1",
|
| 457 |
+
_AutotuneCacheBundlerImpl._get_is_fbcode(inductor_meta),
|
| 458 |
+
"FbRemoteBundledAutotuneCache",
|
| 459 |
+
"RemoteBundledAutotuneCache",
|
| 460 |
+
)
|
| 461 |
+
if not cache:
|
| 462 |
+
return
|
| 463 |
+
|
| 464 |
+
# We're starting a compilation phase. We have a cache key for the code
|
| 465 |
+
# we're compiling. We'll get the individual autotune bundles later (via
|
| 466 |
+
# self.put()). For now create the AutotuneCacheBundler and try to load
|
| 467 |
+
# from the cache.
|
| 468 |
+
|
| 469 |
+
salt = "bundled-autotune-best-configs-v1"
|
| 470 |
+
backend_hash = _AutotuneCacheBundlerImpl._get_backend_hash(inductor_meta)
|
| 471 |
+
# TODO: The autotune cache includes configs_hash in the key. The problem
|
| 472 |
+
# is that the configs_hash includes info from the individual pointwise()
|
| 473 |
+
# calls (size_hints, for example) which we can't know yet. I *think*
|
| 474 |
+
# that info is basically present in the `code_hash` (since it's a
|
| 475 |
+
# parameter to the pointwise decorator) - but is there other info we
|
| 476 |
+
# need to include from inductor_meta?
|
| 477 |
+
key = code_hash + backend_hash + salt
|
| 478 |
+
key = hashlib.sha256(key.encode("utf-8")).hexdigest()
|
| 479 |
+
|
| 480 |
+
bundler = _AutotuneCacheBundlerImpl(key, cache)
|
| 481 |
+
if not bundler._load_cache():
|
| 482 |
+
# We couldn't load from the cache - so save the data so we can store
|
| 483 |
+
# the saved autotunes.
|
| 484 |
+
cls._bundler = bundler
|
| 485 |
+
|
| 486 |
+
# If we get a cache hit don't bother saving any of the individual
|
| 487 |
+
# autotune results.
|
| 488 |
+
|
| 489 |
+
# Call this after all individual autotune results are finished for a
|
| 490 |
+
# inductor python file. If we gathered any individual results then we bundle
|
| 491 |
+
# those and put it into the cache.
|
| 492 |
+
@classmethod
|
| 493 |
+
def end_compile(cls) -> None:
|
| 494 |
+
if bundler := cls._bundler:
|
| 495 |
+
cls._bundler = None
|
| 496 |
+
bundler.end_compile()
|
| 497 |
+
|
| 498 |
+
@classmethod
|
| 499 |
+
def sync(cls) -> None:
|
| 500 |
+
if bundler := cls._bundler:
|
| 501 |
+
bundler.sync()
|
| 502 |
+
|
| 503 |
+
@classmethod
|
| 504 |
+
def put(cls, filename: str, data: JsonDataTy) -> None:
|
| 505 |
+
if bundler := cls._bundler:
|
| 506 |
+
# The filename comes in as something like
|
| 507 |
+
# "/tmp/tmp{random}/{aa}/{basename}.py" (where aa is
|
| 508 |
+
# basename[1:3]). Strip it down and make sure that it looks like a path
|
| 509 |
+
# we could reconstruct (because it's possible for the caller to
|
| 510 |
+
# customize the path).
|
| 511 |
+
basename = os.path.basename(filename)
|
| 512 |
+
|
| 513 |
+
# TODO: check cache_dir() vs filename, then strip dirname
|
| 514 |
+
bundler.put(basename, data)
|
| 515 |
+
|
| 516 |
+
|
| 517 |
+
# Remove the comments from the code (which include things like run ids and file
|
| 518 |
+
# paths) and then hash the result.
|
| 519 |
+
def _comment_stripped_hash(code: str) -> str:
|
| 520 |
+
code = re.sub(r"#.*$", "", code, count=0, flags=re.MULTILINE)
|
| 521 |
+
return torch._inductor.codecache.code_hash(code)
|
| 522 |
+
|
| 523 |
+
|
| 524 |
+
def _should_use_remote_autotune_cache(inductor_meta: _InductorMetaTy) -> bool:
|
| 525 |
+
if (config := inductor_meta.get("autotune_remote_cache")) is not None:
|
| 526 |
+
return bool(config)
|
| 527 |
+
if not inductor_meta.get("is_fbcode"):
|
| 528 |
+
return False
|
| 529 |
+
if torch._utils_internal.is_fb_unit_test():
|
| 530 |
+
return False
|
| 531 |
+
if inductor_meta.get("is_hip"):
|
| 532 |
+
return False
|
| 533 |
+
|
| 534 |
+
try:
|
| 535 |
+
from torch._inductor.fb.remote_cache import REMOTE_CACHE_VERSION
|
| 536 |
+
except ModuleNotFoundError:
|
| 537 |
+
return False
|
| 538 |
+
|
| 539 |
+
return REMOTE_CACHE_VERSION >= torch._utils_internal.justknobs_getval_int(
|
| 540 |
+
"pytorch/remote_cache:autotune_memcache_version"
|
| 541 |
+
)
|
| 542 |
+
|
| 543 |
+
|
| 544 |
+
def _load_cached_autotuning(
|
| 545 |
+
best_config: dict[str, JsonDataTy],
|
| 546 |
+
configs_hash: str,
|
| 547 |
+
configs: list[Config],
|
| 548 |
+
inductor_meta: _InductorMetaTy,
|
| 549 |
+
) -> Config | None:
|
| 550 |
+
if best_config is None:
|
| 551 |
+
return None
|
| 552 |
+
if best_config.pop("configs_hash", None) != configs_hash:
|
| 553 |
+
return None
|
| 554 |
+
|
| 555 |
+
# Remove time taken for comparison
|
| 556 |
+
best_config.pop("time_taken_ms", None)
|
| 557 |
+
|
| 558 |
+
best_config.pop("triton_cache_hash", None)
|
| 559 |
+
|
| 560 |
+
# Extract extra_options if present. This allows third-party backends
|
| 561 |
+
# to restore custom tuned options from the cache.
|
| 562 |
+
extra_options = best_config.pop("extra_options", None)
|
| 563 |
+
|
| 564 |
+
if inductor_meta.get("coordinate_descent_tuning") and best_config.pop(
|
| 565 |
+
"found_by_coordesc", False
|
| 566 |
+
):
|
| 567 |
+
num_warps = best_config.pop("num_warps")
|
| 568 |
+
num_stages = best_config.pop("num_stages")
|
| 569 |
+
|
| 570 |
+
# Extract common arguments
|
| 571 |
+
config_args = {
|
| 572 |
+
"num_warps": num_warps,
|
| 573 |
+
"num_stages": num_stages,
|
| 574 |
+
}
|
| 575 |
+
|
| 576 |
+
if HAS_WARP_SPEC:
|
| 577 |
+
config_args.update(
|
| 578 |
+
{
|
| 579 |
+
"num_consumer_groups": best_config.pop("num_consumer_groups", 0),
|
| 580 |
+
"num_buffers_warp_spec": best_config.pop(
|
| 581 |
+
"num_buffers_warp_spec", 0
|
| 582 |
+
),
|
| 583 |
+
}
|
| 584 |
+
)
|
| 585 |
+
|
| 586 |
+
# Create the triton_config with the appropriate arguments
|
| 587 |
+
# pyrefly: ignore [bad-argument-count, unexpected-keyword]
|
| 588 |
+
triton_config = Config(best_config, **config_args)
|
| 589 |
+
# pyrefly: ignore [missing-attribute]
|
| 590 |
+
triton_config.found_by_coordesc = True
|
| 591 |
+
# Restore extra_options (may be None if not used by backend)
|
| 592 |
+
# pyrefly: ignore [missing-attribute]
|
| 593 |
+
triton_config.extra_options = extra_options
|
| 594 |
+
return triton_config
|
| 595 |
+
|
| 596 |
+
matching_configs = [
|
| 597 |
+
cfg
|
| 598 |
+
for cfg in configs
|
| 599 |
+
# pyrefly: ignore [missing-attribute]
|
| 600 |
+
if all(val == best_config.get(key) for key, val in cfg.kwargs.items())
|
| 601 |
+
# pyrefly: ignore [missing-attribute]
|
| 602 |
+
and cfg.num_warps == best_config.get("num_warps")
|
| 603 |
+
# pyrefly: ignore [missing-attribute]
|
| 604 |
+
and cfg.num_stages == best_config.get("num_stages")
|
| 605 |
+
]
|
| 606 |
+
if len(matching_configs) != 1:
|
| 607 |
+
return None
|
| 608 |
+
|
| 609 |
+
matched_config = matching_configs[0]
|
| 610 |
+
# Restore extra_options (may be None if not used by backend)
|
| 611 |
+
# pyrefly: ignore [missing-attribute]
|
| 612 |
+
matched_config.extra_options = extra_options
|
| 613 |
+
return matched_config
|
| 614 |
+
|
| 615 |
+
|
| 616 |
+
class _LocalAutotuneCacheBackend(RemoteCacheBackend[bytes]):
|
| 617 |
+
@override
|
| 618 |
+
def _get(self, key: str) -> bytes | None:
|
| 619 |
+
try:
|
| 620 |
+
with open(key, "rb") as fd:
|
| 621 |
+
return fd.read()
|
| 622 |
+
except FileNotFoundError:
|
| 623 |
+
return None
|
| 624 |
+
|
| 625 |
+
@override
|
| 626 |
+
def _put(self, key: str, data: bytes) -> None:
|
| 627 |
+
os.makedirs(os.path.dirname(key), exist_ok=True)
|
| 628 |
+
from torch._inductor import codecache
|
| 629 |
+
|
| 630 |
+
codecache.write_atomic(key, data)
|
| 631 |
+
|
| 632 |
+
|
| 633 |
+
class LocalAutotuneCache(RemoteCache[JsonDataTy]):
|
| 634 |
+
def __init__(self) -> None:
|
| 635 |
+
backend = _LocalAutotuneCacheBackend()
|
| 636 |
+
serde = RemoteCacheJsonSerde()
|
| 637 |
+
super().__init__(backend, serde)
|
| 638 |
+
|
| 639 |
+
@override
|
| 640 |
+
def _get(self, key: str, sample: Sample | None) -> JsonDataTy | None:
|
| 641 |
+
AutotuneCacheBundler.sync()
|
| 642 |
+
result = super()._get(key, sample)
|
| 643 |
+
if result is not None:
|
| 644 |
+
assert isinstance(result, dict)
|
| 645 |
+
# What? Why are we doing a put() here? Imagine we have a new model
|
| 646 |
+
# that reuses some existing kernels that have already been
|
| 647 |
+
# compiled. If we didn't do a `put` here (on cache hit) then the new
|
| 648 |
+
# model would only bundle *newly* compiled kernels, not existing
|
| 649 |
+
# kernels that were already compiled and cached.
|
| 650 |
+
AutotuneCacheBundler.put(key, result)
|
| 651 |
+
autotune_artifact_key = os.path.join(*key.split(os.sep)[-2:])
|
| 652 |
+
CacheArtifactManager.record_artifact(
|
| 653 |
+
AutotuneCacheArtifact.type(), autotune_artifact_key, result
|
| 654 |
+
)
|
| 655 |
+
return result
|
| 656 |
+
|
| 657 |
+
@override
|
| 658 |
+
def _put(self, key: str, value: JsonDataTy, sample: Sample | None) -> None:
|
| 659 |
+
AutotuneCacheBundler.put(key, value)
|
| 660 |
+
super()._put(key, value, sample)
|
| 661 |
+
|
| 662 |
+
|
| 663 |
+
def _splitext_nodot(basename: str) -> tuple[str, str]:
|
| 664 |
+
root, ext = os.path.splitext(basename)
|
| 665 |
+
if ext:
|
| 666 |
+
ext = ext[1:]
|
| 667 |
+
return root, ext
|
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/benchmarking.py
ADDED
|
@@ -0,0 +1,549 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import functools
|
| 2 |
+
import inspect
|
| 3 |
+
import time
|
| 4 |
+
from collections.abc import Callable
|
| 5 |
+
from functools import cached_property, wraps
|
| 6 |
+
from itertools import chain
|
| 7 |
+
from statistics import median
|
| 8 |
+
from typing import Any, Concatenate
|
| 9 |
+
from typing_extensions import ParamSpec, Self, TypeVar
|
| 10 |
+
|
| 11 |
+
import torch
|
| 12 |
+
import torch._inductor.config as inductor_config
|
| 13 |
+
import torch.utils._pytree as pytree
|
| 14 |
+
from torch._dynamo.utils import counters
|
| 15 |
+
from torch.utils._debug_mode import DebugMode
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
logger = torch._logging.getArtifactLogger(__name__, "benchmarking")
|
| 19 |
+
use_experimental_benchmarker = (
|
| 20 |
+
inductor_config.use_experimental_benchmarker and torch.cuda.is_available()
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
MILLISECONDS_PER_SECOND = 1000
|
| 25 |
+
|
| 26 |
+
P = ParamSpec("P")
|
| 27 |
+
T = TypeVar("T")
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
# Device-type → benchmarking function registry.
|
| 31 |
+
# Keys must match torch.device.type (e.g., "cpu", "cuda", "mps", "xpu", ...).
|
| 32 |
+
# Values are callables with signature:
|
| 33 |
+
# fn(self: Benchmarker, _callable: Callable[..., Any], *, warmup: int, rep: int, **kwargs) -> Any
|
| 34 |
+
_BENCHMARK_DISPATCH: dict[str, Callable[..., Any]] = {}
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def register_benchmarker(
|
| 38 |
+
device_type: str,
|
| 39 |
+
fn: Callable[..., Any],
|
| 40 |
+
*,
|
| 41 |
+
override: bool = False,
|
| 42 |
+
) -> None:
|
| 43 |
+
"""
|
| 44 |
+
Register a device-type specific benchmarker.
|
| 45 |
+
|
| 46 |
+
Args:
|
| 47 |
+
device_type: torch.device.type string (e.g., "cuda", "cpu", "mps", "xpu").
|
| 48 |
+
fn: callable(self, _callable, *, warmup, rep, **kwargs) -> Any
|
| 49 |
+
override: allow overriding an existing registration.
|
| 50 |
+
"""
|
| 51 |
+
if not isinstance(device_type, str) or not device_type:
|
| 52 |
+
raise ValueError(
|
| 53 |
+
"device_type must be a non-empty string matching torch.device.type"
|
| 54 |
+
)
|
| 55 |
+
if not callable(fn):
|
| 56 |
+
raise TypeError("fn must be callable")
|
| 57 |
+
if not override and device_type in _BENCHMARK_DISPATCH:
|
| 58 |
+
raise ValueError(
|
| 59 |
+
f"Benchmarker for device_type '{device_type}' already registered"
|
| 60 |
+
)
|
| 61 |
+
_BENCHMARK_DISPATCH[device_type] = fn
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def may_distort_benchmarking_result(fn: Callable[..., Any]) -> Callable[..., Any]:
|
| 65 |
+
from torch._inductor import config
|
| 66 |
+
|
| 67 |
+
if config.test_configs.distort_benchmarking_result == "":
|
| 68 |
+
return fn
|
| 69 |
+
|
| 70 |
+
def distort(
|
| 71 |
+
ms: list[float] | tuple[float, ...] | float,
|
| 72 |
+
) -> list[float] | tuple[float, ...] | float:
|
| 73 |
+
if isinstance(ms, (list, tuple)):
|
| 74 |
+
return type(ms)(distort(val) for val in ms) # type: ignore[misc]
|
| 75 |
+
|
| 76 |
+
distort_method = config.test_configs.distort_benchmarking_result
|
| 77 |
+
assert isinstance(ms, float)
|
| 78 |
+
if distort_method == "inverse":
|
| 79 |
+
return 1.0 / ms if ms else 0.0
|
| 80 |
+
elif distort_method == "random":
|
| 81 |
+
import random
|
| 82 |
+
|
| 83 |
+
return random.random()
|
| 84 |
+
else:
|
| 85 |
+
raise RuntimeError(f"Unrecognized distort method {distort_method}")
|
| 86 |
+
|
| 87 |
+
@functools.wraps(fn)
|
| 88 |
+
def wrapper(
|
| 89 |
+
*args: list[Any], **kwargs: dict[str, Any]
|
| 90 |
+
) -> list[float] | tuple[float, ...] | float:
|
| 91 |
+
ms = fn(*args, **kwargs)
|
| 92 |
+
|
| 93 |
+
return distort(ms)
|
| 94 |
+
|
| 95 |
+
return wrapper
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def may_ban_benchmarking() -> None:
|
| 99 |
+
if torch._inductor.config.deterministic:
|
| 100 |
+
raise RuntimeError("""In the deterministic mode of Inductor, we will avoid those
|
| 101 |
+
benchmarkings that would cause non deterministic results. Only benchmarkings in the vetted
|
| 102 |
+
scenarios are allowed. Example include autotuning for triton configs of pointwise kernels.
|
| 103 |
+
|
| 104 |
+
When you see this exception, you can do one of the following two things:
|
| 105 |
+
1. if the benchmarking you are doing does not introduce any non-determinism, you can just
|
| 106 |
+
add is_vetted_benchmarking=True to you benchmark_gpu call. That would solve the issue.
|
| 107 |
+
|
| 108 |
+
2. if the benchmarking you are doing indeed introduces non-determinism, you'll need to disable
|
| 109 |
+
such feature in deterministic mode or find an alternative implementation that is deterministic.
|
| 110 |
+
""")
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def time_and_count(
|
| 114 |
+
fn: Callable[Concatenate[Any, P], T],
|
| 115 |
+
) -> Callable[Concatenate[Any, P], T]:
|
| 116 |
+
"""
|
| 117 |
+
Wraps `fn` to increment the appropriate dynamo counters. It is expected that `fn`
|
| 118 |
+
is a method of `Benchmarker` or one of its subclasses; typing limitations prevent
|
| 119 |
+
us from declaring this directly.
|
| 120 |
+
|
| 121 |
+
NOTE: If you're tempted to add a dynamo_timed call here, this function can be
|
| 122 |
+
called enough that the dynamo_timed overhead is not negligible.
|
| 123 |
+
"""
|
| 124 |
+
|
| 125 |
+
@wraps(fn)
|
| 126 |
+
def wrapper(self: Any, *args: P.args, **kwargs: P.kwargs) -> T:
|
| 127 |
+
fn_qual_name = f"{self.__class__.__name__}.{fn.__name__}"
|
| 128 |
+
counters["inductor"][f"benchmarking.{fn_qual_name}"] += 1
|
| 129 |
+
return fn(self, *args, **kwargs)
|
| 130 |
+
|
| 131 |
+
return wrapper
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
class Benchmarker:
|
| 135 |
+
"""
|
| 136 |
+
A device-agnostic benchmarking utility for measuring the runtime of
|
| 137 |
+
inductor generated callables.
|
| 138 |
+
"""
|
| 139 |
+
|
| 140 |
+
def __init__(self: Self) -> None:
|
| 141 |
+
pass
|
| 142 |
+
|
| 143 |
+
def infer_device(self, *fn_args: Any, **fn_kwargs: Any) -> torch.device:
|
| 144 |
+
inferred_device: torch.device | None = None
|
| 145 |
+
for arg_or_kwarg in chain(fn_args, fn_kwargs.values()):
|
| 146 |
+
# Some callables take nested structures as arguments so use the
|
| 147 |
+
# flattened form to find any tensors
|
| 148 |
+
for arg_or_kwarg_leaf in pytree.tree_leaves(arg_or_kwarg):
|
| 149 |
+
if not isinstance(arg_or_kwarg_leaf, torch.Tensor):
|
| 150 |
+
continue
|
| 151 |
+
if inferred_device is None:
|
| 152 |
+
inferred_device = arg_or_kwarg_leaf.device
|
| 153 |
+
elif arg_or_kwarg_leaf.device != inferred_device:
|
| 154 |
+
raise ValueError(
|
| 155 |
+
"Can't safely infer the device type of `fn` with multiple device types in `fn_args` and `fn_kwargs`!"
|
| 156 |
+
)
|
| 157 |
+
|
| 158 |
+
if inferred_device is None:
|
| 159 |
+
raise ValueError(
|
| 160 |
+
"Can't safely infer the device type of `fn` with no device types"
|
| 161 |
+
" in `fn_args` or `fn_kwargs`. Use a direct benchmarking method instead e.g. "
|
| 162 |
+
"`Benchmarker.benchmark_cpu` or `Benchmarker.benchmark_gpu`."
|
| 163 |
+
)
|
| 164 |
+
|
| 165 |
+
return inferred_device
|
| 166 |
+
|
| 167 |
+
@time_and_count
|
| 168 |
+
def benchmark(
|
| 169 |
+
self: Self,
|
| 170 |
+
fn: Callable[..., Any],
|
| 171 |
+
fn_args: tuple[Any, ...] | None = None,
|
| 172 |
+
fn_kwargs: dict[str, Any] | None = None,
|
| 173 |
+
device: str | torch.device | None = None,
|
| 174 |
+
**kwargs: Any,
|
| 175 |
+
) -> float:
|
| 176 |
+
"""Benchmark `fn(*fn_args, *fn_kwargs)` and return the runtime, in milliseconds (the
|
| 177 |
+
actual runtime calculation is dictated by the benchmarking implementation, but may be
|
| 178 |
+
one of [mean, median, minimum, etc.]). Functions as a convenience wrapper around
|
| 179 |
+
device-specific implementations, like `benchmark_cpu` and `benchmark_gpu`. Raises
|
| 180 |
+
`ValueError(...)` if we can't safely infer the device type of `fn`; for example,
|
| 181 |
+
if multiple device types are found in `fn_args` and `fn_kwargs`, or if no device
|
| 182 |
+
types are found. To bypass device inference, provide the device to the `device`
|
| 183 |
+
parameter.
|
| 184 |
+
|
| 185 |
+
WARNING: if `fn` mutates `fn_args` or `fn_kwargs`, benchmarking may fail unexpectedly.
|
| 186 |
+
For example, if `fn` clears a mutable object, subsequent invocations of `fn` during
|
| 187 |
+
benchmarking will fail. In such cases, `fn` should handle cloning its arguments internally.
|
| 188 |
+
If device inference is required, `Benchmarker.infer_device` can be used prior to calling
|
| 189 |
+
this method without any arguments for `fn_args` and `fn_kwargs`.
|
| 190 |
+
|
| 191 |
+
Arguments:
|
| 192 |
+
- fn: The function to benchmark.
|
| 193 |
+
- fn_args: The function's arguments.
|
| 194 |
+
- fn_kwargs: The function's kwargs.
|
| 195 |
+
|
| 196 |
+
Keyword Arguments:
|
| 197 |
+
- device: Which device to use for benchmarking. If not provided the device will be attempted
|
| 198 |
+
to be inferred from `fn_args` and `fn_kwargs`.
|
| 199 |
+
- **kwargs: The benchmarking implementation's kwargs.
|
| 200 |
+
|
| 201 |
+
Returns:
|
| 202 |
+
- The runtime of `fn(*fn_args, **fn_kwargs)`, in milliseconds.
|
| 203 |
+
"""
|
| 204 |
+
inferred_device: torch.device | None = None
|
| 205 |
+
if device is not None:
|
| 206 |
+
inferred_device = (
|
| 207 |
+
torch.device(device) if isinstance(device, str) else device
|
| 208 |
+
)
|
| 209 |
+
else:
|
| 210 |
+
if fn_args is None and fn_kwargs is None:
|
| 211 |
+
raise ValueError(
|
| 212 |
+
"`fn_args` and `fn_kwargs` cannot both be None if `device` is not provided."
|
| 213 |
+
)
|
| 214 |
+
|
| 215 |
+
fn_args = fn_args or tuple()
|
| 216 |
+
fn_kwargs = fn_kwargs or {}
|
| 217 |
+
inferred_device = self.infer_device(*fn_args, **fn_kwargs)
|
| 218 |
+
|
| 219 |
+
assert isinstance(inferred_device, torch.device)
|
| 220 |
+
|
| 221 |
+
fn_args = fn_args or tuple()
|
| 222 |
+
fn_kwargs = fn_kwargs or {}
|
| 223 |
+
|
| 224 |
+
# No need to wrap if the callable takes no arguments
|
| 225 |
+
if len(fn_args) == 0 and len(fn_kwargs) == 0:
|
| 226 |
+
# Keep a true zero-arg callable type to satisfy type checkers.
|
| 227 |
+
def _callable() -> Any:
|
| 228 |
+
return fn()
|
| 229 |
+
else:
|
| 230 |
+
_args = fn_args
|
| 231 |
+
_kwargs = fn_kwargs
|
| 232 |
+
|
| 233 |
+
def _callable() -> Any:
|
| 234 |
+
return fn(*_args, **_kwargs)
|
| 235 |
+
|
| 236 |
+
warmup = kwargs.pop("warmup", inductor_config.inductor_default_autotune_warmup)
|
| 237 |
+
rep = kwargs.pop("rep", inductor_config.inductor_default_autotune_rep)
|
| 238 |
+
|
| 239 |
+
# Surfacing all kernels during autotuning is super noisy; filtering these out.
|
| 240 |
+
with DebugMode._benchmarking_inductor():
|
| 241 |
+
# First, try a registered device-specific benchmarker
|
| 242 |
+
benchmark_fn: Callable[..., Any] | None = _BENCHMARK_DISPATCH.get(
|
| 243 |
+
inferred_device.type
|
| 244 |
+
)
|
| 245 |
+
if benchmark_fn is not None:
|
| 246 |
+
return benchmark_fn(self, _callable, warmup=warmup, rep=rep, **kwargs)
|
| 247 |
+
|
| 248 |
+
# Backward-compatible default:
|
| 249 |
+
# - CPU -> CPU benchmark path
|
| 250 |
+
# - else -> GPU benchmark path (legacy behavior retained for non-CPU)
|
| 251 |
+
if inferred_device == torch.device("cpu"):
|
| 252 |
+
return self.benchmark_cpu(_callable, warmup=warmup, rep=rep, **kwargs)
|
| 253 |
+
return self.benchmark_gpu(_callable, warmup=warmup, rep=rep, **kwargs)
|
| 254 |
+
|
| 255 |
+
@time_and_count
|
| 256 |
+
def benchmark_cpu(
|
| 257 |
+
self: Self, _callable: Callable[[], Any], warmup: int = 20, rep: int = 100
|
| 258 |
+
) -> float:
|
| 259 |
+
"""Benchmark the CPU callable, `_callable`, and return the median runtime,
|
| 260 |
+
in milliseconds.
|
| 261 |
+
|
| 262 |
+
Arguments:
|
| 263 |
+
- _callable: The CPU callable to benchmark.
|
| 264 |
+
|
| 265 |
+
Keyword Arguments:
|
| 266 |
+
- warmup: Optionally, the duration, in milliseconds, to run `_callable`
|
| 267 |
+
before benchmarking starts.
|
| 268 |
+
- rep: Optionally, the duration, in milliseconds, to run `_callable`
|
| 269 |
+
during benchmarking.
|
| 270 |
+
|
| 271 |
+
Returns:
|
| 272 |
+
- The median runtime of `_callable`, in milliseconds.
|
| 273 |
+
"""
|
| 274 |
+
|
| 275 |
+
def run_for(ms: int) -> list[float]:
|
| 276 |
+
timings = []
|
| 277 |
+
run_start_t = time.perf_counter()
|
| 278 |
+
while True:
|
| 279 |
+
start_t = time.perf_counter()
|
| 280 |
+
_callable()
|
| 281 |
+
end_t = time.perf_counter()
|
| 282 |
+
timings.append((end_t - start_t) * MILLISECONDS_PER_SECOND)
|
| 283 |
+
if ((end_t - run_start_t) * MILLISECONDS_PER_SECOND) > ms:
|
| 284 |
+
break
|
| 285 |
+
return timings
|
| 286 |
+
|
| 287 |
+
run_for(warmup)
|
| 288 |
+
return median(run_for(rep))
|
| 289 |
+
|
| 290 |
+
@time_and_count
|
| 291 |
+
def benchmark_gpu(self: Self, *args: Any, **kwargs: Any) -> float:
|
| 292 |
+
raise NotImplementedError
|
| 293 |
+
|
| 294 |
+
@time_and_count
|
| 295 |
+
def benchmark_gpu_with_cuda_graph(
|
| 296 |
+
self: Self,
|
| 297 |
+
_callable: Callable[[], Any],
|
| 298 |
+
**kwargs: Any,
|
| 299 |
+
) -> float:
|
| 300 |
+
"""Benchmark a GPU callable using CUDA graph capture and replay.
|
| 301 |
+
|
| 302 |
+
This captures the callable into a CUDA graph and benchmarks the graph replay,
|
| 303 |
+
which eliminates kernel launch overhead for fair comparison between different
|
| 304 |
+
implementations.
|
| 305 |
+
"""
|
| 306 |
+
# Warmup
|
| 307 |
+
_callable()
|
| 308 |
+
torch.cuda.synchronize()
|
| 309 |
+
|
| 310 |
+
# Capture into CUDA graph
|
| 311 |
+
cuda_graph = torch.cuda.CUDAGraph()
|
| 312 |
+
with torch.cuda.graph(cuda_graph, capture_error_mode="thread_local"):
|
| 313 |
+
_callable()
|
| 314 |
+
torch.cuda.synchronize()
|
| 315 |
+
|
| 316 |
+
return self.benchmark_gpu(cuda_graph.replay, **kwargs)
|
| 317 |
+
|
| 318 |
+
|
| 319 |
+
# Make built-in defaults explicit via the registry
|
| 320 |
+
def _default_cpu_bench(self, f, *, warmup, rep, **kw):
|
| 321 |
+
return self.benchmark_cpu(f, warmup=warmup, rep=rep, **kw)
|
| 322 |
+
|
| 323 |
+
|
| 324 |
+
def _default_cuda_bench(self, f, *, warmup, rep, **kw):
|
| 325 |
+
return self.benchmark_gpu(f, warmup=warmup, rep=rep, **kw)
|
| 326 |
+
|
| 327 |
+
|
| 328 |
+
def _default_xpu_bench(self, f, *, warmup, rep, **kw):
|
| 329 |
+
return self.benchmark_gpu(f, warmup=warmup, rep=rep, **kw)
|
| 330 |
+
|
| 331 |
+
|
| 332 |
+
register_benchmarker("cpu", _default_cpu_bench, override=True)
|
| 333 |
+
register_benchmarker("cuda", _default_cuda_bench, override=True)
|
| 334 |
+
register_benchmarker("xpu", _default_xpu_bench, override=True)
|
| 335 |
+
|
| 336 |
+
|
| 337 |
+
class TritonBenchmarker(Benchmarker):
|
| 338 |
+
@cached_property
|
| 339 |
+
def triton_do_bench(self: Self) -> Callable[..., Any]:
|
| 340 |
+
"""Lazily import Triton's `do_bench`."""
|
| 341 |
+
try:
|
| 342 |
+
from triton.testing import do_bench
|
| 343 |
+
except ImportError as e:
|
| 344 |
+
raise NotImplementedError("requires Triton") from e
|
| 345 |
+
return do_bench
|
| 346 |
+
|
| 347 |
+
@may_distort_benchmarking_result
|
| 348 |
+
@time_and_count
|
| 349 |
+
# pyrefly: ignore [bad-override]
|
| 350 |
+
def benchmark_gpu(
|
| 351 |
+
self: Self,
|
| 352 |
+
_callable: Callable[[], Any],
|
| 353 |
+
is_vetted_benchmarking: bool = False,
|
| 354 |
+
**kwargs: Any,
|
| 355 |
+
) -> float:
|
| 356 |
+
"""Benchmark the GPU callable, `_callable`, and return the runtime, in milliseconds.
|
| 357 |
+
|
| 358 |
+
Arguments:
|
| 359 |
+
- _callable: The GPU callable to benchmark.
|
| 360 |
+
|
| 361 |
+
Keyword Arguments:
|
| 362 |
+
- quantiles: Optionally, a tuple of floats denoting the requested quantiles.
|
| 363 |
+
- return_mode: Optionally, the requested return mode. Currently, Triton's
|
| 364 |
+
`do_bench` supports min, max, mean, and median return modes.
|
| 365 |
+
- **kwargs: Additional kwargs passed to Triton's `do_bench`.
|
| 366 |
+
|
| 367 |
+
Returns:
|
| 368 |
+
- The runtime of `callable`, in milliseconds. If `kwargs["quantiles"]` is specified,
|
| 369 |
+
this is the first requested quantile. Else, if `kwargs["return_mode"]` is specified,
|
| 370 |
+
this is the requested return mode. Otherwise, this is the median.
|
| 371 |
+
"""
|
| 372 |
+
if not is_vetted_benchmarking:
|
| 373 |
+
may_ban_benchmarking()
|
| 374 |
+
|
| 375 |
+
do_bench_params = inspect.signature(self.triton_do_bench).parameters
|
| 376 |
+
for kwarg in list(kwargs.keys()):
|
| 377 |
+
if kwarg not in do_bench_params:
|
| 378 |
+
del kwargs[kwarg]
|
| 379 |
+
try:
|
| 380 |
+
if "quantiles" in kwargs:
|
| 381 |
+
return self.triton_do_bench(_callable, **kwargs)[0]
|
| 382 |
+
elif "return_mode" in kwargs:
|
| 383 |
+
return self.triton_do_bench(_callable, **kwargs)
|
| 384 |
+
return self.triton_do_bench(_callable, **kwargs, return_mode="median")
|
| 385 |
+
except Exception as e:
|
| 386 |
+
# ErrorInvalidConfiguration
|
| 387 |
+
# Return inf to skip this config during autotuning
|
| 388 |
+
error_str = str(e).lower()
|
| 389 |
+
if "invalid configuration" in error_str:
|
| 390 |
+
logger.warning(
|
| 391 |
+
"Skipping benchmark due to invalid configuration error: %s",
|
| 392 |
+
error_str,
|
| 393 |
+
)
|
| 394 |
+
return float("inf")
|
| 395 |
+
raise
|
| 396 |
+
|
| 397 |
+
|
| 398 |
+
class InductorBenchmarker(TritonBenchmarker): # noqa: docstring_linter
|
| 399 |
+
@cached_property
|
| 400 |
+
def L2_cache_size(self: Self) -> int:
|
| 401 |
+
"""Get the L2 cache size, in bytes, of the current device."""
|
| 402 |
+
device = torch.cuda.current_device()
|
| 403 |
+
props = torch.cuda.get_device_properties(device)
|
| 404 |
+
return props.L2_cache_size
|
| 405 |
+
|
| 406 |
+
def get_event_pairs(
|
| 407 |
+
self: Self, iters: int
|
| 408 |
+
) -> list[tuple[torch.cuda.Event, torch.cuda.Event]]:
|
| 409 |
+
"""Get `iters` pairs of CUDA events."""
|
| 410 |
+
return [
|
| 411 |
+
(
|
| 412 |
+
torch.cuda.Event(enable_timing=True),
|
| 413 |
+
torch.cuda.Event(enable_timing=True),
|
| 414 |
+
)
|
| 415 |
+
for _ in range(iters)
|
| 416 |
+
]
|
| 417 |
+
|
| 418 |
+
def get_event_pairs_min_timing(
|
| 419 |
+
self: Self, event_pairs: list[tuple[torch.cuda.Event, torch.cuda.Event]]
|
| 420 |
+
) -> float:
|
| 421 |
+
"""Get the minimum timing, in milliseconds, for a group of CUDA event pairs."""
|
| 422 |
+
return min(
|
| 423 |
+
[
|
| 424 |
+
start_event.elapsed_time(end_event)
|
| 425 |
+
for start_event, end_event in event_pairs
|
| 426 |
+
]
|
| 427 |
+
)
|
| 428 |
+
|
| 429 |
+
@may_distort_benchmarking_result
|
| 430 |
+
@time_and_count
|
| 431 |
+
def benchmark_gpu( # type: ignore[override]
|
| 432 |
+
self: Self,
|
| 433 |
+
_callable: Callable[[], Any],
|
| 434 |
+
estimation_iters: int = 5,
|
| 435 |
+
memory_warmup_iters: int = 100,
|
| 436 |
+
benchmark_iters: int = 100,
|
| 437 |
+
max_benchmark_duration: int = 25,
|
| 438 |
+
return_mode: str = "min",
|
| 439 |
+
grad_to_none: list[torch.Tensor] | None = None,
|
| 440 |
+
is_vetted_benchmarking: bool = False,
|
| 441 |
+
**kwargs: Any,
|
| 442 |
+
) -> float | list[float]:
|
| 443 |
+
"""Benchmark a GPU callable using a custom benchmarking implementation.
|
| 444 |
+
|
| 445 |
+
Arguments:
|
| 446 |
+
- _callable: The callable to benchmark.
|
| 447 |
+
|
| 448 |
+
Keyword Arguments:
|
| 449 |
+
- estimation_iters: Optionally, the number of iterations to run `_callable`
|
| 450 |
+
during runtime estimation.
|
| 451 |
+
- memory_warmup_iters: Optionally, the number of iterations to flush the L2
|
| 452 |
+
cache before starting benchmarking.
|
| 453 |
+
- benchmark_iters: Optionally, the number of iterations to run `_callable`
|
| 454 |
+
during the benchmarking.
|
| 455 |
+
- max_benchmark_duration: Optionally, the maximum duration of the benchmarking,
|
| 456 |
+
in milliseconds. An estimated duration is calculated based on the values
|
| 457 |
+
of `memory_warmup_iters` and `benchmark_iters`, along with the estimated
|
| 458 |
+
runtime of `_callable` and various other factors, and we then shrink
|
| 459 |
+
`benchmark_iters` to fit in the allotted maximum duration.
|
| 460 |
+
- return_mode: Return mode for benchmark results. Options are "min" (default),
|
| 461 |
+
"all" (returns all measurements).
|
| 462 |
+
- grad_to_none: Optionally, a list of tensors whose gradients should be cleared
|
| 463 |
+
before each benchmark iteration.
|
| 464 |
+
- is_vetted_benchmarking: in deterministic mode, we only allow
|
| 465 |
+
benchmarking in vetted cases.
|
| 466 |
+
- **kwargs: Additional kwargs that may be passed to the fallback.
|
| 467 |
+
|
| 468 |
+
Returns:
|
| 469 |
+
- If return_mode="min": The minimum runtime of `_callable`, in milliseconds.
|
| 470 |
+
- If return_mode="all": List of all runtime measurements, in milliseconds.
|
| 471 |
+
"""
|
| 472 |
+
|
| 473 |
+
if not is_vetted_benchmarking:
|
| 474 |
+
may_ban_benchmarking()
|
| 475 |
+
|
| 476 |
+
# we don't want any outside errors propagating into benchmarking
|
| 477 |
+
torch.cuda.synchronize()
|
| 478 |
+
|
| 479 |
+
# warmup `_callable` (and catches any failures in the process)
|
| 480 |
+
_callable()
|
| 481 |
+
torch.cuda.synchronize()
|
| 482 |
+
|
| 483 |
+
# see https://github.com/triton-lang/triton/pull/840 for why `dtype=torch.int`
|
| 484 |
+
buffer = torch.empty(self.L2_cache_size // 4, dtype=torch.int, device="cuda")
|
| 485 |
+
buffer.zero_()
|
| 486 |
+
|
| 487 |
+
# estimate the runtime of `_callable`
|
| 488 |
+
event_pairs = self.get_event_pairs(estimation_iters)
|
| 489 |
+
for start_event, end_event in event_pairs:
|
| 490 |
+
# Clear gradients before timing (matches triton.testing.do_bench)
|
| 491 |
+
if grad_to_none is not None:
|
| 492 |
+
for x in grad_to_none:
|
| 493 |
+
x.grad = None
|
| 494 |
+
buffer.zero_()
|
| 495 |
+
start_event.record()
|
| 496 |
+
_callable()
|
| 497 |
+
end_event.record()
|
| 498 |
+
torch.cuda.synchronize()
|
| 499 |
+
estimated_timing = self.get_event_pairs_min_timing(event_pairs)
|
| 500 |
+
|
| 501 |
+
# adjust `benchmark_iters` to fit in the maximum benchmarking duration
|
| 502 |
+
if estimated_timing > 0:
|
| 503 |
+
benchmark_iters = max(
|
| 504 |
+
min(benchmark_iters, int(max_benchmark_duration // estimated_timing)), 1
|
| 505 |
+
)
|
| 506 |
+
|
| 507 |
+
# do the memory warmup
|
| 508 |
+
for _ in range(memory_warmup_iters):
|
| 509 |
+
buffer.zero_()
|
| 510 |
+
|
| 511 |
+
# benchmark `_callable`
|
| 512 |
+
event_pairs = self.get_event_pairs(benchmark_iters)
|
| 513 |
+
for start_event, end_event in event_pairs:
|
| 514 |
+
# Clear gradients before timing (matches triton.testing.do_bench)
|
| 515 |
+
if grad_to_none is not None:
|
| 516 |
+
for x in grad_to_none:
|
| 517 |
+
x.grad = None
|
| 518 |
+
buffer.zero_()
|
| 519 |
+
start_event.record()
|
| 520 |
+
_callable()
|
| 521 |
+
end_event.record()
|
| 522 |
+
torch.cuda.synchronize()
|
| 523 |
+
|
| 524 |
+
# explicitly delete the buffer, sometimes helps memory
|
| 525 |
+
# footprint metrics in OSS Inductor performance benchmarks
|
| 526 |
+
del buffer
|
| 527 |
+
|
| 528 |
+
# Return based on the requested mode
|
| 529 |
+
if return_mode == "all":
|
| 530 |
+
# Get all timings from event pairs
|
| 531 |
+
all_timings = [
|
| 532 |
+
start_event.elapsed_time(end_event)
|
| 533 |
+
for start_event, end_event in event_pairs
|
| 534 |
+
]
|
| 535 |
+
return all_timings
|
| 536 |
+
elif return_mode == "min":
|
| 537 |
+
benchmarked_timing = self.get_event_pairs_min_timing(event_pairs)
|
| 538 |
+
# return the minimum of `estimated_timing` and `benchmarked_timing`,
|
| 539 |
+
# we just want the minimum timing overall so we might as well check both
|
| 540 |
+
return min(estimated_timing, benchmarked_timing)
|
| 541 |
+
else:
|
| 542 |
+
raise ValueError(
|
| 543 |
+
f"Unsupported return_mode: {return_mode}. Use 'min' or 'all'."
|
| 544 |
+
)
|
| 545 |
+
|
| 546 |
+
|
| 547 |
+
benchmarker = (
|
| 548 |
+
InductorBenchmarker() if use_experimental_benchmarker else TritonBenchmarker()
|
| 549 |
+
)
|
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/cache_dir_utils.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import getpass
|
| 2 |
+
import os
|
| 3 |
+
import re
|
| 4 |
+
import tempfile
|
| 5 |
+
from collections.abc import Generator
|
| 6 |
+
from contextlib import contextmanager
|
| 7 |
+
|
| 8 |
+
from torch._environment import is_fbcode
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
# Factoring out to file without torch dependencies
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def cache_dir() -> str:
|
| 15 |
+
cache_dir = os.environ.get("TORCHINDUCTOR_CACHE_DIR")
|
| 16 |
+
if cache_dir is None:
|
| 17 |
+
os.environ["TORCHINDUCTOR_CACHE_DIR"] = cache_dir = default_cache_dir()
|
| 18 |
+
os.makedirs(cache_dir, exist_ok=True)
|
| 19 |
+
return cache_dir
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def default_cache_dir() -> str:
|
| 23 |
+
sanitized_username = re.sub(r'[\\/:*?"<>|]', "_", getpass.getuser())
|
| 24 |
+
return os.path.join(
|
| 25 |
+
tempfile.gettempdir() if not is_fbcode() else "/var/tmp",
|
| 26 |
+
"torchinductor_" + sanitized_username,
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def triton_cache_dir(device: int) -> str:
|
| 31 |
+
if (directory := os.getenv("TRITON_CACHE_DIR")) is not None:
|
| 32 |
+
return directory
|
| 33 |
+
return os.path.join(
|
| 34 |
+
cache_dir(),
|
| 35 |
+
"triton",
|
| 36 |
+
str(device),
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
@contextmanager
|
| 41 |
+
def temporary_cache_dir(directory: str) -> Generator[None, None, None]:
|
| 42 |
+
from torch._inductor.utils import clear_caches
|
| 43 |
+
|
| 44 |
+
original = os.environ.get("TORCHINDUCTOR_CACHE_DIR")
|
| 45 |
+
os.environ["TORCHINDUCTOR_CACHE_DIR"] = directory
|
| 46 |
+
try:
|
| 47 |
+
clear_caches()
|
| 48 |
+
yield
|
| 49 |
+
finally:
|
| 50 |
+
clear_caches()
|
| 51 |
+
if original is None:
|
| 52 |
+
del os.environ["TORCHINDUCTOR_CACHE_DIR"]
|
| 53 |
+
else:
|
| 54 |
+
os.environ["TORCHINDUCTOR_CACHE_DIR"] = original
|
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/caching/__init__.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from . import config, encoders, memoizers
|
| 2 |
+
from .context import IsolationSchema, SelectedCompileContext, SelectedRuntimeContext
|
| 3 |
+
from .exceptions import (
|
| 4 |
+
CacheError,
|
| 5 |
+
FileLockTimeoutError,
|
| 6 |
+
KeyEncodingError,
|
| 7 |
+
LockTimeoutError,
|
| 8 |
+
SystemError,
|
| 9 |
+
UserError,
|
| 10 |
+
ValueDecodingError,
|
| 11 |
+
ValueEncodingError,
|
| 12 |
+
)
|
| 13 |
+
from .interfaces import Memoizer, PersistentMemoizer
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
__all__ = [
|
| 17 |
+
"CacheError",
|
| 18 |
+
"FileLockTimeoutError",
|
| 19 |
+
"IsolationSchema",
|
| 20 |
+
"KeyEncodingError",
|
| 21 |
+
"LockTimeoutError",
|
| 22 |
+
"Memoizer",
|
| 23 |
+
"PersistentMemoizer",
|
| 24 |
+
"SelectedCompileContext",
|
| 25 |
+
"SelectedRuntimeContext",
|
| 26 |
+
"SystemError",
|
| 27 |
+
"UserError",
|
| 28 |
+
"ValueDecodingError",
|
| 29 |
+
"ValueEncodingError",
|
| 30 |
+
"encoders",
|
| 31 |
+
"memoizers",
|
| 32 |
+
]
|
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/caching/config.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from collections.abc import Callable
|
| 3 |
+
from functools import cache, partial
|
| 4 |
+
from typing import TypeVar
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
from torch._environment import is_fbcode
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
T = TypeVar("T")
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def _is_force_disable_caches() -> bool:
|
| 14 |
+
"""Check if caching is force disabled via inductor config.
|
| 15 |
+
|
| 16 |
+
This defers importing torch._inductor.config to avoid circular imports.
|
| 17 |
+
|
| 18 |
+
Returns:
|
| 19 |
+
True if force_disable_caches is set in inductor config, False otherwise.
|
| 20 |
+
"""
|
| 21 |
+
from torch._inductor import config as inductor_config
|
| 22 |
+
|
| 23 |
+
return inductor_config.force_disable_caches
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
@cache
|
| 27 |
+
def _env_var_val(env_var: str, default: T) -> str | T:
|
| 28 |
+
"""Get the value of an environment variable or return the default.
|
| 29 |
+
|
| 30 |
+
Args:
|
| 31 |
+
env_var: Environment variable name to check
|
| 32 |
+
default: Default value to return if environment variable is not set
|
| 33 |
+
|
| 34 |
+
Returns:
|
| 35 |
+
The value from the environment variable as a string, or the default if not set
|
| 36 |
+
"""
|
| 37 |
+
return os.environ.get(env_var, default)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
@cache
|
| 41 |
+
def _env_var_config(env_var: str, default: bool) -> bool:
|
| 42 |
+
env_val = _env_var_val(env_var, None)
|
| 43 |
+
if env_val is not None:
|
| 44 |
+
return env_val == "1"
|
| 45 |
+
return default
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
@cache
|
| 49 |
+
def _versioned_config(
|
| 50 |
+
jk_name: str,
|
| 51 |
+
this_version: int,
|
| 52 |
+
oss_default: bool,
|
| 53 |
+
env_var_override: str | None = None,
|
| 54 |
+
) -> bool:
|
| 55 |
+
"""
|
| 56 |
+
A versioned configuration utility that determines boolean settings based on:
|
| 57 |
+
1. Environment variable override (highest priority)
|
| 58 |
+
2. JustKnobs version comparison in fbcode environments
|
| 59 |
+
3. OSS default fallback
|
| 60 |
+
|
| 61 |
+
This function enables gradual rollouts of features in fbcode by comparing
|
| 62 |
+
a local version against a JustKnobs-controlled remote version, while
|
| 63 |
+
allowing environment variable overrides for testing and OSS defaults
|
| 64 |
+
for non-fbcode environments.
|
| 65 |
+
|
| 66 |
+
Args:
|
| 67 |
+
jk_name: JustKnobs key name (e.g., "pytorch/inductor:feature_version")
|
| 68 |
+
this_version: Local version number to compare against JustKnobs version
|
| 69 |
+
oss_default: Default value to use in non-fbcode environments
|
| 70 |
+
env_var_override: Optional environment variable name that, when set,
|
| 71 |
+
overrides all other logic
|
| 72 |
+
|
| 73 |
+
Returns:
|
| 74 |
+
bool: Configuration value determined by the priority order above
|
| 75 |
+
"""
|
| 76 |
+
if (
|
| 77 |
+
env_var_override
|
| 78 |
+
and (env_var_value := os.environ.get(env_var_override)) is not None
|
| 79 |
+
):
|
| 80 |
+
return env_var_value == "1"
|
| 81 |
+
elif is_fbcode():
|
| 82 |
+
# this method returns 0 on failure, which we should check for specifically.
|
| 83 |
+
# in the case of JK failure, the safe bet is to simply disable the config
|
| 84 |
+
jk_version: int = torch._utils_internal.justknobs_getval_int(jk_name)
|
| 85 |
+
return (this_version >= jk_version) and (jk_version != 0)
|
| 86 |
+
return oss_default
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
# toggles the entire caching module, but only when calling through the
|
| 90 |
+
# public facing interfaces. get/insert operations become no-ops in the sense
|
| 91 |
+
# that get will always miss and insert will never insert; record becomes a
|
| 92 |
+
# no-op in the sense that the function will always be called and the cache
|
| 93 |
+
# will never be accessed
|
| 94 |
+
_CACHING_MODULE_VERSION: int = 0
|
| 95 |
+
_CACHING_MODULE_VERSION_JK: str = "pytorch/inductor:caching_module_version"
|
| 96 |
+
_CACHING_MODULE_OSS_DEFAULT: bool = False
|
| 97 |
+
_CACHING_MODULE_ENV_VAR_OVERRIDE: str = "TORCHINDUCTOR_ENABLE_CACHING_MODULE"
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def _is_caching_module_enabled_base() -> bool:
|
| 101 |
+
"""Base check for caching module enablement via versioned config."""
|
| 102 |
+
return _versioned_config(
|
| 103 |
+
_CACHING_MODULE_VERSION_JK,
|
| 104 |
+
_CACHING_MODULE_VERSION,
|
| 105 |
+
_CACHING_MODULE_OSS_DEFAULT,
|
| 106 |
+
_CACHING_MODULE_ENV_VAR_OVERRIDE,
|
| 107 |
+
)
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def IS_CACHING_MODULE_ENABLED() -> bool:
|
| 111 |
+
"""Check if the caching module is enabled.
|
| 112 |
+
|
| 113 |
+
Returns False if:
|
| 114 |
+
- The versioned config disables it
|
| 115 |
+
- force_disable_caches is set in inductor config
|
| 116 |
+
|
| 117 |
+
Returns:
|
| 118 |
+
True if caching module is enabled, False otherwise.
|
| 119 |
+
"""
|
| 120 |
+
if not _is_caching_module_enabled_base():
|
| 121 |
+
return False
|
| 122 |
+
if _is_force_disable_caches():
|
| 123 |
+
return False
|
| 124 |
+
return True
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
# Controls whether the Memoizer dumps its cache to a JSON file on destruction.
|
| 128 |
+
# When enabled, the Memoizer will write its in-memory cache to a JSON file
|
| 129 |
+
# (memoizer_cache.json in the cache directory) on destruction. This dump file
|
| 130 |
+
# can later be used to pre-populate Memoizers via CACHE_DUMP_FILE_PATH.
|
| 131 |
+
#
|
| 132 |
+
# This is useful for:
|
| 133 |
+
# - Debugging and inspection of cached values
|
| 134 |
+
# - Creating cache snapshots that can be reused across runs
|
| 135 |
+
# - Pre-warming caches for subsequent executions
|
| 136 |
+
#
|
| 137 |
+
# Set via environment variable: TORCHINDUCTOR_DUMP_MEMOIZER_CACHE=1
|
| 138 |
+
_DUMP_MEMOIZER_CACHE_ENV_VAR: str = "TORCHINDUCTOR_DUMP_MEMOIZER_CACHE"
|
| 139 |
+
_DUMP_MEMOIZER_CACHE_DEFAULT: bool = False
|
| 140 |
+
IS_DUMP_MEMOIZER_CACHE_ENABLED: Callable[[], bool] = partial(
|
| 141 |
+
_env_var_config,
|
| 142 |
+
_DUMP_MEMOIZER_CACHE_ENV_VAR,
|
| 143 |
+
_DUMP_MEMOIZER_CACHE_DEFAULT,
|
| 144 |
+
)
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
# Path to a cache dump file to pre-populate Memoizers on initialization.
|
| 148 |
+
# This should point to a JSON file produced by IS_DUMP_MEMOIZER_CACHE_ENABLED.
|
| 149 |
+
#
|
| 150 |
+
# When set, Memoizers will load cached entries from this file on initialization,
|
| 151 |
+
# allowing cache values to be reused across separate runs without recomputation.
|
| 152 |
+
# This is particularly useful for:
|
| 153 |
+
# - Pre-warming caches with known-good values
|
| 154 |
+
# - Reproducing behavior from a previous run
|
| 155 |
+
# - Sharing cached computations across different environments
|
| 156 |
+
#
|
| 157 |
+
# The dump file format is produced by the Memoizer's _dump_to_disk method when
|
| 158 |
+
# IS_DUMP_MEMOIZER_CACHE_ENABLED is set to true.
|
| 159 |
+
#
|
| 160 |
+
# Set via environment variable: TORCHINDUCTOR_CACHE_DUMP_FILE_PATH=/path/to/dump.json
|
| 161 |
+
_CACHE_DUMP_FILE_PATH_ENV_VAR: str = "TORCHINDUCTOR_CACHE_DUMP_FILE_PATH"
|
| 162 |
+
_CACHE_DUMP_FILE_PATH_DEFAULT: str | None = None
|
| 163 |
+
CACHE_DUMP_FILE_PATH: Callable[[], str | None] = partial(
|
| 164 |
+
_env_var_val,
|
| 165 |
+
_CACHE_DUMP_FILE_PATH_ENV_VAR,
|
| 166 |
+
_CACHE_DUMP_FILE_PATH_DEFAULT,
|
| 167 |
+
)
|
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/caching/context.py
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Context management for PyTorch Inductor runtime caching.
|
| 2 |
+
|
| 3 |
+
This module provides context classes for collecting configuration and environment
|
| 4 |
+
information used in caching decisions for PyTorch's Inductor runtime.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import json
|
| 8 |
+
from abc import ABC, abstractmethod
|
| 9 |
+
from base64 import b64encode
|
| 10 |
+
from collections.abc import Sequence
|
| 11 |
+
from functools import cache
|
| 12 |
+
from hashlib import sha256
|
| 13 |
+
from typing_extensions import override, TypedDict
|
| 14 |
+
|
| 15 |
+
import torch
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class _Context(ABC):
|
| 19 |
+
"""Abstract base class for context providers.
|
| 20 |
+
|
| 21 |
+
Context providers collect specific configuration and environment information
|
| 22 |
+
that affects compilation and runtime behavior.
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
@staticmethod
|
| 26 |
+
@abstractmethod
|
| 27 |
+
def forms_of_context() -> Sequence[str]:
|
| 28 |
+
"""Return a sequence of context form names provided by this context class.
|
| 29 |
+
|
| 30 |
+
Returns:
|
| 31 |
+
A sequence of strings representing the available context forms.
|
| 32 |
+
"""
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class _RuntimeContext(_Context):
|
| 36 |
+
"""Context provider for runtime configuration and environment settings.
|
| 37 |
+
|
| 38 |
+
Collects configuration settings that affect runtime behavior but not
|
| 39 |
+
compilation, such as Inductor configs, determinism settings, and CUDA
|
| 40 |
+
matmul precision configurations.
|
| 41 |
+
"""
|
| 42 |
+
|
| 43 |
+
@override
|
| 44 |
+
@staticmethod
|
| 45 |
+
def forms_of_context() -> Sequence[str]:
|
| 46 |
+
"""Return the runtime context forms provided by this class.
|
| 47 |
+
|
| 48 |
+
Returns:
|
| 49 |
+
A sequence containing the available runtime context forms:
|
| 50 |
+
- "inductor_configs": PyTorch Inductor configuration settings
|
| 51 |
+
- "torch_determinism_configs": Deterministic algorithm settings
|
| 52 |
+
- "cuda_matmul_precision_configs": CUDA matrix multiplication precision settings
|
| 53 |
+
"""
|
| 54 |
+
return (
|
| 55 |
+
"inductor_configs",
|
| 56 |
+
"torch_determinism_configs",
|
| 57 |
+
"cuda_matmul_precision_configs",
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
@staticmethod
|
| 61 |
+
def inductor_configs() -> dict[str, object]:
|
| 62 |
+
"""Get portable Inductor configuration settings.
|
| 63 |
+
|
| 64 |
+
Returns:
|
| 65 |
+
A dictionary containing Inductor configuration settings,
|
| 66 |
+
including private configs.
|
| 67 |
+
"""
|
| 68 |
+
from torch._inductor import config
|
| 69 |
+
|
| 70 |
+
return config.save_config_portable(ignore_private_configs=False)
|
| 71 |
+
|
| 72 |
+
@staticmethod
|
| 73 |
+
def torch_determinism_configs() -> dict[str, object]:
|
| 74 |
+
"""Get PyTorch deterministic algorithm configuration settings.
|
| 75 |
+
|
| 76 |
+
Returns:
|
| 77 |
+
A dictionary containing deterministic algorithm settings:
|
| 78 |
+
- Whether deterministic algorithms are enabled
|
| 79 |
+
- Whether deterministic algorithm warnings are enabled
|
| 80 |
+
- Fill uninitialized memory setting
|
| 81 |
+
"""
|
| 82 |
+
return {
|
| 83 |
+
"torch.are_deterministic_algorithms_enabled": torch.are_deterministic_algorithms_enabled(),
|
| 84 |
+
"torch.is_deterministic_algorithms_warn_only_enabled": (
|
| 85 |
+
torch.is_deterministic_algorithms_warn_only_enabled()
|
| 86 |
+
),
|
| 87 |
+
"torch.utils.deterministic.fill_uninitialized_memory": getattr(
|
| 88 |
+
torch.utils.deterministic, "fill_uninitialized_memory", None
|
| 89 |
+
),
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
@staticmethod
|
| 93 |
+
def cuda_matmul_precision_configs() -> dict[str, object]:
|
| 94 |
+
"""Get CUDA matrix multiplication precision configuration settings.
|
| 95 |
+
|
| 96 |
+
Returns:
|
| 97 |
+
A dictionary containing CUDA matmul precision settings:
|
| 98 |
+
- FP32 precision setting
|
| 99 |
+
- FP16 reduced precision reduction allowance
|
| 100 |
+
- BF16 reduced precision reduction allowance
|
| 101 |
+
"""
|
| 102 |
+
return {
|
| 103 |
+
"torch.backends.cuda.matmul.fp32_precision": torch.backends.cuda.matmul.fp32_precision,
|
| 104 |
+
"torch.backends.cuda.matmul.allow_fp16_reduced_precision_reduction": (
|
| 105 |
+
torch.backends.cuda.matmul.allow_fp16_reduced_precision_reduction
|
| 106 |
+
),
|
| 107 |
+
"torch.backends.cuda.matmul.allow_bf16_reduced_precision_reduction": (
|
| 108 |
+
torch.backends.cuda.matmul.allow_bf16_reduced_precision_reduction
|
| 109 |
+
),
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
class _CompileContext(_Context):
|
| 114 |
+
"""Context provider for compilation-related configuration and environment settings.
|
| 115 |
+
|
| 116 |
+
Collects information that affects compilation behavior, such as PyTorch and Triton
|
| 117 |
+
versions, runtime environment, and accelerator properties.
|
| 118 |
+
"""
|
| 119 |
+
|
| 120 |
+
@override
|
| 121 |
+
@staticmethod
|
| 122 |
+
def forms_of_context() -> Sequence[str]:
|
| 123 |
+
"""Return the compile context forms provided by this class.
|
| 124 |
+
|
| 125 |
+
Returns:
|
| 126 |
+
A sequence containing the available compile context forms:
|
| 127 |
+
- "torch_version_hash": PyTorch version hash
|
| 128 |
+
- "triton_version_hash": Triton version hash (if available)
|
| 129 |
+
- "runtime": Runtime type (CUDA/HIP/None)
|
| 130 |
+
- "runtime_version": Runtime version string
|
| 131 |
+
- "accelerator_properties": GPU/accelerator properties
|
| 132 |
+
"""
|
| 133 |
+
return (
|
| 134 |
+
"torch_version_hash",
|
| 135 |
+
"triton_version_hash",
|
| 136 |
+
"runtime",
|
| 137 |
+
"runtime_version",
|
| 138 |
+
"accelerator_properties",
|
| 139 |
+
)
|
| 140 |
+
|
| 141 |
+
@cache
|
| 142 |
+
@staticmethod
|
| 143 |
+
def torch_version_hash() -> str:
|
| 144 |
+
"""Get base64-encoded PyTorch version hash.
|
| 145 |
+
|
| 146 |
+
Returns:
|
| 147 |
+
A base64-encoded string representing the PyTorch version hash.
|
| 148 |
+
"""
|
| 149 |
+
from torch._inductor.codecache import torch_key
|
| 150 |
+
|
| 151 |
+
return b64encode(torch_key()).decode()
|
| 152 |
+
|
| 153 |
+
@cache
|
| 154 |
+
@staticmethod
|
| 155 |
+
def triton_version_hash() -> str | None:
|
| 156 |
+
"""Get Triton version key if Triton is available.
|
| 157 |
+
|
| 158 |
+
Returns:
|
| 159 |
+
Triton version key if Triton is available, None otherwise.
|
| 160 |
+
"""
|
| 161 |
+
from torch._inductor.runtime.triton_compat import HAS_TRITON, triton_key
|
| 162 |
+
|
| 163 |
+
return triton_key() if HAS_TRITON else None
|
| 164 |
+
|
| 165 |
+
@cache
|
| 166 |
+
@staticmethod
|
| 167 |
+
def runtime() -> str | None:
|
| 168 |
+
"""Determine the runtime type based on available backends.
|
| 169 |
+
|
| 170 |
+
Returns:
|
| 171 |
+
"CUDA" if CUDA is available, "HIP" if HIP is available, None otherwise.
|
| 172 |
+
"""
|
| 173 |
+
return "CUDA" if torch.version.cuda else "HIP" if torch.version.hip else None
|
| 174 |
+
|
| 175 |
+
@cache
|
| 176 |
+
@staticmethod
|
| 177 |
+
def runtime_version() -> str | None:
|
| 178 |
+
"""Get the version string for the detected runtime.
|
| 179 |
+
|
| 180 |
+
Returns:
|
| 181 |
+
Version string for the current runtime (CUDA or HIP), or None if
|
| 182 |
+
no supported runtime is detected.
|
| 183 |
+
"""
|
| 184 |
+
return {
|
| 185 |
+
"CUDA": torch.version.cuda,
|
| 186 |
+
"HIP": torch.version.hip,
|
| 187 |
+
"None": None,
|
| 188 |
+
}.get(_CompileContext.runtime() or "None")
|
| 189 |
+
|
| 190 |
+
@cache
|
| 191 |
+
@staticmethod
|
| 192 |
+
def accelerator_properties() -> str | None:
|
| 193 |
+
"""Get string representation of CUDA device properties.
|
| 194 |
+
|
| 195 |
+
Returns:
|
| 196 |
+
String representation of CUDA device properties if a runtime is
|
| 197 |
+
available, None otherwise.
|
| 198 |
+
"""
|
| 199 |
+
return (
|
| 200 |
+
repr(torch.cuda.get_device_properties())
|
| 201 |
+
if _CompileContext.runtime() and torch.cuda.is_available()
|
| 202 |
+
else None
|
| 203 |
+
)
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
class SelectedRuntimeContext(TypedDict):
|
| 207 |
+
inductor_configs: bool
|
| 208 |
+
torch_determinism_configs: bool
|
| 209 |
+
cuda_matmul_precision_configs: bool
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
class SelectedCompileContext(TypedDict):
|
| 213 |
+
torch_version_hash: bool
|
| 214 |
+
triton_version_hash: bool
|
| 215 |
+
runtime: bool
|
| 216 |
+
runtime_version: bool
|
| 217 |
+
accelerator_properties: bool
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
class IsolationSchema(TypedDict):
|
| 221 |
+
"""Schema for specifying which context forms to include in cache isolation.
|
| 222 |
+
|
| 223 |
+
Attributes:
|
| 224 |
+
runtime_context: Either True (include all runtime context), False (exclude all),
|
| 225 |
+
or a SelectedRuntimeContext dict specifying which forms to include.
|
| 226 |
+
compile_context: Either True (include all compile context), False (exclude all),
|
| 227 |
+
or a SelectedCompileContext dict specifying which forms to include.
|
| 228 |
+
"""
|
| 229 |
+
|
| 230 |
+
runtime_context: SelectedRuntimeContext | bool
|
| 231 |
+
compile_context: SelectedCompileContext | bool
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
_DEFAULT_ISOLATION_SCHEMA: IsolationSchema = IsolationSchema(
|
| 235 |
+
runtime_context=True, compile_context=True
|
| 236 |
+
)
|
| 237 |
+
|
| 238 |
+
|
| 239 |
+
def _collect_runtime_context(
|
| 240 |
+
selection: SelectedRuntimeContext | bool,
|
| 241 |
+
) -> dict[str, object] | None:
|
| 242 |
+
"""Collect runtime context based on selection.
|
| 243 |
+
|
| 244 |
+
Args:
|
| 245 |
+
selection: True to include all, False to exclude all, or a dict
|
| 246 |
+
specifying which forms to include.
|
| 247 |
+
|
| 248 |
+
Returns:
|
| 249 |
+
Dictionary of selected context data, or None if excluded.
|
| 250 |
+
"""
|
| 251 |
+
return {
|
| 252 |
+
form: getattr(_RuntimeContext, form)()
|
| 253 |
+
for form in _RuntimeContext.forms_of_context()
|
| 254 |
+
if selection is True or (selection and selection.get(form, False))
|
| 255 |
+
} or None
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
def _collect_compile_context(
|
| 259 |
+
selection: SelectedCompileContext | bool,
|
| 260 |
+
) -> dict[str, object] | None:
|
| 261 |
+
"""Collect compile context based on selection.
|
| 262 |
+
|
| 263 |
+
Args:
|
| 264 |
+
selection: True to include all, False to exclude all, or a dict
|
| 265 |
+
specifying which forms to include.
|
| 266 |
+
|
| 267 |
+
Returns:
|
| 268 |
+
Dictionary of selected context data, or None if excluded.
|
| 269 |
+
"""
|
| 270 |
+
return {
|
| 271 |
+
form: getattr(_CompileContext, form)()
|
| 272 |
+
for form in _CompileContext.forms_of_context()
|
| 273 |
+
if selection is True or (selection and selection.get(form, False))
|
| 274 |
+
} or None
|
| 275 |
+
|
| 276 |
+
|
| 277 |
+
def _isolation_context(
|
| 278 |
+
ischema: IsolationSchema = _DEFAULT_ISOLATION_SCHEMA,
|
| 279 |
+
) -> dict[str, object]:
|
| 280 |
+
"""Generate context data based on the isolation schema.
|
| 281 |
+
|
| 282 |
+
Args:
|
| 283 |
+
ischema: Schema specifying which context forms to include.
|
| 284 |
+
Defaults to including all runtime and compile context.
|
| 285 |
+
|
| 286 |
+
Returns:
|
| 287 |
+
A dictionary containing the selected context data with keys
|
| 288 |
+
"runtime_context" and "compile_context", where each value is
|
| 289 |
+
either None (if excluded) or a dict of context form data.
|
| 290 |
+
"""
|
| 291 |
+
return {
|
| 292 |
+
"runtime_context": _collect_runtime_context(ischema["runtime_context"]),
|
| 293 |
+
"compile_context": _collect_compile_context(ischema["compile_context"]),
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
|
| 297 |
+
def _isolation_key(ischema: IsolationSchema = _DEFAULT_ISOLATION_SCHEMA) -> str:
|
| 298 |
+
"""Generate a unique key for the given isolation schema.
|
| 299 |
+
|
| 300 |
+
Args:
|
| 301 |
+
ischema: Schema specifying which context forms to include.
|
| 302 |
+
Defaults to including all runtime and compile context.
|
| 303 |
+
|
| 304 |
+
Returns:
|
| 305 |
+
A 32-character hexadecimal string that uniquely identifies
|
| 306 |
+
the context specified by the isolation schema.
|
| 307 |
+
"""
|
| 308 |
+
return sha256(
|
| 309 |
+
json.dumps(_isolation_context(ischema), sort_keys=True).encode()
|
| 310 |
+
).hexdigest()[:32]
|
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/caching/encoders.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Custom encoder functions
|
| 3 |
+
|
| 4 |
+
This module provides reusable encoder functions that convert function parameters
|
| 5 |
+
into JSON-serializable dictionaries for caching purposes.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from typing_extensions import TypedDict
|
| 9 |
+
|
| 10 |
+
import torch
|
| 11 |
+
from torch import Tensor
|
| 12 |
+
from torch._inductor.pattern_matcher import Match
|
| 13 |
+
from torch._inductor.runtime.caching.utils import _encode_tensor, EncodedTensor
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class ShouldPadEncodedParams(TypedDict):
|
| 17 |
+
"""TypedDict for encoded should_pad parameters."""
|
| 18 |
+
|
| 19 |
+
mat1: EncodedTensor
|
| 20 |
+
mat2: EncodedTensor
|
| 21 |
+
op: str
|
| 22 |
+
input: EncodedTensor | None
|
| 23 |
+
mat1_exclude_padding_time: bool
|
| 24 |
+
mat2_exclude_padding_time: bool
|
| 25 |
+
tf32: bool
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def should_pad_params_encoder(
|
| 29 |
+
match: Match,
|
| 30 |
+
mat1: Tensor,
|
| 31 |
+
mat2: Tensor,
|
| 32 |
+
op: torch._ops.OpOverloadPacket,
|
| 33 |
+
input: Tensor | None = None,
|
| 34 |
+
) -> ShouldPadEncodedParams:
|
| 35 |
+
"""Encode parameters for _should_pad into a human-readable dict.
|
| 36 |
+
|
| 37 |
+
This encoder extracts only the information needed for caching:
|
| 38 |
+
- Tensor shape, stride, and dtype (not the actual data)
|
| 39 |
+
- Whether padding time should be excluded for mat1 and mat2
|
| 40 |
+
- The operation as a string
|
| 41 |
+
|
| 42 |
+
Args:
|
| 43 |
+
match: The pattern match object
|
| 44 |
+
mat1: First matrix tensor
|
| 45 |
+
mat2: Second matrix tensor
|
| 46 |
+
op: The operation being performed
|
| 47 |
+
input: Optional input tensor for addmm
|
| 48 |
+
|
| 49 |
+
Returns:
|
| 50 |
+
A dict containing the encoded parameters in human-readable form
|
| 51 |
+
"""
|
| 52 |
+
# Import here to avoid circular dependency
|
| 53 |
+
from torch._inductor.fx_passes.pad_mm import should_exclude_padding_time
|
| 54 |
+
|
| 55 |
+
return ShouldPadEncodedParams(
|
| 56 |
+
mat1=_encode_tensor(mat1),
|
| 57 |
+
mat2=_encode_tensor(mat2),
|
| 58 |
+
op=str(op),
|
| 59 |
+
input=_encode_tensor(input) if input is not None else None,
|
| 60 |
+
mat1_exclude_padding_time=should_exclude_padding_time(match, "mat1"),
|
| 61 |
+
mat2_exclude_padding_time=should_exclude_padding_time(match, "mat2"),
|
| 62 |
+
tf32=False
|
| 63 |
+
if mat1.dtype != torch.float32
|
| 64 |
+
else bool(
|
| 65 |
+
torch.backends.cuda.matmul.fp32_precision == "tf32"
|
| 66 |
+
or torch.backends.mkldnn.fp32_precision == "tf32"
|
| 67 |
+
),
|
| 68 |
+
)
|
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/caching/exceptions.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Exception classes for PyTorch Inductor runtime caching.
|
| 2 |
+
|
| 3 |
+
This module defines a hierarchy of exceptions used throughout the caching system.
|
| 4 |
+
All custom exceptions inherit from CacheError, with UserError serving as a base
|
| 5 |
+
for user-facing errors that also inherit from TypeError for compatibility.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from threading import Lock
|
| 9 |
+
|
| 10 |
+
from filelock import BaseFileLock
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class CacheError(Exception):
|
| 14 |
+
"""Base class for all caching-related errors.
|
| 15 |
+
|
| 16 |
+
This is the root exception class for all custom exceptions raised by the caching
|
| 17 |
+
module, providing a common interface for error handling and logging.
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class SystemError(CacheError, RuntimeError):
|
| 22 |
+
"""Base class for system-level caching errors.
|
| 23 |
+
|
| 24 |
+
This class represents errors that occur during cache operations, such as
|
| 25 |
+
storage or retrieval failures. It inherits from RuntimeError to indicate
|
| 26 |
+
that the error is not caused by user input.
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class LockTimeoutError(SystemError):
|
| 31 |
+
"""Error raised when a lock operation times out.
|
| 32 |
+
|
| 33 |
+
This exception is raised when a lock operation exceeds the specified timeout
|
| 34 |
+
limit, indicating that the lock could not be acquired within the allotted time.
|
| 35 |
+
"""
|
| 36 |
+
|
| 37 |
+
def __init__(self, lock: Lock, timeout: float) -> None:
|
| 38 |
+
"""Initialize the lock timeout error with detailed lock information.
|
| 39 |
+
|
| 40 |
+
Args:
|
| 41 |
+
lock: The lock object that timed out.
|
| 42 |
+
timeout: The timeout limit that was exceeded.
|
| 43 |
+
"""
|
| 44 |
+
super().__init__(f"Failed to acquire lock {lock} within {timeout} seconds.")
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
class FileLockTimeoutError(SystemError):
|
| 48 |
+
"""Error raised when a file lock operation times out.
|
| 49 |
+
|
| 50 |
+
This exception is raised when a file lock operation exceeds the specified timeout
|
| 51 |
+
limit, indicating that the lock could not be acquired within the allotted time.
|
| 52 |
+
"""
|
| 53 |
+
|
| 54 |
+
def __init__(self, flock: BaseFileLock, timeout: float) -> None:
|
| 55 |
+
"""Initialize the file lock timeout error with detailed lock information.
|
| 56 |
+
|
| 57 |
+
Args:
|
| 58 |
+
flock: The file lock object that timed out.
|
| 59 |
+
timeout: The timeout limit that was exceeded.
|
| 60 |
+
"""
|
| 61 |
+
super().__init__(
|
| 62 |
+
f"Failed to acquire file lock {flock} within {timeout} seconds."
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
class UserError(CacheError, TypeError):
|
| 67 |
+
"""Base class for user-facing cache errors that also inherit from TypeError.
|
| 68 |
+
|
| 69 |
+
This class combines CacheError with TypeError to provide compatibility
|
| 70 |
+
with existing exception handling patterns while maintaining the cache
|
| 71 |
+
error hierarchy. All user-facing cache errors should inherit from this class.
|
| 72 |
+
"""
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
class KeyEncodingError(UserError):
|
| 76 |
+
"""Base class for errors that occur during cache key encoding operations.
|
| 77 |
+
|
| 78 |
+
Raised when cache keys cannot be properly encoded for storage or transmission.
|
| 79 |
+
This includes serialization, hashing, or other encoding-related failures.
|
| 80 |
+
"""
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
class ValueEncodingError(UserError):
|
| 84 |
+
"""Base class for errors that occur during cache value encoding operations.
|
| 85 |
+
|
| 86 |
+
Raised when cache values cannot be properly encoded for storage or transmission.
|
| 87 |
+
This includes serialization, compression, or other encoding-related failures.
|
| 88 |
+
"""
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
class ValueDecodingError(UserError):
|
| 92 |
+
"""Base class for errors that occur during cache value decoding operations.
|
| 93 |
+
|
| 94 |
+
Raised when cached values cannot be properly decoded during retrieval.
|
| 95 |
+
This includes deserialization, decompression, or other decoding-related failures.
|
| 96 |
+
"""
|
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/caching/implementations.py
ADDED
|
@@ -0,0 +1,422 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Cache implementation classes
|
| 2 |
+
|
| 3 |
+
This module provides concrete implementations of caching backends including
|
| 4 |
+
in-memory, on-disk, and remote caching strategies. Each implementation follows
|
| 5 |
+
the abstract _CacheImpl interface and provides thread-safe operations with
|
| 6 |
+
appropriate locking mechanisms.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from abc import ABC, abstractmethod
|
| 10 |
+
from collections.abc import Generator
|
| 11 |
+
from contextlib import contextmanager
|
| 12 |
+
from dataclasses import dataclass
|
| 13 |
+
from hashlib import sha256
|
| 14 |
+
from io import BufferedReader, BufferedWriter
|
| 15 |
+
from os import PathLike
|
| 16 |
+
from pathlib import Path
|
| 17 |
+
from threading import Lock
|
| 18 |
+
from typing import Generic, TypeVar
|
| 19 |
+
from typing_extensions import override
|
| 20 |
+
|
| 21 |
+
from filelock import BaseFileLock, FileLock
|
| 22 |
+
|
| 23 |
+
from . import locks
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
# Type variable for cache value types
|
| 27 |
+
_V = TypeVar("_V")
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
@dataclass
|
| 31 |
+
class Hit(Generic[_V]):
|
| 32 |
+
"""Result wrapper for hits on cache get operations.
|
| 33 |
+
|
| 34 |
+
Allows distinguishing between a cache miss and a cached None value.
|
| 35 |
+
|
| 36 |
+
Attributes:
|
| 37 |
+
value: The cached value. The type depends on the cache implementation:
|
| 38 |
+
- InMemoryCache: object (any Python object)
|
| 39 |
+
- OnDiskCache: bytes
|
| 40 |
+
- RemoteCache: bytes
|
| 41 |
+
"""
|
| 42 |
+
|
| 43 |
+
value: _V
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
class Miss:
|
| 47 |
+
"""Sentinel class representing a cache miss.
|
| 48 |
+
|
| 49 |
+
Used to distinguish between a cached None value and a cache miss
|
| 50 |
+
when None is a valid cached value.
|
| 51 |
+
"""
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
# Singleton instance for cache miss sentinel
|
| 55 |
+
miss = Miss()
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
class _CacheImpl(ABC, Generic[_V]):
|
| 59 |
+
"""Abstract base class for cache implementations.
|
| 60 |
+
|
| 61 |
+
This class defines the interface that all cache implementations must follow.
|
| 62 |
+
It provides thread-safe operations through a locking mechanism and supports
|
| 63 |
+
both get and insert operations.
|
| 64 |
+
|
| 65 |
+
Type Parameters:
|
| 66 |
+
_V: The type of values stored in the cache. Each implementation specifies
|
| 67 |
+
its own value type (e.g., object for in-memory, bytes for on-disk).
|
| 68 |
+
"""
|
| 69 |
+
|
| 70 |
+
def __init__(self) -> None:
|
| 71 |
+
"""Initialize the cache implementation with a threading lock."""
|
| 72 |
+
self._lock: Lock = Lock()
|
| 73 |
+
|
| 74 |
+
@property
|
| 75 |
+
def lock(self) -> locks._LockProtocol:
|
| 76 |
+
"""Get a context manager for acquiring the cache lock.
|
| 77 |
+
|
| 78 |
+
Locking of the cache is not done by the implementation itself, but by the
|
| 79 |
+
interface that uses it. The interface may want to hold the lock for longer
|
| 80 |
+
than a single cache operation, for example when dealing with multiple
|
| 81 |
+
cache implementations at once, so we leave that decision up to the interface.
|
| 82 |
+
|
| 83 |
+
Args:
|
| 84 |
+
timeout: Optional timeout in seconds (float) for acquiring the lock.
|
| 85 |
+
|
| 86 |
+
Returns:
|
| 87 |
+
A callable that returns a context manager for the lock.
|
| 88 |
+
"""
|
| 89 |
+
|
| 90 |
+
def _lock_with_timeout(
|
| 91 |
+
timeout: float | None = None,
|
| 92 |
+
) -> locks._LockContextManager:
|
| 93 |
+
return locks._acquire_lock_with_timeout(self._lock, timeout)
|
| 94 |
+
|
| 95 |
+
return _lock_with_timeout
|
| 96 |
+
|
| 97 |
+
@abstractmethod
|
| 98 |
+
def get(self, key: str) -> Hit[_V] | None:
|
| 99 |
+
"""Retrieve a value from the cache.
|
| 100 |
+
|
| 101 |
+
Args:
|
| 102 |
+
key: The key to look up in the cache (must be str).
|
| 103 |
+
|
| 104 |
+
Returns:
|
| 105 |
+
A Hit object on cache hit where Hit.value is the cached value,
|
| 106 |
+
or None on cache miss.
|
| 107 |
+
"""
|
| 108 |
+
|
| 109 |
+
@abstractmethod
|
| 110 |
+
def insert(self, key: str, value: _V) -> bool:
|
| 111 |
+
"""Insert a key-value pair into the cache.
|
| 112 |
+
|
| 113 |
+
Args:
|
| 114 |
+
key: The key to insert (must be str).
|
| 115 |
+
value: The value to associate with the key.
|
| 116 |
+
|
| 117 |
+
Returns:
|
| 118 |
+
True if the insertion was successful, False if not inserted.
|
| 119 |
+
"""
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
class _InMemoryCacheImpl(_CacheImpl[_V], Generic[_V]):
|
| 123 |
+
"""In-memory cache implementation using a dictionary.
|
| 124 |
+
|
| 125 |
+
This implementation stores key-value pairs in a Python dictionary,
|
| 126 |
+
with keys being pickled for consistent hashing. It provides fast
|
| 127 |
+
access but is limited by available memory and process lifetime.
|
| 128 |
+
|
| 129 |
+
Type Parameters:
|
| 130 |
+
_V: The type of values stored in the cache.
|
| 131 |
+
"""
|
| 132 |
+
|
| 133 |
+
def __init__(self) -> None:
|
| 134 |
+
"""Initialize the in-memory cache with an empty dictionary."""
|
| 135 |
+
super().__init__()
|
| 136 |
+
self._memory: dict[str, _V] = {}
|
| 137 |
+
|
| 138 |
+
@override
|
| 139 |
+
def get(self, key: str) -> Hit[_V] | None:
|
| 140 |
+
"""Retrieve a value from the in-memory cache.
|
| 141 |
+
|
| 142 |
+
Args:
|
| 143 |
+
key: The key to look up (must be str).
|
| 144 |
+
|
| 145 |
+
Returns:
|
| 146 |
+
A Hit object on cache hit where Hit.value is the cached value,
|
| 147 |
+
or None on cache miss.
|
| 148 |
+
"""
|
| 149 |
+
if key in self._memory:
|
| 150 |
+
return Hit(value=self._memory[key])
|
| 151 |
+
return None
|
| 152 |
+
|
| 153 |
+
@override
|
| 154 |
+
def insert(self, key: str, value: _V) -> bool:
|
| 155 |
+
"""Insert a key-value pair into the in-memory cache.
|
| 156 |
+
|
| 157 |
+
Args:
|
| 158 |
+
key: The key to insert (must be str).
|
| 159 |
+
value: The value to associate with the key.
|
| 160 |
+
|
| 161 |
+
Returns:
|
| 162 |
+
True if the insertion was successful (key was new),
|
| 163 |
+
False if not inserted (key already existed).
|
| 164 |
+
"""
|
| 165 |
+
if key not in self._memory:
|
| 166 |
+
self._memory[key] = value
|
| 167 |
+
return True
|
| 168 |
+
return False
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
class _OnDiskCacheImpl(_CacheImpl[bytes]):
|
| 172 |
+
"""On-disk cache implementation using file system storage.
|
| 173 |
+
|
| 174 |
+
This implementation stores cached data as files on disk, with version
|
| 175 |
+
headers to handle cache invalidation. It uses file locking to ensure
|
| 176 |
+
thread safety across processes and provides persistent storage that
|
| 177 |
+
survives process restarts.
|
| 178 |
+
|
| 179 |
+
The value type is 'bytes' since data must be serialized to bytes for disk storage.
|
| 180 |
+
|
| 181 |
+
Attributes:
|
| 182 |
+
_version: _Version number for cache format compatibility.
|
| 183 |
+
_version_header_length: Length of the version header in bytes.
|
| 184 |
+
"""
|
| 185 |
+
|
| 186 |
+
_version: int = 0
|
| 187 |
+
_version_header_length: int = 4
|
| 188 |
+
|
| 189 |
+
def __init__(self, sub_dir: PathLike[str] | None = None) -> None:
|
| 190 |
+
"""Initialize the on-disk cache with a specified subdirectory.
|
| 191 |
+
|
| 192 |
+
Args:
|
| 193 |
+
sub_dir: Subdirectory name within the cache directory.
|
| 194 |
+
Defaults to empty string if not specified.
|
| 195 |
+
"""
|
| 196 |
+
self._cache_dir: Path = self._base_dir / (sub_dir or "")
|
| 197 |
+
self._flock: BaseFileLock = FileLock(str(self._cache_dir / "dir.lock"))
|
| 198 |
+
|
| 199 |
+
@property
|
| 200 |
+
def _base_dir(self) -> Path:
|
| 201 |
+
"""Get the base directory for cache storage.
|
| 202 |
+
|
| 203 |
+
Returns:
|
| 204 |
+
Path to the cache directory based on the default cache dir
|
| 205 |
+
and the specified subdirectory.
|
| 206 |
+
"""
|
| 207 |
+
from torch._inductor.runtime.runtime_utils import default_cache_dir
|
| 208 |
+
|
| 209 |
+
return Path(default_cache_dir(), "cache")
|
| 210 |
+
|
| 211 |
+
def _fpath_from_key(self, key: str) -> Path:
|
| 212 |
+
"""Generate a file path from a cache key.
|
| 213 |
+
|
| 214 |
+
Args:
|
| 215 |
+
key: The cache key to convert to a file path (must be str).
|
| 216 |
+
|
| 217 |
+
Returns:
|
| 218 |
+
A Path object representing the file location for this key.
|
| 219 |
+
"""
|
| 220 |
+
return self._cache_dir / key
|
| 221 |
+
|
| 222 |
+
@classmethod
|
| 223 |
+
def _version_header(cls) -> bytes:
|
| 224 |
+
"""Generate the version header bytes.
|
| 225 |
+
|
| 226 |
+
Returns:
|
| 227 |
+
A byte string representing the current cache version header.
|
| 228 |
+
"""
|
| 229 |
+
return sha256(str(cls._version).encode()).digest()[: cls._version_header_length]
|
| 230 |
+
|
| 231 |
+
def _version_header_matches(self, fp: BufferedReader) -> bool:
|
| 232 |
+
"""Check if the file's version header matches the current version.
|
| 233 |
+
|
| 234 |
+
Args:
|
| 235 |
+
fp: File pointer positioned at the start of the file.
|
| 236 |
+
|
| 237 |
+
Returns:
|
| 238 |
+
True if the version header matches, False otherwise.
|
| 239 |
+
"""
|
| 240 |
+
return fp.read(self._version_header_length) == self._version_header()
|
| 241 |
+
|
| 242 |
+
def _write_version_header(self, fp: BufferedWriter) -> None:
|
| 243 |
+
"""Write the version header to a file.
|
| 244 |
+
|
| 245 |
+
Args:
|
| 246 |
+
fp: File pointer where the version header should be written.
|
| 247 |
+
"""
|
| 248 |
+
fp.write(self._version_header())
|
| 249 |
+
|
| 250 |
+
@override
|
| 251 |
+
@property
|
| 252 |
+
def lock(self) -> locks._LockProtocol:
|
| 253 |
+
"""Get a context manager for acquiring the file lock.
|
| 254 |
+
|
| 255 |
+
Uses file locking to ensure thread safety across processes.
|
| 256 |
+
|
| 257 |
+
Args:
|
| 258 |
+
timeout: Optional timeout in seconds (float) for acquiring the file lock.
|
| 259 |
+
|
| 260 |
+
Returns:
|
| 261 |
+
A callable that returns a context manager for the file lock.
|
| 262 |
+
"""
|
| 263 |
+
|
| 264 |
+
def _lock_with_timeout(
|
| 265 |
+
timeout: float | None = None,
|
| 266 |
+
) -> locks._LockContextManager:
|
| 267 |
+
return locks._acquire_flock_with_timeout(self._flock, timeout)
|
| 268 |
+
|
| 269 |
+
return _lock_with_timeout
|
| 270 |
+
|
| 271 |
+
@override
|
| 272 |
+
def get(self, key: str) -> Hit[bytes] | None:
|
| 273 |
+
"""Retrieve a value from the on-disk cache.
|
| 274 |
+
|
| 275 |
+
Args:
|
| 276 |
+
key: The key to look up in the cache (must be str).
|
| 277 |
+
|
| 278 |
+
Returns:
|
| 279 |
+
A Hit object on cache hit where Hit.value is the cached value (bytes),
|
| 280 |
+
or None on cache miss or version mismatch.
|
| 281 |
+
"""
|
| 282 |
+
fpath: Path = self._fpath_from_key(key)
|
| 283 |
+
|
| 284 |
+
if not fpath.is_file():
|
| 285 |
+
return None
|
| 286 |
+
|
| 287 |
+
pickled_value: bytes | None = None
|
| 288 |
+
with open(fpath, "rb") as fp:
|
| 289 |
+
if self._version_header_matches(fp):
|
| 290 |
+
pickled_value = fp.read()
|
| 291 |
+
|
| 292 |
+
if not pickled_value:
|
| 293 |
+
# if pickled_value is still None, even though the file exists, then
|
| 294 |
+
# we know that the version header did not match. in this case implementation
|
| 295 |
+
# is up to preference, we choose to remove entries that do not match
|
| 296 |
+
# the version header so that the key can be re-cached later with the correct
|
| 297 |
+
# version header
|
| 298 |
+
fpath.unlink()
|
| 299 |
+
return None
|
| 300 |
+
|
| 301 |
+
return Hit(value=pickled_value)
|
| 302 |
+
|
| 303 |
+
@override
|
| 304 |
+
def insert(self, key: str, value: bytes) -> bool:
|
| 305 |
+
"""Insert a key-value pair into the on-disk cache.
|
| 306 |
+
|
| 307 |
+
Args:
|
| 308 |
+
key: The key to insert (must be str).
|
| 309 |
+
value: The value to associate with the key (must be bytes).
|
| 310 |
+
|
| 311 |
+
Returns:
|
| 312 |
+
True if successfully inserted, False if the key already exists
|
| 313 |
+
with a valid version.
|
| 314 |
+
"""
|
| 315 |
+
fpath: Path = self._fpath_from_key(key)
|
| 316 |
+
fpath.parent.mkdir(parents=True, exist_ok=True)
|
| 317 |
+
|
| 318 |
+
r_fp, w_fp, inserted = None, None, False
|
| 319 |
+
try:
|
| 320 |
+
w_fp = open(fpath, "xb") # noqa: SIM115
|
| 321 |
+
except FileExistsError:
|
| 322 |
+
is_stale: bool = False
|
| 323 |
+
with open(fpath, "rb") as r_fp:
|
| 324 |
+
is_stale = not self._version_header_matches(r_fp)
|
| 325 |
+
|
| 326 |
+
if is_stale:
|
| 327 |
+
# same story as above, in this case the version header doesn't
|
| 328 |
+
# match so we choose to remove the old entry so that the new
|
| 329 |
+
# k/v pair can be cached
|
| 330 |
+
fpath.unlink()
|
| 331 |
+
w_fp = open(fpath, "xb") # noqa: SIM115
|
| 332 |
+
else:
|
| 333 |
+
w_fp = None
|
| 334 |
+
finally:
|
| 335 |
+
if w_fp:
|
| 336 |
+
try:
|
| 337 |
+
self._write_version_header(w_fp)
|
| 338 |
+
w_fp.write(value)
|
| 339 |
+
inserted = True
|
| 340 |
+
finally:
|
| 341 |
+
w_fp.close()
|
| 342 |
+
|
| 343 |
+
return inserted
|
| 344 |
+
|
| 345 |
+
|
| 346 |
+
try:
|
| 347 |
+
from .fb.implementations import _RemoteCacheImpl
|
| 348 |
+
except ModuleNotFoundError:
|
| 349 |
+
|
| 350 |
+
class _RemoteCacheImpl(_CacheImpl[bytes]):
|
| 351 |
+
"""Fallback remote cache implementation for non-Facebook environments.
|
| 352 |
+
|
| 353 |
+
This is a no-op implementation that always raises NotImplementedError.
|
| 354 |
+
The actual remote cache implementation is provided in the `.fb` module
|
| 355 |
+
for Facebook-specific environments.
|
| 356 |
+
|
| 357 |
+
The value type is 'bytes' for consistency with the FB implementation.
|
| 358 |
+
|
| 359 |
+
Attributes:
|
| 360 |
+
_version: _Version number for cache format compatibility.
|
| 361 |
+
has_strong_consistency: Whether the remote cache provides strong
|
| 362 |
+
consistency guarantees.
|
| 363 |
+
"""
|
| 364 |
+
|
| 365 |
+
_version: int = 0
|
| 366 |
+
has_strong_consistency: bool = False
|
| 367 |
+
|
| 368 |
+
def __init__(self) -> None:
|
| 369 |
+
"""Initialize the fallback remote cache implementation.
|
| 370 |
+
|
| 371 |
+
Note: We don't need to initialize any form of lock since this
|
| 372 |
+
implementation provides a pseudo-lock context manager.
|
| 373 |
+
"""
|
| 374 |
+
|
| 375 |
+
@override
|
| 376 |
+
@property
|
| 377 |
+
def lock(self) -> locks._LockProtocol:
|
| 378 |
+
"""Get a pseudo lock that does nothing.
|
| 379 |
+
|
| 380 |
+
Most remote cache implementations don't have an ability to implement
|
| 381 |
+
any form of locking, so we provide a no-op pseudo-lock for consistency
|
| 382 |
+
with the interface.
|
| 383 |
+
|
| 384 |
+
Args:
|
| 385 |
+
timeout: Optional timeout in seconds (float). Ignored in this
|
| 386 |
+
|
| 387 |
+
Returns:
|
| 388 |
+
A callable that returns a no-op context manager.
|
| 389 |
+
"""
|
| 390 |
+
|
| 391 |
+
@contextmanager
|
| 392 |
+
def pseudo_lock(
|
| 393 |
+
timeout: float | None = None,
|
| 394 |
+
) -> Generator[None, None, None]:
|
| 395 |
+
yield
|
| 396 |
+
|
| 397 |
+
return pseudo_lock
|
| 398 |
+
|
| 399 |
+
@override
|
| 400 |
+
def get(self, key: str) -> Hit[bytes] | None:
|
| 401 |
+
"""Raise NotImplementedError for remote cache get operations.
|
| 402 |
+
|
| 403 |
+
Args:
|
| 404 |
+
key: The key to look up (must be str, ignored).
|
| 405 |
+
|
| 406 |
+
Raises:
|
| 407 |
+
NotImplementedError: Always raised as this is a fallback implementation.
|
| 408 |
+
"""
|
| 409 |
+
raise NotImplementedError
|
| 410 |
+
|
| 411 |
+
@override
|
| 412 |
+
def insert(self, key: str, value: bytes) -> bool:
|
| 413 |
+
"""Raise NotImplementedError for remote cache insert operations.
|
| 414 |
+
|
| 415 |
+
Args:
|
| 416 |
+
key: The key to insert (must be str, ignored).
|
| 417 |
+
value: The value to insert (must be bytes, ignored).
|
| 418 |
+
|
| 419 |
+
Raises:
|
| 420 |
+
NotImplementedError: Always raised as this is a fallback implementation.
|
| 421 |
+
"""
|
| 422 |
+
raise NotImplementedError
|
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/caching/interfaces.py
ADDED
|
@@ -0,0 +1,1280 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Public interfaces for PyTorch Inductor runtime caching.
|
| 2 |
+
|
| 3 |
+
This module provides high-level caching interfaces for memoization and
|
| 4 |
+
result caching functionality.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import atexit
|
| 8 |
+
import functools
|
| 9 |
+
import json
|
| 10 |
+
import logging
|
| 11 |
+
import pickle
|
| 12 |
+
import shutil
|
| 13 |
+
import threading
|
| 14 |
+
import weakref
|
| 15 |
+
from collections.abc import Callable
|
| 16 |
+
from dataclasses import dataclass, field
|
| 17 |
+
from hashlib import sha256
|
| 18 |
+
from os import PathLike
|
| 19 |
+
from pathlib import Path
|
| 20 |
+
from typing import cast, Generic, Protocol, TypedDict
|
| 21 |
+
from typing_extensions import ParamSpec, TypeVar
|
| 22 |
+
|
| 23 |
+
from filelock import FileLock
|
| 24 |
+
|
| 25 |
+
import torch
|
| 26 |
+
from torch._inductor.runtime.runtime_utils import cache_dir
|
| 27 |
+
from torch._inductor.utils import clear_on_fresh_cache
|
| 28 |
+
|
| 29 |
+
from . import config, implementations, locks
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
logger = torch._logging.getArtifactLogger(__name__, "caching")
|
| 33 |
+
|
| 34 |
+
# Type variable for function parameters
|
| 35 |
+
_P = ParamSpec("_P")
|
| 36 |
+
# Type variable for function return type
|
| 37 |
+
_R = TypeVar("_R")
|
| 38 |
+
# Type variable for encoded result type
|
| 39 |
+
_EncodedR = TypeVar("_EncodedR")
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class CacheDumpEntry(TypedDict):
|
| 43 |
+
"""A single cache entry in the dump format.
|
| 44 |
+
|
| 45 |
+
Attributes:
|
| 46 |
+
params: The encoded function parameters.
|
| 47 |
+
result: The encoded function result.
|
| 48 |
+
"""
|
| 49 |
+
|
| 50 |
+
params: object
|
| 51 |
+
result: object
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
class CacheDump(TypedDict):
|
| 55 |
+
"""The structure of the memoizer cache dump file.
|
| 56 |
+
|
| 57 |
+
Cache entries are organized by sub_key in the collections field.
|
| 58 |
+
Memoizers without a sub_key use None as their key in collections.
|
| 59 |
+
|
| 60 |
+
Multiple Memoizer instances with different sub_keys can coexist in the same file.
|
| 61 |
+
|
| 62 |
+
Attributes:
|
| 63 |
+
collections: Dictionary mapping sub_keys (or None for root entries)
|
| 64 |
+
to their cache entries.
|
| 65 |
+
cache_size: The total number of cache entries across all collections.
|
| 66 |
+
"""
|
| 67 |
+
|
| 68 |
+
collections: dict[str | None, dict[str, CacheDumpEntry]]
|
| 69 |
+
cache_size: int
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
@dataclass
|
| 73 |
+
class CacheEntry:
|
| 74 |
+
"""A cache entry containing encoded parameters and result.
|
| 75 |
+
|
| 76 |
+
This dataclass stores the encoded form of function parameters and
|
| 77 |
+
the (possibly encoded) result for human-readable cache dumps.
|
| 78 |
+
|
| 79 |
+
Attributes:
|
| 80 |
+
encoded_params: The encoded function parameters used for debugging/inspection.
|
| 81 |
+
encoded_result: The (possibly encoded) result of the function call.
|
| 82 |
+
"""
|
| 83 |
+
|
| 84 |
+
encoded_params: object
|
| 85 |
+
encoded_result: object
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
@dataclass
|
| 89 |
+
class InterimResult(Generic[_R]):
|
| 90 |
+
"""Wrapper for an available interim result from a DeferredRecording.
|
| 91 |
+
|
| 92 |
+
When get_interim_result() returns an InterimResult, it means the
|
| 93 |
+
make_interim_result callable was provided and has been invoked.
|
| 94 |
+
The wrapped value is the result of that invocation.
|
| 95 |
+
|
| 96 |
+
This allows distinguishing between:
|
| 97 |
+
- No interim result configured: get_interim_result() returns None
|
| 98 |
+
- Interim result available: get_interim_result() returns InterimResult(value)
|
| 99 |
+
where value may itself be None if the callable returns None.
|
| 100 |
+
|
| 101 |
+
Attributes:
|
| 102 |
+
value: The value returned by calling make_interim_result().
|
| 103 |
+
May legitimately be None if the callable returns None.
|
| 104 |
+
"""
|
| 105 |
+
|
| 106 |
+
value: _R
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
@dataclass
|
| 110 |
+
class DeferredRecording(Generic[_R, _EncodedR]):
|
| 111 |
+
"""Signals that recording should happen at a later time.
|
| 112 |
+
|
| 113 |
+
When returned from a custom_result_encoder, the memoizer will:
|
| 114 |
+
1. Skip immediate caching
|
| 115 |
+
2. Register a completion callback on this object
|
| 116 |
+
|
| 117 |
+
The encoder is responsible for calling `finalize(encoded_result)`
|
| 118 |
+
when the actual result is ready to be cached. This is useful when
|
| 119 |
+
the function returns a value whose final encoded form is not yet
|
| 120 |
+
available (e.g., an object containing a pending computation),
|
| 121 |
+
allowing the expensive work to remain hidden while still enabling caching.
|
| 122 |
+
|
| 123 |
+
This class handles the race condition where the computation might complete
|
| 124 |
+
before the memoizer has registered its callback. If finalize() is called
|
| 125 |
+
first, the result is stored and the callbacks are invoked when registered.
|
| 126 |
+
|
| 127 |
+
Multiple callbacks can be registered - they will all be invoked when
|
| 128 |
+
finalize() is called (or immediately if already completed). Callbacks are
|
| 129 |
+
guaranteed to be invoked in registration order.
|
| 130 |
+
|
| 131 |
+
Optionally, a `make_interim_result` callable can be provided to handle
|
| 132 |
+
subsequent calls while the deferred recording is pending. When a memoized
|
| 133 |
+
function is called again with the same parameters while a deferred recording
|
| 134 |
+
is still pending, the memoizer can use this callable to construct an
|
| 135 |
+
appropriate return value instead of re-executing the function. The callable
|
| 136 |
+
does not necessarily return the same object - it provides a way to derive
|
| 137 |
+
a suitable response based on the pending deferred recording's context.
|
| 138 |
+
|
| 139 |
+
This class is thread-safe: concurrent calls to finalize() and
|
| 140 |
+
register_callback() are properly synchronized.
|
| 141 |
+
|
| 142 |
+
WARNING: Callbacks are executed while holding the internal lock to ensure
|
| 143 |
+
ordering guarantees. Callbacks MUST NOT call back into this DeferredRecording
|
| 144 |
+
instance (e.g., calling finalize() or register_callback() from within a
|
| 145 |
+
callback) as this will cause a deadlock.
|
| 146 |
+
|
| 147 |
+
Example usage in an encoder:
|
| 148 |
+
def my_encoder(*args, **kwargs):
|
| 149 |
+
def encode(result: _R) -> DeferredRecording[_R, _EncodedR]:
|
| 150 |
+
deferred: DeferredRecording[_R, _EncodedR] = DeferredRecording(
|
| 151 |
+
# Construct an appropriate return for subsequent calls while pending
|
| 152 |
+
make_interim_result=lambda: result
|
| 153 |
+
)
|
| 154 |
+
|
| 155 |
+
def on_complete():
|
| 156 |
+
encoded_result: _EncodedR = _encode_result(result)
|
| 157 |
+
deferred.finalize(encoded_result)
|
| 158 |
+
|
| 159 |
+
result.add_completion_callback(on_complete)
|
| 160 |
+
return deferred
|
| 161 |
+
|
| 162 |
+
return encode
|
| 163 |
+
|
| 164 |
+
Attributes:
|
| 165 |
+
_callbacks: List of callbacks to invoke when finalize() is called.
|
| 166 |
+
Set to None after finalize() is called to indicate completion.
|
| 167 |
+
_encoded_result: The encoded result passed to finalize().
|
| 168 |
+
_lock: Lock for thread-safe access to mutable state.
|
| 169 |
+
make_interim_result: Optional callable that constructs a return value for subsequent
|
| 170 |
+
calls while the deferred recording is pending. If set, the memoizer
|
| 171 |
+
calls this instead of re-executing the function. The callable may
|
| 172 |
+
return the same object or construct a new appropriate response.
|
| 173 |
+
"""
|
| 174 |
+
|
| 175 |
+
_callbacks: list[Callable[[_EncodedR], None]] | None = field(
|
| 176 |
+
default_factory=list, repr=False
|
| 177 |
+
)
|
| 178 |
+
_encoded_result: _EncodedR | None = field(default=None, repr=False)
|
| 179 |
+
_lock: threading.Lock = field(default_factory=threading.Lock, repr=False)
|
| 180 |
+
make_interim_result: Callable[[], _R] | None = field(default=None, repr=False)
|
| 181 |
+
|
| 182 |
+
def finalize(self, encoded_result: _EncodedR) -> None:
|
| 183 |
+
"""Finalize the deferred recording with the encoded result.
|
| 184 |
+
|
| 185 |
+
This method should be called by the encoder (typically via a completion
|
| 186 |
+
callback) when the actual result is ready to be cached.
|
| 187 |
+
|
| 188 |
+
All registered callbacks are invoked with the encoded result in
|
| 189 |
+
registration order. If no callbacks are registered yet, the result is
|
| 190 |
+
stored and callbacks will be invoked when registered.
|
| 191 |
+
|
| 192 |
+
This method is thread-safe.
|
| 193 |
+
|
| 194 |
+
WARNING: Callbacks are executed while holding the internal lock.
|
| 195 |
+
Do not call finalize() or register_callback() from within a callback,
|
| 196 |
+
as this will cause a deadlock.
|
| 197 |
+
|
| 198 |
+
Args:
|
| 199 |
+
encoded_result: The encoded result to cache.
|
| 200 |
+
"""
|
| 201 |
+
with self._lock:
|
| 202 |
+
if self._callbacks is None:
|
| 203 |
+
raise RuntimeError(
|
| 204 |
+
"finalize() called multiple times on DeferredRecording"
|
| 205 |
+
)
|
| 206 |
+
|
| 207 |
+
# Store the result and get callbacks to invoke
|
| 208 |
+
self._encoded_result = encoded_result
|
| 209 |
+
callbacks_to_invoke = self._callbacks
|
| 210 |
+
self._callbacks = None
|
| 211 |
+
|
| 212 |
+
# Execute callbacks inside the lock to ensure ordering:
|
| 213 |
+
# - All pre-finalize callbacks execute in registration order
|
| 214 |
+
# - Any callback registered during/after finalize will see
|
| 215 |
+
# _callbacks=None and execute immediately (also under lock)
|
| 216 |
+
for callback in callbacks_to_invoke:
|
| 217 |
+
callback(encoded_result)
|
| 218 |
+
|
| 219 |
+
def register_callback(self, callback: Callable[[_EncodedR], None]) -> None:
|
| 220 |
+
"""Register a completion callback.
|
| 221 |
+
|
| 222 |
+
This method is called by memoizers to register callbacks that
|
| 223 |
+
will insert cache entries. Multiple callbacks can be registered.
|
| 224 |
+
|
| 225 |
+
If finalize() has already been called (e.g., the computation was fast),
|
| 226 |
+
the callback is invoked immediately with the stored result.
|
| 227 |
+
|
| 228 |
+
This method is thread-safe. Callbacks are guaranteed to be invoked
|
| 229 |
+
in registration order.
|
| 230 |
+
|
| 231 |
+
WARNING: Callbacks are executed while holding the internal lock.
|
| 232 |
+
Do not call finalize() or register_callback() from within a callback,
|
| 233 |
+
as this will cause a deadlock.
|
| 234 |
+
|
| 235 |
+
Args:
|
| 236 |
+
callback: The function to call with the encoded result.
|
| 237 |
+
"""
|
| 238 |
+
with self._lock:
|
| 239 |
+
if self._callbacks is None:
|
| 240 |
+
# Already finalized - invoke immediately while holding lock
|
| 241 |
+
# to maintain ordering with respect to other callbacks
|
| 242 |
+
# we need to cast here, we can't assert because the encoded
|
| 243 |
+
# result may genuinely be None
|
| 244 |
+
callback(cast(_EncodedR, self._encoded_result))
|
| 245 |
+
else:
|
| 246 |
+
self._callbacks.append(callback)
|
| 247 |
+
|
| 248 |
+
def get_interim_result(self) -> InterimResult[_R] | None:
|
| 249 |
+
"""Try to get an interim result for use while deferred recording is pending.
|
| 250 |
+
|
| 251 |
+
When a memoized function is called again with the same parameters while
|
| 252 |
+
a deferred recording is still pending, the memoizer uses this method to
|
| 253 |
+
check if an interim result can be returned instead of re-executing the
|
| 254 |
+
function.
|
| 255 |
+
|
| 256 |
+
This method provides thread-safe access to the make_interim_result
|
| 257 |
+
callable. It atomically checks if the callable was provided and invokes
|
| 258 |
+
it if so.
|
| 259 |
+
|
| 260 |
+
This method is thread-safe.
|
| 261 |
+
|
| 262 |
+
Returns:
|
| 263 |
+
InterimResult wrapping the value if make_interim_result was provided,
|
| 264 |
+
or None if no interim result is available.
|
| 265 |
+
|
| 266 |
+
Note:
|
| 267 |
+
The callable is invoked each time this method is called, so if
|
| 268 |
+
make_interim_result returns a new object each time, callers will
|
| 269 |
+
receive different objects. To return the same object, the callable
|
| 270 |
+
should capture and return a reference to a single object.
|
| 271 |
+
"""
|
| 272 |
+
with self._lock:
|
| 273 |
+
if self.make_interim_result is not None:
|
| 274 |
+
return InterimResult(self.make_interim_result())
|
| 275 |
+
return None
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
class ResultEncoderFactory(Protocol[_P, _R, _EncodedR]):
|
| 279 |
+
"""Protocol for custom result encoder factories.
|
| 280 |
+
|
| 281 |
+
A result encoder factory is a callable with the following signature:
|
| 282 |
+
factory(fn) -> params_to_encoder(*args, **kwargs) -> encoder(result) -> encoded
|
| 283 |
+
|
| 284 |
+
The three levels are:
|
| 285 |
+
1. factory(fn): Takes the underlying unwrapped function, returns a params_to_encoder
|
| 286 |
+
2. params_to_encoder(*args, **kwargs): Takes the memoized function's arguments,
|
| 287 |
+
returns an encoder function
|
| 288 |
+
3. encoder(result): Converts the result R -> _EncodedR (or DeferredRecording)
|
| 289 |
+
|
| 290 |
+
The `fn` parameter allows the encoder to call the underlying function without
|
| 291 |
+
triggering memoization, which is useful when the encoder needs to re-execute
|
| 292 |
+
the function (e.g., to get a fresh result for DeferredRecording).
|
| 293 |
+
|
| 294 |
+
Example:
|
| 295 |
+
def my_encoder_factory(fn: Callable) -> Callable:
|
| 296 |
+
def params_to_encoder(*args, **kwargs) -> Callable:
|
| 297 |
+
def encode(result: R) -> EncodedR:
|
| 298 |
+
return {"encoded": result}
|
| 299 |
+
return encode
|
| 300 |
+
return params_to_encoder
|
| 301 |
+
|
| 302 |
+
@memoizer.record(custom_result_encoder=my_encoder_factory)
|
| 303 |
+
def compute(x: int) -> int:
|
| 304 |
+
return x * 2
|
| 305 |
+
"""
|
| 306 |
+
|
| 307 |
+
def __call__(
|
| 308 |
+
self,
|
| 309 |
+
fn: Callable[_P, _R],
|
| 310 |
+
) -> Callable[_P, Callable[[_R], _EncodedR | DeferredRecording[_R, _EncodedR]]]: ...
|
| 311 |
+
|
| 312 |
+
|
| 313 |
+
# pyrefly: ignore [variance-mismatch]
|
| 314 |
+
class ResultDecoderFactory(Protocol[_P, _R, _EncodedR]):
|
| 315 |
+
"""Protocol for custom result decoder factories.
|
| 316 |
+
|
| 317 |
+
A result decoder factory is a callable with the following signature:
|
| 318 |
+
factory(fn) -> params_to_decoder(*args, **kwargs) -> decoder(encoded) -> result
|
| 319 |
+
|
| 320 |
+
The three levels are:
|
| 321 |
+
1. factory(fn): Takes the underlying unwrapped function, returns a params_to_decoder
|
| 322 |
+
2. params_to_decoder(*args, **kwargs): Takes the memoized function's arguments,
|
| 323 |
+
returns a decoder function
|
| 324 |
+
3. decoder(encoded): Converts the encoded result _EncodedR -> R
|
| 325 |
+
|
| 326 |
+
The `fn` parameter allows the decoder to call the underlying function without
|
| 327 |
+
triggering memoization, which is useful for fallback paths when the cached
|
| 328 |
+
result cannot be fully decoded.
|
| 329 |
+
|
| 330 |
+
Example:
|
| 331 |
+
def my_decoder_factory(fn: Callable) -> Callable:
|
| 332 |
+
def params_to_decoder(*args, **kwargs) -> Callable:
|
| 333 |
+
def decode(encoded_result: EncodedR) -> R:
|
| 334 |
+
return encoded_result["value"]
|
| 335 |
+
return decode
|
| 336 |
+
return params_to_decoder
|
| 337 |
+
|
| 338 |
+
@memoizer.replay(custom_result_decoder=my_decoder_factory)
|
| 339 |
+
def compute(x: int) -> int:
|
| 340 |
+
return x * 2
|
| 341 |
+
"""
|
| 342 |
+
|
| 343 |
+
def __call__(
|
| 344 |
+
self,
|
| 345 |
+
fn: Callable[_P, _R],
|
| 346 |
+
) -> Callable[_P, Callable[[_EncodedR], _R]]: ...
|
| 347 |
+
|
| 348 |
+
|
| 349 |
+
class _BaseMemoizer:
|
| 350 |
+
"""Base class for memoization interfaces.
|
| 351 |
+
|
| 352 |
+
This class provides the common memoize method that orchestrates
|
| 353 |
+
record and replay functionality.
|
| 354 |
+
"""
|
| 355 |
+
|
| 356 |
+
@staticmethod
|
| 357 |
+
def _make_key(
|
| 358 |
+
custom_params_encoder: Callable[..., object] | None,
|
| 359 |
+
*args: object,
|
| 360 |
+
**kwargs: object,
|
| 361 |
+
) -> str:
|
| 362 |
+
"""Generate a cache key from function parameters.
|
| 363 |
+
|
| 364 |
+
Args:
|
| 365 |
+
custom_params_encoder: Optional encoder to apply to function parameters.
|
| 366 |
+
If None, params are pickled directly.
|
| 367 |
+
*args: Positional arguments to encode.
|
| 368 |
+
**kwargs: Keyword arguments to encode.
|
| 369 |
+
|
| 370 |
+
Returns:
|
| 371 |
+
A 32-character hex string suitable for use as a cache key.
|
| 372 |
+
"""
|
| 373 |
+
if custom_params_encoder is None:
|
| 374 |
+
# Pickle the parameters directly
|
| 375 |
+
pickled_params: bytes = pickle.dumps((args, kwargs))
|
| 376 |
+
else:
|
| 377 |
+
# Encode the parameters using the custom encoder
|
| 378 |
+
encoded_params = custom_params_encoder(*args, **kwargs)
|
| 379 |
+
# Pickle the encoded output
|
| 380 |
+
pickled_params = pickle.dumps(encoded_params)
|
| 381 |
+
|
| 382 |
+
# Hash the pickled bytes with SHA256
|
| 383 |
+
hash_obj = sha256(pickled_params)
|
| 384 |
+
|
| 385 |
+
# Get hex digest and truncate to 32 characters
|
| 386 |
+
return hash_obj.hexdigest()[:32]
|
| 387 |
+
|
| 388 |
+
def record(
|
| 389 |
+
self,
|
| 390 |
+
custom_params_encoder: Callable[_P, object] | None = None,
|
| 391 |
+
custom_result_encoder: ResultEncoderFactory[_P, _R, _EncodedR] | None = None,
|
| 392 |
+
) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]:
|
| 393 |
+
"""Record a function call result. Must be implemented by subclasses.
|
| 394 |
+
|
| 395 |
+
This can be used standalone to cache results without necessarily
|
| 396 |
+
replaying them, for example for logging or analytics purposes.
|
| 397 |
+
|
| 398 |
+
See memoize() for a description of the arguments.
|
| 399 |
+
"""
|
| 400 |
+
raise NotImplementedError
|
| 401 |
+
|
| 402 |
+
def replay(
|
| 403 |
+
self,
|
| 404 |
+
custom_params_encoder: Callable[_P, object] | None = None,
|
| 405 |
+
custom_result_decoder: ResultDecoderFactory[_P, _R, _EncodedR] | None = None,
|
| 406 |
+
) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]:
|
| 407 |
+
"""Replay a cached function result. Must be implemented by subclasses.
|
| 408 |
+
|
| 409 |
+
This can be used standalone to use cached values that are otherwise
|
| 410 |
+
too hard to automatically record.
|
| 411 |
+
|
| 412 |
+
See memoize() for a description of the arguments.
|
| 413 |
+
"""
|
| 414 |
+
raise NotImplementedError
|
| 415 |
+
|
| 416 |
+
def memoize(
|
| 417 |
+
self,
|
| 418 |
+
custom_params_encoder: Callable[_P, object] | None = None,
|
| 419 |
+
custom_result_encoder: ResultEncoderFactory[_P, _R, _EncodedR] | None = None,
|
| 420 |
+
custom_result_decoder: ResultDecoderFactory[_P, _R, _EncodedR] | None = None,
|
| 421 |
+
) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]:
|
| 422 |
+
"""Memoize a function with record and replay functionality.
|
| 423 |
+
|
| 424 |
+
This is a decorator that attempts to replay cached results first.
|
| 425 |
+
If a cache miss occurs, it records the result by executing the wrapped function.
|
| 426 |
+
|
| 427 |
+
Args:
|
| 428 |
+
custom_params_encoder: Optional encoder for function parameters.
|
| 429 |
+
If None, parameters are pickled directly.
|
| 430 |
+
custom_result_encoder: Optional encoder factory for function results.
|
| 431 |
+
Takes function parameters and returns an encoder
|
| 432 |
+
function that converts R -> _EncodedR.
|
| 433 |
+
custom_result_decoder: Optional decoder factory for cached results.
|
| 434 |
+
Takes function parameters and returns a decoder
|
| 435 |
+
function that converts _EncodedR -> R.
|
| 436 |
+
|
| 437 |
+
Returns:
|
| 438 |
+
A decorator function that can be applied to functions.
|
| 439 |
+
|
| 440 |
+
Example:
|
| 441 |
+
@memoizer.memoize(
|
| 442 |
+
custom_params_encoder=my_param_encoder,
|
| 443 |
+
custom_result_encoder=my_result_encoder_factory,
|
| 444 |
+
custom_result_decoder=my_result_decoder_factory,
|
| 445 |
+
)
|
| 446 |
+
def expensive_function(x, y):
|
| 447 |
+
return x + y
|
| 448 |
+
"""
|
| 449 |
+
|
| 450 |
+
def wrapper(fn: Callable[_P, _R]) -> Callable[_P, _R]:
|
| 451 |
+
"""Wrap the function to enable memoization with replay and record.
|
| 452 |
+
|
| 453 |
+
Args:
|
| 454 |
+
fn: The function to wrap.
|
| 455 |
+
|
| 456 |
+
Returns:
|
| 457 |
+
A wrapped version of the function.
|
| 458 |
+
"""
|
| 459 |
+
# If caching is disabled, return the original function unchanged
|
| 460 |
+
if not config.IS_CACHING_MODULE_ENABLED():
|
| 461 |
+
return fn
|
| 462 |
+
|
| 463 |
+
# Create decorated versions using record and replay
|
| 464 |
+
replay_fn = self.replay(
|
| 465 |
+
custom_params_encoder,
|
| 466 |
+
custom_result_decoder,
|
| 467 |
+
)(fn)
|
| 468 |
+
record_fn = self.record(
|
| 469 |
+
custom_params_encoder,
|
| 470 |
+
custom_result_encoder,
|
| 471 |
+
)(fn)
|
| 472 |
+
|
| 473 |
+
@functools.wraps(fn)
|
| 474 |
+
def inner(*args: _P.args, **kwargs: _P.kwargs) -> _R:
|
| 475 |
+
"""Attempt to replay from cache, or record on cache miss.
|
| 476 |
+
|
| 477 |
+
Args:
|
| 478 |
+
*args: Positional arguments to pass to the function.
|
| 479 |
+
**kwargs: Keyword arguments to pass to the function.
|
| 480 |
+
|
| 481 |
+
Returns:
|
| 482 |
+
The result from cache (if hit) or from executing the function (if miss).
|
| 483 |
+
"""
|
| 484 |
+
# Try to replay first
|
| 485 |
+
try:
|
| 486 |
+
return replay_fn(*args, **kwargs)
|
| 487 |
+
except KeyError:
|
| 488 |
+
# Cache miss - record the result
|
| 489 |
+
return record_fn(*args, **kwargs)
|
| 490 |
+
|
| 491 |
+
return inner
|
| 492 |
+
|
| 493 |
+
return wrapper
|
| 494 |
+
|
| 495 |
+
|
| 496 |
+
class Memoizer(_BaseMemoizer):
|
| 497 |
+
"""In-memory memoization interface for caching function results.
|
| 498 |
+
|
| 499 |
+
This class provides methods for recording, retrieving, and managing
|
| 500 |
+
cached function results in memory with custom encoding/decoding logic.
|
| 501 |
+
|
| 502 |
+
Cache entries are stored as CacheEntry objects containing both the encoded
|
| 503 |
+
parameters and the encoded result. This makes debugging easier since entries
|
| 504 |
+
can be inspected with full context about what inputs produced each result.
|
| 505 |
+
|
| 506 |
+
On memoizer destruction, the cache is automatically dumped to a shared JSON file
|
| 507 |
+
in the cache directory for debugging and inspection purposes. Multiple Memoizer
|
| 508 |
+
instances contribute to the same file additively.
|
| 509 |
+
|
| 510 |
+
Note: Use this over functools.cache when you need to support parameters
|
| 511 |
+
that functools.cache cannot handle, or when you need custom encoding/decoding
|
| 512 |
+
of results.
|
| 513 |
+
"""
|
| 514 |
+
|
| 515 |
+
def __init__(self, sub_key: str | None = None) -> None:
|
| 516 |
+
"""Initialize the Memoizer instance with an in-memory cache.
|
| 517 |
+
|
| 518 |
+
Args:
|
| 519 |
+
sub_key: Optional key for organizing cache entries in the JSON dump.
|
| 520 |
+
If provided, cache entries are stored under cache_entries[sub_key].
|
| 521 |
+
If None, cache entries are merged directly into root cache_entries.
|
| 522 |
+
"""
|
| 523 |
+
self._cache: implementations._InMemoryCacheImpl[CacheEntry] = (
|
| 524 |
+
implementations._InMemoryCacheImpl()
|
| 525 |
+
)
|
| 526 |
+
# Optional sub_key for nested cache structure
|
| 527 |
+
self._sub_key: str | None = sub_key
|
| 528 |
+
# Track pending deferred recordings by cache key.
|
| 529 |
+
# Uses WeakValueDictionary to prevent memory leaks: if the DeferredRecording
|
| 530 |
+
# is no longer referenced elsewhere (e.g., the async computation was cancelled
|
| 531 |
+
# or failed and garbage collected), the entry is automatically removed.
|
| 532 |
+
self._pending_deferred: weakref.WeakValueDictionary[str, DeferredRecording] = (
|
| 533 |
+
weakref.WeakValueDictionary()
|
| 534 |
+
)
|
| 535 |
+
# Register atexit handler to dump cache on program exit
|
| 536 |
+
if config.IS_DUMP_MEMOIZER_CACHE_ENABLED():
|
| 537 |
+
atexit.register(self._dump_to_disk)
|
| 538 |
+
# Pre-populate cache from dump file if configured (with sub_key now set)
|
| 539 |
+
self._maybe_prepopulate_from_dump()
|
| 540 |
+
# Register with clear_on_fresh_cache for fresh_cache() integration
|
| 541 |
+
clear_on_fresh_cache(self)
|
| 542 |
+
|
| 543 |
+
def cache_clear(self) -> None:
|
| 544 |
+
"""Clear the in-memory cache.
|
| 545 |
+
|
| 546 |
+
This method resets the in-memory cache to empty. It is called by
|
| 547 |
+
the fresh_cache() context manager via clear_on_fresh_cache registration.
|
| 548 |
+
"""
|
| 549 |
+
self._cache._memory.clear()
|
| 550 |
+
|
| 551 |
+
def _finalize_deferred_recording(
|
| 552 |
+
self,
|
| 553 |
+
cache_key: str,
|
| 554 |
+
encoded_params: object,
|
| 555 |
+
encoded_result: _EncodedR,
|
| 556 |
+
) -> None:
|
| 557 |
+
"""Finalize a deferred recording by inserting the cache entry.
|
| 558 |
+
|
| 559 |
+
This method is called when a DeferredRecording's finalize() method is invoked.
|
| 560 |
+
It creates the cache entry and inserts it into the in-memory cache.
|
| 561 |
+
|
| 562 |
+
Args:
|
| 563 |
+
cache_key: The cache key for the entry.
|
| 564 |
+
encoded_params: The encoded function parameters.
|
| 565 |
+
encoded_result: The final encoded result to cache.
|
| 566 |
+
"""
|
| 567 |
+
# Remove from pending tracking
|
| 568 |
+
self._pending_deferred.pop(cache_key, None)
|
| 569 |
+
# Insert into cache
|
| 570 |
+
cache_entry = CacheEntry(
|
| 571 |
+
encoded_params=encoded_params,
|
| 572 |
+
encoded_result=encoded_result,
|
| 573 |
+
)
|
| 574 |
+
self._cache.insert(cache_key, cache_entry)
|
| 575 |
+
|
| 576 |
+
@functools.cached_property
|
| 577 |
+
def _shared_cache_filepath(self) -> Path:
|
| 578 |
+
"""Get the shared cache filepath for memoizer cache dumps.
|
| 579 |
+
|
| 580 |
+
Returns:
|
| 581 |
+
The path to the shared memoizer cache JSON file.
|
| 582 |
+
"""
|
| 583 |
+
return Path(cache_dir()) / "memoizer_cache.json"
|
| 584 |
+
|
| 585 |
+
@functools.cached_property
|
| 586 |
+
def _shared_cache_lockfile(self) -> Path:
|
| 587 |
+
"""Get the lock file path for the shared memoizer cache.
|
| 588 |
+
|
| 589 |
+
Returns:
|
| 590 |
+
The path to the lock file for the shared cache.
|
| 591 |
+
"""
|
| 592 |
+
return Path(cache_dir()) / "memoizer_cache.lock"
|
| 593 |
+
|
| 594 |
+
def _read_dump_from_disk(self, filepath: Path | None = None) -> CacheDump | None:
|
| 595 |
+
"""Read a cache dump from disk.
|
| 596 |
+
|
| 597 |
+
Attempts to read and parse a cache JSON file.
|
| 598 |
+
|
| 599 |
+
Args:
|
| 600 |
+
filepath: Path to the dump file to read. If None, uses the
|
| 601 |
+
shared cache filepath (self._shared_cache_filepath).
|
| 602 |
+
|
| 603 |
+
Returns:
|
| 604 |
+
The cache dump if the file exists and is valid JSON, None otherwise.
|
| 605 |
+
"""
|
| 606 |
+
target_path = filepath if filepath is not None else self._shared_cache_filepath
|
| 607 |
+
try:
|
| 608 |
+
with open(target_path) as f:
|
| 609 |
+
data = json.load(f)
|
| 610 |
+
return data
|
| 611 |
+
except FileNotFoundError:
|
| 612 |
+
return None
|
| 613 |
+
except json.JSONDecodeError:
|
| 614 |
+
return None
|
| 615 |
+
|
| 616 |
+
def _write_dump_to_disk(self, dump: CacheDump) -> None:
|
| 617 |
+
"""Write the cache dump to disk.
|
| 618 |
+
|
| 619 |
+
Writes the provided dump to the shared cache JSON file and logs the result.
|
| 620 |
+
|
| 621 |
+
Args:
|
| 622 |
+
dump: The cache dump to write.
|
| 623 |
+
"""
|
| 624 |
+
try:
|
| 625 |
+
with open(self._shared_cache_filepath, "w") as f:
|
| 626 |
+
json.dump(dump, f, indent=2)
|
| 627 |
+
|
| 628 |
+
# Log the filepath
|
| 629 |
+
if self._sub_key:
|
| 630 |
+
logger.log(
|
| 631 |
+
logging.INFO,
|
| 632 |
+
"Memoizer cache (sub_key=%s) dumped to: %s",
|
| 633 |
+
self._sub_key,
|
| 634 |
+
self._shared_cache_filepath,
|
| 635 |
+
)
|
| 636 |
+
else:
|
| 637 |
+
logger.log(
|
| 638 |
+
logging.INFO,
|
| 639 |
+
"Memoizer cache dumped to: %s",
|
| 640 |
+
self._shared_cache_filepath,
|
| 641 |
+
)
|
| 642 |
+
except Exception as e:
|
| 643 |
+
# If dumping fails, just log it and don't crash the program
|
| 644 |
+
logger.log(
|
| 645 |
+
logging.WARNING,
|
| 646 |
+
"Warning: Failed to dump memoizer cache: %s",
|
| 647 |
+
e,
|
| 648 |
+
)
|
| 649 |
+
|
| 650 |
+
def _prepare_dump(self, existing_dump: CacheDump | None) -> CacheDump:
|
| 651 |
+
"""Prepare a cache dump from the current Memoizer state.
|
| 652 |
+
|
| 653 |
+
Takes the existing dump (if any) and merges it with the current
|
| 654 |
+
in-memory cache entries.
|
| 655 |
+
|
| 656 |
+
Args:
|
| 657 |
+
existing_dump: The existing dump to merge with, or None if starting fresh.
|
| 658 |
+
|
| 659 |
+
Returns:
|
| 660 |
+
The prepared cache dump ready to be written to disk.
|
| 661 |
+
"""
|
| 662 |
+
# Start with existing data or empty structure
|
| 663 |
+
if existing_dump is not None:
|
| 664 |
+
dump = existing_dump
|
| 665 |
+
else:
|
| 666 |
+
dump: CacheDump = {"collections": {}, "cache_size": 0}
|
| 667 |
+
|
| 668 |
+
# Ensure collections field exists
|
| 669 |
+
if "collections" not in dump:
|
| 670 |
+
dump["collections"] = {}
|
| 671 |
+
|
| 672 |
+
# JSON serializes None keys as "null" string, so we need to handle both cases
|
| 673 |
+
# When reading from JSON, the key will be "null" (string), not None
|
| 674 |
+
lookup_key = "null" if self._sub_key is None else self._sub_key
|
| 675 |
+
|
| 676 |
+
# Get existing entries for this sub_key to merge with
|
| 677 |
+
existing_entries = dump["collections"].get(lookup_key, {})
|
| 678 |
+
|
| 679 |
+
# Format cache entries as {"params": ..., "result": ...}
|
| 680 |
+
formatted_cache: dict[str, CacheDumpEntry] = dict(existing_entries)
|
| 681 |
+
for key, value in self._cache._memory.items():
|
| 682 |
+
entry = value
|
| 683 |
+
formatted_cache[key] = CacheDumpEntry(
|
| 684 |
+
params=entry.encoded_params,
|
| 685 |
+
result=entry.encoded_result,
|
| 686 |
+
)
|
| 687 |
+
|
| 688 |
+
# Store under sub_key in collections (use lookup_key to maintain JSON compatibility)
|
| 689 |
+
dump["collections"][lookup_key] = formatted_cache
|
| 690 |
+
|
| 691 |
+
# Calculate total cache size
|
| 692 |
+
total_size = sum(len(collection) for collection in dump["collections"].values())
|
| 693 |
+
dump["cache_size"] = total_size
|
| 694 |
+
|
| 695 |
+
return dump
|
| 696 |
+
|
| 697 |
+
def _dump_to_disk(self) -> None:
|
| 698 |
+
"""Dump the in-memory cache to a shared JSON file.
|
| 699 |
+
|
| 700 |
+
This method is automatically called on program exit via atexit.
|
| 701 |
+
It reads any existing cache data, merges it with this instance's cache,
|
| 702 |
+
and writes the combined result back. Multiple Memoizer instances
|
| 703 |
+
contribute to the same file additively.
|
| 704 |
+
|
| 705 |
+
If self._sub_key is set and non-empty, cache entries are stored under
|
| 706 |
+
cache_entries[sub_key]. Otherwise, they're merged directly into cache_entries.
|
| 707 |
+
|
| 708 |
+
Cache entries are formatted as {"params": <encoded_params>, "result": <encoded_result>}
|
| 709 |
+
for better human readability.
|
| 710 |
+
|
| 711 |
+
The filepath where the cache was dumped is logged.
|
| 712 |
+
"""
|
| 713 |
+
# Skip if cache is empty
|
| 714 |
+
if not self._cache._memory:
|
| 715 |
+
return
|
| 716 |
+
|
| 717 |
+
# Ensure parent directory exists
|
| 718 |
+
self._shared_cache_filepath.parent.mkdir(parents=True, exist_ok=True)
|
| 719 |
+
|
| 720 |
+
# Acquire file lock to ensure thread/process safety
|
| 721 |
+
flock = FileLock(str(self._shared_cache_lockfile))
|
| 722 |
+
with locks._acquire_flock_with_timeout(flock):
|
| 723 |
+
existing_dump = self._read_dump_from_disk()
|
| 724 |
+
dump = self._prepare_dump(existing_dump)
|
| 725 |
+
self._write_dump_to_disk(dump)
|
| 726 |
+
|
| 727 |
+
def _maybe_prepopulate_from_dump(self) -> None:
|
| 728 |
+
"""Pre-populate cache entries from a dump file if configured.
|
| 729 |
+
|
| 730 |
+
Checks the CACHE_DUMP_FILE_PATH config option for a path to a JSON dump file
|
| 731 |
+
produced by IS_DUMP_MEMOIZER_CACHE_ENABLED. If a valid path is provided and
|
| 732 |
+
the file exists, loads cache entries from it into the in-memory cache.
|
| 733 |
+
|
| 734 |
+
For Memoizer instances without a sub_key, loads entries from the root cache_entries.
|
| 735 |
+
For Memoizer instances with a sub_key, loads entries from cache_entries[sub_key].
|
| 736 |
+
|
| 737 |
+
This method is called during __init__ to pre-populate the cache.
|
| 738 |
+
"""
|
| 739 |
+
dump_file_path = config.CACHE_DUMP_FILE_PATH()
|
| 740 |
+
|
| 741 |
+
# Skip if no dump file configured
|
| 742 |
+
if not dump_file_path:
|
| 743 |
+
return
|
| 744 |
+
|
| 745 |
+
# Read the dump file using the helper method
|
| 746 |
+
dump = self._read_dump_from_disk(Path(dump_file_path))
|
| 747 |
+
if dump is None:
|
| 748 |
+
return
|
| 749 |
+
|
| 750 |
+
# Extract entries to load from the dump
|
| 751 |
+
entries_to_load = self._extract_entries_from_dump(dump)
|
| 752 |
+
if not entries_to_load:
|
| 753 |
+
return
|
| 754 |
+
|
| 755 |
+
# Populate the cache
|
| 756 |
+
self._populate_cache_from_entries(entries_to_load)
|
| 757 |
+
|
| 758 |
+
# Log the result
|
| 759 |
+
if self._sub_key:
|
| 760 |
+
logger.log(
|
| 761 |
+
logging.INFO,
|
| 762 |
+
"Loaded %d cache entries from %s (sub_key=%s)",
|
| 763 |
+
len(entries_to_load),
|
| 764 |
+
dump_file_path,
|
| 765 |
+
self._sub_key,
|
| 766 |
+
)
|
| 767 |
+
else:
|
| 768 |
+
logger.log(
|
| 769 |
+
logging.INFO,
|
| 770 |
+
"Loaded %d cache entries from %s",
|
| 771 |
+
len(entries_to_load),
|
| 772 |
+
dump_file_path,
|
| 773 |
+
)
|
| 774 |
+
|
| 775 |
+
def _extract_entries_from_dump(self, dump: CacheDump) -> dict[str, CacheDumpEntry]:
|
| 776 |
+
"""Extract cache entries from a dump based on sub_key.
|
| 777 |
+
|
| 778 |
+
Args:
|
| 779 |
+
dump: The cache dump to extract entries from.
|
| 780 |
+
|
| 781 |
+
Returns:
|
| 782 |
+
Dictionary of cache entries to load.
|
| 783 |
+
"""
|
| 784 |
+
collections = dump.get("collections", {})
|
| 785 |
+
# JSON serializes None keys as "null" string, so we need to handle both cases
|
| 786 |
+
# When reading from JSON, the key will be "null" (string), not None
|
| 787 |
+
lookup_key = "null" if self._sub_key is None else self._sub_key
|
| 788 |
+
return collections.get(lookup_key, {})
|
| 789 |
+
|
| 790 |
+
def _populate_cache_from_entries(self, entries: dict[str, CacheDumpEntry]) -> None:
|
| 791 |
+
"""Populate the in-memory cache from dump entries.
|
| 792 |
+
|
| 793 |
+
Args:
|
| 794 |
+
entries: Dictionary of cache entries to load.
|
| 795 |
+
"""
|
| 796 |
+
for key, entry in entries.items():
|
| 797 |
+
cache_entry = CacheEntry(
|
| 798 |
+
encoded_params=entry["params"],
|
| 799 |
+
encoded_result=entry["result"],
|
| 800 |
+
)
|
| 801 |
+
self._cache.insert(key, cache_entry)
|
| 802 |
+
|
| 803 |
+
def record(
|
| 804 |
+
self,
|
| 805 |
+
custom_params_encoder: Callable[_P, object] | None = None,
|
| 806 |
+
custom_result_encoder: ResultEncoderFactory[_P, _R, _EncodedR] | None = None,
|
| 807 |
+
) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]:
|
| 808 |
+
"""Record a function call result with custom encoding.
|
| 809 |
+
|
| 810 |
+
This is a decorator that wraps a function to enable memoization
|
| 811 |
+
with custom encoding/decoding logic.
|
| 812 |
+
|
| 813 |
+
Args:
|
| 814 |
+
custom_params_encoder: Optional encoder for function parameters.
|
| 815 |
+
If None, parameters are pickled directly.
|
| 816 |
+
custom_result_encoder: Optional encoder factory for function results.
|
| 817 |
+
First receives the underlying function, then takes
|
| 818 |
+
function parameters and returns an encoder function
|
| 819 |
+
that converts R -> _EncodedR.
|
| 820 |
+
|
| 821 |
+
Returns:
|
| 822 |
+
A decorator function that can be applied to functions.
|
| 823 |
+
|
| 824 |
+
Example:
|
| 825 |
+
@memoizer.record(
|
| 826 |
+
custom_params_encoder=my_param_encoder,
|
| 827 |
+
custom_result_encoder=my_result_encoder_factory,
|
| 828 |
+
)
|
| 829 |
+
def expensive_function(x, y):
|
| 830 |
+
return x + y
|
| 831 |
+
"""
|
| 832 |
+
|
| 833 |
+
def wrapper(fn: Callable[_P, _R]) -> Callable[_P, _R]:
|
| 834 |
+
"""Wrap the function to enable memoization.
|
| 835 |
+
|
| 836 |
+
Args:
|
| 837 |
+
fn: The function to wrap.
|
| 838 |
+
|
| 839 |
+
Returns:
|
| 840 |
+
A wrapped version of the function.
|
| 841 |
+
"""
|
| 842 |
+
# If caching is disabled, return the original function unchanged
|
| 843 |
+
if not config.IS_CACHING_MODULE_ENABLED():
|
| 844 |
+
return fn
|
| 845 |
+
|
| 846 |
+
@functools.wraps(fn)
|
| 847 |
+
def inner(*args: _P.args, **kwargs: _P.kwargs) -> _R:
|
| 848 |
+
"""Call the original function and cache the result.
|
| 849 |
+
|
| 850 |
+
Args:
|
| 851 |
+
*args: Positional arguments to pass to the function.
|
| 852 |
+
**kwargs: Keyword arguments to pass to the function.
|
| 853 |
+
|
| 854 |
+
Returns:
|
| 855 |
+
The result of calling the original function.
|
| 856 |
+
"""
|
| 857 |
+
# Call the function to compute the result
|
| 858 |
+
result = fn(*args, **kwargs)
|
| 859 |
+
|
| 860 |
+
# Generate cache key from parameters
|
| 861 |
+
cache_key = self._make_key(custom_params_encoder, *args, **kwargs)
|
| 862 |
+
|
| 863 |
+
# Encode params for human-readable dump
|
| 864 |
+
if custom_params_encoder is not None:
|
| 865 |
+
encoded_params = custom_params_encoder(*args, **kwargs)
|
| 866 |
+
else:
|
| 867 |
+
encoded_params = {
|
| 868 |
+
"args": args,
|
| 869 |
+
"kwargs": kwargs,
|
| 870 |
+
}
|
| 871 |
+
|
| 872 |
+
# Encode the result if encoder is provided
|
| 873 |
+
if custom_result_encoder is not None:
|
| 874 |
+
# Get the encoder function by calling the factory factory:
|
| 875 |
+
# 1. First call with fn to get params_to_encoder factory
|
| 876 |
+
# 2. Second call with params to get the encoder function
|
| 877 |
+
params_to_encoder = custom_result_encoder(fn)
|
| 878 |
+
encoder_fn = params_to_encoder(*args, **kwargs)
|
| 879 |
+
encoded_result = encoder_fn(result)
|
| 880 |
+
else:
|
| 881 |
+
encoded_result = result
|
| 882 |
+
|
| 883 |
+
# Check for deferred recording
|
| 884 |
+
if isinstance(encoded_result, DeferredRecording):
|
| 885 |
+
# Track the pending deferred recording
|
| 886 |
+
self._pending_deferred[cache_key] = encoded_result
|
| 887 |
+
# Register the callback - handles race condition if encoded_result already completed
|
| 888 |
+
encoded_result.register_callback(
|
| 889 |
+
functools.partial(
|
| 890 |
+
self._finalize_deferred_recording,
|
| 891 |
+
cache_key,
|
| 892 |
+
encoded_params,
|
| 893 |
+
)
|
| 894 |
+
)
|
| 895 |
+
return result # Return without caching
|
| 896 |
+
|
| 897 |
+
# Store CacheEntry in cache
|
| 898 |
+
cache_entry = CacheEntry(
|
| 899 |
+
encoded_params=encoded_params,
|
| 900 |
+
encoded_result=encoded_result,
|
| 901 |
+
)
|
| 902 |
+
self._cache.insert(cache_key, cache_entry)
|
| 903 |
+
|
| 904 |
+
# Return the original result (not the encoded version)
|
| 905 |
+
return result
|
| 906 |
+
|
| 907 |
+
return inner
|
| 908 |
+
|
| 909 |
+
return wrapper
|
| 910 |
+
|
| 911 |
+
def replay(
|
| 912 |
+
self,
|
| 913 |
+
custom_params_encoder: Callable[_P, object] | None = None,
|
| 914 |
+
custom_result_decoder: ResultDecoderFactory[_P, _R, _EncodedR] | None = None,
|
| 915 |
+
) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]:
|
| 916 |
+
"""Replay a cached function result without executing the function.
|
| 917 |
+
|
| 918 |
+
This is a decorator that retrieves cached results instead of executing
|
| 919 |
+
the function. Raises an exception if no cached result exists.
|
| 920 |
+
|
| 921 |
+
Args:
|
| 922 |
+
custom_params_encoder: Optional encoder for function parameters.
|
| 923 |
+
If None, parameters are pickled directly.
|
| 924 |
+
custom_result_decoder: Optional decoder factory for cached results.
|
| 925 |
+
First receives the underlying function, then takes
|
| 926 |
+
function parameters and returns a decoder function
|
| 927 |
+
that converts _EncodedR -> R.
|
| 928 |
+
|
| 929 |
+
Returns:
|
| 930 |
+
A decorator function that can be applied to functions.
|
| 931 |
+
|
| 932 |
+
Example:
|
| 933 |
+
@memoizer.replay(
|
| 934 |
+
custom_params_encoder=my_param_encoder,
|
| 935 |
+
custom_result_decoder=my_result_decoder_factory,
|
| 936 |
+
)
|
| 937 |
+
def expensive_function(x, y):
|
| 938 |
+
return x + y
|
| 939 |
+
"""
|
| 940 |
+
|
| 941 |
+
def wrapper(fn: Callable[_P, _R]) -> Callable[_P, _R]:
|
| 942 |
+
"""Wrap the function to retrieve from cache.
|
| 943 |
+
|
| 944 |
+
Args:
|
| 945 |
+
fn: The function to wrap (not actually called).
|
| 946 |
+
|
| 947 |
+
Returns:
|
| 948 |
+
A wrapped version of the function.
|
| 949 |
+
"""
|
| 950 |
+
# If caching is disabled, always raise KeyError (cache miss)
|
| 951 |
+
if not config.IS_CACHING_MODULE_ENABLED():
|
| 952 |
+
|
| 953 |
+
@functools.wraps(fn)
|
| 954 |
+
def always_miss(*args: _P.args, **kwargs: _P.kwargs) -> _R:
|
| 955 |
+
raise KeyError("Caching is disabled")
|
| 956 |
+
|
| 957 |
+
return always_miss
|
| 958 |
+
|
| 959 |
+
@functools.wraps(fn)
|
| 960 |
+
def inner(*args: _P.args, **kwargs: _P.kwargs) -> _R:
|
| 961 |
+
"""Retrieve the cached result without calling the function.
|
| 962 |
+
|
| 963 |
+
Args:
|
| 964 |
+
*args: Positional arguments to generate the cache key.
|
| 965 |
+
**kwargs: Keyword arguments to generate the cache key.
|
| 966 |
+
|
| 967 |
+
Returns:
|
| 968 |
+
The cached result (decoded if decoder is provided).
|
| 969 |
+
|
| 970 |
+
Raises:
|
| 971 |
+
KeyError: If no cached result exists for the given parameters.
|
| 972 |
+
"""
|
| 973 |
+
# Generate cache key from parameters
|
| 974 |
+
cache_key = self._make_key(custom_params_encoder, *args, **kwargs)
|
| 975 |
+
|
| 976 |
+
# Check if result is cached
|
| 977 |
+
cached_hit = self._cache.get(cache_key)
|
| 978 |
+
if cached_hit is not None:
|
| 979 |
+
# Extract the cached value
|
| 980 |
+
cache_entry = cached_hit.value
|
| 981 |
+
|
| 982 |
+
# Decode and return the cached result
|
| 983 |
+
if custom_result_decoder is not None:
|
| 984 |
+
# Get the decoder function by calling the factory factory:
|
| 985 |
+
# 1. First call with fn to get params_to_decoder factory
|
| 986 |
+
# 2. Second call with params to get the decoder function
|
| 987 |
+
params_to_decoder = custom_result_decoder(fn)
|
| 988 |
+
decoder_fn = params_to_decoder(*args, **kwargs)
|
| 989 |
+
return decoder_fn(cast(_EncodedR, cache_entry.encoded_result))
|
| 990 |
+
return cast(_R, cache_entry.encoded_result)
|
| 991 |
+
|
| 992 |
+
# Check for pending deferred recording with interim result
|
| 993 |
+
pending = self._pending_deferred.get(cache_key)
|
| 994 |
+
if pending is not None:
|
| 995 |
+
interim = pending.get_interim_result()
|
| 996 |
+
if interim is not None:
|
| 997 |
+
return interim.value
|
| 998 |
+
|
| 999 |
+
raise KeyError(f"No cached result found for key: {cache_key}")
|
| 1000 |
+
|
| 1001 |
+
return inner
|
| 1002 |
+
|
| 1003 |
+
return wrapper
|
| 1004 |
+
|
| 1005 |
+
|
| 1006 |
+
class PersistentMemoizer(_BaseMemoizer):
|
| 1007 |
+
"""Persistent memoization interface for caching function results to disk.
|
| 1008 |
+
|
| 1009 |
+
This class provides methods for recording, retrieving, and managing
|
| 1010 |
+
cached function results using a two-level cache strategy:
|
| 1011 |
+
1. In-memory cache (fast, checked first) - via Memoizer instance
|
| 1012 |
+
2. On-disk cache (persistent, checked on memory miss)
|
| 1013 |
+
|
| 1014 |
+
Results are persisted across process restarts.
|
| 1015 |
+
|
| 1016 |
+
On program exit, the in-memory cache entries are automatically dumped to
|
| 1017 |
+
the shared JSON file. If sub_dir is non-empty, entries are stored under
|
| 1018 |
+
a nested structure based on the sub_dir. If sub_dir is empty, entries are
|
| 1019 |
+
merged directly into the root cache_entries.
|
| 1020 |
+
|
| 1021 |
+
Note: Use this over functools.cache when you need to support parameters
|
| 1022 |
+
that functools.cache cannot handle, custom result encoding and/or decoding,
|
| 1023 |
+
or when you need disk caching to persist results across program boundaries.
|
| 1024 |
+
"""
|
| 1025 |
+
|
| 1026 |
+
def __init__(self, sub_dir: PathLike[str] | None = None) -> None:
|
| 1027 |
+
"""Initialize the PersistentMemoizer with two-level caching.
|
| 1028 |
+
|
| 1029 |
+
Args:
|
| 1030 |
+
sub_dir: Optional subdirectory within the cache directory for
|
| 1031 |
+
organizing cached results. Defaults to empty string if not specified.
|
| 1032 |
+
If non-empty, cache entries will be stored under cache_entries[sub_dir].
|
| 1033 |
+
If empty, cache entries are merged into root cache_entries.
|
| 1034 |
+
"""
|
| 1035 |
+
# Use a Memoizer instance for in-memory caching
|
| 1036 |
+
self._memoizer: Memoizer = Memoizer(sub_key=str(sub_dir) if sub_dir else None)
|
| 1037 |
+
# Store on-disk cache as a separate attribute
|
| 1038 |
+
self._disk_cache: implementations._OnDiskCacheImpl = (
|
| 1039 |
+
implementations._OnDiskCacheImpl(sub_dir=sub_dir)
|
| 1040 |
+
)
|
| 1041 |
+
# Register with clear_on_fresh_cache for fresh_cache() integration
|
| 1042 |
+
clear_on_fresh_cache(self)
|
| 1043 |
+
|
| 1044 |
+
def cache_clear(self) -> None:
|
| 1045 |
+
"""Clear the on-disk cache.
|
| 1046 |
+
|
| 1047 |
+
This method removes the on-disk cache directory. The in-memory cache
|
| 1048 |
+
is cleared automatically by the underlying Memoizer instance, which
|
| 1049 |
+
registers itself with clear_on_fresh_cache.
|
| 1050 |
+
|
| 1051 |
+
This is called by the fresh_cache() context manager via clear_on_fresh_cache
|
| 1052 |
+
registration.
|
| 1053 |
+
"""
|
| 1054 |
+
# Clear on-disk cache by removing the cache directory
|
| 1055 |
+
# Note: in-memory cache is cleared by the Memoizer's own cache_clear
|
| 1056 |
+
if self._disk_cache._cache_dir.exists():
|
| 1057 |
+
shutil.rmtree(self._disk_cache._cache_dir, ignore_errors=True)
|
| 1058 |
+
|
| 1059 |
+
def record(
|
| 1060 |
+
self,
|
| 1061 |
+
custom_params_encoder: Callable[_P, object] | None = None,
|
| 1062 |
+
custom_result_encoder: ResultEncoderFactory[_P, _R, _EncodedR] | None = None,
|
| 1063 |
+
) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]:
|
| 1064 |
+
"""Record a function call result with custom encoding to both caches.
|
| 1065 |
+
|
| 1066 |
+
This is a decorator that wraps a function to enable memoization
|
| 1067 |
+
with custom encoding/decoding logic. Results are stored in both
|
| 1068 |
+
the in-memory cache and the on-disk cache.
|
| 1069 |
+
|
| 1070 |
+
This method delegates to the underlying Memoizer for memory caching,
|
| 1071 |
+
then adds disk persistence. For deferred recordings, it registers
|
| 1072 |
+
an additional callback to persist to disk when the recording completes.
|
| 1073 |
+
|
| 1074 |
+
Args:
|
| 1075 |
+
custom_params_encoder: Optional encoder for function parameters.
|
| 1076 |
+
If None, parameters are pickled directly.
|
| 1077 |
+
custom_result_encoder: Optional encoder factory for function results.
|
| 1078 |
+
First receives the underlying function, then takes
|
| 1079 |
+
function parameters and returns an encoder function
|
| 1080 |
+
that converts R -> _EncodedR.
|
| 1081 |
+
|
| 1082 |
+
Returns:
|
| 1083 |
+
A decorator function that can be applied to functions.
|
| 1084 |
+
|
| 1085 |
+
Example:
|
| 1086 |
+
@persistent_memoizer.record(
|
| 1087 |
+
custom_params_encoder=my_param_encoder,
|
| 1088 |
+
custom_result_encoder=my_result_encoder_factory,
|
| 1089 |
+
)
|
| 1090 |
+
def expensive_function(x, y):
|
| 1091 |
+
return x + y
|
| 1092 |
+
"""
|
| 1093 |
+
|
| 1094 |
+
def wrapper(fn: Callable[_P, _R]) -> Callable[_P, _R]:
|
| 1095 |
+
"""Wrap the function to enable memoization.
|
| 1096 |
+
|
| 1097 |
+
Args:
|
| 1098 |
+
fn: The function to wrap.
|
| 1099 |
+
|
| 1100 |
+
Returns:
|
| 1101 |
+
A wrapped version of the function.
|
| 1102 |
+
"""
|
| 1103 |
+
# If caching is disabled, return the original function unchanged
|
| 1104 |
+
if not config.IS_CACHING_MODULE_ENABLED():
|
| 1105 |
+
return fn
|
| 1106 |
+
|
| 1107 |
+
# Get the memory-cached version from the memoizer
|
| 1108 |
+
memory_record_fn = self._memoizer.record(
|
| 1109 |
+
custom_params_encoder, custom_result_encoder
|
| 1110 |
+
)(fn)
|
| 1111 |
+
|
| 1112 |
+
@functools.wraps(fn)
|
| 1113 |
+
def inner(*args: _P.args, **kwargs: _P.kwargs) -> _R:
|
| 1114 |
+
"""Call the original function and cache the result in both caches.
|
| 1115 |
+
|
| 1116 |
+
Args:
|
| 1117 |
+
*args: Positional arguments to pass to the function.
|
| 1118 |
+
**kwargs: Keyword arguments to pass to the function.
|
| 1119 |
+
|
| 1120 |
+
Returns:
|
| 1121 |
+
The result of calling the original function.
|
| 1122 |
+
"""
|
| 1123 |
+
# Call the memory-cached version (which calls fn and caches in memory)
|
| 1124 |
+
result = memory_record_fn(*args, **kwargs)
|
| 1125 |
+
|
| 1126 |
+
# Also store in disk cache
|
| 1127 |
+
cache_key = self._make_key(custom_params_encoder, *args, **kwargs)
|
| 1128 |
+
|
| 1129 |
+
# Check if this was a deferred recording
|
| 1130 |
+
pending = self._memoizer._pending_deferred.get(cache_key)
|
| 1131 |
+
if pending is not None:
|
| 1132 |
+
# Deferred recording - register our own callback for disk persistence
|
| 1133 |
+
# By the time our callback runs, Memoizer's callback will have
|
| 1134 |
+
# already inserted the cache entry, so we just read and persist it
|
| 1135 |
+
pending.register_callback(
|
| 1136 |
+
functools.partial(self._persist_to_disk, cache_key)
|
| 1137 |
+
)
|
| 1138 |
+
return result
|
| 1139 |
+
|
| 1140 |
+
# Persist the cache entry to disk
|
| 1141 |
+
self._persist_to_disk(cache_key)
|
| 1142 |
+
|
| 1143 |
+
return result
|
| 1144 |
+
|
| 1145 |
+
return inner
|
| 1146 |
+
|
| 1147 |
+
return wrapper
|
| 1148 |
+
|
| 1149 |
+
def _persist_to_disk(
|
| 1150 |
+
self,
|
| 1151 |
+
cache_key: str,
|
| 1152 |
+
_callback_result: object = None,
|
| 1153 |
+
) -> None:
|
| 1154 |
+
"""Persist a cache entry to disk.
|
| 1155 |
+
|
| 1156 |
+
This method handles disk persistence for both immediate and deferred recordings:
|
| 1157 |
+
- For immediate recordings: called directly after memory caching
|
| 1158 |
+
- For deferred recordings: registered as a callback that runs after the
|
| 1159 |
+
Memoizer's callback has inserted the entry into memory
|
| 1160 |
+
|
| 1161 |
+
Args:
|
| 1162 |
+
cache_key: The cache key for the entry.
|
| 1163 |
+
_callback_result: Unused. When called as a callback from DeferredRecording,
|
| 1164 |
+
this receives the encoded result, but we ignore it and
|
| 1165 |
+
read the full CacheEntry from the Memoizer's cache instead.
|
| 1166 |
+
"""
|
| 1167 |
+
# Always read from memory cache - Memoizer's callback has already inserted it
|
| 1168 |
+
cached_hit = self._memoizer._cache.get(cache_key)
|
| 1169 |
+
assert cached_hit, "Cache entry must exist in memory cache"
|
| 1170 |
+
cache_entry = cached_hit.value
|
| 1171 |
+
|
| 1172 |
+
# Store the full CacheEntry in disk cache for easier debugging
|
| 1173 |
+
pickled_entry: bytes = pickle.dumps(cache_entry)
|
| 1174 |
+
self._disk_cache.insert(cache_key, pickled_entry)
|
| 1175 |
+
|
| 1176 |
+
def replay(
|
| 1177 |
+
self,
|
| 1178 |
+
custom_params_encoder: Callable[_P, object] | None = None,
|
| 1179 |
+
custom_result_decoder: ResultDecoderFactory[_P, _R, _EncodedR] | None = None,
|
| 1180 |
+
) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]:
|
| 1181 |
+
"""Replay a cached function result without executing the function.
|
| 1182 |
+
|
| 1183 |
+
This is a decorator that retrieves cached results using a two-level
|
| 1184 |
+
cache strategy. It checks the in-memory cache first (fast), then
|
| 1185 |
+
falls back to the on-disk cache. If found on disk, the result is
|
| 1186 |
+
cached in memory for future access.
|
| 1187 |
+
|
| 1188 |
+
Args:
|
| 1189 |
+
custom_params_encoder: Optional encoder for function parameters.
|
| 1190 |
+
If None, parameters are pickled directly.
|
| 1191 |
+
custom_result_decoder: Optional decoder factory for cached results.
|
| 1192 |
+
First receives the underlying function, then takes
|
| 1193 |
+
function parameters and returns a decoder function
|
| 1194 |
+
that converts _EncodedR -> R.
|
| 1195 |
+
|
| 1196 |
+
Returns:
|
| 1197 |
+
A decorator function that can be applied to functions.
|
| 1198 |
+
|
| 1199 |
+
Example:
|
| 1200 |
+
@persistent_memoizer.replay(
|
| 1201 |
+
custom_params_encoder=my_param_encoder,
|
| 1202 |
+
custom_result_decoder=my_result_decoder_factory,
|
| 1203 |
+
)
|
| 1204 |
+
def expensive_function(x, y):
|
| 1205 |
+
return x + y
|
| 1206 |
+
"""
|
| 1207 |
+
|
| 1208 |
+
def wrapper(fn: Callable[_P, _R]) -> Callable[_P, _R]:
|
| 1209 |
+
"""Wrap the function to retrieve from cache.
|
| 1210 |
+
|
| 1211 |
+
Args:
|
| 1212 |
+
fn: The function to wrap (not actually called).
|
| 1213 |
+
|
| 1214 |
+
Returns:
|
| 1215 |
+
A wrapped version of the function.
|
| 1216 |
+
"""
|
| 1217 |
+
# If caching is disabled, always raise KeyError (cache miss)
|
| 1218 |
+
if not config.IS_CACHING_MODULE_ENABLED():
|
| 1219 |
+
|
| 1220 |
+
@functools.wraps(fn)
|
| 1221 |
+
def always_miss(*args: _P.args, **kwargs: _P.kwargs) -> _R:
|
| 1222 |
+
raise KeyError("Caching is disabled")
|
| 1223 |
+
|
| 1224 |
+
return always_miss
|
| 1225 |
+
|
| 1226 |
+
# Get the memory replay function
|
| 1227 |
+
memory_replay_fn = self._memoizer.replay(
|
| 1228 |
+
custom_params_encoder, custom_result_decoder
|
| 1229 |
+
)(fn)
|
| 1230 |
+
|
| 1231 |
+
@functools.wraps(fn)
|
| 1232 |
+
def inner(*args: _P.args, **kwargs: _P.kwargs) -> _R:
|
| 1233 |
+
"""Retrieve the cached result without calling the function.
|
| 1234 |
+
|
| 1235 |
+
Checks memory cache first, then disk cache. Populates memory
|
| 1236 |
+
cache from disk on a disk hit.
|
| 1237 |
+
|
| 1238 |
+
Args:
|
| 1239 |
+
*args: Positional arguments to generate the cache key.
|
| 1240 |
+
**kwargs: Keyword arguments to generate the cache key.
|
| 1241 |
+
|
| 1242 |
+
Returns:
|
| 1243 |
+
The cached result (decoded if decoder is provided).
|
| 1244 |
+
|
| 1245 |
+
Raises:
|
| 1246 |
+
KeyError: If no cached result exists for the given parameters.
|
| 1247 |
+
"""
|
| 1248 |
+
# Try memory cache first via memoizer
|
| 1249 |
+
try:
|
| 1250 |
+
return memory_replay_fn(*args, **kwargs)
|
| 1251 |
+
except KeyError:
|
| 1252 |
+
pass # Memory miss, check disk
|
| 1253 |
+
|
| 1254 |
+
# Memory miss - check disk cache
|
| 1255 |
+
cache_key = self._make_key(custom_params_encoder, *args, **kwargs)
|
| 1256 |
+
disk_hit = self._disk_cache.get(cache_key)
|
| 1257 |
+
if disk_hit is not None:
|
| 1258 |
+
# Disk cache hit - unpickle the CacheEntry
|
| 1259 |
+
pickled_value = disk_hit.value
|
| 1260 |
+
cache_entry = cast(CacheEntry, pickle.loads(pickled_value))
|
| 1261 |
+
|
| 1262 |
+
# Populate memory cache for future access
|
| 1263 |
+
self._memoizer._cache.insert(cache_key, cache_entry)
|
| 1264 |
+
|
| 1265 |
+
# Decode and return
|
| 1266 |
+
if custom_result_decoder is not None:
|
| 1267 |
+
# Get the decoder function by calling the factory factory:
|
| 1268 |
+
# 1. First call with fn to get params_to_decoder factory
|
| 1269 |
+
# 2. Second call with params to get the decoder function
|
| 1270 |
+
params_to_decoder = custom_result_decoder(fn)
|
| 1271 |
+
decoder_fn = params_to_decoder(*args, **kwargs)
|
| 1272 |
+
return decoder_fn(cast(_EncodedR, cache_entry.encoded_result))
|
| 1273 |
+
return cast(_R, cache_entry.encoded_result)
|
| 1274 |
+
|
| 1275 |
+
# Complete miss
|
| 1276 |
+
raise KeyError(f"No cached result found for key: {cache_key}")
|
| 1277 |
+
|
| 1278 |
+
return inner
|
| 1279 |
+
|
| 1280 |
+
return wrapper
|
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/caching/locks.py
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Lock acquisition utilities
|
| 2 |
+
|
| 3 |
+
This module provides safe and unsafe lock acquisition functions for both threading.Lock
|
| 4 |
+
and FileLock objects, with configurable timeout behaviors. It supports three timeout modes:
|
| 5 |
+
blocking (infinite wait), non-blocking (immediate), and blocking with timeout (finite wait).
|
| 6 |
+
|
| 7 |
+
The module offers both context manager and manual acquisition patterns:
|
| 8 |
+
- Safe acquisition: Uses context managers that automatically handle lock release
|
| 9 |
+
- Unsafe acquisition: Manual acquisition that requires explicit release by the caller
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
from contextlib import _GeneratorContextManager, contextmanager, ExitStack
|
| 15 |
+
from typing import TYPE_CHECKING, TypeAlias
|
| 16 |
+
from typing_extensions import Protocol
|
| 17 |
+
|
| 18 |
+
from filelock import BaseFileLock, Timeout
|
| 19 |
+
|
| 20 |
+
from . import exceptions
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
if TYPE_CHECKING:
|
| 24 |
+
from collections.abc import Generator
|
| 25 |
+
from threading import Lock
|
| 26 |
+
|
| 27 |
+
from .implementations import _CacheImpl
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
_LockContextManager: TypeAlias = _GeneratorContextManager[None, None, None]
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class _LockProtocol(Protocol): # noqa: PYI046
|
| 34 |
+
def __call__(self, timeout: float | None = None) -> _LockContextManager: ...
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
# Infinite timeout - blocks indefinitely until lock is acquired.
|
| 38 |
+
_BLOCKING: float = -1
|
| 39 |
+
# No timeout - returns immediately if lock cannot be acquired.
|
| 40 |
+
_NON_BLOCKING: float = 0
|
| 41 |
+
# Finite timeout - blocks for a specified duration before raising a timeout error.
|
| 42 |
+
_BLOCKING_WITH_TIMEOUT: float = 60.0
|
| 43 |
+
# Default timeout for lock acquisition.
|
| 44 |
+
_DEFAULT_TIMEOUT: float = _BLOCKING_WITH_TIMEOUT
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
@contextmanager
|
| 48 |
+
def _acquire_lock_with_timeout(
|
| 49 |
+
lock: Lock,
|
| 50 |
+
timeout: float | None = None,
|
| 51 |
+
) -> Generator[None, None, None]:
|
| 52 |
+
"""Context manager that safely acquires a threading.Lock with timeout and automatically releases it.
|
| 53 |
+
|
| 54 |
+
This function provides a safe way to acquire a lock with timeout support, ensuring
|
| 55 |
+
the lock is always released even if an exception occurs during execution.
|
| 56 |
+
|
| 57 |
+
Args:
|
| 58 |
+
lock: The threading.Lock object to acquire
|
| 59 |
+
timeout: Timeout in seconds. If None, uses _DEFAULT_TIMEOUT.
|
| 60 |
+
- Use _BLOCKING (-1.0) for infinite wait
|
| 61 |
+
- Use _NON_BLOCKING (0.0) for immediate return
|
| 62 |
+
- Use positive value for finite timeout
|
| 63 |
+
|
| 64 |
+
Yields:
|
| 65 |
+
None: Yields control to the caller while holding the lock
|
| 66 |
+
|
| 67 |
+
Raises:
|
| 68 |
+
LockTimeoutError: If the lock cannot be acquired within the timeout period
|
| 69 |
+
|
| 70 |
+
Example:
|
| 71 |
+
with _acquire_lock_with_timeout(my_lock, timeout=30.0):
|
| 72 |
+
# Critical section - lock is held
|
| 73 |
+
perform_critical_operation()
|
| 74 |
+
# Lock is automatically released here
|
| 75 |
+
"""
|
| 76 |
+
_unsafe_acquire_lock_with_timeout(lock, timeout=timeout)
|
| 77 |
+
|
| 78 |
+
try:
|
| 79 |
+
yield
|
| 80 |
+
finally:
|
| 81 |
+
lock.release()
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def _unsafe_acquire_lock_with_timeout(lock: Lock, timeout: float | None = None) -> None:
|
| 85 |
+
"""Acquire a threading.Lock with timeout without automatic release (unsafe).
|
| 86 |
+
|
| 87 |
+
This function acquires a lock with timeout support but does NOT automatically
|
| 88 |
+
release it. The caller is responsible for releasing the lock explicitly.
|
| 89 |
+
Use this only when you need manual control over lock lifetime.
|
| 90 |
+
|
| 91 |
+
Args:
|
| 92 |
+
lock: The threading.Lock object to acquire
|
| 93 |
+
timeout: Timeout in seconds. If None, uses _DEFAULT_TIMEOUT.
|
| 94 |
+
- Use _BLOCKING (-1.0) for infinite wait
|
| 95 |
+
- Use _NON_BLOCKING (0.0) for immediate return
|
| 96 |
+
- Use positive value for finite timeout
|
| 97 |
+
|
| 98 |
+
Raises:
|
| 99 |
+
LockTimeoutError: If the lock cannot be acquired within the timeout period
|
| 100 |
+
|
| 101 |
+
Warning:
|
| 102 |
+
This is an "unsafe" function because it does not automatically release
|
| 103 |
+
the lock. Always call lock.release() when done, preferably in a try/finally
|
| 104 |
+
block or use the safe _acquire_lock_with_timeout context manager instead.
|
| 105 |
+
|
| 106 |
+
Example:
|
| 107 |
+
lock = Lock()
|
| 108 |
+
try:
|
| 109 |
+
_unsafe_acquire_lock_with_timeout(lock, timeout=30.0)
|
| 110 |
+
# Critical section - lock is held
|
| 111 |
+
perform_critical_operation()
|
| 112 |
+
finally:
|
| 113 |
+
lock.release() # Must manually release!
|
| 114 |
+
"""
|
| 115 |
+
_timeout: float = timeout if timeout is not None else _DEFAULT_TIMEOUT
|
| 116 |
+
if not lock.acquire(timeout=_timeout):
|
| 117 |
+
raise exceptions.LockTimeoutError(lock, _timeout)
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
@contextmanager
|
| 121 |
+
def _acquire_flock_with_timeout(
|
| 122 |
+
flock: BaseFileLock,
|
| 123 |
+
timeout: float | None = None,
|
| 124 |
+
) -> Generator[None, None, None]:
|
| 125 |
+
"""Context manager that safely acquires a FileLock with timeout and automatically releases it.
|
| 126 |
+
|
| 127 |
+
This function provides a safe way to acquire a file lock with timeout support, ensuring
|
| 128 |
+
the lock is always released even if an exception occurs during execution.
|
| 129 |
+
|
| 130 |
+
Args:
|
| 131 |
+
flock: The FileLock object to acquire
|
| 132 |
+
timeout: Timeout in seconds. If None, uses _DEFAULT_TIMEOUT.
|
| 133 |
+
- Use _BLOCKING (-1.0) for infinite wait
|
| 134 |
+
- Use _NON_BLOCKING (0.0) for immediate return
|
| 135 |
+
- Use positive value for finite timeout
|
| 136 |
+
|
| 137 |
+
Yields:
|
| 138 |
+
None: Yields control to the caller while holding the file lock
|
| 139 |
+
|
| 140 |
+
Raises:
|
| 141 |
+
FileLockTimeoutError: If the file lock cannot be acquired within the timeout period
|
| 142 |
+
|
| 143 |
+
Example:
|
| 144 |
+
flock = FileLock("/tmp/my_process.lock")
|
| 145 |
+
with _acquire_flock_with_timeout(flock, timeout=30.0):
|
| 146 |
+
# Critical section - file lock is held
|
| 147 |
+
perform_exclusive_file_operation()
|
| 148 |
+
# File lock is automatically released here
|
| 149 |
+
"""
|
| 150 |
+
_unsafe_acquire_flock_with_timeout(flock, timeout=timeout)
|
| 151 |
+
|
| 152 |
+
try:
|
| 153 |
+
yield
|
| 154 |
+
finally:
|
| 155 |
+
flock.release()
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def _unsafe_acquire_flock_with_timeout(
|
| 159 |
+
flock: BaseFileLock,
|
| 160 |
+
timeout: float | None,
|
| 161 |
+
) -> None:
|
| 162 |
+
"""Acquire a FileLock with timeout without automatic release (unsafe).
|
| 163 |
+
|
| 164 |
+
This function acquires a file lock with timeout support but does NOT automatically
|
| 165 |
+
release it. The caller is responsible for releasing the lock explicitly.
|
| 166 |
+
Use this only when you need manual control over lock lifetime.
|
| 167 |
+
|
| 168 |
+
Args:
|
| 169 |
+
flock: The FileLock object to acquire
|
| 170 |
+
timeout: Timeout in seconds. If None, uses _DEFAULT_TIMEOUT.
|
| 171 |
+
- Use _BLOCKING (-1.0) for infinite wait
|
| 172 |
+
- Use _NON_BLOCKING (0.0) for immediate return
|
| 173 |
+
- Use positive value for finite timeout
|
| 174 |
+
|
| 175 |
+
Raises:
|
| 176 |
+
FileLockTimeoutError: If the file lock cannot be acquired within the timeout period
|
| 177 |
+
|
| 178 |
+
Warning:
|
| 179 |
+
This is an "unsafe" function because it does not automatically release
|
| 180 |
+
the lock. Always call flock.release() when done, preferably in a try/finally
|
| 181 |
+
block or use the safe _acquire_flock_with_timeout context manager instead.
|
| 182 |
+
|
| 183 |
+
Example:
|
| 184 |
+
flock = FileLock("/tmp/my_process.lock")
|
| 185 |
+
try:
|
| 186 |
+
_unsafe_acquire_flock_with_timeout(flock, timeout=30.0)
|
| 187 |
+
# Critical section - file lock is held
|
| 188 |
+
perform_exclusive_file_operation()
|
| 189 |
+
finally:
|
| 190 |
+
flock.release() # Must manually release!
|
| 191 |
+
"""
|
| 192 |
+
_timeout: float = timeout if timeout is not None else _DEFAULT_TIMEOUT
|
| 193 |
+
try:
|
| 194 |
+
_ = flock.acquire(timeout=_timeout)
|
| 195 |
+
except Timeout as err:
|
| 196 |
+
raise exceptions.FileLockTimeoutError(flock, _timeout) from err
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
@contextmanager
|
| 200 |
+
def _acquire_many_impl_locks_with_timeout(
|
| 201 |
+
*impls: _CacheImpl,
|
| 202 |
+
timeout: float | None = None,
|
| 203 |
+
) -> Generator[None, None, None]:
|
| 204 |
+
with ExitStack() as stack:
|
| 205 |
+
for impl in impls:
|
| 206 |
+
stack.enter_context(impl.lock(timeout))
|
| 207 |
+
yield
|
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/caching/memoizers.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
|
| 3 |
+
from .interfaces import PersistentMemoizer
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
# Memoizer for _should_pad in pad_mm
|
| 7 |
+
should_pad_memoizer = PersistentMemoizer(sub_dir=Path("should_pad"))
|
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/caching/utils.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Utility functions
|
| 2 |
+
|
| 3 |
+
This module provides helper functions for LRU caching decorators used
|
| 4 |
+
throughout the caching system.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from collections.abc import Callable
|
| 8 |
+
from functools import lru_cache, wraps
|
| 9 |
+
from typing_extensions import ParamSpec, TypedDict, TypeVar
|
| 10 |
+
|
| 11 |
+
from torch import Tensor
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
# Type specification for function parameters
|
| 15 |
+
P = ParamSpec("P")
|
| 16 |
+
# Type variable for function return values
|
| 17 |
+
R = TypeVar("R")
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def _lru_cache(fn: Callable[P, R]) -> Callable[P, R]:
|
| 21 |
+
"""LRU cache decorator with TypeError fallback.
|
| 22 |
+
|
| 23 |
+
Provides LRU caching with a fallback mechanism that calls the original
|
| 24 |
+
function if caching fails due to unhashable arguments. Uses a cache
|
| 25 |
+
size of 64 with typed comparison.
|
| 26 |
+
|
| 27 |
+
Args:
|
| 28 |
+
fn: The function to be cached.
|
| 29 |
+
|
| 30 |
+
Returns:
|
| 31 |
+
A wrapper function that attempts caching with fallback to original function.
|
| 32 |
+
"""
|
| 33 |
+
cached_fn = lru_cache(maxsize=64, typed=True)(fn)
|
| 34 |
+
|
| 35 |
+
@wraps(fn)
|
| 36 |
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
| 37 |
+
try:
|
| 38 |
+
return cached_fn(*args, **kwargs)
|
| 39 |
+
except TypeError:
|
| 40 |
+
return fn(*args, **kwargs)
|
| 41 |
+
|
| 42 |
+
return wrapper
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
class EncodedTensor(TypedDict):
|
| 46 |
+
"""TypedDict for encoded tensor metadata."""
|
| 47 |
+
|
| 48 |
+
shape: tuple[int, ...]
|
| 49 |
+
stride: tuple[int, ...]
|
| 50 |
+
dtype: str
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def _encode_tensor(t: Tensor) -> EncodedTensor:
|
| 54 |
+
"""Encode a tensor's metadata into a JSON-serializable dict.
|
| 55 |
+
|
| 56 |
+
Args:
|
| 57 |
+
t: PyTorch tensor to encode
|
| 58 |
+
|
| 59 |
+
Returns:
|
| 60 |
+
Dict containing shape, stride, and dtype information
|
| 61 |
+
"""
|
| 62 |
+
return EncodedTensor(
|
| 63 |
+
shape=tuple(t.shape),
|
| 64 |
+
stride=tuple(t.stride()),
|
| 65 |
+
dtype=str(t.dtype),
|
| 66 |
+
)
|
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/compile_tasks.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import functools
|
| 4 |
+
import linecache
|
| 5 |
+
import os
|
| 6 |
+
import sys
|
| 7 |
+
import time
|
| 8 |
+
import warnings
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
from types import ModuleType
|
| 11 |
+
from typing import Any, TYPE_CHECKING
|
| 12 |
+
|
| 13 |
+
from torch._utils_internal import log_triton_builds
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
if TYPE_CHECKING:
|
| 17 |
+
from collections.abc import Callable
|
| 18 |
+
|
| 19 |
+
from torch._inductor.runtime.triton_heuristics import CachingAutotuner
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def _reload_python_module(
|
| 23 |
+
key: str, path: str, set_sys_modules: bool = True
|
| 24 |
+
) -> ModuleType:
|
| 25 |
+
with open(path) as f:
|
| 26 |
+
try:
|
| 27 |
+
code = compile(f.read(), path, "exec", dont_inherit=True)
|
| 28 |
+
except Exception as e:
|
| 29 |
+
raise RuntimeError(
|
| 30 |
+
f"Failed to import {path}\n{type(e).__name__}: {e}"
|
| 31 |
+
) from None
|
| 32 |
+
mod = ModuleType(f"{__name__}.{key}")
|
| 33 |
+
mod.__file__ = path
|
| 34 |
+
mod.key = key # type: ignore[attr-defined]
|
| 35 |
+
exec(code, mod.__dict__, mod.__dict__)
|
| 36 |
+
if set_sys_modules:
|
| 37 |
+
sys.modules[mod.__name__] = mod
|
| 38 |
+
return mod
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
@functools.cache
|
| 42 |
+
def _set_triton_ptxas_path() -> None:
|
| 43 |
+
if os.environ.get("TRITON_PTXAS_PATH") is not None:
|
| 44 |
+
return
|
| 45 |
+
ptxas = Path(__file__).absolute().parents[2] / "bin" / "ptxas"
|
| 46 |
+
if not ptxas.exists():
|
| 47 |
+
return
|
| 48 |
+
if ptxas.is_file() and os.access(ptxas, os.X_OK):
|
| 49 |
+
os.environ["TRITON_PTXAS_PATH"] = str(ptxas)
|
| 50 |
+
else:
|
| 51 |
+
warnings.warn(f"{ptxas} exists but is not an executable")
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def _set_triton_libdevice_path() -> None:
|
| 55 |
+
"""
|
| 56 |
+
Use the CUDA toolkit's libdevice instead of Triton's bundled version.
|
| 57 |
+
This ensures Triton's pow matches CUDA's powf for bitwise precision.
|
| 58 |
+
Gated by config.eager_numerics.use_pytorch_libdevice.
|
| 59 |
+
"""
|
| 60 |
+
from torch._inductor import config
|
| 61 |
+
|
| 62 |
+
if not config.eager_numerics.use_pytorch_libdevice:
|
| 63 |
+
return
|
| 64 |
+
|
| 65 |
+
_set_triton_libdevice_path_impl()
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def _set_triton_libdevice_path_impl() -> None:
|
| 69 |
+
try:
|
| 70 |
+
from triton import knobs
|
| 71 |
+
except ImportError:
|
| 72 |
+
return
|
| 73 |
+
|
| 74 |
+
env_path = os.environ.get("TRITON_LIBDEVICE_PATH")
|
| 75 |
+
if env_path is not None:
|
| 76 |
+
knobs.nvidia.libdevice_path = env_path
|
| 77 |
+
return
|
| 78 |
+
|
| 79 |
+
if knobs.nvidia.libdevice_path is not None:
|
| 80 |
+
return
|
| 81 |
+
|
| 82 |
+
try:
|
| 83 |
+
from torch.utils.cpp_extension import CUDA_HOME
|
| 84 |
+
|
| 85 |
+
if CUDA_HOME is None:
|
| 86 |
+
warnings.warn(
|
| 87 |
+
"CUDA_HOME not set; using Triton's bundled libdevice which may "
|
| 88 |
+
"cause minor precision differences in pow operations. "
|
| 89 |
+
"To fix: set TRITON_LIBDEVICE_PATH to your CUDA toolkit's libdevice, "
|
| 90 |
+
"e.g., export TRITON_LIBDEVICE_PATH=/usr/local/cuda/nvvm/libdevice/libdevice.10.bc",
|
| 91 |
+
stacklevel=3,
|
| 92 |
+
)
|
| 93 |
+
return
|
| 94 |
+
libdevice = Path(CUDA_HOME) / "nvvm" / "libdevice" / "libdevice.10.bc"
|
| 95 |
+
if libdevice.is_file():
|
| 96 |
+
knobs.nvidia.libdevice_path = str(libdevice)
|
| 97 |
+
# Also set env var so subprocess compile workers inherit it
|
| 98 |
+
os.environ["TRITON_LIBDEVICE_PATH"] = str(libdevice)
|
| 99 |
+
else:
|
| 100 |
+
warnings.warn(
|
| 101 |
+
f"CUDA libdevice not found at {libdevice}; using Triton's bundled "
|
| 102 |
+
"libdevice which may cause minor precision differences in pow operations. "
|
| 103 |
+
"To fix: set TRITON_LIBDEVICE_PATH to your CUDA toolkit's libdevice, "
|
| 104 |
+
"e.g., export TRITON_LIBDEVICE_PATH=/usr/local/cuda/nvvm/libdevice/libdevice.10.bc",
|
| 105 |
+
stacklevel=3,
|
| 106 |
+
)
|
| 107 |
+
except ImportError:
|
| 108 |
+
warnings.warn(
|
| 109 |
+
"torch.utils.cpp_extension not available; using Triton's bundled "
|
| 110 |
+
"libdevice which may cause minor precision differences in pow operations. "
|
| 111 |
+
"To fix: set TRITON_LIBDEVICE_PATH to your CUDA toolkit's libdevice, "
|
| 112 |
+
"e.g., export TRITON_LIBDEVICE_PATH=/usr/local/cuda/nvvm/libdevice/libdevice.10.bc",
|
| 113 |
+
stacklevel=3,
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def _worker_compile_triton(
|
| 118 |
+
load_kernel: Callable[[], CachingAutotuner],
|
| 119 |
+
extra_env: dict[str, str],
|
| 120 |
+
extra_config: dict[str, Any],
|
| 121 |
+
) -> tuple[CachingAutotuner, int]:
|
| 122 |
+
_set_triton_ptxas_path()
|
| 123 |
+
os.environ.update(extra_env)
|
| 124 |
+
# Set libdevice path if passed via env from main process
|
| 125 |
+
libdevice_path = extra_env.get("TRITON_LIBDEVICE_PATH")
|
| 126 |
+
if libdevice_path:
|
| 127 |
+
try:
|
| 128 |
+
from triton import knobs
|
| 129 |
+
|
| 130 |
+
knobs.nvidia.libdevice_path = libdevice_path
|
| 131 |
+
except ImportError:
|
| 132 |
+
pass
|
| 133 |
+
from torch._inductor import config
|
| 134 |
+
|
| 135 |
+
with config.patch(extra_config):
|
| 136 |
+
fail = None
|
| 137 |
+
try:
|
| 138 |
+
start_ns = time.time_ns()
|
| 139 |
+
kernel = load_kernel()
|
| 140 |
+
kernel.precompile(warm_cache_only=True)
|
| 141 |
+
elapsed_ns = time.time_ns() - start_ns
|
| 142 |
+
kernel.prepare_for_pickle()
|
| 143 |
+
# We can release this memory in the compile subprocesses:
|
| 144 |
+
linecache.clearcache()
|
| 145 |
+
return kernel, elapsed_ns // 1000
|
| 146 |
+
except Exception as e:
|
| 147 |
+
fail = str(e)
|
| 148 |
+
raise
|
| 149 |
+
finally:
|
| 150 |
+
log_triton_builds(fail=fail)
|
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/coordinate_descent_tuner.py
ADDED
|
@@ -0,0 +1,412 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# mypy: allow-untyped-defs
|
| 2 |
+
import copy
|
| 3 |
+
import itertools
|
| 4 |
+
import logging
|
| 5 |
+
from collections.abc import Callable
|
| 6 |
+
from typing import TYPE_CHECKING
|
| 7 |
+
|
| 8 |
+
from torch.utils._ordered_set import OrderedSet
|
| 9 |
+
|
| 10 |
+
from ..utils import get_max_numwarps
|
| 11 |
+
from .hints import TRITON_MAX_BLOCK
|
| 12 |
+
from .runtime_utils import red_text, triton_config_to_hashable
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
if TYPE_CHECKING:
|
| 16 |
+
from .triton_compat import triton
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
log = logging.getLogger(__name__)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def get_field(config, name):
|
| 23 |
+
if name == "num_warps":
|
| 24 |
+
return config.num_warps
|
| 25 |
+
elif name == "num_stages":
|
| 26 |
+
return config.num_stages
|
| 27 |
+
elif name == "waves_per_eu":
|
| 28 |
+
return config.kwargs.get(name, int(8 // config.num_warps))
|
| 29 |
+
else:
|
| 30 |
+
return config.kwargs.get(name, None)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def set_field(config, name, value):
|
| 34 |
+
if name == "num_warps":
|
| 35 |
+
config.num_warps = value
|
| 36 |
+
elif name == "num_stages":
|
| 37 |
+
config.num_stages = value
|
| 38 |
+
else:
|
| 39 |
+
config.kwargs[name] = value
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class CoordescTuner:
|
| 43 |
+
"""
|
| 44 |
+
The coordinate descent tuner. Tune one field/coordinate at a time.
|
| 45 |
+
|
| 46 |
+
TODO will it be necessary to tune multiple fields simultaneously.
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
TODO: what if both increasing and decreasing a field can improve perf.
|
| 50 |
+
i.e., there are multiple local optima..
|
| 51 |
+
"""
|
| 52 |
+
|
| 53 |
+
def __init__(
|
| 54 |
+
self,
|
| 55 |
+
is_mm=False,
|
| 56 |
+
is_native_matmul=False,
|
| 57 |
+
is_mix_order_reduction=False,
|
| 58 |
+
name="unknown",
|
| 59 |
+
size_hints=None,
|
| 60 |
+
inductor_meta=None,
|
| 61 |
+
frozen_fields=None,
|
| 62 |
+
):
|
| 63 |
+
self.is_mm = is_mm # we will tune num_stages for mm
|
| 64 |
+
|
| 65 |
+
# Native matmul codegen assumes ZBLOCK=1 always.
|
| 66 |
+
# This is because 3d tl.dot is slow and so we want to tile y and x only.
|
| 67 |
+
# tl.dot also does not support size smaller than 16; we put this restriction.
|
| 68 |
+
self.is_native_matmul = is_native_matmul
|
| 69 |
+
assert not (self.is_mm and self.is_native_matmul)
|
| 70 |
+
self.is_mix_order_reduction = is_mix_order_reduction
|
| 71 |
+
self.cached_benchmark_results = {}
|
| 72 |
+
self.name = name
|
| 73 |
+
self.size_hints = size_hints
|
| 74 |
+
self.inductor_meta = inductor_meta or {}
|
| 75 |
+
self.frozen_fields: OrderedSet[str] = (
|
| 76 |
+
OrderedSet(frozen_fields) if frozen_fields is not None else OrderedSet()
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
def get_config_max(self, prefix: str) -> int:
|
| 80 |
+
max_block = TRITON_MAX_BLOCK[prefix.upper()]
|
| 81 |
+
size_hint = self.size_hints.get(prefix) if self.size_hints is not None else None
|
| 82 |
+
return min(max_block, size_hint) if size_hint is not None else max_block
|
| 83 |
+
|
| 84 |
+
def get_warpsmax(self):
|
| 85 |
+
# Avoid querying device directly if device properties are populated in inductor_meta
|
| 86 |
+
warp_size = self.inductor_meta.get("warp_size")
|
| 87 |
+
max_threads_per_block = self.inductor_meta.get("max_threads_per_block")
|
| 88 |
+
if warp_size and max_threads_per_block:
|
| 89 |
+
return max_threads_per_block // warp_size
|
| 90 |
+
else:
|
| 91 |
+
return get_max_numwarps()
|
| 92 |
+
|
| 93 |
+
def cache_benchmark_result(self, config, timing):
|
| 94 |
+
self.cached_benchmark_results[triton_config_to_hashable(config)] = timing
|
| 95 |
+
|
| 96 |
+
def lookup_in_cache(self, config):
|
| 97 |
+
return self.cached_benchmark_results.get(triton_config_to_hashable(config))
|
| 98 |
+
|
| 99 |
+
def call_func(self, func, config):
|
| 100 |
+
found = self.lookup_in_cache(config)
|
| 101 |
+
if found is not None:
|
| 102 |
+
log.debug(" CACHED")
|
| 103 |
+
return found
|
| 104 |
+
timing = func(config)
|
| 105 |
+
self.cache_benchmark_result(config, timing)
|
| 106 |
+
return timing
|
| 107 |
+
|
| 108 |
+
@property
|
| 109 |
+
def tunable_fields(self):
|
| 110 |
+
out = [
|
| 111 |
+
"XBLOCK",
|
| 112 |
+
"YBLOCK",
|
| 113 |
+
"ZBLOCK",
|
| 114 |
+
# NOTE: we should not tune R0_BLOCK for persistent reduction.
|
| 115 |
+
# We rely on the fact that persistent reduction's triton.Config
|
| 116 |
+
# does not have the R0_BLOCK field to guarantee that.
|
| 117 |
+
"R0_BLOCK",
|
| 118 |
+
"R1_BLOCK",
|
| 119 |
+
# the following 3 are for mm
|
| 120 |
+
"BLOCK_M",
|
| 121 |
+
"BLOCK_N",
|
| 122 |
+
"BLOCK_K",
|
| 123 |
+
"num_warps",
|
| 124 |
+
]
|
| 125 |
+
if self.is_mm:
|
| 126 |
+
out.append("num_stages")
|
| 127 |
+
if self.inductor_meta.get("is_hip") is True:
|
| 128 |
+
out.append("waves_per_eu")
|
| 129 |
+
if self.is_native_matmul:
|
| 130 |
+
out.append("num_stages")
|
| 131 |
+
out.remove("ZBLOCK") # ZBLOCK=1 always in native matmul
|
| 132 |
+
|
| 133 |
+
if self.is_mix_order_reduction:
|
| 134 |
+
# unlike TritonConfig.num_stages, this one is
|
| 135 |
+
# put in TritonConfig.kwargs["NUM_STAGES"] and is used to
|
| 136 |
+
# control the stage of pipelining of tl.range.
|
| 137 |
+
out.append("NUM_STAGES")
|
| 138 |
+
|
| 139 |
+
return [f for f in out if f not in self.frozen_fields]
|
| 140 |
+
|
| 141 |
+
def value_too_large(self, name: str, val: int) -> bool:
|
| 142 |
+
block_suffix = "BLOCK"
|
| 143 |
+
if name.endswith(block_suffix):
|
| 144 |
+
prefix = name.strip(block_suffix).lower()
|
| 145 |
+
return val > self.get_config_max(prefix)
|
| 146 |
+
if name == "num_warps":
|
| 147 |
+
return val > self.get_warpsmax()
|
| 148 |
+
if name == "waves_per_eu":
|
| 149 |
+
return val > 8
|
| 150 |
+
|
| 151 |
+
return False
|
| 152 |
+
|
| 153 |
+
def value_too_small(self, name: str, val: int) -> bool:
|
| 154 |
+
# In native matmul, block size should be >= 16 for tl.dot
|
| 155 |
+
if self.is_native_matmul:
|
| 156 |
+
if name in ["YBLOCK", "XBLOCK", "R0_BLOCK"]:
|
| 157 |
+
return val < 16
|
| 158 |
+
|
| 159 |
+
# Break if value becomes 0/neg
|
| 160 |
+
return val <= 0
|
| 161 |
+
|
| 162 |
+
def get_neighbour_values(self, name, orig_val, radius=None, include_self=False):
|
| 163 |
+
"""
|
| 164 |
+
Get neighbour values in 'radius' steps. The original value is not
|
| 165 |
+
returned as it's own neighbour.
|
| 166 |
+
"""
|
| 167 |
+
if radius is None:
|
| 168 |
+
radius = 1
|
| 169 |
+
if name == "NUM_STAGES":
|
| 170 |
+
# we see cases that
|
| 171 |
+
# NUM_STAGES=1 is better than NUM_STAGES=2
|
| 172 |
+
# while NUM_STAGES=1 is worse than NUM_STAGES=3
|
| 173 |
+
radius = max(radius, 2)
|
| 174 |
+
|
| 175 |
+
assert radius >= 1
|
| 176 |
+
|
| 177 |
+
def update(cur_val, inc=True):
|
| 178 |
+
if name in ["num_stages", "NUM_STAGES"]:
|
| 179 |
+
if inc:
|
| 180 |
+
return cur_val + 1
|
| 181 |
+
else:
|
| 182 |
+
return cur_val - 1
|
| 183 |
+
else:
|
| 184 |
+
if inc:
|
| 185 |
+
return cur_val * 2
|
| 186 |
+
else:
|
| 187 |
+
return cur_val // 2
|
| 188 |
+
|
| 189 |
+
out = []
|
| 190 |
+
# increment loop
|
| 191 |
+
cur_val = orig_val
|
| 192 |
+
for _ in range(radius):
|
| 193 |
+
cur_val = update(cur_val, True)
|
| 194 |
+
if self.value_too_large(name, cur_val):
|
| 195 |
+
break
|
| 196 |
+
out.append(cur_val)
|
| 197 |
+
|
| 198 |
+
# decrement loop
|
| 199 |
+
cur_val = orig_val
|
| 200 |
+
for _ in range(radius):
|
| 201 |
+
cur_val = update(cur_val, False)
|
| 202 |
+
if self.value_too_small(name, cur_val):
|
| 203 |
+
break
|
| 204 |
+
out.append(cur_val)
|
| 205 |
+
|
| 206 |
+
if include_self:
|
| 207 |
+
out.append(orig_val)
|
| 208 |
+
return out
|
| 209 |
+
|
| 210 |
+
@staticmethod
|
| 211 |
+
def has_improvement(baseline, test):
|
| 212 |
+
threshold = 0.001 # 0.1%
|
| 213 |
+
return test is not None and test < baseline * (1 - threshold)
|
| 214 |
+
|
| 215 |
+
def is_valid_config(self, config) -> bool:
|
| 216 |
+
if self.is_mix_order_reduction:
|
| 217 |
+
# Mix order reduction has an extra constraint that
|
| 218 |
+
# we should not tune XBLOCK beyond RSPLIT_SIZE
|
| 219 |
+
xblock = config.kwargs["XBLOCK"]
|
| 220 |
+
split_size = config.kwargs["RSPLIT_SIZE"]
|
| 221 |
+
return xblock <= split_size
|
| 222 |
+
return True
|
| 223 |
+
|
| 224 |
+
def check_all_tuning_directions(
|
| 225 |
+
self,
|
| 226 |
+
# pyrefly: ignore [missing-attribute]
|
| 227 |
+
func: Callable[["triton.Config"], float],
|
| 228 |
+
best_config,
|
| 229 |
+
best_timing,
|
| 230 |
+
):
|
| 231 |
+
"""
|
| 232 |
+
Check all directions. We only do this once the regular coordinate
|
| 233 |
+
descent tuning find no better choices any more.
|
| 234 |
+
We only have a few tunable fields, so this should be fine.
|
| 235 |
+
"""
|
| 236 |
+
candidate_values_list = []
|
| 237 |
+
effective_fields = []
|
| 238 |
+
for field in self.tunable_fields:
|
| 239 |
+
old_value = get_field(best_config, field)
|
| 240 |
+
if old_value is None:
|
| 241 |
+
continue
|
| 242 |
+
radius = self.inductor_meta.get("coordinate_descent_search_radius", 1)
|
| 243 |
+
candidate_values = self.get_neighbour_values(
|
| 244 |
+
field,
|
| 245 |
+
old_value,
|
| 246 |
+
radius=radius,
|
| 247 |
+
include_self=True,
|
| 248 |
+
)
|
| 249 |
+
candidate_values_list.append(candidate_values)
|
| 250 |
+
effective_fields.append(field)
|
| 251 |
+
|
| 252 |
+
choices = itertools.product(*candidate_values_list)
|
| 253 |
+
improved = False
|
| 254 |
+
for choice in choices:
|
| 255 |
+
assert len(choice) == len(effective_fields)
|
| 256 |
+
candidate_config = copy.deepcopy(best_config)
|
| 257 |
+
for new_val, field in zip(choice, effective_fields):
|
| 258 |
+
set_field(candidate_config, field, new_val)
|
| 259 |
+
if not self.is_valid_config(candidate_config):
|
| 260 |
+
continue
|
| 261 |
+
cmp_res, candidate_timing = self.compare_config(
|
| 262 |
+
func, candidate_config, best_config, best_timing
|
| 263 |
+
)
|
| 264 |
+
if cmp_res:
|
| 265 |
+
improved = True
|
| 266 |
+
best_config = candidate_config
|
| 267 |
+
best_timing = candidate_timing
|
| 268 |
+
|
| 269 |
+
return improved, best_config, best_timing
|
| 270 |
+
|
| 271 |
+
def compare_config(self, func, candidate_config, best_config, best_timing):
|
| 272 |
+
"""
|
| 273 |
+
Check if candidate_config is better than best_config.
|
| 274 |
+
|
| 275 |
+
Return a tuple of (compare_result, candidate_timing).
|
| 276 |
+
compare_result is true iff candidate_config is better.
|
| 277 |
+
"""
|
| 278 |
+
log.debug("Try config %s", candidate_config)
|
| 279 |
+
try:
|
| 280 |
+
candidate_timing = self.call_func(func, candidate_config)
|
| 281 |
+
except Exception as e:
|
| 282 |
+
log.debug("Got exception %s", e) # noqa: G200
|
| 283 |
+
return False, float("inf")
|
| 284 |
+
|
| 285 |
+
if self.has_improvement(best_timing, candidate_timing):
|
| 286 |
+
log.debug(
|
| 287 |
+
"Tune from %s %f -> %s %f",
|
| 288 |
+
best_config,
|
| 289 |
+
best_timing,
|
| 290 |
+
candidate_config,
|
| 291 |
+
candidate_timing,
|
| 292 |
+
)
|
| 293 |
+
|
| 294 |
+
return True, candidate_timing
|
| 295 |
+
return False, candidate_timing
|
| 296 |
+
|
| 297 |
+
def autotune(
|
| 298 |
+
self,
|
| 299 |
+
# pyrefly: ignore [missing-attribute]
|
| 300 |
+
func: Callable[["triton.Config"], float],
|
| 301 |
+
# pyrefly: ignore [missing-attribute]
|
| 302 |
+
baseline_config: "triton.Config",
|
| 303 |
+
baseline_timing: float | None = None,
|
| 304 |
+
) -> "triton.Config": # pyrefly: ignore # missing-attribute
|
| 305 |
+
if baseline_timing is None:
|
| 306 |
+
baseline_timing = self.call_func(func, baseline_config)
|
| 307 |
+
|
| 308 |
+
log.debug("= Do coordinate descent tuning for %s =", self.name)
|
| 309 |
+
log.debug(
|
| 310 |
+
"%s: Baseline Config %s, baseline timing %f",
|
| 311 |
+
self.name,
|
| 312 |
+
baseline_config,
|
| 313 |
+
baseline_timing,
|
| 314 |
+
)
|
| 315 |
+
improved = True
|
| 316 |
+
best_config = baseline_config
|
| 317 |
+
best_timing = baseline_timing
|
| 318 |
+
tunable_fields = self.tunable_fields
|
| 319 |
+
|
| 320 |
+
while improved:
|
| 321 |
+
improved = False
|
| 322 |
+
|
| 323 |
+
for name in tunable_fields:
|
| 324 |
+
cur_val = get_field(best_config, name)
|
| 325 |
+
# some kernel don't have R0_BLOCK/YBLOCK/ZBLOCK. So cur_val may be None
|
| 326 |
+
if cur_val is None:
|
| 327 |
+
continue
|
| 328 |
+
|
| 329 |
+
# It's possible that candidate_values is empty.
|
| 330 |
+
# E.g., if XBLOCK is 1 initially and size_hint for x is also 1.
|
| 331 |
+
# We would not try either larger or smaller XBLOCK in this case.
|
| 332 |
+
candidate_values = self.get_neighbour_values(name, cur_val)
|
| 333 |
+
|
| 334 |
+
for next_val in candidate_values:
|
| 335 |
+
candidate_config = copy.deepcopy(best_config)
|
| 336 |
+
set_field(candidate_config, name, next_val)
|
| 337 |
+
|
| 338 |
+
if not self.is_valid_config(candidate_config):
|
| 339 |
+
continue
|
| 340 |
+
cmp_res, candidate_timing = self.compare_config(
|
| 341 |
+
func, candidate_config, best_config, best_timing
|
| 342 |
+
)
|
| 343 |
+
if cmp_res:
|
| 344 |
+
improved = True
|
| 345 |
+
best_config, best_timing = candidate_config, candidate_timing
|
| 346 |
+
|
| 347 |
+
if not improved and self.inductor_meta.get(
|
| 348 |
+
"coordinate_descent_check_all_directions"
|
| 349 |
+
):
|
| 350 |
+
old_best_timing = best_timing
|
| 351 |
+
improved, best_config, best_timing = self.check_all_tuning_directions(
|
| 352 |
+
func, best_config, best_timing
|
| 353 |
+
)
|
| 354 |
+
|
| 355 |
+
if improved:
|
| 356 |
+
msg = red_text(
|
| 357 |
+
"%s: Coordinate descend tuning found improvement of %.3fx by looking in all directions."
|
| 358 |
+
)
|
| 359 |
+
log.debug(
|
| 360 |
+
msg,
|
| 361 |
+
self.name,
|
| 362 |
+
old_best_timing / best_timing,
|
| 363 |
+
)
|
| 364 |
+
|
| 365 |
+
log.debug(
|
| 366 |
+
"%s: Improve from %s %f -> %s %f, %.3fx",
|
| 367 |
+
self.name,
|
| 368 |
+
baseline_config,
|
| 369 |
+
baseline_timing,
|
| 370 |
+
best_config,
|
| 371 |
+
best_timing,
|
| 372 |
+
baseline_timing / best_timing,
|
| 373 |
+
)
|
| 374 |
+
|
| 375 |
+
return best_config
|
| 376 |
+
|
| 377 |
+
@staticmethod
|
| 378 |
+
def autotune_single_field(fn, init_val, min_val=None, max_val=None):
|
| 379 |
+
"""
|
| 380 |
+
fn is a function that takes the field value and returns the benchmarking result
|
| 381 |
+
init_val is the starting point of autotuning.
|
| 382 |
+
|
| 383 |
+
Should work well for parabola like curve. Here is a real example
|
| 384 |
+
for split-size of mix-order-reduction: https://github.com/pytorch/pytorch/pull/166461
|
| 385 |
+
"""
|
| 386 |
+
cache = {}
|
| 387 |
+
|
| 388 |
+
def _bench(val):
|
| 389 |
+
if val not in cache:
|
| 390 |
+
cache[val] = fn(val)
|
| 391 |
+
# print(f"split size {val} -> {cache[val]:.3f} ms")
|
| 392 |
+
return cache[val]
|
| 393 |
+
|
| 394 |
+
if min_val is None:
|
| 395 |
+
min_val = 1
|
| 396 |
+
if max_val is None:
|
| 397 |
+
max_val = 2**30 # some arbitrary large value
|
| 398 |
+
|
| 399 |
+
best_val = init_val
|
| 400 |
+
improved = True
|
| 401 |
+
while improved:
|
| 402 |
+
improved = False
|
| 403 |
+
candlist = [best_val // 2, best_val * 2]
|
| 404 |
+
for cand in candlist:
|
| 405 |
+
cand = max(cand, min_val)
|
| 406 |
+
cand = min(cand, max_val)
|
| 407 |
+
|
| 408 |
+
if _bench(cand) < _bench(best_val):
|
| 409 |
+
best_val = cand
|
| 410 |
+
improved = True
|
| 411 |
+
|
| 412 |
+
return best_val
|
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/debug_utils.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import functools
|
| 2 |
+
import logging
|
| 3 |
+
import threading
|
| 4 |
+
import weakref
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
from torch.utils._ordered_set import OrderedSet
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
log = logging.getLogger(__name__)
|
| 11 |
+
|
| 12 |
+
local = threading.local()
|
| 13 |
+
local.memory_tracker = None
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class BufferMemoryTracker:
|
| 17 |
+
"""
|
| 18 |
+
Tracks inductor runtime allocations and deallocations to compare against
|
| 19 |
+
expected behavior.
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
def __init__(self) -> None:
|
| 23 |
+
self.tensor_tracker: dict[str, torch.storage.UntypedStorage] = (
|
| 24 |
+
weakref.WeakValueDictionary() # type: ignore[assignment]
|
| 25 |
+
)
|
| 26 |
+
self.died_since_last_step: OrderedSet[str] = OrderedSet()
|
| 27 |
+
self.added_since_last_step: OrderedSet[str] = OrderedSet()
|
| 28 |
+
self.error = (
|
| 29 |
+
torch._inductor.config.test_configs.track_memory_lifecycle == "assert"
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
def set_tensor(self, name: str, tensor: torch.Tensor) -> None:
|
| 33 |
+
storage = tensor.untyped_storage()
|
| 34 |
+
|
| 35 |
+
self.added_since_last_step.add(name)
|
| 36 |
+
self.tensor_tracker[name] = storage
|
| 37 |
+
|
| 38 |
+
def on_tensor_death() -> None:
|
| 39 |
+
self.died_since_last_step.add(name)
|
| 40 |
+
|
| 41 |
+
weakref.finalize(storage, on_tensor_death)
|
| 42 |
+
|
| 43 |
+
def advance_step(self) -> None:
|
| 44 |
+
self.died_since_last_step.clear()
|
| 45 |
+
self.added_since_last_step.clear()
|
| 46 |
+
|
| 47 |
+
def log_or_raise(self, msg: str) -> None:
|
| 48 |
+
if self.error:
|
| 49 |
+
raise RuntimeError(msg)
|
| 50 |
+
else:
|
| 51 |
+
log.info(msg)
|
| 52 |
+
|
| 53 |
+
def check_step_delta(
|
| 54 |
+
self,
|
| 55 |
+
expected_allocated: list[str],
|
| 56 |
+
expected_freed: list[str],
|
| 57 |
+
is_final_step: bool,
|
| 58 |
+
) -> None:
|
| 59 |
+
"""Check only the delta changes since last step"""
|
| 60 |
+
|
| 61 |
+
# Check expected deaths - we dont currently distinguish between nodes which die in last step
|
| 62 |
+
# and are returned as outputs, so skip if final_step.
|
| 63 |
+
if not is_final_step:
|
| 64 |
+
missing_deaths = OrderedSet(expected_freed) - self.died_since_last_step
|
| 65 |
+
if missing_deaths:
|
| 66 |
+
self.log_or_raise(
|
| 67 |
+
f"Expected tensors to die but still alive: {missing_deaths}"
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
# Check for unexpected deaths
|
| 71 |
+
unexpected_deaths = self.died_since_last_step - OrderedSet(expected_freed)
|
| 72 |
+
if unexpected_deaths:
|
| 73 |
+
self.log_or_raise(f"Unexpected tensor deaths: {unexpected_deaths}")
|
| 74 |
+
|
| 75 |
+
# Check newly alive tensors - separate messages like deaths
|
| 76 |
+
actual_allocated = self.added_since_last_step
|
| 77 |
+
expected_allocated_set = OrderedSet(expected_allocated)
|
| 78 |
+
|
| 79 |
+
extra_alive = actual_allocated - expected_allocated_set
|
| 80 |
+
if extra_alive:
|
| 81 |
+
self.log_or_raise(f"Unexpected allocated tensors: {extra_alive}")
|
| 82 |
+
|
| 83 |
+
missing_alive = expected_allocated_set - actual_allocated
|
| 84 |
+
if missing_alive:
|
| 85 |
+
self.log_or_raise(
|
| 86 |
+
f"Expected allocated tensors but missing: {missing_alive}"
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
# Reset for next step
|
| 90 |
+
self.advance_step()
|
| 91 |
+
|
| 92 |
+
if is_final_step:
|
| 93 |
+
local.memory_tracker = None
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def get_mem_tracker() -> BufferMemoryTracker:
|
| 97 |
+
if local.memory_tracker is None:
|
| 98 |
+
local.memory_tracker = BufferMemoryTracker()
|
| 99 |
+
return local.memory_tracker
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def track_tensor(tensor: torch.Tensor, name: str) -> None:
|
| 103 |
+
get_mem_tracker().set_tensor(name, tensor)
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def tracked_empty_strided(
|
| 107 |
+
size: list[int],
|
| 108 |
+
stride: list[int],
|
| 109 |
+
*,
|
| 110 |
+
dtype: torch.dtype,
|
| 111 |
+
device: torch.device,
|
| 112 |
+
name: str,
|
| 113 |
+
) -> torch.Tensor:
|
| 114 |
+
o = torch.empty_strided(size, stride, dtype=dtype, device=device)
|
| 115 |
+
track_tensor(o, name)
|
| 116 |
+
return o
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def check_memory_step(
|
| 120 |
+
allocated: list[str], freed: list[str], is_final_step: bool = False
|
| 121 |
+
) -> None:
|
| 122 |
+
tracker = get_mem_tracker()
|
| 123 |
+
tracker.check_step_delta(allocated, freed, is_final_step)
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
@functools.lru_cache(None)
|
| 127 |
+
def register_check_mem_op() -> None:
|
| 128 |
+
lib = torch.library.Library("_inductor_debug", "FRAGMENT") # noqa: TOR901
|
| 129 |
+
lib.define(
|
| 130 |
+
"check_memory_step(str[] allocated, str[] freed, bool is_final_step) -> ()"
|
| 131 |
+
)
|
| 132 |
+
lib.impl("check_memory_step", check_memory_step, "BackendSelect")
|
| 133 |
+
from torch._higher_order_ops.effects import _EffectType, _register_effectful_op
|
| 134 |
+
|
| 135 |
+
_register_effectful_op(
|
| 136 |
+
torch.ops._inductor_debug.check_memory_step.default,
|
| 137 |
+
_EffectType.ORDERED,
|
| 138 |
+
)
|
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/halide_helpers.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# mypy: allow-untyped-defs
|
| 2 |
+
import math
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
try:
|
| 6 |
+
import halide as hl # type: ignore[import-untyped, import-not-found]
|
| 7 |
+
except ImportError:
|
| 8 |
+
hl = None
|
| 9 |
+
|
| 10 |
+
PHILOX_N_ROUNDS_DEFAULT = 10 # Default number of rounds for philox
|
| 11 |
+
|
| 12 |
+
if hl is not None:
|
| 13 |
+
PHILOX_KEY_A_U32 = hl.u32(0x9E3779B9)
|
| 14 |
+
PHILOX_KEY_B_U32 = hl.u32(0xBB67AE85)
|
| 15 |
+
PHILOX_ROUND_A_U32 = hl.u32(0xD2511F53)
|
| 16 |
+
PHILOX_ROUND_B_U32 = hl.u32(0xCD9E8D57)
|
| 17 |
+
else:
|
| 18 |
+
PHILOX_KEY_A_U32 = None
|
| 19 |
+
PHILOX_KEY_B_U32 = None
|
| 20 |
+
PHILOX_ROUND_A_U32 = None
|
| 21 |
+
PHILOX_ROUND_B_U32 = None
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def _pair_uniform_to_normal(u1, u2):
|
| 25 |
+
"""Box-Muller transform"""
|
| 26 |
+
u1 = hl.max(hl.f32(1.0e-7), u1)
|
| 27 |
+
th = hl.f32(math.tau) * u2
|
| 28 |
+
r = hl.sqrt(hl.f32(-2.0) * hl.log(u1))
|
| 29 |
+
return r * hl.cos(th), r * hl.sin(th)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def _uint_to_uniform_float(x):
|
| 33 |
+
"""
|
| 34 |
+
Numerically stable function to convert a random uint into a random float uniformly sampled in [0, 1).
|
| 35 |
+
"""
|
| 36 |
+
|
| 37 |
+
# TODO:
|
| 38 |
+
# conditions can be simplified
|
| 39 |
+
# scale is ((2**23 - 1) / 2**23) * 2**(N_BITS - 1)
|
| 40 |
+
# https://github.com/triton-lang/triton/blob/e4a0d93ff1a367c7d4eeebbcd7079ed267e6b06f/python/triton/language/random.py#L116-L132.
|
| 41 |
+
assert x.type() == hl.UInt(32) or x.type() == hl.Int(32)
|
| 42 |
+
x = hl.cast(hl.Int(32), x)
|
| 43 |
+
scale = hl.f64(4.6566127342e-10)
|
| 44 |
+
x = hl.select(x < 0, -x - 1, x)
|
| 45 |
+
return x * scale
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def philox_impl(c0, c1, c2, c3, k0, k1, n_rounds):
|
| 49 |
+
def umulhi(a, b):
|
| 50 |
+
a = hl.cast(hl.UInt(64), a)
|
| 51 |
+
b = hl.cast(hl.UInt(64), b)
|
| 52 |
+
return hl.cast(hl.UInt(32), ((a * b) >> 32) & hl.u64(0xFFFFFFFF))
|
| 53 |
+
|
| 54 |
+
for _ in range(n_rounds):
|
| 55 |
+
_c0, _c2 = c0, c2
|
| 56 |
+
|
| 57 |
+
c0 = umulhi(PHILOX_ROUND_B_U32, _c2) ^ c1 ^ k0
|
| 58 |
+
c2 = umulhi(PHILOX_ROUND_A_U32, _c0) ^ c3 ^ k1
|
| 59 |
+
c1 = PHILOX_ROUND_B_U32 * _c2
|
| 60 |
+
c3 = PHILOX_ROUND_A_U32 * _c0
|
| 61 |
+
# raise key
|
| 62 |
+
k0 = k0 + PHILOX_KEY_A_U32
|
| 63 |
+
k1 = k1 + PHILOX_KEY_B_U32
|
| 64 |
+
|
| 65 |
+
return c0, c1, c2, c3
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def halide_philox(seed, c0, c1, c2, c3, n_rounds):
|
| 69 |
+
seed = hl.cast(hl.UInt(64), seed)
|
| 70 |
+
|
| 71 |
+
assert c0.type().bits() == 32
|
| 72 |
+
|
| 73 |
+
seed_hi = hl.cast(hl.UInt(32), (seed >> 32) & hl.u64(0xFFFFFFFF))
|
| 74 |
+
seed_lo = hl.cast(hl.UInt(32), seed & hl.u64(0xFFFFFFFF))
|
| 75 |
+
|
| 76 |
+
return philox_impl(c0, c1, c2, c3, seed_lo, seed_hi, n_rounds)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def randint4x(seed, offset, n_rounds):
|
| 80 |
+
offset = hl.cast(hl.UInt(32), offset)
|
| 81 |
+
_0 = hl.u32(0)
|
| 82 |
+
return halide_philox(seed, offset, _0, _0, _0, n_rounds)
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def rand4x(seed, offset, n_rounds=PHILOX_N_ROUNDS_DEFAULT):
|
| 86 |
+
i1, i2, i3, i4 = randint4x(seed, offset, n_rounds)
|
| 87 |
+
u1 = _uint_to_uniform_float(i1)
|
| 88 |
+
u2 = _uint_to_uniform_float(i2)
|
| 89 |
+
u3 = _uint_to_uniform_float(i3)
|
| 90 |
+
u4 = _uint_to_uniform_float(i4)
|
| 91 |
+
return u1, u2, u3, u4
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def randint(seed, offset, n_rounds=PHILOX_N_ROUNDS_DEFAULT):
|
| 95 |
+
ret, _, _, _ = randint4x(seed, offset, n_rounds)
|
| 96 |
+
return ret
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def rand(seed, offset, n_rounds=PHILOX_N_ROUNDS_DEFAULT):
|
| 100 |
+
source = randint(seed, offset, n_rounds)
|
| 101 |
+
return _uint_to_uniform_float(source)
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def rand_eager_kernel(seed, offset_blocks, tid, VEC, n_rounds=PHILOX_N_ROUNDS_DEFAULT):
|
| 105 |
+
inv = hl.cast(hl.Float(32), 1.0 / 4294967296.0) # 2^-32
|
| 106 |
+
half = hl.cast(hl.Float(32), 0.5) * inv
|
| 107 |
+
|
| 108 |
+
tid_u64 = hl.cast(hl.UInt(64), tid)
|
| 109 |
+
VEC_u64 = hl.cast(hl.UInt(64), VEC)
|
| 110 |
+
subseq = tid_u64 // VEC_u64
|
| 111 |
+
which4 = (tid_u64 % VEC_u64) // hl.cast(hl.UInt(64), 4)
|
| 112 |
+
lane = tid_u64 % hl.cast(hl.UInt(64), 4)
|
| 113 |
+
|
| 114 |
+
offblk = hl.cast(hl.UInt(64), offset_blocks) + which4
|
| 115 |
+
|
| 116 |
+
c0 = hl.cast(hl.UInt(32), offblk & hl.cast(hl.UInt(64), 0xFFFFFFFF))
|
| 117 |
+
c1 = hl.cast(
|
| 118 |
+
hl.UInt(32),
|
| 119 |
+
(offblk >> hl.cast(hl.UInt(64), 32)) & hl.cast(hl.UInt(64), 0xFFFFFFFF),
|
| 120 |
+
)
|
| 121 |
+
c2 = hl.cast(hl.UInt(32), subseq & hl.cast(hl.UInt(64), 0xFFFFFFFF))
|
| 122 |
+
c3 = hl.cast(
|
| 123 |
+
hl.UInt(32),
|
| 124 |
+
(subseq >> hl.cast(hl.UInt(64), 32)) & hl.cast(hl.UInt(64), 0xFFFFFFFF),
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
u0, u1, u2, u3 = halide_philox(seed, c0, c1, c2, c3, n_rounds)
|
| 128 |
+
|
| 129 |
+
v01 = hl.select(lane == hl.cast(hl.UInt(64), 0), u0, u1)
|
| 130 |
+
v23 = hl.select(lane == hl.cast(hl.UInt(64), 2), u2, u3)
|
| 131 |
+
rand_int = hl.select(
|
| 132 |
+
(lane == hl.cast(hl.UInt(64), 0)) | (lane == hl.cast(hl.UInt(64), 1)), v01, v23
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
return hl.cast(hl.Float(32), 1.0) - (hl.cast(hl.Float(32), rand_int) * inv + half)
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
def randn(seed, offset):
|
| 139 |
+
i1, i2, _, _ = randint4x(seed, offset, PHILOX_N_ROUNDS_DEFAULT)
|
| 140 |
+
u1 = _uint_to_uniform_float(i1)
|
| 141 |
+
u2 = _uint_to_uniform_float(i2)
|
| 142 |
+
n1, _ = _pair_uniform_to_normal(u1, u2)
|
| 143 |
+
return n1
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def randint64(seed, offset, low, high):
|
| 147 |
+
r0, r1, _r2, _r3 = randint4x(seed, offset, PHILOX_N_ROUNDS_DEFAULT)
|
| 148 |
+
r0 = hl.cast(hl.UInt(64), r0)
|
| 149 |
+
r1 = hl.cast(hl.UInt(64), r1)
|
| 150 |
+
|
| 151 |
+
result = r0 | (r1 << 32)
|
| 152 |
+
size = high - low
|
| 153 |
+
result = result % hl.cast(hl.UInt(64), size)
|
| 154 |
+
result = hl.cast(hl.Int(64), result) + low
|
| 155 |
+
return result
|
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/hints.py
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# mypy: allow-untyped-defs
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import collections
|
| 5 |
+
import functools
|
| 6 |
+
import typing
|
| 7 |
+
from enum import auto, Enum
|
| 8 |
+
|
| 9 |
+
import torch
|
| 10 |
+
from torch.utils._triton import has_triton_package
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
# The following maximums only apply to runtime autotuning, when using FixedTritonConfig one may see larger values
|
| 14 |
+
# NOTE: if these fail asserts submit a PR to increase them
|
| 15 |
+
TRITON_MAX_BLOCK = {
|
| 16 |
+
"X": 8192 if torch.version.hip else 4096,
|
| 17 |
+
"Y": 1024,
|
| 18 |
+
"Z": 1024,
|
| 19 |
+
"R0_": 4096 * 16, # * 16 is multi-kernel only
|
| 20 |
+
"R1_": 2048 * 16, # * 16 is multi-kernel only
|
| 21 |
+
}
|
| 22 |
+
TRITON_MAX_RSPLIT = 64
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class ReductionHint(Enum):
|
| 26 |
+
INNER = 0
|
| 27 |
+
OUTER = 1
|
| 28 |
+
OUTER_TINY = 2
|
| 29 |
+
DEFAULT = 3
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
class TileHint(Enum):
|
| 33 |
+
SQUARE = 0
|
| 34 |
+
DEFAULT = 1
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
# Define `AttrsDescriptorWrapper` function with clear conditional handling
|
| 38 |
+
if has_triton_package():
|
| 39 |
+
import triton
|
| 40 |
+
import triton.backends.compiler
|
| 41 |
+
import triton.compiler.compiler
|
| 42 |
+
|
| 43 |
+
if hasattr(triton.backends.compiler, "AttrsDescriptor"):
|
| 44 |
+
# Triton 3.2.0 - the second implementation
|
| 45 |
+
from triton.backends.compiler import AttrsDescriptor
|
| 46 |
+
|
| 47 |
+
def AttrsDescriptorWrapper(
|
| 48 |
+
divisible_by_16=None,
|
| 49 |
+
equal_to_1=None,
|
| 50 |
+
pointer_range_32=None,
|
| 51 |
+
):
|
| 52 |
+
# Prepare the arguments for AttrsDescriptor
|
| 53 |
+
kwargs = {
|
| 54 |
+
"tt.divisibility": divisible_by_16,
|
| 55 |
+
"tt.equal_to": equal_to_1,
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
# Instantiate AttrsDescriptor with the prepared arguments
|
| 59 |
+
res = AttrsDescriptor.from_dict(
|
| 60 |
+
{"arg_properties": kwargs, "cls": AttrsDescriptor.__name__}
|
| 61 |
+
)
|
| 62 |
+
assert res.property_values["tt.divisibility"] == 16
|
| 63 |
+
assert res.property_values["tt.equal_to"] == 1
|
| 64 |
+
return res
|
| 65 |
+
|
| 66 |
+
elif hasattr(triton.compiler.compiler, "AttrsDescriptor"):
|
| 67 |
+
# Triton 3.0.0 - the original implementation
|
| 68 |
+
from triton.compiler.compiler import AttrsDescriptor
|
| 69 |
+
|
| 70 |
+
def AttrsDescriptorWrapper(
|
| 71 |
+
divisible_by_16=None,
|
| 72 |
+
equal_to_1=None,
|
| 73 |
+
pointer_range_32=None,
|
| 74 |
+
):
|
| 75 |
+
# Prepare the arguments for AttrsDescriptor
|
| 76 |
+
kwargs = {
|
| 77 |
+
"divisible_by_16": divisible_by_16,
|
| 78 |
+
"equal_to_1": equal_to_1,
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
# Instantiate AttrsDescriptor with the prepared arguments
|
| 82 |
+
return AttrsDescriptor(**kwargs)
|
| 83 |
+
|
| 84 |
+
else:
|
| 85 |
+
# Triton in 2025:
|
| 86 |
+
# note: there's also a range of triton commits not currently supported
|
| 87 |
+
# from ~Dec 9, 2024 to Jan 1 2025, in which AttrsDescriptors are still
|
| 88 |
+
# used, but the contents are different.
|
| 89 |
+
|
| 90 |
+
def AttrsDescriptorWrapper(
|
| 91 |
+
divisible_by_16=None,
|
| 92 |
+
equal_to_1=None,
|
| 93 |
+
pointer_range_32=None,
|
| 94 |
+
):
|
| 95 |
+
# pyrefly: ignore [not-iterable]
|
| 96 |
+
# Build attr dict merging divisibility and pointer_range per arg index,
|
| 97 |
+
# since a single arg can carry both attributes.
|
| 98 |
+
result = {(x,): [["tt.divisibility", 16]] for x in (divisible_by_16 or ())}
|
| 99 |
+
for x in pointer_range_32 or ():
|
| 100 |
+
key = (x,)
|
| 101 |
+
if key in result:
|
| 102 |
+
result[key].append(["tt.pointer_range", 32])
|
| 103 |
+
else:
|
| 104 |
+
result[key] = [["tt.pointer_range", 32]]
|
| 105 |
+
return result
|
| 106 |
+
|
| 107 |
+
else:
|
| 108 |
+
# Define a namedtuple as a fallback when AttrsDescriptor is not available
|
| 109 |
+
AttrsDescriptorWrapper = collections.namedtuple( # type: ignore[no-redef, name-match]
|
| 110 |
+
# pyrefly: ignore [invalid-argument]
|
| 111 |
+
"AttrsDescriptor",
|
| 112 |
+
["divisible_by_16", "equal_to_1", "pointer_range_32"],
|
| 113 |
+
defaults=[(), (), ()],
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
_NUM_THREADS_PER_WARP = 32
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
class HeuristicType(Enum):
|
| 121 |
+
PERSISTENT_REDUCTION = auto()
|
| 122 |
+
POINTWISE = auto()
|
| 123 |
+
REDUCTION = auto()
|
| 124 |
+
SPLIT_SCAN = auto()
|
| 125 |
+
TEMPLATE = auto()
|
| 126 |
+
USER_AUTOTUNE = auto()
|
| 127 |
+
FIXED = auto()
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
class AutotuneHint(Enum):
|
| 131 |
+
ONE_ELEMENT_PER_THREAD = 0
|
| 132 |
+
|
| 133 |
+
# Triton codegen tries to codegen set of AutotuneHints.
|
| 134 |
+
# Enum.__repr__ looks like "<AutotuneHint.ELEMENTS_PER_WARP_32: 0>""
|
| 135 |
+
# which isn't valid python.
|
| 136 |
+
# Enum.__str__ will just return "AutotuneHint.ELEMENTS_PER_WARP_32".
|
| 137 |
+
__repr__ = Enum.__str__
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
class DeviceProperties(typing.NamedTuple):
|
| 141 |
+
"""Copy device properties into a data structure not requiring torch to be imported"""
|
| 142 |
+
|
| 143 |
+
type: str # type: ignore[assignment]
|
| 144 |
+
index: int # type: ignore[assignment]
|
| 145 |
+
multi_processor_count: int
|
| 146 |
+
cc: int
|
| 147 |
+
major: int | None = None
|
| 148 |
+
regs_per_multiprocessor: int | None = None
|
| 149 |
+
max_threads_per_multi_processor: int | None = None
|
| 150 |
+
max_threads_per_block: int | None = None
|
| 151 |
+
warp_size: int | None = None
|
| 152 |
+
|
| 153 |
+
@classmethod
|
| 154 |
+
@functools.cache
|
| 155 |
+
def create(cls, device) -> DeviceProperties:
|
| 156 |
+
import torch
|
| 157 |
+
from torch._dynamo.device_interface import get_interface_for_device
|
| 158 |
+
|
| 159 |
+
device_type = device.type
|
| 160 |
+
|
| 161 |
+
if torch.version.hip and device_type == "cuda":
|
| 162 |
+
device_type = "hip"
|
| 163 |
+
|
| 164 |
+
device_interface = get_interface_for_device(device)
|
| 165 |
+
props = device_interface.get_device_properties(device)
|
| 166 |
+
try:
|
| 167 |
+
multi_processor_count = props.multi_processor_count
|
| 168 |
+
except AttributeError:
|
| 169 |
+
if device_type == "xpu":
|
| 170 |
+
multi_processor_count = props.gpu_subslice_count
|
| 171 |
+
elif device_type == "mtia":
|
| 172 |
+
multi_processor_count = 64
|
| 173 |
+
else:
|
| 174 |
+
raise
|
| 175 |
+
return cls(
|
| 176 |
+
type=device_type,
|
| 177 |
+
index=device.index,
|
| 178 |
+
multi_processor_count=multi_processor_count,
|
| 179 |
+
cc=device_interface.get_compute_capability(device),
|
| 180 |
+
major=getattr(props, "major", None),
|
| 181 |
+
regs_per_multiprocessor=getattr(props, "regs_per_multiprocessor", None),
|
| 182 |
+
max_threads_per_multi_processor=getattr(
|
| 183 |
+
props, "max_threads_per_multi_processor", None
|
| 184 |
+
),
|
| 185 |
+
max_threads_per_block=getattr(props, "max_threads_per_block", 1024),
|
| 186 |
+
warp_size=getattr(props, "warp_size", 32 if device_type != "cpu" else None),
|
| 187 |
+
)
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
class HalideInputSpec(typing.NamedTuple):
|
| 191 |
+
ctype: str
|
| 192 |
+
name: str
|
| 193 |
+
shape: list[str] | None = None
|
| 194 |
+
stride: list[str] | None = None
|
| 195 |
+
offset: str | None = None
|
| 196 |
+
alias_of: str | None = None
|
| 197 |
+
|
| 198 |
+
def bindings_type(self) -> str:
|
| 199 |
+
if self.ctype in ("at::Half*", "at::BFloat16*"):
|
| 200 |
+
return "uint16_t*" # half not defined
|
| 201 |
+
return self.ctype
|
| 202 |
+
|
| 203 |
+
def halide_type(self) -> str:
|
| 204 |
+
if self.ctype == "at::Half*":
|
| 205 |
+
return "halide_type_t(halide_type_float, 16)" # half not defined
|
| 206 |
+
if self.ctype == "at::BFloat16*":
|
| 207 |
+
return "halide_type_t(halide_type_bfloat, 16)" # half not defined
|
| 208 |
+
return f"halide_type_of<{self.ctype.replace('*', '')}>()"
|
| 209 |
+
|
| 210 |
+
def is_scalar(self) -> bool:
|
| 211 |
+
return self.shape is None
|
| 212 |
+
|
| 213 |
+
def is_buffer(self) -> bool:
|
| 214 |
+
return self.shape is not None
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
class HalideMeta(typing.NamedTuple):
|
| 218 |
+
argtypes: list[HalideInputSpec]
|
| 219 |
+
target: str
|
| 220 |
+
scheduler: str | None = None
|
| 221 |
+
scheduler_flags: dict[str, int | str] | None = None
|
| 222 |
+
cuda_device: int | None = None
|
| 223 |
+
|
| 224 |
+
def args(self) -> list[str]:
|
| 225 |
+
"""Command line args to pass to halide generator"""
|
| 226 |
+
args = [f"target={self.target}"]
|
| 227 |
+
if self.scheduler:
|
| 228 |
+
args.append(f"autoscheduler={self.scheduler}")
|
| 229 |
+
if self.scheduler_flags:
|
| 230 |
+
assert self.scheduler
|
| 231 |
+
for k, v in self.scheduler_flags.items():
|
| 232 |
+
args.append(f"autoscheduler.{k}={v}")
|
| 233 |
+
return args
|
| 234 |
+
|
| 235 |
+
def is_cuda(self) -> bool:
|
| 236 |
+
return self.cuda_device is not None
|
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/proton_utils.py
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Utilities for post-processing Triton Proton profiling trace files.
|
| 3 |
+
|
| 4 |
+
This module provides functions to transform proton-generated Chrome trace files
|
| 5 |
+
for better visualization in Perfetto/Chrome tracing tools.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import gzip
|
| 9 |
+
import json
|
| 10 |
+
import os
|
| 11 |
+
import re
|
| 12 |
+
from typing import Any
|
| 13 |
+
|
| 14 |
+
from torch.utils._ordered_set import OrderedSet
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def _write_trace(path: str, data: dict[str, Any]) -> None:
|
| 18 |
+
"""Write trace data to a gzipped JSON file."""
|
| 19 |
+
with gzip.open(path, "wt", encoding="utf-8") as f:
|
| 20 |
+
json.dump(data, f)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def _read_trace(path: str) -> dict[str, Any]:
|
| 24 |
+
"""Read trace data from a JSON file (gzipped or not)."""
|
| 25 |
+
if path.endswith(".gz"):
|
| 26 |
+
with gzip.open(path, "rt", encoding="utf-8") as f:
|
| 27 |
+
return json.load(f)
|
| 28 |
+
else:
|
| 29 |
+
with open(path) as f:
|
| 30 |
+
return json.load(f)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def _get_base_name(trace_path: str) -> str:
|
| 34 |
+
"""Get the base name of a trace file, stripping known extensions."""
|
| 35 |
+
base_name = os.path.basename(trace_path)
|
| 36 |
+
for ext in [".trace.json.gz", ".chrome_trace", ".json.gz", ".json"]:
|
| 37 |
+
if base_name.endswith(ext):
|
| 38 |
+
return base_name[: -len(ext)]
|
| 39 |
+
return os.path.splitext(base_name)[0]
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def _group_events_by_sm(events: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
| 43 |
+
"""
|
| 44 |
+
Group events so all CTAs on the same SM share one track.
|
| 45 |
+
|
| 46 |
+
Transforms:
|
| 47 |
+
- pid: "Core0 CTA6" -> "Core0"
|
| 48 |
+
- tid: "warp0" -> "CTA6 warp0"
|
| 49 |
+
"""
|
| 50 |
+
core_cta_pattern = re.compile(r"^(.*?)\s*(Core\d+)\s+(CTA\d+)$")
|
| 51 |
+
|
| 52 |
+
for event in events:
|
| 53 |
+
pid = event.get("pid", "")
|
| 54 |
+
tid = event.get("tid", "")
|
| 55 |
+
|
| 56 |
+
match = core_cta_pattern.match(str(pid))
|
| 57 |
+
if match:
|
| 58 |
+
prefix = match.group(1).strip()
|
| 59 |
+
core = match.group(2)
|
| 60 |
+
cta = match.group(3)
|
| 61 |
+
event["pid"] = f"{prefix} {core}" if prefix else core
|
| 62 |
+
event["tid"] = f"{cta} {tid}" if tid else cta
|
| 63 |
+
|
| 64 |
+
return events
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def _group_events_per_cta_occupancy(
|
| 68 |
+
events: list[dict[str, Any]],
|
| 69 |
+
) -> list[dict[str, Any]]:
|
| 70 |
+
"""
|
| 71 |
+
Process warp events into CTA-level events with slot assignment.
|
| 72 |
+
|
| 73 |
+
Each CTA gets: min warp start, max warp end.
|
| 74 |
+
CTAs are assigned to slots per SM such that non-overlapping CTAs share slots.
|
| 75 |
+
"""
|
| 76 |
+
core_cta_pattern = re.compile(r"^(.*?)\s*(Core\d+)\s+(CTA\d+)$")
|
| 77 |
+
|
| 78 |
+
# Group events by (prefix, SM, CTA)
|
| 79 |
+
cta_events: dict[tuple[str, str, str], list[dict[str, Any]]] = {}
|
| 80 |
+
|
| 81 |
+
for event in events:
|
| 82 |
+
pid = event.get("pid", "")
|
| 83 |
+
match = core_cta_pattern.match(str(pid))
|
| 84 |
+
if match:
|
| 85 |
+
prefix = match.group(1).strip()
|
| 86 |
+
core = match.group(2)
|
| 87 |
+
cta = match.group(3)
|
| 88 |
+
key = (prefix, core, cta)
|
| 89 |
+
if key not in cta_events:
|
| 90 |
+
cta_events[key] = []
|
| 91 |
+
cta_events[key].append(event)
|
| 92 |
+
|
| 93 |
+
# For each CTA, compute min start and max end
|
| 94 |
+
cta_intervals: dict[tuple[str, str, str], tuple[float, float]] = {}
|
| 95 |
+
for key, evts in cta_events.items():
|
| 96 |
+
min_start = float("inf")
|
| 97 |
+
max_end = float("-inf")
|
| 98 |
+
for evt in evts:
|
| 99 |
+
ts = evt.get("ts", 0)
|
| 100 |
+
dur = evt.get("dur", 0)
|
| 101 |
+
min_start = min(min_start, ts)
|
| 102 |
+
max_end = max(max_end, ts + dur)
|
| 103 |
+
if min_start != float("inf"):
|
| 104 |
+
cta_intervals[key] = (min_start, max_end)
|
| 105 |
+
|
| 106 |
+
# Group CTAs by (prefix, SM) and assign to slots
|
| 107 |
+
sm_ctas: dict[tuple[str, str], list[tuple[str, float, float]]] = {}
|
| 108 |
+
for (prefix, core, cta), (start, end) in cta_intervals.items():
|
| 109 |
+
sm_key = (prefix, core)
|
| 110 |
+
if sm_key not in sm_ctas:
|
| 111 |
+
sm_ctas[sm_key] = []
|
| 112 |
+
sm_ctas[sm_key].append((cta, start, end))
|
| 113 |
+
|
| 114 |
+
# Assign CTAs to slots using interval scheduling (greedy)
|
| 115 |
+
cta_slot_assignments: dict[tuple[str, str, str], int] = {}
|
| 116 |
+
for sm_key, ctas in sm_ctas.items():
|
| 117 |
+
prefix, core = sm_key
|
| 118 |
+
sorted_ctas = sorted(ctas, key=lambda x: x[1])
|
| 119 |
+
slots: list[float] = []
|
| 120 |
+
for cta, start, end in sorted_ctas:
|
| 121 |
+
assigned_slot = None
|
| 122 |
+
for i, slot_end in enumerate(slots):
|
| 123 |
+
if start >= slot_end:
|
| 124 |
+
assigned_slot = i
|
| 125 |
+
slots[i] = end
|
| 126 |
+
break
|
| 127 |
+
if assigned_slot is None:
|
| 128 |
+
assigned_slot = len(slots)
|
| 129 |
+
slots.append(end)
|
| 130 |
+
cta_slot_assignments[(prefix, core, cta)] = assigned_slot
|
| 131 |
+
|
| 132 |
+
# Build numeric ID mappings for pids and tids
|
| 133 |
+
# Chrome trace format requires numeric pid/tid values
|
| 134 |
+
pid_names: dict[str, int] = {}
|
| 135 |
+
tid_names: dict[tuple[int, str], int] = {} # (pid, tid_name) -> tid_num
|
| 136 |
+
|
| 137 |
+
for key in cta_intervals:
|
| 138 |
+
prefix, core, cta = key
|
| 139 |
+
pid_name = f"{prefix} {core}" if prefix else core
|
| 140 |
+
if pid_name not in pid_names:
|
| 141 |
+
pid_names[pid_name] = len(pid_names)
|
| 142 |
+
|
| 143 |
+
new_events: list[dict[str, Any]] = []
|
| 144 |
+
|
| 145 |
+
# Add process name metadata events
|
| 146 |
+
for pid_name, pid_num in pid_names.items():
|
| 147 |
+
new_events.append(
|
| 148 |
+
{
|
| 149 |
+
"name": "process_name",
|
| 150 |
+
"ph": "M",
|
| 151 |
+
"pid": pid_num,
|
| 152 |
+
"args": {"name": pid_name},
|
| 153 |
+
}
|
| 154 |
+
)
|
| 155 |
+
|
| 156 |
+
# Create CTA events with numeric IDs and collect tid mappings
|
| 157 |
+
for key, (start, end) in cta_intervals.items():
|
| 158 |
+
prefix, core, cta = key
|
| 159 |
+
slot = cta_slot_assignments[key]
|
| 160 |
+
pid_name = f"{prefix} {core}" if prefix else core
|
| 161 |
+
tid_name = f"slot{slot}"
|
| 162 |
+
pid_num = pid_names[pid_name]
|
| 163 |
+
|
| 164 |
+
if (pid_num, tid_name) not in tid_names:
|
| 165 |
+
tid_num = len(tid_names)
|
| 166 |
+
tid_names[(pid_num, tid_name)] = tid_num
|
| 167 |
+
# Add thread name metadata event
|
| 168 |
+
new_events.append(
|
| 169 |
+
{
|
| 170 |
+
"name": "thread_name",
|
| 171 |
+
"ph": "M",
|
| 172 |
+
"pid": pid_num,
|
| 173 |
+
"tid": tid_num,
|
| 174 |
+
"args": {"name": tid_name},
|
| 175 |
+
}
|
| 176 |
+
)
|
| 177 |
+
else:
|
| 178 |
+
tid_num = tid_names[(pid_num, tid_name)]
|
| 179 |
+
|
| 180 |
+
new_events.append(
|
| 181 |
+
{
|
| 182 |
+
"name": cta,
|
| 183 |
+
"cat": "cta",
|
| 184 |
+
"ph": "X",
|
| 185 |
+
"ts": start,
|
| 186 |
+
"dur": end - start,
|
| 187 |
+
"pid": pid_num,
|
| 188 |
+
"tid": tid_num,
|
| 189 |
+
}
|
| 190 |
+
)
|
| 191 |
+
|
| 192 |
+
return new_events
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
def _apply_grouping(
|
| 196 |
+
events: list[dict[str, Any]], group_by_sm: bool, per_cta_occupancy: bool
|
| 197 |
+
) -> list[dict[str, Any]]:
|
| 198 |
+
"""Apply grouping transformation to events."""
|
| 199 |
+
if per_cta_occupancy:
|
| 200 |
+
return _group_events_per_cta_occupancy(events)
|
| 201 |
+
elif group_by_sm:
|
| 202 |
+
return _group_events_by_sm(events)
|
| 203 |
+
return events
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
def _split_events_by_invocation(
|
| 207 |
+
events: list[dict[str, Any]],
|
| 208 |
+
gap_threshold_ns: float = 1000.0,
|
| 209 |
+
) -> list[list[dict[str, Any]]]:
|
| 210 |
+
"""Split events into separate invocations based on time gaps."""
|
| 211 |
+
if not events:
|
| 212 |
+
return []
|
| 213 |
+
|
| 214 |
+
events_sorted = sorted(events, key=lambda e: e.get("ts", 0))
|
| 215 |
+
invocations: list[list[dict[str, Any]]] = [[]]
|
| 216 |
+
prev_end = events_sorted[0].get("ts", 0)
|
| 217 |
+
|
| 218 |
+
for event in events_sorted:
|
| 219 |
+
ts = event.get("ts", 0)
|
| 220 |
+
dur = event.get("dur", 0)
|
| 221 |
+
if ts - prev_end > gap_threshold_ns and invocations[-1]:
|
| 222 |
+
invocations.append([])
|
| 223 |
+
invocations[-1].append(event)
|
| 224 |
+
prev_end = max(prev_end, ts + dur)
|
| 225 |
+
|
| 226 |
+
return [inv for inv in invocations if inv]
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
def _normalize_timestamps(
|
| 230 |
+
events: list[dict[str, Any]], scale_factor: float = 1.0
|
| 231 |
+
) -> list[dict[str, Any]]:
|
| 232 |
+
"""Normalize timestamps to start at 0 and optionally scale."""
|
| 233 |
+
if not events:
|
| 234 |
+
return events
|
| 235 |
+
|
| 236 |
+
# Only consider duration events (ph=X) for min timestamp calculation
|
| 237 |
+
duration_events = [e for e in events if e.get("ph") == "X"]
|
| 238 |
+
if not duration_events:
|
| 239 |
+
return events
|
| 240 |
+
|
| 241 |
+
min_ts = min(e.get("ts", 0) for e in duration_events)
|
| 242 |
+
for event in events:
|
| 243 |
+
if event.get("ph") == "X":
|
| 244 |
+
event["ts"] = (event.get("ts", 0) - min_ts) * scale_factor
|
| 245 |
+
if "dur" in event:
|
| 246 |
+
event["dur"] = event["dur"] * scale_factor
|
| 247 |
+
|
| 248 |
+
return events
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
def process_proton_trace(
|
| 252 |
+
trace_path: str,
|
| 253 |
+
output_dir: str | None = None,
|
| 254 |
+
group_by_sm: bool = True,
|
| 255 |
+
split_invocations: bool = True,
|
| 256 |
+
scale_factor: float = 1.0,
|
| 257 |
+
gap_threshold_ns: float = 1000.0,
|
| 258 |
+
per_cta_occupancy: bool = True,
|
| 259 |
+
) -> list[str]:
|
| 260 |
+
"""
|
| 261 |
+
Process a proton trace file with various transformations.
|
| 262 |
+
|
| 263 |
+
Always produces 1 + N files: one complete trace and N per-invocation traces.
|
| 264 |
+
Grouping options (group_by_sm, per_cta_occupancy) apply uniformly to all outputs.
|
| 265 |
+
|
| 266 |
+
Args:
|
| 267 |
+
trace_path: Path to the input Chrome trace file
|
| 268 |
+
output_dir: Directory to write output files. If None, uses same directory as input.
|
| 269 |
+
group_by_sm: If True, group CTAs by SM into single tracks.
|
| 270 |
+
split_invocations: If True, also produce per-invocation trace files.
|
| 271 |
+
scale_factor: Factor to scale durations by (helps visibility in Perfetto).
|
| 272 |
+
gap_threshold_ns: Time gap (in nanoseconds) that indicates a new invocation.
|
| 273 |
+
per_cta_occupancy: If True, process warp tracks into CTA tracks and assign
|
| 274 |
+
CTAs to slots per SM such that CTAs do not overlap.
|
| 275 |
+
|
| 276 |
+
Returns:
|
| 277 |
+
List of paths to the output files.
|
| 278 |
+
"""
|
| 279 |
+
if output_dir is None:
|
| 280 |
+
output_dir = os.path.dirname(trace_path) or "."
|
| 281 |
+
|
| 282 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 283 |
+
base_name = _get_base_name(trace_path)
|
| 284 |
+
|
| 285 |
+
data = _read_trace(trace_path)
|
| 286 |
+
events = data.get("traceEvents", [])
|
| 287 |
+
|
| 288 |
+
# Split into invocations first (before grouping, to avoid merging across invocations)
|
| 289 |
+
invocations = _split_events_by_invocation(events, gap_threshold_ns)
|
| 290 |
+
|
| 291 |
+
# Apply grouping transformation to each invocation
|
| 292 |
+
invocations = [
|
| 293 |
+
_apply_grouping(inv_events, group_by_sm, per_cta_occupancy)
|
| 294 |
+
for inv_events in invocations
|
| 295 |
+
]
|
| 296 |
+
|
| 297 |
+
output_files = []
|
| 298 |
+
|
| 299 |
+
# Write complete trace (dedupe metadata events)
|
| 300 |
+
complete_events = []
|
| 301 |
+
seen_metadata: OrderedSet[tuple[str | None, int | None, int | None]] = OrderedSet()
|
| 302 |
+
for inv_events in invocations:
|
| 303 |
+
for event in inv_events:
|
| 304 |
+
if event.get("ph") == "M":
|
| 305 |
+
# Dedupe metadata events by (name, pid, tid)
|
| 306 |
+
key = (event.get("name"), event.get("pid"), event.get("tid"))
|
| 307 |
+
if key in seen_metadata:
|
| 308 |
+
continue
|
| 309 |
+
seen_metadata.add(key)
|
| 310 |
+
complete_events.append(event)
|
| 311 |
+
complete_path = os.path.join(output_dir, f"{base_name}.trace.json.gz")
|
| 312 |
+
_write_trace(complete_path, {"traceEvents": complete_events})
|
| 313 |
+
output_files.append(complete_path)
|
| 314 |
+
|
| 315 |
+
# Write per-invocation traces
|
| 316 |
+
if split_invocations:
|
| 317 |
+
for i, inv_events in enumerate(invocations):
|
| 318 |
+
inv_events = _normalize_timestamps(list(inv_events), scale_factor)
|
| 319 |
+
inv_path = os.path.join(
|
| 320 |
+
output_dir, f"{base_name}_invocation_{i}.trace.json.gz"
|
| 321 |
+
)
|
| 322 |
+
_write_trace(inv_path, {"traceEvents": inv_events})
|
| 323 |
+
output_files.append(inv_path)
|
| 324 |
+
|
| 325 |
+
return output_files
|
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/runtime_utils.py
ADDED
|
@@ -0,0 +1,829 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import functools
|
| 4 |
+
import math
|
| 5 |
+
import operator
|
| 6 |
+
from typing import Any, TYPE_CHECKING
|
| 7 |
+
|
| 8 |
+
import sympy
|
| 9 |
+
|
| 10 |
+
import torch
|
| 11 |
+
|
| 12 |
+
# NOTE: other files rely on the imports below
|
| 13 |
+
from torch._dynamo import callback as compilation_callback # noqa: F401
|
| 14 |
+
from torch._inductor.runtime.cache_dir_utils import ( # noqa: F401
|
| 15 |
+
cache_dir,
|
| 16 |
+
default_cache_dir,
|
| 17 |
+
triton_cache_dir,
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
if TYPE_CHECKING:
|
| 22 |
+
from collections.abc import Hashable
|
| 23 |
+
|
| 24 |
+
from .triton_compat import Config
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def conditional_product(*args: int) -> int:
|
| 28 |
+
return functools.reduce(operator.mul, [x for x in args if x])
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def ceildiv(number: int, denom: int) -> int:
|
| 32 |
+
return -(number // -denom)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def is_power_of_2(n: int) -> bool:
|
| 36 |
+
"""Returns whether n = 2 ** m for some integer m."""
|
| 37 |
+
return n > 0 and n & n - 1 == 0
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def next_power_of_2(n: int) -> int:
|
| 41 |
+
"""Return the smallest power of 2 greater than or equal to n"""
|
| 42 |
+
if isinstance(n, sympy.Integer):
|
| 43 |
+
n = int(n)
|
| 44 |
+
if n <= 0:
|
| 45 |
+
return 1
|
| 46 |
+
return 1 << (n - 1).bit_length()
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def last_power_of_2(n: int) -> int:
|
| 50 |
+
"""Return the largest power of 2 less than or equal to n"""
|
| 51 |
+
next_pow2 = next_power_of_2(n)
|
| 52 |
+
return next_pow2 // 2 if next_pow2 > n else next_pow2
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def get_num_bytes(*args: torch.Tensor, num_in_out_args: int = 0) -> int:
|
| 56 |
+
"""
|
| 57 |
+
Return the total number of bytes the arguments of tensor type takes.
|
| 58 |
+
|
| 59 |
+
For in/out args, tensor sizes are counted twice: once for reading and
|
| 60 |
+
once for writing.
|
| 61 |
+
|
| 62 |
+
The first num_in_out_args arguments are in out tensors.
|
| 63 |
+
"""
|
| 64 |
+
return sum(
|
| 65 |
+
arg.numel() * arg.element_size() * (1 + int(i < num_in_out_args))
|
| 66 |
+
for i, arg in enumerate(args)
|
| 67 |
+
if isinstance(arg, torch.Tensor)
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def triton_config_to_hashable(cfg: Config) -> Hashable:
|
| 72 |
+
"""
|
| 73 |
+
Convert triton config to a tuple that can uniquely identify it. We can use
|
| 74 |
+
the return value as a dictionary key.
|
| 75 |
+
"""
|
| 76 |
+
# pyrefly: ignore [missing-attribute]
|
| 77 |
+
items = sorted(cfg.kwargs.items())
|
| 78 |
+
# pyrefly: ignore [missing-attribute]
|
| 79 |
+
items.append(("num_warps", cfg.num_warps))
|
| 80 |
+
# pyrefly: ignore [missing-attribute]
|
| 81 |
+
items.append(("num_stages", cfg.num_stages))
|
| 82 |
+
return tuple(items)
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def validate_triton_config(cfg: Config) -> None:
|
| 86 |
+
# [Note: Triton pre_hook in inductor]
|
| 87 |
+
# pre-hook is a lambda function, which we don't attempt to serialize.
|
| 88 |
+
# right now, if a pre-hook is attached to the config, it will not be saved;
|
| 89 |
+
# and then it won't be used when the config is loaded from cache.
|
| 90 |
+
# So we assert - if we do get a pre_hook, it might get ignored after caching.
|
| 91 |
+
assert getattr(cfg, "pre_hook", None) is None, (
|
| 92 |
+
"triton configs with pre_hooks not supported"
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def create_bandwidth_info_str(
|
| 97 |
+
ms: float,
|
| 98 |
+
num_gb: float,
|
| 99 |
+
gb_per_s: float,
|
| 100 |
+
prefix: str = "",
|
| 101 |
+
suffix: str = "",
|
| 102 |
+
color: bool = True,
|
| 103 |
+
) -> str:
|
| 104 |
+
info_str = f"{prefix}{ms:.3f}ms \t{num_gb:.3f} GB \t {gb_per_s:7.2f}GB/s{suffix}"
|
| 105 |
+
slow = ms > 0.012 and gb_per_s < 650
|
| 106 |
+
return red_text(info_str) if color and slow else info_str
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def get_max_y_grid() -> int:
|
| 110 |
+
return 65535
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
try:
|
| 114 |
+
import colorama
|
| 115 |
+
|
| 116 |
+
HAS_COLORAMA = True
|
| 117 |
+
except ModuleNotFoundError:
|
| 118 |
+
HAS_COLORAMA = False
|
| 119 |
+
colorama = None # type: ignore[assignment]
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
if HAS_COLORAMA:
|
| 123 |
+
|
| 124 |
+
def _color_text(msg: str, color: str) -> str:
|
| 125 |
+
# pyrefly: ignore [missing-attribute]
|
| 126 |
+
return getattr(colorama.Fore, color.upper()) + msg + colorama.Fore.RESET
|
| 127 |
+
|
| 128 |
+
else:
|
| 129 |
+
|
| 130 |
+
def _color_text(msg: str, color: str) -> str:
|
| 131 |
+
return msg
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def green_text(msg: str) -> str:
|
| 135 |
+
return _color_text(msg, "green")
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
def yellow_text(msg: str) -> str:
|
| 139 |
+
return _color_text(msg, "yellow")
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def red_text(msg: str) -> str:
|
| 143 |
+
return _color_text(msg, "red")
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def blue_text(msg: str) -> str:
|
| 147 |
+
return _color_text(msg, "blue")
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
def get_first_attr(obj: Any, *attrs: str) -> Any:
|
| 151 |
+
"""
|
| 152 |
+
Return the first available attribute or throw an exception if none is present.
|
| 153 |
+
"""
|
| 154 |
+
for attr in attrs:
|
| 155 |
+
if hasattr(obj, attr):
|
| 156 |
+
return getattr(obj, attr)
|
| 157 |
+
|
| 158 |
+
raise AssertionError(f"{obj} does not has any of the attributes: {attrs}")
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
dynamo_timed = torch._dynamo.utils.dynamo_timed # type: ignore[has-type]
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
def triton_hash_to_path_key(key: str) -> str:
|
| 165 |
+
# In early versions of Triton, the hash is directly used in the path name.
|
| 166 |
+
# Later, the hash is converted to base64 before being used in the path name.
|
| 167 |
+
# Later, the base64 conversion was replaced to the base32
|
| 168 |
+
#
|
| 169 |
+
# This code tries to import _base64 and falls back to _base32 if _base64 is unavailable.
|
| 170 |
+
#
|
| 171 |
+
# To handle this, try to import the to-base64-conversion function.
|
| 172 |
+
# If it exists, use it; otherwise, try using _base32; if both are unavailable, use the hash directly.
|
| 173 |
+
try:
|
| 174 |
+
from triton.runtime.cache import _base64
|
| 175 |
+
|
| 176 |
+
return _base64(key)
|
| 177 |
+
except Exception:
|
| 178 |
+
try:
|
| 179 |
+
from triton.runtime.cache import _base32
|
| 180 |
+
|
| 181 |
+
return _base32(key)
|
| 182 |
+
except Exception:
|
| 183 |
+
return key
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
def compile_mps_shader(source: str) -> Any:
|
| 187 |
+
"""
|
| 188 |
+
Compiles shader source but raise more actionable error message when needed
|
| 189 |
+
"""
|
| 190 |
+
try:
|
| 191 |
+
return torch.mps.compile_shader(source)
|
| 192 |
+
except SyntaxError as err:
|
| 193 |
+
raise SyntaxError(f"failed to compile {source} with {err.msg}") from err
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
def compile_mps_shaders(
|
| 197 |
+
kernels: list[tuple[str, str, list[str]]],
|
| 198 |
+
) -> dict[str, Any]:
|
| 199 |
+
"""Compile a batch of Metal kernels into one library.
|
| 200 |
+
|
| 201 |
+
Args:
|
| 202 |
+
kernels: list of (kernel_name, metal_source, headers) tuples.
|
| 203 |
+
headers are bare names resolved as <c10/metal/{name}.h>.
|
| 204 |
+
|
| 205 |
+
Returns:
|
| 206 |
+
dict mapping each kernel_name to its compiled function handle.
|
| 207 |
+
"""
|
| 208 |
+
from torch.utils._ordered_set import OrderedSet
|
| 209 |
+
|
| 210 |
+
all_headers = sorted(OrderedSet(h for _, _, hs in kernels for h in hs))
|
| 211 |
+
header_src = "\n".join(f"#include <c10/metal/{h}.h>" for h in all_headers)
|
| 212 |
+
body_src = "\n".join(src for _, src, _ in kernels)
|
| 213 |
+
lib = compile_mps_shader(header_src + "\n" + body_src)
|
| 214 |
+
return {name: getattr(lib, name) for name, _, _ in kernels}
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
def torch_dtype_to_jax_runtime(dtype: torch.dtype) -> Any:
|
| 218 |
+
"""
|
| 219 |
+
Map PyTorch dtype to actual JAX dtype object at runtime.
|
| 220 |
+
|
| 221 |
+
This helper is used in generated Pallas kernels at runtime to convert
|
| 222 |
+
PyTorch dtypes to JAX dtype objects (not string representations).
|
| 223 |
+
|
| 224 |
+
Args:
|
| 225 |
+
dtype: PyTorch dtype to convert
|
| 226 |
+
|
| 227 |
+
Returns:
|
| 228 |
+
JAX dtype object (e.g., jnp.float32 object itself)
|
| 229 |
+
"""
|
| 230 |
+
import jax.numpy as jnp # pyrefly: ignore [import-error, missing-import]
|
| 231 |
+
|
| 232 |
+
dtype_map = {
|
| 233 |
+
torch.float32: jnp.float32,
|
| 234 |
+
torch.float64: jnp.float64,
|
| 235 |
+
torch.float16: jnp.float16,
|
| 236 |
+
torch.bfloat16: jnp.bfloat16,
|
| 237 |
+
torch.int32: jnp.int32,
|
| 238 |
+
torch.int64: jnp.int64,
|
| 239 |
+
torch.int16: jnp.int16,
|
| 240 |
+
torch.int8: jnp.int8,
|
| 241 |
+
torch.uint8: jnp.uint8,
|
| 242 |
+
torch.bool: jnp.bool_,
|
| 243 |
+
torch.complex64: jnp.complex64,
|
| 244 |
+
torch.complex128: jnp.complex128,
|
| 245 |
+
}
|
| 246 |
+
if dtype not in dtype_map:
|
| 247 |
+
raise ValueError(f"Unsupported dtype for JAX conversion: {dtype}")
|
| 248 |
+
return dtype_map[dtype]
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
def torch_dtype_to_jax(dtype: torch.dtype) -> str:
|
| 252 |
+
"""
|
| 253 |
+
Map PyTorch dtype to JAX dtype expression string.
|
| 254 |
+
|
| 255 |
+
This helper is used at compile time in codegen to generate
|
| 256 |
+
JAX dtype expressions for Pallas kernels.
|
| 257 |
+
|
| 258 |
+
Args:
|
| 259 |
+
dtype: PyTorch dtype to convert
|
| 260 |
+
|
| 261 |
+
Returns:
|
| 262 |
+
JAX dtype expression as string (e.g., "jnp.float32")
|
| 263 |
+
"""
|
| 264 |
+
jax_dtype = torch_dtype_to_jax_runtime(dtype)
|
| 265 |
+
dtype_name = jax_dtype.__name__
|
| 266 |
+
if dtype_name == "bool":
|
| 267 |
+
dtype_name = "bool_"
|
| 268 |
+
return f"jnp.{dtype_name}"
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
def pallas_gpu_pad_inputs(inputs: list[Any], alignment: int = 128) -> list[Any]:
|
| 272 |
+
"""Flatten and pad each input JAX array to a multiple of alignment."""
|
| 273 |
+
import jax.numpy as jnp # pyrefly: ignore [import-error, missing-import]
|
| 274 |
+
|
| 275 |
+
padded = []
|
| 276 |
+
for inp in inputs:
|
| 277 |
+
flat = inp.flatten()
|
| 278 |
+
orig_size = flat.size
|
| 279 |
+
aligned_size = ((orig_size + alignment - 1) // alignment) * alignment
|
| 280 |
+
if orig_size != aligned_size:
|
| 281 |
+
padded.append(jnp.pad(flat, (0, aligned_size - orig_size)))
|
| 282 |
+
else:
|
| 283 |
+
padded.append(flat)
|
| 284 |
+
return padded
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
def pallas_gpu_align_output_specs(
|
| 288 |
+
out_shapes: tuple[Any, ...],
|
| 289 |
+
out_dtypes: tuple[Any, ...],
|
| 290 |
+
alignment: int = 128,
|
| 291 |
+
) -> tuple[tuple[Any, ...], list[bool]]:
|
| 292 |
+
"""Build aligned output ShapeDtypeStruct specs for GPU kernels.
|
| 293 |
+
|
| 294 |
+
Returns (aligned_specs, is_scalar_output) where is_scalar_output[i] is True
|
| 295 |
+
when the i-th output is scalar and should not be padded/unpadded.
|
| 296 |
+
"""
|
| 297 |
+
import jax # pyrefly: ignore [import-error, missing-import]
|
| 298 |
+
|
| 299 |
+
aligned_specs = []
|
| 300 |
+
is_scalar = []
|
| 301 |
+
for shape, dtype in zip(out_shapes, out_dtypes):
|
| 302 |
+
numel = math.prod(shape)
|
| 303 |
+
if numel <= 1:
|
| 304 |
+
aligned_specs.append(jax.ShapeDtypeStruct(shape, dtype))
|
| 305 |
+
is_scalar.append(True)
|
| 306 |
+
else:
|
| 307 |
+
aligned_numel = ((numel + alignment - 1) // alignment) * alignment
|
| 308 |
+
aligned_specs.append(jax.ShapeDtypeStruct((aligned_numel,), dtype))
|
| 309 |
+
is_scalar.append(False)
|
| 310 |
+
return tuple(aligned_specs), is_scalar
|
| 311 |
+
|
| 312 |
+
|
| 313 |
+
def pallas_gpu_unpad_results(
|
| 314 |
+
results: Any,
|
| 315 |
+
orig_shapes: tuple[Any, ...],
|
| 316 |
+
is_scalar_output: list[bool] | None = None,
|
| 317 |
+
) -> Any:
|
| 318 |
+
"""Remove padding from GPU kernel results and reshape to original shapes.
|
| 319 |
+
|
| 320 |
+
If is_scalar_output is None, all outputs are treated as non-scalar.
|
| 321 |
+
"""
|
| 322 |
+
if not isinstance(results, tuple):
|
| 323 |
+
results = (results,)
|
| 324 |
+
unpadded = []
|
| 325 |
+
for i, (res, shape) in enumerate(zip(results, orig_shapes)):
|
| 326 |
+
if is_scalar_output is not None and is_scalar_output[i]:
|
| 327 |
+
unpadded.append(res)
|
| 328 |
+
else:
|
| 329 |
+
orig_numel = math.prod(shape)
|
| 330 |
+
unpadded.append(res[:orig_numel].reshape(shape))
|
| 331 |
+
return unpadded[0] if len(unpadded) == 1 else tuple(unpadded)
|
| 332 |
+
|
| 333 |
+
|
| 334 |
+
# ---------------------------------------------------------------------------
|
| 335 |
+
# Pallas CPU / TPU tiling helpers
|
| 336 |
+
# ---------------------------------------------------------------------------
|
| 337 |
+
# TPU alignment: last dim must be full or a multiple of 128,
|
| 338 |
+
# second-to-last dim must be full or a multiple of 8.
|
| 339 |
+
_TPU_ALIGN_LAST = 128
|
| 340 |
+
_TPU_ALIGN_SECOND_LAST = 8
|
| 341 |
+
|
| 342 |
+
|
| 343 |
+
def _pallas_tile_size(
|
| 344 |
+
dim: int, alignment: int, max_tile: int = 1024, is_tpu: bool = False
|
| 345 |
+
) -> int:
|
| 346 |
+
"""Pick the largest aligned tile size <= max_tile for *dim*.
|
| 347 |
+
|
| 348 |
+
If *dim* is already <= alignment the full dimension is used (no tiling
|
| 349 |
+
on this axis).
|
| 350 |
+
"""
|
| 351 |
+
if dim == 0:
|
| 352 |
+
# Tile size >= 1 avoids division by zero in JAX's _pad_to_block_dimension
|
| 353 |
+
return alignment if is_tpu else 1
|
| 354 |
+
|
| 355 |
+
if is_tpu:
|
| 356 |
+
# On TPU, Mosaic requires block dimensions to perfectly align to hardware
|
| 357 |
+
# registers (128 for inner, 8 for outer). We MUST pad the block spec up to
|
| 358 |
+
# the next alignment boundary (Mosaic handles the OOB masking).
|
| 359 |
+
if dim <= alignment:
|
| 360 |
+
return alignment
|
| 361 |
+
t = min(max_tile, dim)
|
| 362 |
+
# Use ceiling division to ensure the tile covers the dimension or aligns upward
|
| 363 |
+
t = ((t + alignment - 1) // alignment) * alignment
|
| 364 |
+
return t
|
| 365 |
+
|
| 366 |
+
if dim <= alignment:
|
| 367 |
+
return dim
|
| 368 |
+
t = min(max_tile, dim)
|
| 369 |
+
t = (t // alignment) * alignment
|
| 370 |
+
return max(alignment, t)
|
| 371 |
+
|
| 372 |
+
|
| 373 |
+
def pallas_permute(x, perm):
|
| 374 |
+
"""Permute array x according to perm, working around Mosaic TPU bugs.
|
| 375 |
+
|
| 376 |
+
Mosaic's jnp.permute_dims produces corrupted output for certain 3D+
|
| 377 |
+
permutations at large sizes. Decomposes into: loop over smallest output
|
| 378 |
+
dim via unrolled Python loop + stack, with recursive sub-permutation.
|
| 379 |
+
At the 2D base case, uses jnp.permute_dims (which works on Mosaic).
|
| 380 |
+
|
| 381 |
+
Callers should ensure tiles are small enough via tiling so that the
|
| 382 |
+
Mosaic bug doesn't trigger (threshold ~ 256K elements).
|
| 383 |
+
"""
|
| 384 |
+
import jax.numpy as jnp # pyrefly: ignore [import-error, missing-import]
|
| 385 |
+
|
| 386 |
+
ndim = len(perm)
|
| 387 |
+
if ndim <= 2:
|
| 388 |
+
return jnp.permute_dims(x, perm)
|
| 389 |
+
if perm == tuple(range(ndim)):
|
| 390 |
+
return x # identity
|
| 391 |
+
|
| 392 |
+
shape = x.shape
|
| 393 |
+
out_shape = tuple(shape[p] for p in perm)
|
| 394 |
+
|
| 395 |
+
# Choose the output dimension with the smallest size to loop over.
|
| 396 |
+
loop_out_dim = min(range(ndim), key=lambda d: out_shape[d])
|
| 397 |
+
loop_size = out_shape[loop_out_dim]
|
| 398 |
+
loop_in_dim = perm[loop_out_dim]
|
| 399 |
+
|
| 400 |
+
# Build the sub-permutation for the remaining (ndim-1) dimensions.
|
| 401 |
+
remaining_in_dims = [d for d in range(ndim) if d != loop_in_dim]
|
| 402 |
+
remaining_out_perm_raw = [perm[d] for d in range(ndim) if d != loop_out_dim]
|
| 403 |
+
dim_map = {old: new for new, old in enumerate(remaining_in_dims)}
|
| 404 |
+
sub_perm = tuple(dim_map[d] for d in remaining_out_perm_raw)
|
| 405 |
+
|
| 406 |
+
# Unrolled loop: extract slices with static indices, apply sub-perm,
|
| 407 |
+
# then stack along the loop output dimension.
|
| 408 |
+
slices = []
|
| 409 |
+
# pyrefly: ignore [bad-argument-type]
|
| 410 |
+
for i in range(loop_size):
|
| 411 |
+
idx: list[Any] = [slice(None)] * ndim
|
| 412 |
+
idx[loop_in_dim] = i
|
| 413 |
+
slc = x[tuple(idx)] # static index, removes loop_in_dim
|
| 414 |
+
slc_p = pallas_permute(slc, sub_perm)
|
| 415 |
+
slices.append(slc_p)
|
| 416 |
+
|
| 417 |
+
return jnp.stack(slices, axis=loop_out_dim)
|
| 418 |
+
|
| 419 |
+
|
| 420 |
+
# Maximum tile elements for permutation kernels. Mosaic's
|
| 421 |
+
# jnp.permute_dims produces corrupted output above ~1.5M elements for
|
| 422 |
+
# certain 3D+ permutations. Keep tiles well under that threshold.
|
| 423 |
+
_PERMUTE_MAX_TILE_ELEMS = 1 << 18 # ~256K elements
|
| 424 |
+
|
| 425 |
+
|
| 426 |
+
def pallas_compute_tiling(
|
| 427 |
+
ref_shape: tuple[int, ...],
|
| 428 |
+
transpose: bool = False, # constrain tile size for pallas_permute VMEM
|
| 429 |
+
skip_last_n: int = 0,
|
| 430 |
+
exact_only: bool = False,
|
| 431 |
+
is_tpu: bool = False,
|
| 432 |
+
permutations: list[tuple[int, ...]] | None = None,
|
| 433 |
+
max_grid_product: int | None = None,
|
| 434 |
+
) -> tuple[tuple[int, ...], tuple[int, ...], dict[int, int]]:
|
| 435 |
+
"""Compute tile shape, grid and axis→grid-dim mapping for CPU/TPU.
|
| 436 |
+
|
| 437 |
+
Always uses TPU-compatible alignment (last dim multiple of 128,
|
| 438 |
+
second-to-last multiple of 8) so that the same generated kernel works
|
| 439 |
+
on both CPU-interpret and real TPU.
|
| 440 |
+
|
| 441 |
+
*transpose* constrains tile sizes to keep VMEM usage within safe
|
| 442 |
+
limits for pallas_permute (gather-based permutation).
|
| 443 |
+
|
| 444 |
+
*skip_last_n* prevents tiling the last N dimensions (used when those
|
| 445 |
+
dims correspond to internal reduction ranges that must remain full).
|
| 446 |
+
|
| 447 |
+
*exact_only* restricts tiling to dimensions that divide evenly by the
|
| 448 |
+
tile size (no remainder blocks). Required on TPU where Mosaic needs
|
| 449 |
+
block shapes to match the XLA memory layout.
|
| 450 |
+
|
| 451 |
+
Returns ``(tile_shape, grid, axis_to_grid)`` where *axis_to_grid*
|
| 452 |
+
maps each tiled reference-shape axis index to its position in the
|
| 453 |
+
grid tuple.
|
| 454 |
+
|
| 455 |
+
When no dimension benefits from tiling the grid is ``(1,)`` and the
|
| 456 |
+
tile covers the full tensor.
|
| 457 |
+
"""
|
| 458 |
+
nd = len(ref_shape)
|
| 459 |
+
if nd == 0:
|
| 460 |
+
return (), (1,), {}
|
| 461 |
+
|
| 462 |
+
# Effective number of dims eligible for tiling
|
| 463 |
+
tileable_nd = nd - skip_last_n
|
| 464 |
+
|
| 465 |
+
tile = list(ref_shape)
|
| 466 |
+
grid_parts: list[int] = []
|
| 467 |
+
axis_to_grid: dict[int, int] = {} # ref axis → grid dim
|
| 468 |
+
|
| 469 |
+
# Pick alignment based on the physical position of the axis in the
|
| 470 |
+
# tensor, not its position in the tileable subset. The TPU requires
|
| 471 |
+
# the physical last dim to be a multiple of 128 and the physical
|
| 472 |
+
# second-to-last dim to be a multiple of 8.
|
| 473 |
+
#
|
| 474 |
+
# When permutations are present, an output dim `ax` maps to input
|
| 475 |
+
# dim `perm[ax]`. If `perm[ax]` is the input's last dim, that
|
| 476 |
+
# output dim must also satisfy _TPU_ALIGN_LAST. Compute the
|
| 477 |
+
# strictest alignment across all buffers.
|
| 478 |
+
def _input_align(input_ax: int, input_nd: int) -> int:
|
| 479 |
+
return _TPU_ALIGN_LAST if input_ax == input_nd - 1 else _TPU_ALIGN_SECOND_LAST
|
| 480 |
+
|
| 481 |
+
def _align(ax: int) -> int:
|
| 482 |
+
out_align = _TPU_ALIGN_LAST if ax == nd - 1 else _TPU_ALIGN_SECOND_LAST
|
| 483 |
+
if permutations:
|
| 484 |
+
for perm in permutations:
|
| 485 |
+
if perm is not None and len(perm) == nd:
|
| 486 |
+
in_ax = perm[ax]
|
| 487 |
+
in_align = _input_align(in_ax, len(perm))
|
| 488 |
+
out_align = max(out_align, in_align)
|
| 489 |
+
return out_align
|
| 490 |
+
|
| 491 |
+
def _can_tile_ax(ax: int, dim: int, t: int) -> bool:
|
| 492 |
+
"""Check if tiling dim to t is valid."""
|
| 493 |
+
if t >= dim:
|
| 494 |
+
# For TPU padding, we allow tiles >= dimension for the aligned axes
|
| 495 |
+
if _align(ax) == _TPU_ALIGN_LAST or _align(ax) == _TPU_ALIGN_SECOND_LAST:
|
| 496 |
+
return True
|
| 497 |
+
return False
|
| 498 |
+
if exact_only and dim % t != 0:
|
| 499 |
+
if _align(ax) == _TPU_ALIGN_LAST or _align(ax) == _TPU_ALIGN_SECOND_LAST:
|
| 500 |
+
# TPU DMA `#tpu.element_window` natively masks out-of-bounds remainder tiles
|
| 501 |
+
return True
|
| 502 |
+
return False
|
| 503 |
+
return True
|
| 504 |
+
|
| 505 |
+
# Second-to-last tileable dim (added first so it becomes grid dim 0)
|
| 506 |
+
if tileable_nd >= 2:
|
| 507 |
+
ax = tileable_nd - 2
|
| 508 |
+
t = _pallas_tile_size(ref_shape[ax], _align(ax), is_tpu=is_tpu)
|
| 509 |
+
if _can_tile_ax(ax, ref_shape[ax], t):
|
| 510 |
+
tile[ax] = t
|
| 511 |
+
axis_to_grid[ax] = len(grid_parts)
|
| 512 |
+
grid_parts.append((ref_shape[ax] + t - 1) // t)
|
| 513 |
+
|
| 514 |
+
# Last tileable dim
|
| 515 |
+
if tileable_nd >= 1:
|
| 516 |
+
ax = tileable_nd - 1
|
| 517 |
+
t = _pallas_tile_size(ref_shape[ax], _align(ax), is_tpu=is_tpu)
|
| 518 |
+
if _can_tile_ax(ax, ref_shape[ax], t):
|
| 519 |
+
tile[ax] = t
|
| 520 |
+
axis_to_grid[ax] = len(grid_parts)
|
| 521 |
+
grid_parts.append((ref_shape[ax] + t - 1) // t)
|
| 522 |
+
|
| 523 |
+
# When transpose is active, tile dimensions to keep the total tile
|
| 524 |
+
# elements below the safe threshold for Mosaic's permute_dims.
|
| 525 |
+
# Mosaic pads the last dimension to _TPU_ALIGN_LAST (128 for f32),
|
| 526 |
+
# so when the last dim is small (e.g. 2), VMEM usage is much higher
|
| 527 |
+
# than the logical tile size. We compute an effective element count
|
| 528 |
+
# that accounts for this padding.
|
| 529 |
+
if transpose:
|
| 530 |
+
tile_elems = 1
|
| 531 |
+
for t in tile:
|
| 532 |
+
tile_elems *= t
|
| 533 |
+
last_dim = tile[nd - 1] if nd > 0 else 1
|
| 534 |
+
padded_last = max(last_dim, _TPU_ALIGN_LAST)
|
| 535 |
+
# pallas_permute decomposes ndim>2 permutations by looping over
|
| 536 |
+
# the smallest dim and recursing, creating (ndim-1)-D intermediates
|
| 537 |
+
# at each level. Mosaic pads the last physical dim of each
|
| 538 |
+
# intermediate to _TPU_ALIGN_LAST (128), which can cause VMEM OOM
|
| 539 |
+
# when a small dim (e.g. 4) ends up as the last dim.
|
| 540 |
+
#
|
| 541 |
+
# For ndim >= 3, compute effective_max from the worst-case VMEM
|
| 542 |
+
# of the (ndim-1)-D intermediate created by pallas_permute.
|
| 543 |
+
# The intermediate removes the loop dim (smallest output dim) and
|
| 544 |
+
# may have a small dim as its last physical dim -> padded to 128.
|
| 545 |
+
if nd >= 4:
|
| 546 |
+
# For 4D+, pallas_permute creates (nd-1)-D intermediates
|
| 547 |
+
# that can have small dims as their last physical dim,
|
| 548 |
+
# which Mosaic pads to _TPU_ALIGN_LAST (128). This can
|
| 549 |
+
# cause VMEM OOM. (3D pallas_permute only creates 2D
|
| 550 |
+
# intermediates which Mosaic handles efficiently.)
|
| 551 |
+
sorted_dims = sorted(tile)
|
| 552 |
+
# The loop dim is the smallest; intermediate has the rest.
|
| 553 |
+
inter_dims = sorted_dims[1:] # (nd-1) dims, sorted
|
| 554 |
+
smallest_inter = inter_dims[0]
|
| 555 |
+
# Padded intermediate: smallest remaining dim padded to 128.
|
| 556 |
+
padded_inter_elems = 1
|
| 557 |
+
for d in inter_dims:
|
| 558 |
+
padded_inter_elems *= d
|
| 559 |
+
pad_factor = max(smallest_inter, _TPU_ALIGN_LAST) // max(smallest_inter, 1)
|
| 560 |
+
padded_inter_elems *= pad_factor
|
| 561 |
+
padded_inter_bytes = padded_inter_elems * 4
|
| 562 |
+
# Conservative VMEM budget: 2MB per intermediate buffer.
|
| 563 |
+
# pallas_permute creates multiple live intermediates (input
|
| 564 |
+
# slices, sub-permutation results, stack temps). Empirically,
|
| 565 |
+
# 2MB per worst-case intermediate avoids OOM on TPU v4 (64MB VMEM).
|
| 566 |
+
vmem_per_buf = 2 * 1024 * 1024 # 2MB
|
| 567 |
+
if padded_inter_bytes > vmem_per_buf:
|
| 568 |
+
scale = vmem_per_buf / padded_inter_bytes
|
| 569 |
+
effective_max = max(int(tile_elems * scale), 1024)
|
| 570 |
+
else:
|
| 571 |
+
effective_max = _PERMUTE_MAX_TILE_ELEMS
|
| 572 |
+
else:
|
| 573 |
+
# For ndim <= 3: pallas_permute creates 2D intermediates
|
| 574 |
+
# (fine) but jnp.stack creates a 3D result with the same
|
| 575 |
+
# shape as the output. If the last dim is small, Mosaic
|
| 576 |
+
# pads it to _TPU_ALIGN_LAST (128), which can exhaust VMEM.
|
| 577 |
+
# Empirically: 8MB per padded buffer keeps us safe on TPU v4
|
| 578 |
+
# (64MB VMEM, multiple buffers live simultaneously).
|
| 579 |
+
vmem_per_buf = 8 * 1024 * 1024 # 8MB
|
| 580 |
+
max_nonlast_product = vmem_per_buf // (padded_last * 4)
|
| 581 |
+
effective_max = min(
|
| 582 |
+
max_nonlast_product * last_dim,
|
| 583 |
+
_PERMUTE_MAX_TILE_ELEMS,
|
| 584 |
+
)
|
| 585 |
+
effective_max = min(effective_max, _PERMUTE_MAX_TILE_ELEMS)
|
| 586 |
+
effective_max = max(effective_max, 1024)
|
| 587 |
+
|
| 588 |
+
if tile_elems > effective_max:
|
| 589 |
+
# Collect tileable dims. For ndim >= 4 with VMEM-constrained
|
| 590 |
+
# intermediates, include ALL tileable dims (even the last)
|
| 591 |
+
# since we may need to shrink every dim to fit in VMEM.
|
| 592 |
+
# For ndim <= 3, exclude the last dim (it's often small
|
| 593 |
+
# and already at minimum).
|
| 594 |
+
if nd >= 4:
|
| 595 |
+
tileable_axes = [
|
| 596 |
+
ax for ax in range(tileable_nd - 1, -1, -1) if ref_shape[ax] > 1
|
| 597 |
+
]
|
| 598 |
+
else:
|
| 599 |
+
tileable_axes = [
|
| 600 |
+
ax for ax in range(tileable_nd - 2, -1, -1) if ref_shape[ax] > 1
|
| 601 |
+
]
|
| 602 |
+
# Distribute reduction proportionally across dims.
|
| 603 |
+
reduction = tile_elems / effective_max
|
| 604 |
+
n = len(tileable_axes)
|
| 605 |
+
per_dim = reduction ** (1.0 / max(n, 1))
|
| 606 |
+
|
| 607 |
+
for ax in tileable_axes:
|
| 608 |
+
if tile_elems <= effective_max:
|
| 609 |
+
break
|
| 610 |
+
dim = ref_shape[ax]
|
| 611 |
+
cur_t = tile[ax]
|
| 612 |
+
align = _align(ax)
|
| 613 |
+
# Target tile: proportional reduction, but also check
|
| 614 |
+
# against the remaining budget for subsequent dims.
|
| 615 |
+
target = int(cur_t / per_dim)
|
| 616 |
+
remaining_budget = effective_max * cur_t // tile_elems
|
| 617 |
+
target = min(target, remaining_budget)
|
| 618 |
+
target = (target // align) * align
|
| 619 |
+
target = max(target, align)
|
| 620 |
+
# Find largest aligned divisor of dim <= target.
|
| 621 |
+
best_t = cur_t
|
| 622 |
+
for candidate in range(target, 0, -align):
|
| 623 |
+
if candidate >= cur_t:
|
| 624 |
+
continue
|
| 625 |
+
if dim % candidate != 0:
|
| 626 |
+
continue
|
| 627 |
+
best_t = candidate
|
| 628 |
+
break
|
| 629 |
+
if best_t < cur_t:
|
| 630 |
+
if ax in axis_to_grid:
|
| 631 |
+
grid_parts[axis_to_grid[ax]] = dim // best_t
|
| 632 |
+
else:
|
| 633 |
+
axis_to_grid[ax] = len(grid_parts)
|
| 634 |
+
grid_parts.append(dim // best_t)
|
| 635 |
+
tile_elems = tile_elems * best_t // cur_t
|
| 636 |
+
tile[ax] = best_t
|
| 637 |
+
|
| 638 |
+
grid = tuple(grid_parts) if grid_parts else (1,)
|
| 639 |
+
|
| 640 |
+
# Enforce max_grid_product by scaling up tiles on tiled axes.
|
| 641 |
+
if max_grid_product is not None and grid_parts:
|
| 642 |
+
grid_product = 1
|
| 643 |
+
for g in grid_parts:
|
| 644 |
+
grid_product *= g
|
| 645 |
+
if grid_product > max_grid_product:
|
| 646 |
+
# Sort tiled axes by their grid contribution (descending)
|
| 647 |
+
# and increase tiles to reduce the grid.
|
| 648 |
+
tiled_axes = sorted(
|
| 649 |
+
axis_to_grid.keys(),
|
| 650 |
+
key=lambda ax: grid_parts[axis_to_grid[ax]],
|
| 651 |
+
reverse=True,
|
| 652 |
+
)
|
| 653 |
+
for ax in tiled_axes:
|
| 654 |
+
if grid_product <= max_grid_product:
|
| 655 |
+
break
|
| 656 |
+
gi = axis_to_grid[ax]
|
| 657 |
+
cur_grid = grid_parts[gi]
|
| 658 |
+
cur_tile = tile[ax]
|
| 659 |
+
dim = ref_shape[ax]
|
| 660 |
+
align = _align(ax)
|
| 661 |
+
# Target grid for this axis: proportional share of reduction
|
| 662 |
+
target_grid = max(1, int(cur_grid * max_grid_product / grid_product))
|
| 663 |
+
target_tile = (dim + target_grid - 1) // target_grid
|
| 664 |
+
# Round up to alignment
|
| 665 |
+
target_tile = ((target_tile + align - 1) // align) * align
|
| 666 |
+
target_tile = min(target_tile, dim)
|
| 667 |
+
# Find smallest aligned value >= target_tile that divides dim
|
| 668 |
+
# (or just use the rounded-up value if exact_only is False)
|
| 669 |
+
best_tile = dim # fallback: full dim
|
| 670 |
+
if exact_only:
|
| 671 |
+
for candidate in range(target_tile, dim + 1, align):
|
| 672 |
+
if dim % candidate == 0:
|
| 673 |
+
best_tile = candidate
|
| 674 |
+
break
|
| 675 |
+
else:
|
| 676 |
+
best_tile = target_tile
|
| 677 |
+
if best_tile > cur_tile:
|
| 678 |
+
new_grid = (dim + best_tile - 1) // best_tile
|
| 679 |
+
grid_product = grid_product * new_grid // cur_grid
|
| 680 |
+
grid_parts[gi] = new_grid
|
| 681 |
+
tile[ax] = best_tile
|
| 682 |
+
grid = tuple(grid_parts)
|
| 683 |
+
|
| 684 |
+
return tuple(tile), grid, axis_to_grid
|
| 685 |
+
|
| 686 |
+
|
| 687 |
+
def pallas_make_block_spec(
|
| 688 |
+
buf_shape: tuple[int, ...],
|
| 689 |
+
ref_shape: tuple[int, ...],
|
| 690 |
+
tile_shape: tuple[int, ...],
|
| 691 |
+
axis_to_grid: dict[int, int],
|
| 692 |
+
n_grid: int,
|
| 693 |
+
permutation: tuple[int, ...] | None = None,
|
| 694 |
+
is_output: bool = False,
|
| 695 |
+
) -> Any:
|
| 696 |
+
"""Build a ``pl.BlockSpec`` for *buf_shape* given tiling of *ref_shape*.
|
| 697 |
+
|
| 698 |
+
Lower-ndim buffers are right-aligned with the reference shape (numpy
|
| 699 |
+
broadcast rules). Dimensions that match a tiled reference dimension
|
| 700 |
+
are tiled; broadcast dimensions (size 1 or absent) are kept full.
|
| 701 |
+
|
| 702 |
+
When *buf_nd > ref_nd* (reduction inputs), we find an alignment offset
|
| 703 |
+
so the ref dims map into the buffer. Extra dims are kept at full size
|
| 704 |
+
with index 0 in the index_map.
|
| 705 |
+
|
| 706 |
+
When *permutation* is given (a tuple mapping ref axis -> buf axis),
|
| 707 |
+
the buffer dimensions are accessed in permuted order relative to the
|
| 708 |
+
reference shape. For example, permutation=(1, 0) swaps the last two.
|
| 709 |
+
|
| 710 |
+
When *is_output* is True and *buf_nd < ref_nd*, left-alignment is used
|
| 711 |
+
as a fallback (for reduction outputs whose trailing dims were reduced).
|
| 712 |
+
"""
|
| 713 |
+
from jax.experimental import ( # pyrefly: ignore [import-error, missing-import]
|
| 714 |
+
pallas as pl,
|
| 715 |
+
)
|
| 716 |
+
|
| 717 |
+
buf_nd = len(buf_shape)
|
| 718 |
+
ref_nd = len(ref_shape)
|
| 719 |
+
|
| 720 |
+
if buf_nd == 0:
|
| 721 |
+
# Scalar — untouched regardless of grid shape.
|
| 722 |
+
return pl.BlockSpec((1,), _make_index_map([], 1, n_grid))
|
| 723 |
+
|
| 724 |
+
bs = list(buf_shape)
|
| 725 |
+
tiled_pairs: list[tuple[int, int]] = []
|
| 726 |
+
|
| 727 |
+
if buf_nd > ref_nd:
|
| 728 |
+
# Reduction input: find alignment offset k where ref dims map into buf.
|
| 729 |
+
align_k = 0
|
| 730 |
+
for k in range(buf_nd - ref_nd + 1):
|
| 731 |
+
ok = True
|
| 732 |
+
for i in range(ref_nd):
|
| 733 |
+
if ref_shape[i] == 1:
|
| 734 |
+
continue
|
| 735 |
+
if buf_shape[k + i] != ref_shape[i]:
|
| 736 |
+
ok = False
|
| 737 |
+
break
|
| 738 |
+
if ok:
|
| 739 |
+
align_k = k
|
| 740 |
+
break
|
| 741 |
+
|
| 742 |
+
for ref_ax, grid_dim in axis_to_grid.items():
|
| 743 |
+
buf_ax = align_k + ref_ax
|
| 744 |
+
if 0 <= buf_ax < buf_nd and buf_shape[buf_ax] == ref_shape[ref_ax]:
|
| 745 |
+
bs[buf_ax] = tile_shape[ref_ax]
|
| 746 |
+
tiled_pairs.append((buf_ax, grid_dim))
|
| 747 |
+
|
| 748 |
+
elif permutation is not None and buf_nd == ref_nd:
|
| 749 |
+
# Permuted buffer: map ref axes through the permutation.
|
| 750 |
+
for ref_ax, grid_dim in axis_to_grid.items():
|
| 751 |
+
buf_ax = permutation[ref_ax]
|
| 752 |
+
if 0 <= buf_ax < buf_nd and buf_shape[buf_ax] == ref_shape[ref_ax]:
|
| 753 |
+
bs[buf_ax] = tile_shape[ref_ax]
|
| 754 |
+
tiled_pairs.append((buf_ax, grid_dim))
|
| 755 |
+
|
| 756 |
+
else:
|
| 757 |
+
# Standard right-alignment, with left-alignment fallback for
|
| 758 |
+
# reduction outputs (e.g. sum(dim=-1) on (10,10) → (10,)).
|
| 759 |
+
for ref_ax, grid_dim in axis_to_grid.items():
|
| 760 |
+
buf_ax = ref_ax - (ref_nd - buf_nd)
|
| 761 |
+
if 0 <= buf_ax < buf_nd and buf_shape[buf_ax] == ref_shape[ref_ax]:
|
| 762 |
+
bs[buf_ax] = tile_shape[ref_ax]
|
| 763 |
+
tiled_pairs.append((buf_ax, grid_dim))
|
| 764 |
+
elif (
|
| 765 |
+
is_output
|
| 766 |
+
and buf_nd < ref_nd
|
| 767 |
+
and 0 <= ref_ax < buf_nd
|
| 768 |
+
and buf_shape[ref_ax] == ref_shape[ref_ax]
|
| 769 |
+
):
|
| 770 |
+
# Left-aligned match for output: buf dim i matches ref dim i
|
| 771 |
+
# (reduction output whose trailing dims were reduced away)
|
| 772 |
+
bs[ref_ax] = tile_shape[ref_ax]
|
| 773 |
+
tiled_pairs.append((ref_ax, grid_dim))
|
| 774 |
+
|
| 775 |
+
return pl.BlockSpec(
|
| 776 |
+
tuple(bs),
|
| 777 |
+
_make_index_map(tiled_pairs, buf_nd, n_grid),
|
| 778 |
+
)
|
| 779 |
+
|
| 780 |
+
|
| 781 |
+
def _make_index_map(
|
| 782 |
+
tiled_pairs: list[tuple[int, int]],
|
| 783 |
+
buf_nd: int,
|
| 784 |
+
n_grid: int,
|
| 785 |
+
) -> Any:
|
| 786 |
+
"""Return an index_map callable for ``pl.BlockSpec``.
|
| 787 |
+
|
| 788 |
+
*tiled_pairs* is a list of ``(buf_axis, grid_dim)`` indicating which
|
| 789 |
+
buffer axes receive a grid index. All other axes return 0 (full block).
|
| 790 |
+
|
| 791 |
+
All returned values are explicitly ``jnp.int32`` so that TPU Mosaic
|
| 792 |
+
lowering (which rejects 64-bit types) works when ``jax_enable_x64`` is
|
| 793 |
+
active. The casts are created inside the function body (not captured)
|
| 794 |
+
to satisfy JAX's "index_map must not capture constants" rule.
|
| 795 |
+
"""
|
| 796 |
+
import jax.numpy as jnp # pyrefly: ignore [import-error, missing-import]
|
| 797 |
+
|
| 798 |
+
# Pre-build the mapping so the returned lambda is a plain lookup.
|
| 799 |
+
mapping = dict(tiled_pairs)
|
| 800 |
+
|
| 801 |
+
if n_grid == 0 or (n_grid == 1 and not mapping):
|
| 802 |
+
return lambda _i: tuple(jnp.int32(0) for _ in range(buf_nd))
|
| 803 |
+
|
| 804 |
+
def index_map(*grid_args):
|
| 805 |
+
return tuple(
|
| 806 |
+
jnp.int32(grid_args[mapping[d]]) if d in mapping else jnp.int32(0)
|
| 807 |
+
for d in range(buf_nd)
|
| 808 |
+
)
|
| 809 |
+
|
| 810 |
+
return index_map
|
| 811 |
+
|
| 812 |
+
|
| 813 |
+
def pallas_ensure_nonzero_rank(x: torch.Tensor) -> torch.Tensor:
|
| 814 |
+
if len(x.shape) == 0:
|
| 815 |
+
return x.reshape((1,))
|
| 816 |
+
return x
|
| 817 |
+
|
| 818 |
+
|
| 819 |
+
def pallas_make_block_spec_non_tiled(shape: tuple[int, ...]) -> Any:
|
| 820 |
+
import jax.numpy as jnp # pyrefly: ignore [import-error, missing-import]
|
| 821 |
+
from jax.experimental import ( # pyrefly: ignore [import-error, missing-import]
|
| 822 |
+
pallas as pl,
|
| 823 |
+
)
|
| 824 |
+
|
| 825 |
+
nonzero_rank_shape = shape if len(shape) > 0 else (1,)
|
| 826 |
+
return pl.BlockSpec(
|
| 827 |
+
nonzero_rank_shape,
|
| 828 |
+
lambda i: [jnp.int32(i)] * len(nonzero_rank_shape),
|
| 829 |
+
)
|
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/static_triton_launcher.py
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import functools
|
| 2 |
+
import os
|
| 3 |
+
from functools import cached_property
|
| 4 |
+
from typing import Any
|
| 5 |
+
from typing_extensions import Unpack
|
| 6 |
+
|
| 7 |
+
from ..utils import is_rocm
|
| 8 |
+
from .triton_compat import ASTSource, CompiledKernel, knobs as triton_knobs
|
| 9 |
+
from .triton_helpers import get_constexprs
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class StaticallyLaunchedTritonKernel:
|
| 13 |
+
"""
|
| 14 |
+
Parses the metadata of a CompiledKernel from Triton into a structure that can
|
| 15 |
+
launch the cuda kernel directly. Only works for triton kernels compiled to cubin.
|
| 16 |
+
|
| 17 |
+
Doing this avoids C++ codegen and compilation during compile, since we can use a
|
| 18 |
+
statically compiled library to launch the kernel. To avoid mallocing for the arguments,
|
| 19 |
+
we have a launcher for different numbers of arguments up to a max. StaticCudaLauncher
|
| 20 |
+
only supports # of arguments up until 10 for now.
|
| 21 |
+
|
| 22 |
+
Workflow:
|
| 23 |
+
Compile time:
|
| 24 |
+
1. Compile a kernel with triton and get a CompiledKernel
|
| 25 |
+
2. Instantiate kernel = StaticallyLaunchedTritonKernel(triton_kernel)
|
| 26 |
+
3. Write to a cubin file: kernel.write_cubin_to_file(filepath)
|
| 27 |
+
4. Call kernel.load_kernel() (CUDA should be initialized by this point) to load the cubin
|
| 28 |
+
Runtime:
|
| 29 |
+
5. Call kernel.run(grid, stream, args) to launch the kernel
|
| 30 |
+
|
| 31 |
+
Note that after step 3, StaticallyLaunchedTritonKernel is fully pickleable/serializable.
|
| 32 |
+
This allows it to be cached by FXGraphCache/TritonBundler, as well as sent from the worker
|
| 33 |
+
to the parent process in inductor.
|
| 34 |
+
|
| 35 |
+
There are two main versions of triton that we wish to support: 3.3 and 3.2. Triton makes considerable changes
|
| 36 |
+
to how it handles constants in 3.3, so there's some special logic necessary to handle both versions.
|
| 37 |
+
"""
|
| 38 |
+
|
| 39 |
+
@cached_property
|
| 40 |
+
def C_impl(self):
|
| 41 |
+
raise NotImplementedError
|
| 42 |
+
|
| 43 |
+
def __init__(self, kernel: CompiledKernel) -> None:
|
| 44 |
+
# pyrefly: ignore [missing-attribute]
|
| 45 |
+
self.name = kernel.src.fn.__name__
|
| 46 |
+
# pyrefly: ignore [missing-attribute]
|
| 47 |
+
self.cubin_path = kernel._cubin_path
|
| 48 |
+
|
| 49 |
+
# Used by torch.compile to filter constants in older triton versions
|
| 50 |
+
# pyrefly: ignore [missing-attribute]
|
| 51 |
+
self.arg_names = kernel.src.fn.arg_names
|
| 52 |
+
|
| 53 |
+
# Const exprs that are declared by the triton kernel directly
|
| 54 |
+
# Used to generate the kernel launcher's def args
|
| 55 |
+
# pyrefly: ignore [missing-attribute]
|
| 56 |
+
self.declared_constexprs = get_constexprs(kernel.src.fn)
|
| 57 |
+
|
| 58 |
+
# pyrefly: ignore [missing-attribute]
|
| 59 |
+
self.hash = kernel.hash
|
| 60 |
+
|
| 61 |
+
if triton_knobs is None:
|
| 62 |
+
# pyrefly: ignore [missing-attribute]
|
| 63 |
+
launch_enter = kernel.__class__.launch_enter_hook
|
| 64 |
+
# pyrefly: ignore [missing-attribute]
|
| 65 |
+
launch_exit = kernel.__class__.launch_exit_hook
|
| 66 |
+
else:
|
| 67 |
+
launch_enter = triton_knobs.runtime.launch_enter_hook
|
| 68 |
+
launch_exit = triton_knobs.runtime.launch_exit_hook
|
| 69 |
+
|
| 70 |
+
def hook_is_empty(hook: Any) -> bool:
|
| 71 |
+
if hook is None:
|
| 72 |
+
return True
|
| 73 |
+
if (
|
| 74 |
+
triton_knobs
|
| 75 |
+
and (HookChain := getattr(triton_knobs, "HookChain", None)) is not None
|
| 76 |
+
and isinstance(hook, HookChain)
|
| 77 |
+
):
|
| 78 |
+
# Support hooks after https://github.com/triton-lang/triton/pull/7866
|
| 79 |
+
return len(hook.calls) == 0
|
| 80 |
+
return False
|
| 81 |
+
|
| 82 |
+
if not hook_is_empty(launch_enter) or not hook_is_empty(launch_exit):
|
| 83 |
+
raise NotImplementedError(
|
| 84 |
+
"We don't support launch enter or launch exit hooks"
|
| 85 |
+
)
|
| 86 |
+
# pyrefly: ignore [missing-attribute]
|
| 87 |
+
self.num_warps = kernel.metadata.num_warps
|
| 88 |
+
self.shared = (
|
| 89 |
+
# pyrefly: ignore [missing-attribute]
|
| 90 |
+
kernel.shared if hasattr(kernel, "shared") else kernel.metadata.shared
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
def needs_scratch_arg(scratch_name: str, param_name: str) -> bool:
|
| 94 |
+
# pyrefly: ignore [missing-attribute]
|
| 95 |
+
if hasattr(kernel.metadata, param_name):
|
| 96 |
+
# pyrefly: ignore [missing-attribute]
|
| 97 |
+
if getattr(kernel.metadata, param_name) > 0:
|
| 98 |
+
raise NotImplementedError(
|
| 99 |
+
f"{scratch_name} scratch not yet supported"
|
| 100 |
+
)
|
| 101 |
+
return True
|
| 102 |
+
return False
|
| 103 |
+
|
| 104 |
+
# Newer triton versions pass an extra global scratch parameter to the compiled cuda kernel.
|
| 105 |
+
# Inductor never uses this field or enables it, but we still have to pass
|
| 106 |
+
# an extra None into the set of params if its enabled
|
| 107 |
+
self.has_global_scratch = needs_scratch_arg("Global", "global_scratch_size")
|
| 108 |
+
# same situation for profile scratch - triton-lang/triton#7258
|
| 109 |
+
self.has_profile_scratch = needs_scratch_arg("Profile", "profile_scratch_size")
|
| 110 |
+
|
| 111 |
+
# pyrefly: ignore [missing-attribute]
|
| 112 |
+
self.arg_tys = self.arg_ty_from_signature(kernel.src)
|
| 113 |
+
self.function: int | None = None # Loaded by load_kernel(on the parent process)
|
| 114 |
+
num_ctas = 1
|
| 115 |
+
if hasattr(kernel, "num_ctas"):
|
| 116 |
+
num_ctas = kernel.num_ctas
|
| 117 |
+
elif hasattr(kernel, "metadata"):
|
| 118 |
+
num_ctas = kernel.metadata.num_ctas
|
| 119 |
+
|
| 120 |
+
if num_ctas != 1:
|
| 121 |
+
raise NotImplementedError(
|
| 122 |
+
"Static cuda launcher only supports num_ctas == 1"
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
def reload_cubin_from_raw(self, filepath: str) -> str:
|
| 126 |
+
"""
|
| 127 |
+
If the cubin file triton generated gets deleted under us, we can
|
| 128 |
+
reload it from the raw cubin file.
|
| 129 |
+
"""
|
| 130 |
+
if self.cubin_path is None:
|
| 131 |
+
assert self.cubin_raw is not None
|
| 132 |
+
os.makedirs(os.path.dirname(filepath), exist_ok=True)
|
| 133 |
+
with open(filepath, "wb") as f:
|
| 134 |
+
f.write(self.cubin_raw)
|
| 135 |
+
self.cubin_path = filepath # pyre-ignore
|
| 136 |
+
return self.cubin_path
|
| 137 |
+
|
| 138 |
+
def load_kernel(self, device: int) -> None:
|
| 139 |
+
if self.function is not None:
|
| 140 |
+
return
|
| 141 |
+
|
| 142 |
+
assert hasattr(self, "cubin_path")
|
| 143 |
+
assert self.cubin_path is not None
|
| 144 |
+
(self.function, self.n_regs, self.n_spills) = self.C_impl._load_kernel(
|
| 145 |
+
self.cubin_path, self.name, self.shared, device
|
| 146 |
+
)
|
| 147 |
+
# Don't need the cubin path anymore now that we've loaded
|
| 148 |
+
self.cubin_path = None
|
| 149 |
+
self.cubin_raw = None
|
| 150 |
+
|
| 151 |
+
@staticmethod
|
| 152 |
+
@functools.lru_cache
|
| 153 |
+
def type_mappings() -> dict[str, str]:
|
| 154 |
+
return {
|
| 155 |
+
"i1": "i",
|
| 156 |
+
"i8": "b",
|
| 157 |
+
"i16": "h",
|
| 158 |
+
"i32": "i",
|
| 159 |
+
"i64": "l",
|
| 160 |
+
"u1": "I",
|
| 161 |
+
"u8": "B",
|
| 162 |
+
"u16": "H",
|
| 163 |
+
"u32": "I",
|
| 164 |
+
"u64": "K",
|
| 165 |
+
"fp16": "f",
|
| 166 |
+
"bf16": "f",
|
| 167 |
+
"fp32": "f",
|
| 168 |
+
"f32": "f",
|
| 169 |
+
"fp64": "d",
|
| 170 |
+
# TODO handle nvTmaDesc/CUtensormap
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
def extract_type(self, ty: str) -> str:
|
| 174 |
+
"""
|
| 175 |
+
Takes a triton type from CompiledKernel.signature and
|
| 176 |
+
converts it into a single char encoding. _StaticCudaLauncher
|
| 177 |
+
will switch on this char to figure out what type the underlying
|
| 178 |
+
value should be passed to the triton kernel as.
|
| 179 |
+
"""
|
| 180 |
+
if ty[0] == "*":
|
| 181 |
+
return "O"
|
| 182 |
+
elif ty == "nvTmaDesc":
|
| 183 |
+
raise NotImplementedError("nvTmaDesc kernels are not yet supported")
|
| 184 |
+
return StaticallyLaunchedTritonKernel.type_mappings()[ty]
|
| 185 |
+
|
| 186 |
+
def arg_ty_from_signature(self, src: ASTSource) -> str:
|
| 187 |
+
def index_key(i: Any) -> int:
|
| 188 |
+
if isinstance(i, str):
|
| 189 |
+
# pyrefly: ignore [missing-attribute]
|
| 190 |
+
return src.fn.arg_names.index(i)
|
| 191 |
+
elif isinstance(i, tuple):
|
| 192 |
+
# In triton 3.3, src.fn.constants has tuples as a key
|
| 193 |
+
return i[0]
|
| 194 |
+
else:
|
| 195 |
+
return i
|
| 196 |
+
|
| 197 |
+
# pyrefly: ignore [missing-attribute]
|
| 198 |
+
signature = {index_key(key): value for key, value in src.signature.items()}
|
| 199 |
+
# Triton uses these as the main way to filter out constants passed to their cubin
|
| 200 |
+
constants = [index_key(key) for key in getattr(src, "constants", dict())]
|
| 201 |
+
# This value is always a superset of kernel.fn.constexprs: kernel.fn.constexprs are
|
| 202 |
+
# constants declared by the triton kernel directly, whereas this list can have
|
| 203 |
+
# constants that are unused by the triton kernel that triton figured out during
|
| 204 |
+
# compilation.
|
| 205 |
+
self.full_constexprs = constants
|
| 206 |
+
# Despite requiring them to be passed in, the triton CUDA launcher
|
| 207 |
+
# completely ignores the constexprs passed into it when generating code.
|
| 208 |
+
# So we can ignore them here too
|
| 209 |
+
params = []
|
| 210 |
+
|
| 211 |
+
for i in sorted(signature.keys()):
|
| 212 |
+
ty = signature[i]
|
| 213 |
+
# In newer triton versions, constants are passed in to signature with type `constexpr`
|
| 214 |
+
# In older triton versions, there can be constants in src.constants that are not `constexpr` in signature
|
| 215 |
+
# so we check both here
|
| 216 |
+
if ty == "constexpr" or i in constants:
|
| 217 |
+
pass
|
| 218 |
+
else:
|
| 219 |
+
# pyrefly: ignore [bad-argument-type]
|
| 220 |
+
params.append(self.extract_type(ty))
|
| 221 |
+
return "".join(params)
|
| 222 |
+
|
| 223 |
+
def __getstate__(self) -> dict[str, Any]:
|
| 224 |
+
# Remove objects that are no longer valid for pickling
|
| 225 |
+
state = self.__dict__.copy()
|
| 226 |
+
state["function"] = None
|
| 227 |
+
# Cubin paths aren't consistent across processes, so we clear
|
| 228 |
+
# and reload them.
|
| 229 |
+
state["cubin_path"] = None
|
| 230 |
+
return state
|
| 231 |
+
|
| 232 |
+
def run(
|
| 233 |
+
self,
|
| 234 |
+
grid_x: int,
|
| 235 |
+
grid_y: int,
|
| 236 |
+
grid_z: int,
|
| 237 |
+
stream: int,
|
| 238 |
+
*args: Unpack[tuple[object, ...]],
|
| 239 |
+
) -> None:
|
| 240 |
+
"""Actually run the kernel at runtime. This function is the hot codepath."""
|
| 241 |
+
|
| 242 |
+
# Assert load_kernel() has been called and args match
|
| 243 |
+
assert self.function is not None
|
| 244 |
+
|
| 245 |
+
# TODO: actually, if the args *don't* match, we probably should
|
| 246 |
+
# throw an exception. But if inductor is the only one calling this
|
| 247 |
+
# thing, it should always match.
|
| 248 |
+
# Get rid of constants before passing to cubin launcher
|
| 249 |
+
|
| 250 |
+
arg_tys = self.arg_tys
|
| 251 |
+
|
| 252 |
+
if is_rocm():
|
| 253 |
+
# ROCm/HIP kernel ABI: The Triton HIP backend ALWAYS includes both
|
| 254 |
+
# global_scratch and profile_scratch parameters in the kernel signature,
|
| 255 |
+
# even when the kernel doesn't use them (i.e., when has_*_scratch is False).
|
| 256 |
+
#
|
| 257 |
+
# This differs fundamentally from CUDA, where these parameters are only
|
| 258 |
+
# present in the signature if the corresponding has_*_scratch flag is True.
|
| 259 |
+
#
|
| 260 |
+
# The flags indicate whether memory will be allocated/used:
|
| 261 |
+
# - has_global_scratch: Whether global scratch workspace is needed
|
| 262 |
+
# - has_profile_scratch: Whether profiling instrumentation is enabled
|
| 263 |
+
#
|
| 264 |
+
# However, regardless of flag values, we MUST always pass both parameters
|
| 265 |
+
# to match the HIP kernel ABI. Passing None is safe:
|
| 266 |
+
#
|
| 267 |
+
# - If scratch is not needed (has_*_scratch=False or scratch_size=0):
|
| 268 |
+
# The None becomes nullptr, which the kernel never dereferences
|
| 269 |
+
#
|
| 270 |
+
# - If scratch is needed (has_*_scratch=True and scratch_size>0):
|
| 271 |
+
# The None becomes nullptr initially, but the HIP runtime intercepts
|
| 272 |
+
# the kernel launch, allocates the required scratch memory based on
|
| 273 |
+
# kernel metadata, and replaces the nullptr with a valid pointer before
|
| 274 |
+
# the kernel actually executes
|
| 275 |
+
#
|
| 276 |
+
# Not passing both parameters causes segmentation faults because the kernel
|
| 277 |
+
# expects them at specific positions in the argument array.
|
| 278 |
+
arg_tys = arg_tys + "OO"
|
| 279 |
+
args = (*args, None, None)
|
| 280 |
+
|
| 281 |
+
else:
|
| 282 |
+
for has_scratch in [self.has_global_scratch, self.has_profile_scratch]:
|
| 283 |
+
if has_scratch:
|
| 284 |
+
arg_tys = arg_tys + "O"
|
| 285 |
+
args = (*args, None)
|
| 286 |
+
# pyrefly: ignore [bad-argument-type]
|
| 287 |
+
assert len(args) == len(arg_tys)
|
| 288 |
+
|
| 289 |
+
# TODO: can handle grid functions here or in C++, so
|
| 290 |
+
# that we don't need the grid handler above.
|
| 291 |
+
self.C_impl._launch_kernel(
|
| 292 |
+
self.function,
|
| 293 |
+
grid_x,
|
| 294 |
+
grid_y,
|
| 295 |
+
grid_z,
|
| 296 |
+
self.num_warps,
|
| 297 |
+
self.shared,
|
| 298 |
+
arg_tys,
|
| 299 |
+
args,
|
| 300 |
+
stream,
|
| 301 |
+
)
|
| 302 |
+
|
| 303 |
+
|
| 304 |
+
class StaticallyLaunchedCudaKernel(StaticallyLaunchedTritonKernel):
|
| 305 |
+
@cached_property
|
| 306 |
+
def C_impl(self):
|
| 307 |
+
from torch._C import _StaticCudaLauncher
|
| 308 |
+
|
| 309 |
+
return _StaticCudaLauncher
|
| 310 |
+
|
| 311 |
+
def __init__(self, kernel: CompiledKernel) -> None:
|
| 312 |
+
# pyrefly: ignore [missing-attribute]
|
| 313 |
+
if "hsaco" in kernel.asm:
|
| 314 |
+
# pyrefly: ignore [missing-attribute]
|
| 315 |
+
self.cubin_raw = kernel.asm["hsaco"]
|
| 316 |
+
|
| 317 |
+
# pyrefly: ignore [missing-attribute]
|
| 318 |
+
elif "cubin" in kernel.asm:
|
| 319 |
+
# pyrefly: ignore [missing-attribute]
|
| 320 |
+
self.cubin_raw = kernel.asm["cubin"]
|
| 321 |
+
else:
|
| 322 |
+
raise RuntimeError(
|
| 323 |
+
"Expected either 'hsaco' (ROCm) or 'cubin' (CUDA) in kernel.asm"
|
| 324 |
+
)
|
| 325 |
+
super().__init__(kernel)
|
| 326 |
+
|
| 327 |
+
|
| 328 |
+
class StaticallyLaunchedXpuKernel(StaticallyLaunchedTritonKernel):
|
| 329 |
+
@cached_property
|
| 330 |
+
def C_impl(self):
|
| 331 |
+
from torch._C import _StaticXpuLauncher
|
| 332 |
+
|
| 333 |
+
return _StaticXpuLauncher
|
| 334 |
+
|
| 335 |
+
def __init__(self, kernel: CompiledKernel) -> None:
|
| 336 |
+
# pyrefly: ignore [missing-attribute]
|
| 337 |
+
self.cubin_raw = kernel.asm.get("zebin", None)
|
| 338 |
+
super().__init__(kernel)
|
| 339 |
+
|
| 340 |
+
|
| 341 |
+
def statically_launched_kernel_by_device(
|
| 342 |
+
kernel: CompiledKernel, device_type: str = "cuda"
|
| 343 |
+
) -> StaticallyLaunchedTritonKernel:
|
| 344 |
+
if device_type in ("cuda", "hip"):
|
| 345 |
+
return StaticallyLaunchedCudaKernel(kernel)
|
| 346 |
+
elif device_type == "xpu":
|
| 347 |
+
return StaticallyLaunchedXpuKernel(kernel)
|
| 348 |
+
else:
|
| 349 |
+
raise NotImplementedError(
|
| 350 |
+
f"Device type {device_type} is not supported for static launcher"
|
| 351 |
+
)
|
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/triton_compat.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import inspect
|
| 4 |
+
from typing import Any
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
try:
|
| 10 |
+
import triton
|
| 11 |
+
except ImportError:
|
| 12 |
+
triton = None
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
if triton is not None:
|
| 16 |
+
import triton.language as tl
|
| 17 |
+
from triton import Config
|
| 18 |
+
from triton.compiler import CompiledKernel
|
| 19 |
+
from triton.runtime.autotuner import OutOfResources
|
| 20 |
+
from triton.runtime.jit import JITFunction, KernelInterface
|
| 21 |
+
|
| 22 |
+
try:
|
| 23 |
+
from triton.runtime.autotuner import PTXASError
|
| 24 |
+
except ImportError:
|
| 25 |
+
|
| 26 |
+
class PTXASError(Exception): # type: ignore[no-redef]
|
| 27 |
+
pass
|
| 28 |
+
|
| 29 |
+
try:
|
| 30 |
+
from triton.compiler.compiler import ASTSource
|
| 31 |
+
except ImportError:
|
| 32 |
+
ASTSource = None
|
| 33 |
+
|
| 34 |
+
try:
|
| 35 |
+
from triton.backends.compiler import GPUTarget
|
| 36 |
+
except ImportError:
|
| 37 |
+
|
| 38 |
+
def GPUTarget(
|
| 39 |
+
backend: str,
|
| 40 |
+
arch: int | str,
|
| 41 |
+
warp_size: int,
|
| 42 |
+
) -> Any:
|
| 43 |
+
if torch.version.hip:
|
| 44 |
+
return [backend, arch, warp_size]
|
| 45 |
+
return (backend, arch)
|
| 46 |
+
|
| 47 |
+
# In the latest triton, math functions were shuffled around into different modules:
|
| 48 |
+
# https://github.com/triton-lang/triton/pull/3172
|
| 49 |
+
try:
|
| 50 |
+
from triton.language.extra import libdevice
|
| 51 |
+
|
| 52 |
+
libdevice = tl.extra.libdevice # noqa: F811
|
| 53 |
+
math = tl.math
|
| 54 |
+
except ImportError:
|
| 55 |
+
if hasattr(tl.extra, "cuda") and hasattr(tl.extra.cuda, "libdevice"):
|
| 56 |
+
libdevice = tl.extra.cuda.libdevice
|
| 57 |
+
math = tl.math
|
| 58 |
+
elif hasattr(tl.extra, "intel") and hasattr(tl.extra.intel, "libdevice"):
|
| 59 |
+
libdevice = tl.extra.intel.libdevice
|
| 60 |
+
math = tl.math
|
| 61 |
+
else:
|
| 62 |
+
libdevice = tl.math
|
| 63 |
+
math = tl
|
| 64 |
+
|
| 65 |
+
try:
|
| 66 |
+
from triton.language.standard import _log2
|
| 67 |
+
except ImportError:
|
| 68 |
+
|
| 69 |
+
def _log2(x: Any) -> Any:
|
| 70 |
+
raise NotImplementedError
|
| 71 |
+
|
| 72 |
+
def _triton_config_has(param_name: str) -> bool:
|
| 73 |
+
if not hasattr(triton, "Config"):
|
| 74 |
+
return False
|
| 75 |
+
if not hasattr(triton.Config, "__init__"):
|
| 76 |
+
return False
|
| 77 |
+
return param_name in inspect.signature(triton.Config.__init__).parameters
|
| 78 |
+
|
| 79 |
+
# Drop the legacy support of autoWS
|
| 80 |
+
HAS_WARP_SPEC = False
|
| 81 |
+
|
| 82 |
+
try:
|
| 83 |
+
from triton import knobs
|
| 84 |
+
except ImportError:
|
| 85 |
+
knobs = None
|
| 86 |
+
|
| 87 |
+
try:
|
| 88 |
+
from triton.runtime.cache import triton_key # type: ignore[attr-defined]
|
| 89 |
+
except ImportError:
|
| 90 |
+
from triton.compiler.compiler import (
|
| 91 |
+
triton_key, # type: ignore[attr-defined,no-redef]
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
try:
|
| 95 |
+
from triton.runtime.errors import IntelGPUError
|
| 96 |
+
except ImportError:
|
| 97 |
+
|
| 98 |
+
class IntelGPUError(Exception): # type: ignore[no-redef]
|
| 99 |
+
pass
|
| 100 |
+
|
| 101 |
+
builtins_use_semantic_kwarg = (
|
| 102 |
+
"_semantic" in inspect.signature(triton.language.core.view).parameters
|
| 103 |
+
)
|
| 104 |
+
HAS_TRITON = True
|
| 105 |
+
else:
|
| 106 |
+
|
| 107 |
+
def _raise_error(*args: Any, **kwargs: Any) -> Any:
|
| 108 |
+
raise RuntimeError("triton package is not installed")
|
| 109 |
+
|
| 110 |
+
class OutOfResources(Exception): # type: ignore[no-redef]
|
| 111 |
+
pass
|
| 112 |
+
|
| 113 |
+
class PTXASError(Exception): # type: ignore[no-redef]
|
| 114 |
+
pass
|
| 115 |
+
|
| 116 |
+
class IntelGPUError(Exception): # type: ignore[no-redef]
|
| 117 |
+
pass
|
| 118 |
+
|
| 119 |
+
Config = object
|
| 120 |
+
CompiledKernel = object
|
| 121 |
+
KernelInterface = object
|
| 122 |
+
ASTSource = None
|
| 123 |
+
GPUTarget = None
|
| 124 |
+
_log2 = _raise_error
|
| 125 |
+
libdevice = None
|
| 126 |
+
math = None
|
| 127 |
+
knobs = None
|
| 128 |
+
builtins_use_semantic_kwarg = False
|
| 129 |
+
|
| 130 |
+
class triton: # type: ignore[no-redef]
|
| 131 |
+
@staticmethod
|
| 132 |
+
def jit(*args: Any, **kwargs: Any) -> Any:
|
| 133 |
+
return _raise_error
|
| 134 |
+
|
| 135 |
+
class tl: # type: ignore[no-redef]
|
| 136 |
+
@staticmethod
|
| 137 |
+
def constexpr(val: Any) -> Any:
|
| 138 |
+
return val
|
| 139 |
+
|
| 140 |
+
tensor = Any
|
| 141 |
+
dtype = Any
|
| 142 |
+
|
| 143 |
+
class JITFunction: # type: ignore[no-redef]
|
| 144 |
+
pass
|
| 145 |
+
|
| 146 |
+
HAS_WARP_SPEC = False
|
| 147 |
+
triton_key = _raise_error
|
| 148 |
+
HAS_TRITON = False
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def cc_warp_size(cc: str | int) -> int:
|
| 152 |
+
if torch.version.hip:
|
| 153 |
+
if "gfx9" in str(cc):
|
| 154 |
+
return 64
|
| 155 |
+
else:
|
| 156 |
+
return 32
|
| 157 |
+
else:
|
| 158 |
+
return 32
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
try:
|
| 162 |
+
autograd_profiler = torch.autograd.profiler
|
| 163 |
+
except AttributeError: # Compile workers only have a mock version of torch
|
| 164 |
+
|
| 165 |
+
class autograd_profiler: # type: ignore[no-redef]
|
| 166 |
+
_is_profiler_enabled = False
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
__all__ = [
|
| 170 |
+
"Config",
|
| 171 |
+
"CompiledKernel",
|
| 172 |
+
"OutOfResources",
|
| 173 |
+
"KernelInterface",
|
| 174 |
+
"PTXASError",
|
| 175 |
+
"IntelGPUError",
|
| 176 |
+
"ASTSource",
|
| 177 |
+
"GPUTarget",
|
| 178 |
+
"tl",
|
| 179 |
+
"_log2",
|
| 180 |
+
"libdevice",
|
| 181 |
+
"math",
|
| 182 |
+
"triton",
|
| 183 |
+
"cc_warp_size",
|
| 184 |
+
"knobs",
|
| 185 |
+
"triton_key",
|
| 186 |
+
]
|
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/triton_helpers.py
ADDED
|
@@ -0,0 +1,850 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# mypy: allow-untyped-decorators
|
| 2 |
+
# mypy: allow-untyped-defs
|
| 3 |
+
import math as pymath
|
| 4 |
+
import warnings
|
| 5 |
+
from collections.abc import Callable
|
| 6 |
+
from typing import Any, TypeVar
|
| 7 |
+
|
| 8 |
+
from .triton_compat import ( # noqa: F401
|
| 9 |
+
_log2,
|
| 10 |
+
builtins_use_semantic_kwarg,
|
| 11 |
+
JITFunction,
|
| 12 |
+
libdevice,
|
| 13 |
+
math,
|
| 14 |
+
tl,
|
| 15 |
+
triton,
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
_T = TypeVar("_T")
|
| 20 |
+
_LOG_2_E: tl.constexpr = tl.constexpr(pymath.log2(pymath.e))
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def set_driver_to_cpu():
|
| 24 |
+
driver = triton.runtime.driver
|
| 25 |
+
if backend := triton.backends.backends.get("cpu", None):
|
| 26 |
+
if isinstance(driver.active, backend.driver):
|
| 27 |
+
# Don't re-initialize backend if it is already active
|
| 28 |
+
return
|
| 29 |
+
driver.set_active(backend.driver())
|
| 30 |
+
return
|
| 31 |
+
# This can be a hard error once triton-cpu is merged into fbcode
|
| 32 |
+
warnings.warn(
|
| 33 |
+
"Could not find an active CPU backend. Generated kernels will not be executable!"
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def _is_backend_active(name, backend):
|
| 38 |
+
if backend.driver.is_active():
|
| 39 |
+
return True
|
| 40 |
+
# Triton may fail to detect the GPU in subprocess workers when using
|
| 41 |
+
# ctypes-based driver detection (triton-lang/triton#9578). Fall back
|
| 42 |
+
# to torch's own device checks which are more reliable in these environments.
|
| 43 |
+
if name == "nvidia":
|
| 44 |
+
import torch
|
| 45 |
+
|
| 46 |
+
return torch.cuda.is_available() and torch.version.hip is None
|
| 47 |
+
if name == "amd":
|
| 48 |
+
import torch
|
| 49 |
+
|
| 50 |
+
return torch.cuda.is_available() and torch.version.hip is not None
|
| 51 |
+
return False
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def set_driver_to_gpu():
|
| 55 |
+
driver = triton.runtime.driver
|
| 56 |
+
for name, backend in triton.backends.backends.items():
|
| 57 |
+
if _is_backend_active(name, backend) and name != "cpu":
|
| 58 |
+
# After https://github.com/triton-lang/triton/commit/b844d519bc5e86edf00fe6b3c6c2d1badcd509a4,
|
| 59 |
+
# `driver.active` can be of `LazyProxy` type and the sign of this - `_obj` attribute.
|
| 60 |
+
if (
|
| 61 |
+
isinstance(driver.active, backend.driver)
|
| 62 |
+
or hasattr(driver.active, "_obj")
|
| 63 |
+
and isinstance(driver.active._obj, backend.driver)
|
| 64 |
+
):
|
| 65 |
+
# Don't re-initialize backend if it is already active
|
| 66 |
+
return
|
| 67 |
+
driver.set_active(backend.driver())
|
| 68 |
+
return
|
| 69 |
+
raise RuntimeError("Could not find an active GPU backend")
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def get_backend_options():
|
| 73 |
+
from triton.runtime import driver
|
| 74 |
+
|
| 75 |
+
target = driver.active.get_current_target()
|
| 76 |
+
backend = triton.compiler.compiler.make_backend(target)
|
| 77 |
+
options = backend.parse_options(dict())
|
| 78 |
+
return options.__dict__
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def get_constexprs(kernel: JITFunction) -> list[int]:
|
| 82 |
+
return [p.num for p in kernel.params if p.is_constexpr]
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
@triton.jit
|
| 86 |
+
def promote_to_tensor(x):
|
| 87 |
+
# Addition promotes to tensor for us
|
| 88 |
+
return x + tl.zeros((1,), tl.int1)
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
@triton.jit
|
| 92 |
+
def div_floor_integer(a, b):
|
| 93 |
+
# NOTE: a // b is C division, but we want floor division
|
| 94 |
+
# Based on c10::div_floor_integer
|
| 95 |
+
quot = a // b
|
| 96 |
+
remainder = a % b
|
| 97 |
+
fixed = tl.where(remainder != 0, quot - 1, quot)
|
| 98 |
+
return tl.where((a < 0) != (b < 0), fixed, quot)
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
@triton.jit
|
| 102 |
+
def remainder_integer(a, b):
|
| 103 |
+
# NOTE: a % b matches C division, not floor division
|
| 104 |
+
remainder = a % b
|
| 105 |
+
return tl.where((remainder != 0) & ((a < 0) != (b < 0)), remainder + b, remainder)
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
@triton.jit
|
| 109 |
+
def pow_integer(base, exponent):
|
| 110 |
+
# Triton has no exact integer pow primitive; use repeated squaring for
|
| 111 |
+
# nonnegative integer exponents so integral scalar pow does not round
|
| 112 |
+
# through libdevice.pow before casting back to int.
|
| 113 |
+
exponent_dtype: tl.constexpr = tl.core.get_int_dtype(
|
| 114 |
+
exponent.dtype.primitive_bitwidth, signed=False
|
| 115 |
+
)
|
| 116 |
+
exp = exponent.to(exponent_dtype)
|
| 117 |
+
result = tl.full(base.shape, 1, base.dtype)
|
| 118 |
+
for _ in tl.static_range(exponent_dtype.primitive_bitwidth):
|
| 119 |
+
result = tl.where((exp & 1) != 0, result * base, result)
|
| 120 |
+
exp = exp >> 1
|
| 121 |
+
base = base * base
|
| 122 |
+
return result
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
@triton.jit
|
| 126 |
+
def is_floating(x):
|
| 127 |
+
return promote_to_tensor(x).dtype.is_floating()
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
@triton.jit
|
| 131 |
+
def _prod_accumulate(a, b):
|
| 132 |
+
return a * b
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
@triton.jit
|
| 136 |
+
def prod(input, axis):
|
| 137 |
+
return tl.reduce(input, axis, _prod_accumulate)
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
@triton.jit
|
| 141 |
+
def minimum(a, b):
|
| 142 |
+
mask = a < b
|
| 143 |
+
if is_floating(a):
|
| 144 |
+
mask |= a != a
|
| 145 |
+
return tl.where(mask, a, b)
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
@triton.jit
|
| 149 |
+
def maximum(a, b):
|
| 150 |
+
mask = a > b
|
| 151 |
+
if is_floating(a):
|
| 152 |
+
mask |= a != a
|
| 153 |
+
return tl.where(mask, a, b)
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
@triton.jit
|
| 157 |
+
def min2(a, dim):
|
| 158 |
+
return tl.reduce(a, dim, minimum)
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
@triton.jit
|
| 162 |
+
def max2(a, dim):
|
| 163 |
+
return tl.reduce(a, dim, maximum)
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
@triton.jit
|
| 167 |
+
def minimum_with_index(a_value, a_index, b_value, b_index):
|
| 168 |
+
mask = a_value < b_value
|
| 169 |
+
equal = a_value == b_value
|
| 170 |
+
if is_floating(a_value):
|
| 171 |
+
a_isnan = a_value != a_value
|
| 172 |
+
b_isnan = b_value != b_value
|
| 173 |
+
mask |= a_isnan & (not b_isnan)
|
| 174 |
+
# Consider NaNs as equal
|
| 175 |
+
equal |= a_isnan & b_isnan
|
| 176 |
+
|
| 177 |
+
# Prefer lowest index if values are equal
|
| 178 |
+
mask |= equal & (a_index < b_index)
|
| 179 |
+
return tl.where(mask, a_value, b_value), tl.where(mask, a_index, b_index)
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
@triton.jit
|
| 183 |
+
def maximum_with_index(a_value, a_index, b_value, b_index):
|
| 184 |
+
mask = a_value > b_value
|
| 185 |
+
equal = a_value == b_value
|
| 186 |
+
if is_floating(a_value):
|
| 187 |
+
a_isnan = a_value != a_value
|
| 188 |
+
b_isnan = b_value != b_value
|
| 189 |
+
mask |= a_isnan & (not b_isnan)
|
| 190 |
+
# Consider NaNs as equal
|
| 191 |
+
equal |= a_isnan & b_isnan
|
| 192 |
+
|
| 193 |
+
# Prefer lowest index if values are equal
|
| 194 |
+
mask |= equal & (a_index < b_index)
|
| 195 |
+
return tl.where(mask, a_value, b_value), tl.where(mask, a_index, b_index)
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
@triton.jit
|
| 199 |
+
def min_with_index(value, index, dim):
|
| 200 |
+
return tl.reduce((value, index), dim, minimum_with_index)
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
@triton.jit
|
| 204 |
+
def max_with_index(value, index, dim):
|
| 205 |
+
return tl.reduce((value, index), dim, maximum_with_index)
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
@triton.jit
|
| 209 |
+
def exp(x, use_fast_math: tl.constexpr):
|
| 210 |
+
if use_fast_math:
|
| 211 |
+
return math.exp(x)
|
| 212 |
+
else:
|
| 213 |
+
return libdevice.exp(x)
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
@triton.jit
|
| 217 |
+
def online_softmax_reduce(lhs_max, lhs_sum, dim, use_fast_math: tl.constexpr):
|
| 218 |
+
out_max = max2(lhs_max, dim)
|
| 219 |
+
out_max_keepdim = tl.expand_dims(out_max, dim)
|
| 220 |
+
delta = tl.where(out_max_keepdim == float("-inf"), 0, lhs_max - out_max_keepdim)
|
| 221 |
+
out_sum = tl.sum(lhs_sum * exp(delta, use_fast_math), dim)
|
| 222 |
+
return out_max, out_sum
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
@triton.jit
|
| 226 |
+
def online_softmax_combine(lhs_max, lhs_sum, rhs_max, use_fast_math: tl.constexpr):
|
| 227 |
+
"""
|
| 228 |
+
When we do combine, we assume lhs is the accumulator and rhs is the next
|
| 229 |
+
block of data.
|
| 230 |
+
Then rhs_sum is always 1. With that assumption, we can save some registers
|
| 231 |
+
and computation.
|
| 232 |
+
"""
|
| 233 |
+
out_max = maximum(lhs_max, rhs_max)
|
| 234 |
+
|
| 235 |
+
lhs_scale = tl.where(
|
| 236 |
+
out_max == float("-inf"), 1.0, exp(lhs_max - out_max, use_fast_math)
|
| 237 |
+
)
|
| 238 |
+
rhs_scale = tl.where(
|
| 239 |
+
out_max == float("-inf"), 1.0, exp(rhs_max - out_max, use_fast_math)
|
| 240 |
+
)
|
| 241 |
+
|
| 242 |
+
# Should be
|
| 243 |
+
# out_sum = lhs_sum * lhs_scale + rhs_sum * rhs_scale
|
| 244 |
+
# but since rhs_sum is all 1, we can simplify it.
|
| 245 |
+
out_sum = lhs_sum * lhs_scale + rhs_scale
|
| 246 |
+
return out_max, out_sum
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
@triton.jit
|
| 250 |
+
def welford_reduce(value, mean, m2, weight, first_iteration):
|
| 251 |
+
if first_iteration:
|
| 252 |
+
new_weight = tl.full(weight.shape, 1, weight.dtype)
|
| 253 |
+
new_mean = value
|
| 254 |
+
new_m2 = tl.zeros_like(m2)
|
| 255 |
+
else:
|
| 256 |
+
delta = value - mean
|
| 257 |
+
new_weight = weight + 1
|
| 258 |
+
new_mean = mean + delta / new_weight
|
| 259 |
+
new_m2 = m2 + delta * (value - new_mean)
|
| 260 |
+
return new_mean, new_m2, new_weight
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
@triton.jit
|
| 264 |
+
def welford_combine(mean_1, m2_1, weight_1, mean_2, m2_2, weight_2):
|
| 265 |
+
delta = mean_2 - mean_1
|
| 266 |
+
new_weight = weight_1 + weight_2
|
| 267 |
+
w2_over_w = tl.where(new_weight == 0.0, 0.0, weight_2 / new_weight)
|
| 268 |
+
return (
|
| 269 |
+
mean_1 + delta * w2_over_w,
|
| 270 |
+
m2_1 + m2_2 + delta * delta * weight_1 * w2_over_w,
|
| 271 |
+
new_weight,
|
| 272 |
+
)
|
| 273 |
+
|
| 274 |
+
|
| 275 |
+
@triton.jit
|
| 276 |
+
def welford(mean, m2, weight, dim):
|
| 277 |
+
return tl.reduce((mean, m2, weight), dim, welford_combine)
|
| 278 |
+
|
| 279 |
+
|
| 280 |
+
@triton.jit
|
| 281 |
+
def device_assert_then(cond, msg, r):
|
| 282 |
+
tl.device_assert(cond, msg)
|
| 283 |
+
return r
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
@triton.jit
|
| 287 |
+
def rand_eager_kernel(seed, offset_blocks, tid: tl.tensor, VEC: tl.constexpr):
|
| 288 |
+
inv = 1.0 / 4294967296.0
|
| 289 |
+
half = inv * 0.5
|
| 290 |
+
|
| 291 |
+
tid_u64 = tid.to(tl.uint64)
|
| 292 |
+
|
| 293 |
+
subseq = tid_u64 // VEC
|
| 294 |
+
which4 = (tid_u64 % VEC) // 4
|
| 295 |
+
lane = tid_u64 % 4
|
| 296 |
+
|
| 297 |
+
offblk = offset_blocks.to(tl.uint64) + which4
|
| 298 |
+
|
| 299 |
+
u0, u1, u2, u3 = tl.philox(
|
| 300 |
+
seed,
|
| 301 |
+
(offblk & 0xFFFFFFFF).to(tl.uint32),
|
| 302 |
+
((offblk >> 32) & 0xFFFFFFFF).to(tl.uint32),
|
| 303 |
+
(subseq & 0xFFFFFFFF).to(tl.uint32),
|
| 304 |
+
((subseq >> 32) & 0xFFFFFFFF).to(tl.uint32),
|
| 305 |
+
)
|
| 306 |
+
|
| 307 |
+
v01 = tl.where(lane == 0, u0, u1)
|
| 308 |
+
v23 = tl.where(lane == 2, u2, u3)
|
| 309 |
+
rand_int = tl.where((lane == 0) | (lane == 1), v01, v23)
|
| 310 |
+
|
| 311 |
+
return 1.0 - (rand_int.to(tl.float32) * inv + half)
|
| 312 |
+
|
| 313 |
+
|
| 314 |
+
@triton.jit
|
| 315 |
+
def randint64(seed, offset, low, high):
|
| 316 |
+
r0, r1, _r2, _r3 = tl.randint4x(seed, offset)
|
| 317 |
+
r0 = r0.to(tl.uint64)
|
| 318 |
+
r1 = r1.to(tl.uint64)
|
| 319 |
+
result = r0 | (r1 << 32)
|
| 320 |
+
size = high - low
|
| 321 |
+
result = result % size.to(tl.uint64)
|
| 322 |
+
result = result.to(tl.int64) + low
|
| 323 |
+
return result
|
| 324 |
+
|
| 325 |
+
|
| 326 |
+
@triton.jit
|
| 327 |
+
def _any_combine(a, b):
|
| 328 |
+
return a | b
|
| 329 |
+
|
| 330 |
+
|
| 331 |
+
@triton.jit
|
| 332 |
+
def any(a, dim):
|
| 333 |
+
return tl.reduce(a, dim, _any_combine)
|
| 334 |
+
|
| 335 |
+
|
| 336 |
+
@triton.jit
|
| 337 |
+
def bucketize_binary_search(
|
| 338 |
+
values: tl.tensor,
|
| 339 |
+
boundaries_ptr: tl.tensor,
|
| 340 |
+
BOUNDARIES_SIZE: int,
|
| 341 |
+
BOUNDARIES_UNDERLYING_NUMEL: int,
|
| 342 |
+
BOUNDARIES_STRIDE: int,
|
| 343 |
+
boundary_indices: tl.tensor,
|
| 344 |
+
indexing_dtype: tl.dtype,
|
| 345 |
+
right: "bool", # triton can't handle the unquoted bool annotation
|
| 346 |
+
sorter_ptr: tl.tensor,
|
| 347 |
+
SORTER_STRIDE: int,
|
| 348 |
+
sorter_indices: tl.tensor,
|
| 349 |
+
):
|
| 350 |
+
"""
|
| 351 |
+
See [Note: Inductor bucketize op]
|
| 352 |
+
|
| 353 |
+
Inputs:
|
| 354 |
+
-------
|
| 355 |
+
values: the values to bucketize.
|
| 356 |
+
boundaries_ptr: a pointer to the beginning of the boundaries tensor, in 1-D.
|
| 357 |
+
BOUNDARIES_SIZE: the length of the last dimension of the boundaries tensor (i.e. one
|
| 358 |
+
individual set of boundaries).
|
| 359 |
+
BOUNDARIES_UNDERLYING_NUMEL: the length of the boundaries tensor, in 1-D, ignoring
|
| 360 |
+
any striding.
|
| 361 |
+
BOUNDARIES_STRIDE: the stride of the last dimension of the boundaries tensor
|
| 362 |
+
boundary_indices: a tensor of the same size as "values"; each element is an index
|
| 363 |
+
into a 1-D, un-strided boundaries tensor, pointing to the first element in the set
|
| 364 |
+
of boundaries used for that value.
|
| 365 |
+
indexing_dtype: the dtype used for indexing into the boundaries tensor, and the
|
| 366 |
+
return dtype.
|
| 367 |
+
right: if true, use boundary intervals closed on the left; otherwise use intervals
|
| 368 |
+
closed on the right.
|
| 369 |
+
sorter_ptr: an optional pointer to a sorter tensor of the same shape as boundaries,
|
| 370 |
+
but potentially different striding. If present, this allows us to treat boundaries
|
| 371 |
+
as sorted even if the elements of boundaries are unsorted.
|
| 372 |
+
SORTER_STRIDE: must be present if sorter_ptr is non-None; the stride of the last
|
| 373 |
+
dimension of the sorter tensor.
|
| 374 |
+
sorter_indices: must be present if sorter_ptr is non-None; see "boundary_indices".
|
| 375 |
+
BLOCK_SHAPE: the shape of the data block being processed.
|
| 376 |
+
"""
|
| 377 |
+
|
| 378 |
+
low = tl.zeros(values.shape, dtype=indexing_dtype)
|
| 379 |
+
high = tl.full(values.shape, BOUNDARIES_SIZE, dtype=indexing_dtype)
|
| 380 |
+
|
| 381 |
+
full_range = BOUNDARIES_SIZE + 1
|
| 382 |
+
while full_range > 1:
|
| 383 |
+
mid = (high + low) // 2
|
| 384 |
+
mask = (
|
| 385 |
+
(mid * BOUNDARIES_STRIDE + boundary_indices) < BOUNDARIES_UNDERLYING_NUMEL
|
| 386 |
+
).logical_and(mid < BOUNDARIES_SIZE)
|
| 387 |
+
mid_indices = (
|
| 388 |
+
mid
|
| 389 |
+
if sorter_ptr is None or SORTER_STRIDE is None
|
| 390 |
+
else tl.load(
|
| 391 |
+
sorter_ptr + sorter_indices + SORTER_STRIDE * mid,
|
| 392 |
+
mask=mask,
|
| 393 |
+
other=0,
|
| 394 |
+
)
|
| 395 |
+
)
|
| 396 |
+
|
| 397 |
+
bucket_upper_bound = tl.load(
|
| 398 |
+
boundaries_ptr + boundary_indices + BOUNDARIES_STRIDE * mid_indices,
|
| 399 |
+
mask=mask,
|
| 400 |
+
other=0,
|
| 401 |
+
)
|
| 402 |
+
if right:
|
| 403 |
+
is_above = values >= bucket_upper_bound
|
| 404 |
+
else:
|
| 405 |
+
is_above = values > bucket_upper_bound
|
| 406 |
+
|
| 407 |
+
if is_floating(values):
|
| 408 |
+
is_above = is_above | (values != values)
|
| 409 |
+
|
| 410 |
+
low = tl.where(is_above & mask, mid + 1, low)
|
| 411 |
+
high = tl.where(is_above, high, mid)
|
| 412 |
+
|
| 413 |
+
full_range = (full_range + 1) // 2
|
| 414 |
+
|
| 415 |
+
return low
|
| 416 |
+
|
| 417 |
+
|
| 418 |
+
@triton.jit
|
| 419 |
+
def pack_value_flag(
|
| 420 |
+
value,
|
| 421 |
+
flag,
|
| 422 |
+
DTYPE_VALUE_AS_UINT: tl.constexpr,
|
| 423 |
+
DTYPE_PACK: tl.constexpr,
|
| 424 |
+
):
|
| 425 |
+
# Workaround for triton bug, tensor.to doesn't unwrap constexpr values
|
| 426 |
+
DTYPE_VALUE_AS_UINT = tl.core._unwrap_if_constexpr(DTYPE_VALUE_AS_UINT)
|
| 427 |
+
bitwidth = DTYPE_VALUE_AS_UINT.primitive_bitwidth
|
| 428 |
+
uv = value.to(DTYPE_VALUE_AS_UINT, bitcast=True).to(DTYPE_PACK)
|
| 429 |
+
return flag.to(DTYPE_PACK) | (uv << bitwidth)
|
| 430 |
+
|
| 431 |
+
|
| 432 |
+
@triton.jit
|
| 433 |
+
def unpack_value(
|
| 434 |
+
pack,
|
| 435 |
+
DTYPE_VALUE,
|
| 436 |
+
DTYPE_VALUE_AS_UINT,
|
| 437 |
+
):
|
| 438 |
+
# Workaround for triton bug, tensor.to doesn't unwrap constexpr values
|
| 439 |
+
DTYPE_VALUE = tl.core._unwrap_if_constexpr(DTYPE_VALUE)
|
| 440 |
+
DTYPE_VALUE_AS_UINT = tl.core._unwrap_if_constexpr(DTYPE_VALUE_AS_UINT)
|
| 441 |
+
bitwidth = DTYPE_VALUE_AS_UINT.primitive_bitwidth
|
| 442 |
+
value_uint = (pack >> bitwidth).to(DTYPE_VALUE_AS_UINT)
|
| 443 |
+
return value_uint.to(DTYPE_VALUE, bitcast=True)
|
| 444 |
+
|
| 445 |
+
|
| 446 |
+
@triton.jit
|
| 447 |
+
def unpack_flag(pack, DTYPE_FLAG):
|
| 448 |
+
return pack.to(DTYPE_FLAG)
|
| 449 |
+
|
| 450 |
+
|
| 451 |
+
@triton.jit
|
| 452 |
+
def exclusive_scan_decoupled_lookback(
|
| 453 |
+
scratch_base,
|
| 454 |
+
block_value,
|
| 455 |
+
index,
|
| 456 |
+
combine_fn,
|
| 457 |
+
DTYPE_VALUE_AS_UINT: tl.constexpr,
|
| 458 |
+
DTYPE_PACK: tl.constexpr,
|
| 459 |
+
):
|
| 460 |
+
"""Compute exclusive scan of a scalar value between blocks
|
| 461 |
+
|
| 462 |
+
Ref: https://research.nvidia.com/publication/2016-03_single-pass-parallel-prefix-scan-decoupled-look-back
|
| 463 |
+
|
| 464 |
+
scratch_base: Pointer to scratch space in global memory
|
| 465 |
+
block_value: Scalar value for this block
|
| 466 |
+
index: Scalar index of this block relative to the current scan
|
| 467 |
+
combine_fn: Function ``(value, value) -> value`` which is scanned over
|
| 468 |
+
DTYPE_VALUE_AS_UINT: A tl.uint{n} type equal in size to ``block_value``
|
| 469 |
+
DTYPE_PACK: Unsigned type twice the width of block_value
|
| 470 |
+
|
| 471 |
+
NOTE: This function is limited to values which are 32-bits or less because
|
| 472 |
+
we need to pack (value, flag) into a single unsigned int.
|
| 473 |
+
"""
|
| 474 |
+
# Publish block sum so subsequent blocks don't get stuck waiting for us
|
| 475 |
+
DTYPE_VALUE = block_value.dtype
|
| 476 |
+
pack = pack_value_flag(
|
| 477 |
+
block_value,
|
| 478 |
+
tl.full(block_value.shape, 1, DTYPE_VALUE_AS_UINT),
|
| 479 |
+
DTYPE_VALUE_AS_UINT,
|
| 480 |
+
DTYPE_PACK,
|
| 481 |
+
)
|
| 482 |
+
if index > 0:
|
| 483 |
+
tl.atomic_xchg(scratch_base + index, pack, sem="relaxed")
|
| 484 |
+
|
| 485 |
+
# Calculate exclusive prefix scan
|
| 486 |
+
exclusive_prefix = tl.zeros([], DTYPE_VALUE)
|
| 487 |
+
prefix_valid = False
|
| 488 |
+
test_target = index - 1
|
| 489 |
+
while test_target >= 0:
|
| 490 |
+
# tl.atomic_load
|
| 491 |
+
flag = tl.full([], 0, DTYPE_VALUE_AS_UINT)
|
| 492 |
+
while flag == 0:
|
| 493 |
+
pack = tl.atomic_add(scratch_base + test_target, 0, sem="relaxed")
|
| 494 |
+
flag = unpack_flag(pack, DTYPE_VALUE_AS_UINT)
|
| 495 |
+
|
| 496 |
+
value = unpack_value(pack, DTYPE_VALUE, DTYPE_VALUE_AS_UINT)
|
| 497 |
+
if prefix_valid:
|
| 498 |
+
exclusive_prefix = combine_fn(value, exclusive_prefix)
|
| 499 |
+
else:
|
| 500 |
+
exclusive_prefix = value
|
| 501 |
+
prefix_valid = True
|
| 502 |
+
|
| 503 |
+
if flag == 2:
|
| 504 |
+
test_target = -1
|
| 505 |
+
else:
|
| 506 |
+
test_target = test_target - 1
|
| 507 |
+
|
| 508 |
+
# Make inclusive block sum visible to other blocks
|
| 509 |
+
if prefix_valid:
|
| 510 |
+
inclusive_prefix = combine_fn(exclusive_prefix, block_value)
|
| 511 |
+
else:
|
| 512 |
+
inclusive_prefix = block_value
|
| 513 |
+
pack = pack_value_flag(
|
| 514 |
+
inclusive_prefix,
|
| 515 |
+
tl.full([], 2, DTYPE_VALUE_AS_UINT),
|
| 516 |
+
DTYPE_VALUE_AS_UINT,
|
| 517 |
+
DTYPE_PACK,
|
| 518 |
+
)
|
| 519 |
+
tl.atomic_xchg(scratch_base + index, pack, sem="relaxed")
|
| 520 |
+
return exclusive_prefix
|
| 521 |
+
|
| 522 |
+
|
| 523 |
+
@triton.jit
|
| 524 |
+
def exclusive_scan_decoupled_lookback_64(scratch_base, block_value, index, combine_fn):
|
| 525 |
+
"""Compute exclusive scan of a scalar value between blocks
|
| 526 |
+
|
| 527 |
+
Ref: https://research.nvidia.com/publication/2016-03_single-pass-parallel-prefix-scan-decoupled-look-back
|
| 528 |
+
|
| 529 |
+
scratch_base: Pointer to scratch space in global memory
|
| 530 |
+
block_value: Scalar value for this block, must be 64-bits wide
|
| 531 |
+
index: Scalar index of this block relative to the current scan
|
| 532 |
+
combine_fn: Function ``(value, value) -> value`` which is scanned over
|
| 533 |
+
init: Scalar value equal to the identity of combine_fn
|
| 534 |
+
"""
|
| 535 |
+
# Publish block sum so subsequent blocks don't get stuck waiting for us
|
| 536 |
+
if index > 0:
|
| 537 |
+
block_value_u64 = block_value.to(tl.uint64, bitcast=True)
|
| 538 |
+
tl.store(scratch_base + 3 * index + 1, block_value_u64)
|
| 539 |
+
tl.debug_barrier()
|
| 540 |
+
flag_one = tl.full([], 1, tl.uint64)
|
| 541 |
+
tl.atomic_xchg(scratch_base + 3 * index + 0, flag_one, sem="release")
|
| 542 |
+
|
| 543 |
+
# Calculate exclusive prefix scan
|
| 544 |
+
exclusive_prefix = tl.zeros([], block_value.dtype)
|
| 545 |
+
prefix_valid = False
|
| 546 |
+
test_target = index - 1
|
| 547 |
+
while test_target >= 0:
|
| 548 |
+
flag = tl.full([], 0, tl.uint64)
|
| 549 |
+
while flag == 0:
|
| 550 |
+
flag = tl.atomic_add(scratch_base + 3 * test_target + 0, 0, sem="acquire")
|
| 551 |
+
|
| 552 |
+
value_u64 = tl.load(scratch_base + 3 * test_target + flag.to(tl.int32))
|
| 553 |
+
value = value_u64.to(block_value.dtype, bitcast=True)
|
| 554 |
+
if prefix_valid:
|
| 555 |
+
exclusive_prefix = combine_fn(value, exclusive_prefix)
|
| 556 |
+
else:
|
| 557 |
+
exclusive_prefix = value
|
| 558 |
+
prefix_valid = True
|
| 559 |
+
|
| 560 |
+
if flag == 2:
|
| 561 |
+
test_target = tl.full([], -1, index.dtype) # Match the original type
|
| 562 |
+
else:
|
| 563 |
+
test_target = test_target - 1
|
| 564 |
+
|
| 565 |
+
# Make inclusive block sum visible to other blocks
|
| 566 |
+
if prefix_valid:
|
| 567 |
+
inclusive_prefix = combine_fn(exclusive_prefix, block_value)
|
| 568 |
+
else:
|
| 569 |
+
inclusive_prefix = block_value
|
| 570 |
+
inclusive_prefix_u64 = inclusive_prefix.to(tl.uint64, bitcast=True)
|
| 571 |
+
tl.store(scratch_base + 3 * index + 2, inclusive_prefix_u64)
|
| 572 |
+
tl.debug_barrier()
|
| 573 |
+
flag_two = tl.full([], 2, tl.uint64)
|
| 574 |
+
tl.atomic_xchg(scratch_base + 3 * index + 0, flag_two, sem="release")
|
| 575 |
+
|
| 576 |
+
return exclusive_prefix
|
| 577 |
+
|
| 578 |
+
|
| 579 |
+
@triton.jit
|
| 580 |
+
def frexp(x):
|
| 581 |
+
# TODO(isuruf): use inline_asm_elementwise here
|
| 582 |
+
y = libdevice.ilogb(x) + 1
|
| 583 |
+
exponent = tl.where(x == 0, 0, y)
|
| 584 |
+
mantissa = tl.where(x == 0, 0, libdevice.ldexp(x, -y))
|
| 585 |
+
return mantissa, exponent
|
| 586 |
+
|
| 587 |
+
|
| 588 |
+
@triton.jit
|
| 589 |
+
def _compare_and_swap_with_index(
|
| 590 |
+
x,
|
| 591 |
+
idxs,
|
| 592 |
+
rnumel,
|
| 593 |
+
flip,
|
| 594 |
+
i: tl.constexpr,
|
| 595 |
+
n_dims: tl.constexpr,
|
| 596 |
+
stable: tl.constexpr,
|
| 597 |
+
descending: tl.constexpr,
|
| 598 |
+
):
|
| 599 |
+
n_outer: tl.constexpr = x.numel >> n_dims
|
| 600 |
+
shape: tl.constexpr = [n_outer * 2**i, 2, 2 ** (n_dims - i - 1)]
|
| 601 |
+
|
| 602 |
+
idtype = tl.core.get_int_dtype(bitwidth=x.dtype.primitive_bitwidth, signed=True)
|
| 603 |
+
|
| 604 |
+
y = tl.reshape(x, shape)
|
| 605 |
+
iy = y.to(idtype, bitcast=True)
|
| 606 |
+
# slice left/right with 'stride' 2**(n_dims - i - 1)
|
| 607 |
+
right_mask = tl.arange(0, 2)[None, :, None].to(idtype)
|
| 608 |
+
left_mask = (1 - right_mask).to(idtype)
|
| 609 |
+
ileft = tl.broadcast_to(tl.sum(iy * left_mask, 1).to(idtype)[:, None, :], shape)
|
| 610 |
+
iright = tl.broadcast_to(tl.sum(iy * right_mask, 1).to(idtype)[:, None, :], shape)
|
| 611 |
+
ileft = tl.reshape(ileft, x.shape)
|
| 612 |
+
iright = tl.reshape(iright, x.shape)
|
| 613 |
+
left = ileft.to(x.dtype, bitcast=True)
|
| 614 |
+
right = iright.to(x.dtype, bitcast=True)
|
| 615 |
+
|
| 616 |
+
# idx
|
| 617 |
+
y_idx = tl.reshape(idxs, shape)
|
| 618 |
+
left_idx = tl.broadcast_to(
|
| 619 |
+
tl.sum(y_idx * left_mask.to(y_idx.dtype), 1)[:, None, :], shape
|
| 620 |
+
)
|
| 621 |
+
right_idx = tl.broadcast_to(
|
| 622 |
+
tl.sum(y_idx * right_mask.to(y_idx.dtype), 1)[:, None, :], shape
|
| 623 |
+
)
|
| 624 |
+
left_idx = tl.reshape(left_idx, x.shape)
|
| 625 |
+
right_idx = tl.reshape(right_idx, x.shape)
|
| 626 |
+
|
| 627 |
+
# valid
|
| 628 |
+
if rnumel is None:
|
| 629 |
+
left_valid_mask = tl.full(x.shape, True, tl.int1)
|
| 630 |
+
right_valid_mask = tl.full(x.shape, True, tl.int1)
|
| 631 |
+
else:
|
| 632 |
+
left_valid_mask = left_idx < rnumel
|
| 633 |
+
right_valid_mask = right_idx < rnumel
|
| 634 |
+
|
| 635 |
+
# actual compare-and-swap
|
| 636 |
+
ix = x.to(idtype, bitcast=True)
|
| 637 |
+
|
| 638 |
+
# sort treats nan as having the higher value. comparisons with nan always return False.
|
| 639 |
+
# to align with sort semantics, we need to update descending to check if right_isnan,
|
| 640 |
+
# and ascending to check if left_isnan.
|
| 641 |
+
left_isnan = left != left
|
| 642 |
+
right_isnan = right != right
|
| 643 |
+
|
| 644 |
+
if descending:
|
| 645 |
+
cond = left < right
|
| 646 |
+
if is_floating(left):
|
| 647 |
+
if not stable:
|
| 648 |
+
cond = cond | right_isnan
|
| 649 |
+
else:
|
| 650 |
+
cond = cond | (right_isnan & (~left_isnan))
|
| 651 |
+
|
| 652 |
+
else:
|
| 653 |
+
cond = left > right
|
| 654 |
+
if is_floating(left):
|
| 655 |
+
if not stable:
|
| 656 |
+
cond = cond | left_isnan
|
| 657 |
+
else:
|
| 658 |
+
cond = cond | (left_isnan & (~right_isnan))
|
| 659 |
+
|
| 660 |
+
if stable:
|
| 661 |
+
# When stable sorting, tie break by index
|
| 662 |
+
eq = left == right
|
| 663 |
+
if is_floating(left):
|
| 664 |
+
eq = eq | (left_isnan & right_isnan)
|
| 665 |
+
cond = cond | (eq & (left_idx > right_idx))
|
| 666 |
+
|
| 667 |
+
cond = (right_valid_mask > left_valid_mask) | (
|
| 668 |
+
(right_valid_mask == left_valid_mask) & cond
|
| 669 |
+
)
|
| 670 |
+
cond = (cond ^ flip).to(tl.int1)
|
| 671 |
+
ret = ix ^ tl.where(cond, ileft ^ iright, tl.zeros_like(ix))
|
| 672 |
+
new_idxs = idxs ^ tl.where(cond, left_idx ^ right_idx, tl.zeros_like(idxs))
|
| 673 |
+
|
| 674 |
+
return ret.to(x.dtype, bitcast=True), new_idxs
|
| 675 |
+
|
| 676 |
+
|
| 677 |
+
@triton.jit
|
| 678 |
+
def _bitonic_merge_with_index(
|
| 679 |
+
x,
|
| 680 |
+
idxs,
|
| 681 |
+
rnumel,
|
| 682 |
+
stage: tl.constexpr,
|
| 683 |
+
alternating: tl.constexpr,
|
| 684 |
+
n_dims: tl.constexpr,
|
| 685 |
+
stable: tl.constexpr,
|
| 686 |
+
descending: tl.constexpr,
|
| 687 |
+
):
|
| 688 |
+
n_outer: tl.constexpr = x.numel >> n_dims
|
| 689 |
+
tl.static_assert(stage <= n_dims)
|
| 690 |
+
# flip denotes whether to re-arrange sub-sequences of elements in ascending or
|
| 691 |
+
# descending order.
|
| 692 |
+
# if flip = 00000000... then all elements will be re-arranged ascendingly at this stage
|
| 693 |
+
# if flip = 00110011... then all the elements will be re-arranged alternatingly (with
|
| 694 |
+
# a stride of 2) at this stage
|
| 695 |
+
if alternating:
|
| 696 |
+
shape: tl.constexpr = [n_outer * 2 ** (n_dims - 1 - stage), 2, 2**stage]
|
| 697 |
+
flip = tl.reshape(
|
| 698 |
+
tl.broadcast_to(tl.arange(0, 2)[None, :, None], shape), x.shape
|
| 699 |
+
)
|
| 700 |
+
else:
|
| 701 |
+
flip = False
|
| 702 |
+
# perform `stage` rounds of `compare-and-swap`
|
| 703 |
+
for i in tl.static_range(stage):
|
| 704 |
+
x, idxs = _compare_and_swap_with_index(
|
| 705 |
+
x, idxs, rnumel, flip, i + (n_dims - stage), n_dims, stable, descending
|
| 706 |
+
)
|
| 707 |
+
return x, idxs
|
| 708 |
+
|
| 709 |
+
|
| 710 |
+
@triton.jit
|
| 711 |
+
def sort_with_index(
|
| 712 |
+
x, # value
|
| 713 |
+
idxs, # index
|
| 714 |
+
rnumel, # number of elements
|
| 715 |
+
dim: tl.constexpr = None,
|
| 716 |
+
stable: tl.constexpr = tl.constexpr(False),
|
| 717 |
+
descending: tl.constexpr = tl.constexpr(False),
|
| 718 |
+
):
|
| 719 |
+
x, idxs = tl.broadcast(x, idxs)
|
| 720 |
+
# handle default dimension or check that it is the most minor dim
|
| 721 |
+
_dim: tl.constexpr = len(x.shape) - 1 if dim is None else dim
|
| 722 |
+
tl.static_assert(
|
| 723 |
+
_dim == len(x.shape) - 1, "only minor dimension is currently supported"
|
| 724 |
+
)
|
| 725 |
+
# iteratively run bitonic merge-sort steps
|
| 726 |
+
n_dims: tl.constexpr = _log2(x.shape[_dim])
|
| 727 |
+
|
| 728 |
+
for i in tl.static_range(1, n_dims + 1):
|
| 729 |
+
x, idxs = _bitonic_merge_with_index(
|
| 730 |
+
x,
|
| 731 |
+
idxs,
|
| 732 |
+
rnumel,
|
| 733 |
+
i,
|
| 734 |
+
alternating=i < n_dims,
|
| 735 |
+
n_dims=n_dims,
|
| 736 |
+
stable=stable,
|
| 737 |
+
descending=descending,
|
| 738 |
+
)
|
| 739 |
+
return x, idxs
|
| 740 |
+
|
| 741 |
+
|
| 742 |
+
@triton.jit
|
| 743 |
+
def select_one(x, mask, dim, keep_dims=False):
|
| 744 |
+
idtype = tl.core.get_int_dtype(x.dtype.primitive_bitwidth, signed=False)
|
| 745 |
+
ix = x.to(idtype, bitcast=True)
|
| 746 |
+
iy = tl.sum(ix * mask, dim, keep_dims=keep_dims)
|
| 747 |
+
return iy.to(idtype).to(x.dtype, bitcast=True)
|
| 748 |
+
|
| 749 |
+
|
| 750 |
+
@triton.jit
|
| 751 |
+
def x_grid_barrier(sem):
|
| 752 |
+
"""
|
| 753 |
+
Wait for all other thread blocks in grid sharing same y/z program_id
|
| 754 |
+
to reach this barrier before returning.
|
| 755 |
+
|
| 756 |
+
Args:
|
| 757 |
+
sem: an uint32 semaphores, zero or 0x80000000 initialized. Must be unique to each y/z program ID.
|
| 758 |
+
"""
|
| 759 |
+
# ensure stores before this are visible
|
| 760 |
+
tl.debug_barrier()
|
| 761 |
+
|
| 762 |
+
one_i32 = 1
|
| 763 |
+
one_u32 = one_i32.to(tl.uint32) # type: ignore[attr-defined]
|
| 764 |
+
expected = tl.num_programs(0).to(tl.uint32)
|
| 765 |
+
if tl.program_id(0) == 0:
|
| 766 |
+
nb = 0x80000000 - (expected - one_u32)
|
| 767 |
+
else:
|
| 768 |
+
nb = one_u32
|
| 769 |
+
|
| 770 |
+
old_arrive = tl.atomic_add(sem, nb, sem="release")
|
| 771 |
+
|
| 772 |
+
bar_flipped = False
|
| 773 |
+
while not bar_flipped:
|
| 774 |
+
# want a `ld.acquire.gpu.u32 $0,[$1];` but Triton doesn't have it
|
| 775 |
+
current_arrive = tl.atomic_add(sem, 0, sem="acquire")
|
| 776 |
+
# current_arrive = tl.load(sem, volatile=True)
|
| 777 |
+
bar_flipped = ((old_arrive ^ current_arrive) & 0x80000000) != 0
|
| 778 |
+
|
| 779 |
+
# TODO(jansel): is this needed?
|
| 780 |
+
tl.debug_barrier()
|
| 781 |
+
|
| 782 |
+
|
| 783 |
+
def triton_builtin(f: Callable[..., _T]) -> Callable[..., _T]:
|
| 784 |
+
"""
|
| 785 |
+
Decorator to mark a function as a Triton built-in function. These functions
|
| 786 |
+
are evaluated at compile time.
|
| 787 |
+
|
| 788 |
+
Args:
|
| 789 |
+
f (function): The function to be marked as a Triton built-in.
|
| 790 |
+
|
| 791 |
+
Returns:
|
| 792 |
+
function: The same function, marked as a Triton built-in.
|
| 793 |
+
"""
|
| 794 |
+
if builtins_use_semantic_kwarg:
|
| 795 |
+
# support Triton before and after https://github.com/triton-lang/triton/pull/7054
|
| 796 |
+
# and after https://github.com/triton-lang/triton/pull/7239
|
| 797 |
+
def wrapper(*args, _semantic, **kwargs):
|
| 798 |
+
kwargs["_builder"] = _semantic
|
| 799 |
+
return f(*args, **kwargs)
|
| 800 |
+
else:
|
| 801 |
+
wrapper = f # type: ignore[assignment]
|
| 802 |
+
|
| 803 |
+
wrapper.__triton_builtin__ = True # type: ignore[attr-defined]
|
| 804 |
+
return wrapper
|
| 805 |
+
|
| 806 |
+
|
| 807 |
+
@triton_builtin
|
| 808 |
+
def constexpr_next_power_of_2(
|
| 809 |
+
n: tl.constexpr, *, _builder: object = None
|
| 810 |
+
) -> tl.constexpr:
|
| 811 |
+
"""
|
| 812 |
+
A version triton.next_power_of_two that can be used within a kernel on constants.
|
| 813 |
+
"""
|
| 814 |
+
assert isinstance(n, tl.constexpr)
|
| 815 |
+
return tl.constexpr(triton.next_power_of_2(n.value))
|
| 816 |
+
|
| 817 |
+
|
| 818 |
+
@triton_builtin
|
| 819 |
+
def if_mask(mask: Any, val, *, _builder: object = None) -> tl.constexpr:
|
| 820 |
+
"""
|
| 821 |
+
Work around triton compile error: `ValueError: `other` cannot be provided without `mask``
|
| 822 |
+
A compile-time to check to return either `val` or `None` depending on the value of mask.
|
| 823 |
+
"""
|
| 824 |
+
if isinstance(mask, tl.constexpr) and mask.value is None:
|
| 825 |
+
return tl.constexpr(None)
|
| 826 |
+
return val
|
| 827 |
+
|
| 828 |
+
|
| 829 |
+
@triton.jit
|
| 830 |
+
def inline_asm_pack(x, pack: tl.constexpr):
|
| 831 |
+
"""Ravel to 1D and pad (via join with zeros) so numel is divisible by pack."""
|
| 832 |
+
result = x.ravel()
|
| 833 |
+
# Only pad when the block size is smaller than pack. When block >= pack
|
| 834 |
+
# the numel is already divisible by pack (both are powers of 2).
|
| 835 |
+
n_pad: tl.constexpr = _log2(pack) - _log2(result.numel)
|
| 836 |
+
for _ in tl.static_range(n_pad):
|
| 837 |
+
result = tl.reshape(
|
| 838 |
+
tl.join(result, tl.zeros_like(result)), (result.shape[0] * 2,)
|
| 839 |
+
)
|
| 840 |
+
return result
|
| 841 |
+
|
| 842 |
+
|
| 843 |
+
@triton.jit
|
| 844 |
+
def inline_asm_unpack(x, orig, pack: tl.constexpr):
|
| 845 |
+
"""Unpad and reshape back to orig's shape."""
|
| 846 |
+
result = x
|
| 847 |
+
n_pad: tl.constexpr = _log2(pack) - _log2(orig.numel)
|
| 848 |
+
for _ in tl.static_range(n_pad):
|
| 849 |
+
result, _ = tl.split(tl.reshape(result, (result.shape[0] // 2, 2)))
|
| 850 |
+
return tl.reshape(result, orig.shape)
|
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/triton_heuristics.py
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/triton_lazy_compile.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Lazy Triton kernel compilation for C++ wrapper.
|
| 3 |
+
|
| 4 |
+
This module provides functionality to compile and autotune Triton kernels at runtime
|
| 5 |
+
when the C++ wrapper is used without autotune_at_compile_time.
|
| 6 |
+
|
| 7 |
+
The workflow is:
|
| 8 |
+
1. At model initialization: Call start_kernel_compile() for all kernels to start
|
| 9 |
+
parallel compilation using multi-process async_compile
|
| 10 |
+
2. At kernel execution time: Call run_triton_kernel_with_autotune() which waits
|
| 11 |
+
for the specific kernel to be ready, then runs it with autotuning
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
|
| 16 |
+
import dataclasses
|
| 17 |
+
import logging
|
| 18 |
+
import re
|
| 19 |
+
from typing import Any
|
| 20 |
+
|
| 21 |
+
from .triton_heuristics import CachingAutotuner
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
log = logging.getLogger(__name__)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
@dataclasses.dataclass
|
| 28 |
+
class TritonKernelCompileResult:
|
| 29 |
+
cubin_path: str
|
| 30 |
+
mangled_name: str
|
| 31 |
+
num_warps: int
|
| 32 |
+
shared_mem: int
|
| 33 |
+
xblock: int
|
| 34 |
+
yblock: int
|
| 35 |
+
zblock: int
|
| 36 |
+
r0block: int
|
| 37 |
+
rsplit: int
|
| 38 |
+
rsplit_size: int
|
| 39 |
+
config_index: int | None
|
| 40 |
+
global_scratch: int | None
|
| 41 |
+
profile_scratch: int | None
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
_async_compile: Any = None
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def _get_async_compile() -> Any:
|
| 48 |
+
"""Get or create the shared AsyncCompile instance."""
|
| 49 |
+
global _async_compile
|
| 50 |
+
if _async_compile is None:
|
| 51 |
+
from torch._inductor.async_compile import AsyncCompile
|
| 52 |
+
|
| 53 |
+
_async_compile = AsyncCompile()
|
| 54 |
+
return _async_compile
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def _wrap_tma_args(args: list[Any], kernel_fn: CachingAutotuner) -> list[Any]:
|
| 58 |
+
"""Wrap tensor args with TMA descriptors where the signature requires them."""
|
| 59 |
+
signature = kernel_fn.triton_meta.get("signature", {})
|
| 60 |
+
sig_items = list(signature.items())
|
| 61 |
+
|
| 62 |
+
# Track args index separately from sig_items index since the signature
|
| 63 |
+
# may include constexpr entries that are not present in args.
|
| 64 |
+
tma_indices = []
|
| 65 |
+
arg_idx = 0
|
| 66 |
+
for name, sig_type in sig_items:
|
| 67 |
+
if isinstance(sig_type, str) and sig_type == "constexpr":
|
| 68 |
+
continue
|
| 69 |
+
if isinstance(sig_type, str) and sig_type == "nvTmaDesc":
|
| 70 |
+
raise RuntimeError(
|
| 71 |
+
f"nvTmaDesc (experimental TMA API) is not supported in lazy compile "
|
| 72 |
+
f"for arg '{name}'. Use the stable tensordesc API instead."
|
| 73 |
+
)
|
| 74 |
+
if isinstance(sig_type, str) and sig_type.startswith("tensordesc<"):
|
| 75 |
+
tma_indices.append((arg_idx, name, sig_type))
|
| 76 |
+
arg_idx += 1
|
| 77 |
+
|
| 78 |
+
if not tma_indices:
|
| 79 |
+
return args
|
| 80 |
+
|
| 81 |
+
from triton.tools.tensor_descriptor import TensorDescriptor
|
| 82 |
+
|
| 83 |
+
wrapped = list(args)
|
| 84 |
+
for arg_idx, name, sig_type in tma_indices:
|
| 85 |
+
if arg_idx >= len(wrapped):
|
| 86 |
+
raise RuntimeError(
|
| 87 |
+
f"TMA arg index {arg_idx} for '{name}' exceeds arg count {len(wrapped)}"
|
| 88 |
+
)
|
| 89 |
+
tensor = wrapped[arg_idx]
|
| 90 |
+
# Parse block_shape from tensordesc<dtype[dim0, dim1, ...]>
|
| 91 |
+
match = re.match(r"tensordesc<[^[]*\[([^\]]*)\]", sig_type)
|
| 92 |
+
if match:
|
| 93 |
+
block_shape = [int(x.strip()) for x in match.group(1).split(",")]
|
| 94 |
+
wrapped[arg_idx] = TensorDescriptor.from_tensor(tensor, block_shape)
|
| 95 |
+
|
| 96 |
+
return wrapped
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def start_kernel_compile(
|
| 100 |
+
pending_kernels: dict[str, Any], kernel_name: str, kernel_source: str
|
| 101 |
+
) -> None:
|
| 102 |
+
"""
|
| 103 |
+
This function is called from C++ at model initialization time for each kernel.
|
| 104 |
+
It starts the compilation in a background process but does NOT wait for it.
|
| 105 |
+
The actual kernel execution happens later in run_triton_kernel_with_autotune().
|
| 106 |
+
|
| 107 |
+
The pending_kernels dict is per-module, created in C++ and passed through
|
| 108 |
+
to avoid global state collisions across compiled modules.
|
| 109 |
+
"""
|
| 110 |
+
if kernel_name in pending_kernels:
|
| 111 |
+
return
|
| 112 |
+
|
| 113 |
+
async_compile = _get_async_compile() # noqa: F841 (used by eval below)
|
| 114 |
+
|
| 115 |
+
# Evaluate the kernel source to get the Future or CachingAutotuner
|
| 116 |
+
# The kernel_source is like: async_compile.triton('name', '''...''', ...)
|
| 117 |
+
kernel_obj = eval(kernel_source.strip()) # noqa: S307
|
| 118 |
+
|
| 119 |
+
pending_kernels[kernel_name] = kernel_obj
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def run_triton_kernel_with_autotune(
|
| 123 |
+
pending_kernels: dict[str, Any],
|
| 124 |
+
kernel_name: str,
|
| 125 |
+
stream: Any,
|
| 126 |
+
args: list[Any],
|
| 127 |
+
) -> TritonKernelCompileResult:
|
| 128 |
+
"""
|
| 129 |
+
Run a Triton kernel with full autotuning using actual tensor arguments.
|
| 130 |
+
"""
|
| 131 |
+
from torch._inductor.codecache import CodeCacheFuture, CudaKernelParamCache
|
| 132 |
+
from torch._inductor.runtime.triton_heuristics import config_to_dict
|
| 133 |
+
|
| 134 |
+
if kernel_name not in pending_kernels:
|
| 135 |
+
raise RuntimeError(f"Kernel {kernel_name} not found in pending kernels.")
|
| 136 |
+
kernel_obj = pending_kernels[kernel_name]
|
| 137 |
+
|
| 138 |
+
if isinstance(kernel_obj, CodeCacheFuture):
|
| 139 |
+
kernel_fn = kernel_obj.result()
|
| 140 |
+
elif isinstance(kernel_obj, CachingAutotuner):
|
| 141 |
+
kernel_fn = kernel_obj
|
| 142 |
+
else:
|
| 143 |
+
raise RuntimeError(f"Unexpected kernel object type: {type(kernel_obj)}")
|
| 144 |
+
|
| 145 |
+
assert isinstance(kernel_fn, CachingAutotuner)
|
| 146 |
+
|
| 147 |
+
inductor_meta = kernel_fn.inductor_meta
|
| 148 |
+
inductor_meta["store_cubin"] = True
|
| 149 |
+
|
| 150 |
+
# For TMA kernels, wrap tensor args with TMA descriptors
|
| 151 |
+
args = _wrap_tma_args(args, kernel_fn)
|
| 152 |
+
|
| 153 |
+
# Run the kernel with the provided arguments
|
| 154 |
+
# This will trigger autotuning if there are multiple configs
|
| 155 |
+
kernel_fn.run(*args, stream=stream)
|
| 156 |
+
if not kernel_fn.launchers:
|
| 157 |
+
raise RuntimeError("Kernel run did not produce any launchers")
|
| 158 |
+
launcher = kernel_fn.launchers[0]
|
| 159 |
+
|
| 160 |
+
cached_params: dict[str, Any] | None = CudaKernelParamCache.get(kernel_name)
|
| 161 |
+
if cached_params is None:
|
| 162 |
+
raise RuntimeError(f"Failed to get cached params for kernel {kernel_name}")
|
| 163 |
+
|
| 164 |
+
from torch._inductor.codecache import get_cpp_wrapper_cubin_path_name
|
| 165 |
+
|
| 166 |
+
cubin_path_name = get_cpp_wrapper_cubin_path_name()
|
| 167 |
+
for key_name in (cubin_path_name, "mangled_name", "num_warps", "shared_mem"):
|
| 168 |
+
if key_name not in cached_params:
|
| 169 |
+
raise RuntimeError(
|
| 170 |
+
f"{key_name} not found in cached params for {kernel_name}"
|
| 171 |
+
)
|
| 172 |
+
cubin_path = cached_params[cubin_path_name]
|
| 173 |
+
mangled_name = cached_params["mangled_name"]
|
| 174 |
+
num_warps = cached_params["num_warps"]
|
| 175 |
+
shared_mem = cached_params["shared_mem"]
|
| 176 |
+
|
| 177 |
+
config = config_to_dict(launcher.config) if launcher.config else {}
|
| 178 |
+
|
| 179 |
+
# For combo/foreach kernels, the autotuned config may have empty kwargs
|
| 180 |
+
# (e.g., the foreach heuristic only tunes num_warps, not XBLOCK).
|
| 181 |
+
# In that case, use the default_config from combo_grid_meta
|
| 182 |
+
combo_grid_meta = inductor_meta.get("combo_grid_meta") if inductor_meta else None
|
| 183 |
+
default_config = combo_grid_meta.get("default_config") if combo_grid_meta else None
|
| 184 |
+
if default_config:
|
| 185 |
+
config = {**default_config, **config}
|
| 186 |
+
|
| 187 |
+
xblock = config.get("XBLOCK", 128)
|
| 188 |
+
yblock = config.get("YBLOCK", 1)
|
| 189 |
+
zblock = config.get("ZBLOCK", 1)
|
| 190 |
+
r0block = config.get("R0_BLOCK", 1)
|
| 191 |
+
rsplit = config.get("RSPLIT", 1)
|
| 192 |
+
rsplit_size = config.get("RSPLIT_SIZE", 1)
|
| 193 |
+
|
| 194 |
+
config_index = None
|
| 195 |
+
grid_type = inductor_meta.get("grid_type") if inductor_meta else None
|
| 196 |
+
if grid_type == "PrecomputedGrid" and inductor_meta:
|
| 197 |
+
# PrecomputedGrid selects one of precomputed_grids. We use config_index
|
| 198 |
+
# to remember which grid is chosen.
|
| 199 |
+
precomputed_grids = inductor_meta.get("precomputed_grids", [])
|
| 200 |
+
for idx, entry in enumerate(precomputed_grids):
|
| 201 |
+
entry_config = entry.get("config", {})
|
| 202 |
+
if all(config.get(k) == v for k, v in entry_config.items()):
|
| 203 |
+
config_index = idx
|
| 204 |
+
break
|
| 205 |
+
|
| 206 |
+
global_scratch: int | None = cached_params.get("global_scratch")
|
| 207 |
+
profile_scratch: int | None = cached_params.get("profile_scratch")
|
| 208 |
+
|
| 209 |
+
log.debug(
|
| 210 |
+
"Successfully autotuned Triton kernel: cubin_path=%s, mangled_name=%s, "
|
| 211 |
+
"num_warps=%d, shared_mem=%d, xblock=%d, yblock=%d, zblock=%d, r0block=%d, "
|
| 212 |
+
"rsplit=%d, rsplit_size=%d, config_index=%s, global_scratch=%s, profile_scratch=%s",
|
| 213 |
+
cubin_path,
|
| 214 |
+
mangled_name,
|
| 215 |
+
num_warps,
|
| 216 |
+
shared_mem,
|
| 217 |
+
xblock,
|
| 218 |
+
yblock,
|
| 219 |
+
zblock,
|
| 220 |
+
r0block,
|
| 221 |
+
rsplit,
|
| 222 |
+
rsplit_size,
|
| 223 |
+
config_index,
|
| 224 |
+
global_scratch,
|
| 225 |
+
profile_scratch,
|
| 226 |
+
)
|
| 227 |
+
|
| 228 |
+
result = TritonKernelCompileResult(
|
| 229 |
+
cubin_path=cubin_path,
|
| 230 |
+
mangled_name=mangled_name,
|
| 231 |
+
num_warps=num_warps,
|
| 232 |
+
shared_mem=shared_mem,
|
| 233 |
+
xblock=xblock,
|
| 234 |
+
yblock=yblock,
|
| 235 |
+
zblock=zblock,
|
| 236 |
+
r0block=r0block,
|
| 237 |
+
rsplit=rsplit,
|
| 238 |
+
rsplit_size=rsplit_size,
|
| 239 |
+
config_index=config_index,
|
| 240 |
+
global_scratch=global_scratch,
|
| 241 |
+
profile_scratch=profile_scratch,
|
| 242 |
+
)
|
| 243 |
+
return result
|
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/scheduler.py
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/script.ld
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
SECTIONS {
|
| 2 |
+
/* By default, in LLD 16, .lrodata is placed immediately after .rodata.
|
| 3 |
+
* However, .lrodata can be very large in our compiled models, which leads to
|
| 4 |
+
* relocation out-of-range errors for relative relocations. So we place it
|
| 5 |
+
* after other the sections that are referenced from .text using relative
|
| 6 |
+
* relocations. This is the default behavior in GNU ld. */
|
| 7 |
+
.lrodata : { *(.lrodata) }
|
| 8 |
+
} INSERT AFTER .bss;
|
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/select_algorithm.py
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/shape_propagation.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import functools
|
| 2 |
+
from collections.abc import Callable, Sequence
|
| 3 |
+
from typing import Protocol
|
| 4 |
+
|
| 5 |
+
import sympy
|
| 6 |
+
|
| 7 |
+
import torch
|
| 8 |
+
|
| 9 |
+
from .virtualized import OpsValue, V
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
BlockShapeType = Sequence[int | str] | None
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class ShapeVar(Protocol):
|
| 16 |
+
@property
|
| 17 |
+
def shape(self) -> BlockShapeType: ...
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
ShapeArg = ShapeVar | torch.types.Number | str | OpsValue | torch.dtype
|
| 21 |
+
|
| 22 |
+
# Inputs need to be cacheable (e.g., not a CSEVar) in order for the cache to be effective
|
| 23 |
+
# So first decompose CSEVars -> tuple before calling this
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
@functools.lru_cache(None)
|
| 27 |
+
def get_broadcasted_shape(a: BlockShapeType, b: BlockShapeType) -> BlockShapeType:
|
| 28 |
+
assert isinstance(a, Sequence)
|
| 29 |
+
assert isinstance(b, Sequence)
|
| 30 |
+
if len(a) > len(b):
|
| 31 |
+
return get_broadcasted_shape(a, (*[1] * (len(a) - len(b)), *b))
|
| 32 |
+
elif len(a) < len(b):
|
| 33 |
+
b, a = a, b
|
| 34 |
+
return get_broadcasted_shape(a, (*[1] * (len(a) - len(b)), *b))
|
| 35 |
+
else:
|
| 36 |
+
|
| 37 |
+
def _get_broadcasted_dim(d1: int | str, d2: int | str) -> int | str:
|
| 38 |
+
if str(d1) == "1":
|
| 39 |
+
return d2
|
| 40 |
+
elif str(d2) == "1":
|
| 41 |
+
return d1
|
| 42 |
+
assert str(d1) == str(d2)
|
| 43 |
+
return d1
|
| 44 |
+
|
| 45 |
+
return tuple(_get_broadcasted_dim(d1, d2) for d1, d2 in zip(a, b))
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def broadcast_shapes_for_args(args: Sequence[ShapeArg]) -> BlockShapeType:
|
| 49 |
+
result_shape: BlockShapeType = None
|
| 50 |
+
|
| 51 |
+
for arg in args:
|
| 52 |
+
if hasattr(arg, "shape"):
|
| 53 |
+
shape = arg.shape
|
| 54 |
+
if shape is None:
|
| 55 |
+
return None
|
| 56 |
+
elif result_shape is None:
|
| 57 |
+
result_shape = tuple(shape)
|
| 58 |
+
else:
|
| 59 |
+
result_shape = get_broadcasted_shape(result_shape, tuple(shape))
|
| 60 |
+
elif isinstance(arg, (int, float)):
|
| 61 |
+
if result_shape is None:
|
| 62 |
+
result_shape = ()
|
| 63 |
+
elif isinstance(arg, torch.dtype):
|
| 64 |
+
continue
|
| 65 |
+
else:
|
| 66 |
+
from torch._inductor.loop_body import LoopBody, LoopBodyBlock
|
| 67 |
+
|
| 68 |
+
if isinstance(arg, (LoopBodyBlock, LoopBody, OpsValue)):
|
| 69 |
+
# TODO: fix me
|
| 70 |
+
return None
|
| 71 |
+
raise TypeError(f"Unknown type: {type(arg)}")
|
| 72 |
+
|
| 73 |
+
return result_shape
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
class ShapePropagationOpsHandler:
|
| 77 |
+
"""
|
| 78 |
+
Propagate shape from args to output
|
| 79 |
+
"""
|
| 80 |
+
|
| 81 |
+
@staticmethod
|
| 82 |
+
def constant(value: torch.types.Number, dtype: torch.dtype) -> BlockShapeType:
|
| 83 |
+
# TritonKernelOverrides.constant uses tl.full with shape=[1]*ndim for all types
|
| 84 |
+
from torch._inductor.codegen.triton import TritonKernel
|
| 85 |
+
|
| 86 |
+
if isinstance(V.kernel, TritonKernel):
|
| 87 |
+
ndim = V.kernel.triton_tensor_ndim()
|
| 88 |
+
return tuple([1] * ndim)
|
| 89 |
+
else:
|
| 90 |
+
return ()
|
| 91 |
+
|
| 92 |
+
@staticmethod
|
| 93 |
+
def store_reduction(name: str, index: int, value: ShapeArg) -> None:
|
| 94 |
+
return None
|
| 95 |
+
|
| 96 |
+
@staticmethod
|
| 97 |
+
def reduction(
|
| 98 |
+
dtype: torch.dtype,
|
| 99 |
+
src_dtype: torch.dtype,
|
| 100 |
+
reduction_type: str,
|
| 101 |
+
value: ShapeArg | tuple[ShapeArg, ...],
|
| 102 |
+
) -> BlockShapeType | tuple[BlockShapeType, ...]:
|
| 103 |
+
raise NotImplementedError
|
| 104 |
+
|
| 105 |
+
@staticmethod
|
| 106 |
+
def store(name: str, index: int, value: ShapeArg, mode: str | None = None) -> None:
|
| 107 |
+
return None
|
| 108 |
+
|
| 109 |
+
@staticmethod
|
| 110 |
+
def to_dtype(
|
| 111 |
+
value: ShapeVar,
|
| 112 |
+
dtype: torch.dtype,
|
| 113 |
+
src_dtype: torch.dtype | None = None,
|
| 114 |
+
use_compute_types: bool = True,
|
| 115 |
+
) -> BlockShapeType:
|
| 116 |
+
return value.shape
|
| 117 |
+
|
| 118 |
+
@staticmethod
|
| 119 |
+
def dot(a: sympy.Expr, b: sympy.Expr) -> BlockShapeType:
|
| 120 |
+
from torch._inductor.codegen.triton import TritonKernel
|
| 121 |
+
|
| 122 |
+
assert isinstance(V.kernel, TritonKernel), "dot supports Triton only"
|
| 123 |
+
return ("YBLOCK", "XBLOCK")
|
| 124 |
+
|
| 125 |
+
@staticmethod
|
| 126 |
+
def index_expr(expr: sympy.Expr, dtype: torch.dtype) -> BlockShapeType:
|
| 127 |
+
# shape is implicitly embedded in expr.
|
| 128 |
+
return None
|
| 129 |
+
|
| 130 |
+
@staticmethod
|
| 131 |
+
def load_seed(name: str, offset: int) -> BlockShapeType:
|
| 132 |
+
return ()
|
| 133 |
+
|
| 134 |
+
@staticmethod
|
| 135 |
+
def indirect_indexing(
|
| 136 |
+
var: ShapeArg,
|
| 137 |
+
size: sympy.Expr | int,
|
| 138 |
+
check: bool = True,
|
| 139 |
+
wrap_neg: bool = True,
|
| 140 |
+
) -> None:
|
| 141 |
+
return None
|
| 142 |
+
|
| 143 |
+
def __getattr__(self, name: str) -> Callable[..., BlockShapeType]:
|
| 144 |
+
return lambda *args, **kwargs: broadcast_shapes_for_args(args)
|
| 145 |
+
|
| 146 |
+
@staticmethod
|
| 147 |
+
def device_assert_async(cond: ShapeArg, msg: str) -> None:
|
| 148 |
+
return None
|
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/sizevars.py
ADDED
|
@@ -0,0 +1,1193 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# mypy: allow-untyped-defs
|
| 2 |
+
import functools
|
| 3 |
+
import itertools
|
| 4 |
+
import logging
|
| 5 |
+
from collections.abc import Callable, Iterable, Sequence
|
| 6 |
+
from typing import Any, cast
|
| 7 |
+
|
| 8 |
+
import sympy
|
| 9 |
+
from sympy import Expr
|
| 10 |
+
|
| 11 |
+
from torch import SymInt
|
| 12 |
+
from torch.fx.experimental._size_hinting import (
|
| 13 |
+
_guarding_hint_or_throw_base,
|
| 14 |
+
_maybe_realize_expr,
|
| 15 |
+
_optimization_hint_base,
|
| 16 |
+
)
|
| 17 |
+
from torch.fx.experimental.symbolic_shapes import (
|
| 18 |
+
free_symbols,
|
| 19 |
+
free_unbacked_symbols,
|
| 20 |
+
IterateExprs,
|
| 21 |
+
ShapeEnv,
|
| 22 |
+
SymNode,
|
| 23 |
+
)
|
| 24 |
+
from torch.utils._ordered_set import OrderedSet
|
| 25 |
+
from torch.utils._sympy.functions import FloorDiv, Mod, ModularIndexing
|
| 26 |
+
from torch.utils._sympy.symbol import symbol_is_type, SymT
|
| 27 |
+
from torch.utils._sympy.value_ranges import IntInfinity, ValueRanges
|
| 28 |
+
|
| 29 |
+
from . import config
|
| 30 |
+
from .runtime.runtime_utils import is_power_of_2
|
| 31 |
+
from .utils import (
|
| 32 |
+
has_free_symbols,
|
| 33 |
+
sympy_index_symbol,
|
| 34 |
+
sympy_index_symbol_with_prefix,
|
| 35 |
+
sympy_subs,
|
| 36 |
+
VarRanges,
|
| 37 |
+
)
|
| 38 |
+
from .virtualized import V
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
log = logging.getLogger(__name__)
|
| 42 |
+
|
| 43 |
+
# Symbols created by CppTemplateKernel.slice_nd → parse_expr_with_index_symbols
|
| 44 |
+
# and are internal to C++ GEMM codegen (not tracked by ShapeEnv).
|
| 45 |
+
_GEMM_TEMPLATE_SYMBOL_NAMES = OrderedSet(
|
| 46 |
+
[
|
| 47 |
+
"m_start",
|
| 48 |
+
"m_end",
|
| 49 |
+
"n_start",
|
| 50 |
+
"n_end",
|
| 51 |
+
"k_start",
|
| 52 |
+
"k_end",
|
| 53 |
+
"m_size",
|
| 54 |
+
"n_size",
|
| 55 |
+
"m_offset",
|
| 56 |
+
"m_start_unsliced",
|
| 57 |
+
"m_end_unsliced",
|
| 58 |
+
"m_size_unsliced",
|
| 59 |
+
]
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def statically_known_true(
|
| 64 |
+
shape_env: ShapeEnv,
|
| 65 |
+
expr: sympy.Basic | bool,
|
| 66 |
+
axioms: tuple[sympy.Expr] | None = None,
|
| 67 |
+
var_to_range: tuple[tuple[sympy.Symbol, ValueRanges[Any]]] | None = None,
|
| 68 |
+
) -> bool:
|
| 69 |
+
if expr in (True, False):
|
| 70 |
+
return bool(expr)
|
| 71 |
+
|
| 72 |
+
try:
|
| 73 |
+
simplified = shape_env._maybe_evaluate_static(
|
| 74 |
+
expr,
|
| 75 |
+
axioms=axioms,
|
| 76 |
+
var_to_range=var_to_range,
|
| 77 |
+
)
|
| 78 |
+
if simplified is not None:
|
| 79 |
+
return bool(simplified)
|
| 80 |
+
except Exception:
|
| 81 |
+
log.debug("Could not simplify %s", expr, exc_info=True)
|
| 82 |
+
|
| 83 |
+
return False
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
# This class is a little awkward, because ShapeEnv is doing most of the heavy
|
| 87 |
+
# lifting and in some cases we should be directly passing through to ShapeEnv,
|
| 88 |
+
# but there is some extra inductor logic that needs to be handled here
|
| 89 |
+
class SizeVarAllocator:
|
| 90 |
+
"""
|
| 91 |
+
A class that manages symbolic size variables and their relationships.
|
| 92 |
+
|
| 93 |
+
This class works with the ShapeEnv to handle symbolic shape expressions,
|
| 94 |
+
simplify them, and provide utilities for guarding, checking, and evaluating
|
| 95 |
+
symbolic expressions. It also manages precomputed replacements and stride
|
| 96 |
+
calculations for tensor operations.
|
| 97 |
+
"""
|
| 98 |
+
|
| 99 |
+
def __init__(self, shape_env=None) -> None:
|
| 100 |
+
super().__init__()
|
| 101 |
+
# Note: this can lead to bugs. Reasoning APIs depends on existing information in
|
| 102 |
+
# in the shape_env. For example! var_to_ranges can't be empty!
|
| 103 |
+
if shape_env is None:
|
| 104 |
+
shape_env = ShapeEnv()
|
| 105 |
+
self.shape_env = shape_env
|
| 106 |
+
self.backed_var_to_val = self.shape_env.backed_var_to_val
|
| 107 |
+
self.var_to_hint_override = self.shape_env.var_to_hint_override
|
| 108 |
+
self.replacements: dict[sympy.Symbol, Expr] = self.shape_env.replacements
|
| 109 |
+
self.unbacked_replacements: dict[Expr, Expr] | None = None
|
| 110 |
+
# Maps of dynamic sizes that have to be precomputed on the host to the kernel args.
|
| 111 |
+
# The basic idea is if we have some complicated sympy expression
|
| 112 |
+
# f(s0), we may choose to precompute it on the host and then replace
|
| 113 |
+
# all occurrences of that sympy expression with ps0, so that when we
|
| 114 |
+
# codegen we simply reference ps0 directly without repeating
|
| 115 |
+
# f(s0). Unlike regular size variables, ps variables cannot be
|
| 116 |
+
# guarded upon; so if we are asked to guard on a Sympy expression
|
| 117 |
+
# which potentially could have already had a precomputed replacement
|
| 118 |
+
# on it, we are obligated to invert the precomputed replacements
|
| 119 |
+
# (inv_precomputed_replacements).
|
| 120 |
+
self.precomputed_replacements: dict[Expr, sympy.Symbol] = {}
|
| 121 |
+
self.inv_precomputed_replacements: dict[sympy.Symbol, Expr] = {}
|
| 122 |
+
self.stride_vars = self.make_stride_vars_cache()
|
| 123 |
+
self.simplify_with_ranges = self.make_simplify_with_ranges_cache()
|
| 124 |
+
self._simplify_loops = self.make_simplify_loops_cache()
|
| 125 |
+
|
| 126 |
+
def simplify(self, expr: Expr):
|
| 127 |
+
return sympy.expand(expr).xreplace(self.replacements)
|
| 128 |
+
|
| 129 |
+
def make_simplify_with_ranges_cache(self) -> Callable[[Expr, VarRanges], Expr]:
|
| 130 |
+
"""
|
| 131 |
+
self._simplify_with_ranges() can be expensive, cache its results
|
| 132 |
+
"""
|
| 133 |
+
cache: dict[tuple[Any, ...], Expr] = {}
|
| 134 |
+
replacement_count = len(self.replacements)
|
| 135 |
+
|
| 136 |
+
def simplify_with_ranges(expr: Expr, var_ranges: VarRanges) -> Expr:
|
| 137 |
+
nonlocal replacement_count
|
| 138 |
+
if replacement_count != len(self.replacements):
|
| 139 |
+
# new replacements invalidates cached results
|
| 140 |
+
cache.clear()
|
| 141 |
+
replacement_count = len(self.replacements)
|
| 142 |
+
key = (expr, *var_ranges.items())
|
| 143 |
+
result = cache.get(key)
|
| 144 |
+
if result is None:
|
| 145 |
+
result = self._simplify_with_ranges(expr, var_ranges)
|
| 146 |
+
cache[key] = result
|
| 147 |
+
if result != expr:
|
| 148 |
+
cache[(result, *var_ranges.items())] = result
|
| 149 |
+
return result
|
| 150 |
+
|
| 151 |
+
return simplify_with_ranges
|
| 152 |
+
|
| 153 |
+
def make_simplify_loops_cache(self):
|
| 154 |
+
"""
|
| 155 |
+
self._simplify_with_ranges() can be expensive, cache its results
|
| 156 |
+
"""
|
| 157 |
+
cache: dict[tuple[Any, ...], Any] = {}
|
| 158 |
+
replacement_count = len(self.replacements)
|
| 159 |
+
|
| 160 |
+
def simplify_loops(index_vars, sizes, index_formulas):
|
| 161 |
+
nonlocal replacement_count
|
| 162 |
+
if replacement_count != len(self.replacements):
|
| 163 |
+
# new replacements invalidates cached results
|
| 164 |
+
cache.clear()
|
| 165 |
+
replacement_count = len(self.replacements)
|
| 166 |
+
key = (*index_vars, *sizes, *index_formulas)
|
| 167 |
+
result = cache.get(key)
|
| 168 |
+
if result is None:
|
| 169 |
+
result = self._simplify_loops_impl(index_vars, sizes, index_formulas)
|
| 170 |
+
cache[key] = result
|
| 171 |
+
return result
|
| 172 |
+
|
| 173 |
+
return simplify_loops
|
| 174 |
+
|
| 175 |
+
def _simplify_with_ranges(self, expr: Expr, var_ranges: VarRanges) -> Expr:
|
| 176 |
+
"""
|
| 177 |
+
Simplify indexing expression with knowledge of the ranges of
|
| 178 |
+
iteration variables.
|
| 179 |
+
"""
|
| 180 |
+
|
| 181 |
+
expr = join_dimensions(self.simplify(expr))
|
| 182 |
+
original_expr = expr
|
| 183 |
+
|
| 184 |
+
var_to_range = dict(self.shape_env.var_to_range)
|
| 185 |
+
var_to_range.update(
|
| 186 |
+
{
|
| 187 |
+
k: ValueRanges(
|
| 188 |
+
0, max(0, v - 1) if not has_free_symbols([v]) else IntInfinity()
|
| 189 |
+
)
|
| 190 |
+
for k, v in var_ranges.items()
|
| 191 |
+
}
|
| 192 |
+
)
|
| 193 |
+
for var in expr.free_symbols:
|
| 194 |
+
if var not in var_to_range:
|
| 195 |
+
var_to_range[var] = ValueRanges(0, IntInfinity())
|
| 196 |
+
|
| 197 |
+
var_to_range_tuple = cast(
|
| 198 |
+
tuple[tuple[sympy.Symbol, ValueRanges[sympy.Expr]]],
|
| 199 |
+
tuple(var_to_range.items()),
|
| 200 |
+
)
|
| 201 |
+
|
| 202 |
+
axioms = []
|
| 203 |
+
for var, upper_bound in var_ranges.items():
|
| 204 |
+
axioms.append(0 <= var)
|
| 205 |
+
axioms.append(var < upper_bound)
|
| 206 |
+
axioms = tuple(axioms) + self.shape_env.get_axioms()
|
| 207 |
+
|
| 208 |
+
def statically_known(expr):
|
| 209 |
+
evaluated = self.shape_env._maybe_evaluate_static(
|
| 210 |
+
expr,
|
| 211 |
+
# pyrefly: ignore [bad-argument-type]
|
| 212 |
+
axioms=axioms,
|
| 213 |
+
var_to_range=var_to_range_tuple,
|
| 214 |
+
)
|
| 215 |
+
return bool(evaluated)
|
| 216 |
+
|
| 217 |
+
def remove_zero_terms(base, divisor):
|
| 218 |
+
"""Symbols smaller than the divisor are zero"""
|
| 219 |
+
if not statically_known(base >= 0):
|
| 220 |
+
return base
|
| 221 |
+
|
| 222 |
+
for v in base.free_symbols:
|
| 223 |
+
if v in var_ranges:
|
| 224 |
+
rest = sympy.Wild("_rest", exclude=[v])
|
| 225 |
+
m = base.match(v + rest)
|
| 226 |
+
if m and v not in m[rest].free_symbols:
|
| 227 |
+
# v can be removed if it doesn't affect the FloorDiv.
|
| 228 |
+
# rest is always a multiple of gcd(rest, divisor), so
|
| 229 |
+
# rest % divisor is also a multiple of that gcd. The
|
| 230 |
+
# worst case is rest % divisor == divisor - gcd, so
|
| 231 |
+
# adding v is safe when v < gcd.
|
| 232 |
+
gcd = sympy.gcd(m[rest], divisor)
|
| 233 |
+
if statically_known(v < gcd):
|
| 234 |
+
base = m[rest]
|
| 235 |
+
return base
|
| 236 |
+
|
| 237 |
+
def visit_indexing_div(base, divisor):
|
| 238 |
+
base = remove_zero_terms(base, divisor)
|
| 239 |
+
if statically_known(base >= 0) and statically_known(base < divisor):
|
| 240 |
+
return sympy.S.Zero
|
| 241 |
+
# FloorDiv(ModularIndexing(b, d1, m), d2) = ModularIndexing(b, d1*d2, m//d2)
|
| 242 |
+
if isinstance(base, ModularIndexing) and isinstance(divisor, sympy.Integer):
|
| 243 |
+
b, d1, m = base.args
|
| 244 |
+
if m % divisor == 0:
|
| 245 |
+
return ModularIndexing(b, d1 * divisor, FloorDiv(m, divisor))
|
| 246 |
+
return FloorDiv(base, divisor)
|
| 247 |
+
|
| 248 |
+
def visit_modular_indexing(base, divisor, modulus):
|
| 249 |
+
base = remove_zero_terms(base, divisor)
|
| 250 |
+
|
| 251 |
+
can_remove_mod = statically_known(base >= 0) and statically_known(
|
| 252 |
+
base < modulus * divisor
|
| 253 |
+
)
|
| 254 |
+
|
| 255 |
+
if can_remove_mod:
|
| 256 |
+
return FloorDiv(base, divisor)
|
| 257 |
+
return ModularIndexing(base, divisor, modulus)
|
| 258 |
+
|
| 259 |
+
if expr.has(ModularIndexing):
|
| 260 |
+
expr = expr.replace(
|
| 261 |
+
ModularIndexing(
|
| 262 |
+
sympy.Wild("base", integer=True),
|
| 263 |
+
sympy.Wild("divisor", integer=True),
|
| 264 |
+
sympy.Wild("modulus", integer=True),
|
| 265 |
+
),
|
| 266 |
+
visit_modular_indexing,
|
| 267 |
+
)
|
| 268 |
+
|
| 269 |
+
if expr.has(FloorDiv):
|
| 270 |
+
expr = expr.replace(
|
| 271 |
+
FloorDiv(
|
| 272 |
+
sympy.Wild("base", integer=True),
|
| 273 |
+
sympy.Wild("divisor", integer=True),
|
| 274 |
+
),
|
| 275 |
+
visit_indexing_div,
|
| 276 |
+
)
|
| 277 |
+
|
| 278 |
+
if expr != original_expr:
|
| 279 |
+
return self._simplify_with_ranges(expr, var_ranges)
|
| 280 |
+
return expr
|
| 281 |
+
|
| 282 |
+
def _simplify_loops_impl(
|
| 283 |
+
self, index_vars: list[sympy.Symbol], sizes, index_formulas
|
| 284 |
+
):
|
| 285 |
+
"""
|
| 286 |
+
Try to remove as many axis from loop iterations as possible, by:
|
| 287 |
+
1) removing size==1 dimensions
|
| 288 |
+
2) fuse contiguous dimensions into a single loop
|
| 289 |
+
If channel_last = True, we will prevent the last dim fused with other dims
|
| 290 |
+
"""
|
| 291 |
+
sizes = list(map(self.simplify, sizes))
|
| 292 |
+
|
| 293 |
+
strides = [
|
| 294 |
+
# index_formulas may contain boolean expressions (e.g. s0 < 10),
|
| 295 |
+
# for which "strides" don't make sense so we ignore them here.
|
| 296 |
+
# NOTE: These expressions may still block merging dims in the sound
|
| 297 |
+
# substitution test performed in can_merge_dims.
|
| 298 |
+
(
|
| 299 |
+
self.stride_vars(x, index_vars)
|
| 300 |
+
if isinstance(x, sympy.Expr)
|
| 301 |
+
else [0] * len(index_vars)
|
| 302 |
+
)
|
| 303 |
+
for x in index_formulas
|
| 304 |
+
]
|
| 305 |
+
assert len(sizes) == len(strides[0]), (len(sizes), len(strides[0]))
|
| 306 |
+
|
| 307 |
+
for i in range(len(sizes)):
|
| 308 |
+
if sizes[i] == 1:
|
| 309 |
+
# remove dim
|
| 310 |
+
sizes[i] = None
|
| 311 |
+
|
| 312 |
+
def can_merge_dims(a, b):
|
| 313 |
+
for k in range(len(strides)):
|
| 314 |
+
if self.simplify(strides[k][a] * sizes[a]) == self.simplify(
|
| 315 |
+
strides[k][b]
|
| 316 |
+
):
|
| 317 |
+
# approximate test passed, try sound version
|
| 318 |
+
va = index_vars[a]
|
| 319 |
+
vb = index_vars[b]
|
| 320 |
+
m1 = sympy_index_symbol("_merge_tester1")
|
| 321 |
+
m2 = sympy_index_symbol("_merge_tester2")
|
| 322 |
+
# NOTE: can't sub vb=0 here in case va * vb appears in the expression,
|
| 323 |
+
# in which case both expr1 and expr2 would be zero!
|
| 324 |
+
expr1 = sympy_subs(index_formulas[k], {va: m1 * sizes[a], vb: m2})
|
| 325 |
+
expr2 = sympy_subs(index_formulas[k], {va: 0, vb: (m1 + m2)})
|
| 326 |
+
if self.simplify(expr1) == self.simplify(expr2):
|
| 327 |
+
continue
|
| 328 |
+
return False
|
| 329 |
+
return True
|
| 330 |
+
|
| 331 |
+
changed = True
|
| 332 |
+
while changed:
|
| 333 |
+
changed = False
|
| 334 |
+
for i, j in itertools.product(
|
| 335 |
+
reversed(range(len(sizes))), reversed(range(len(sizes)))
|
| 336 |
+
):
|
| 337 |
+
if i == j or sizes[i] is None or sizes[j] is None:
|
| 338 |
+
continue
|
| 339 |
+
if can_merge_dims(i, j):
|
| 340 |
+
changed = True
|
| 341 |
+
sizes[i] = sizes[i] * sizes[j]
|
| 342 |
+
sizes[j] = None
|
| 343 |
+
|
| 344 |
+
def reindex(index):
|
| 345 |
+
it = list(reversed(index))
|
| 346 |
+
new_index = []
|
| 347 |
+
for size in sizes:
|
| 348 |
+
if size is None:
|
| 349 |
+
new_index.append(sympy.S.Zero)
|
| 350 |
+
else:
|
| 351 |
+
new_index.append(it.pop())
|
| 352 |
+
assert not it
|
| 353 |
+
return new_index
|
| 354 |
+
|
| 355 |
+
def prune(index):
|
| 356 |
+
assert len(index) == len(sizes)
|
| 357 |
+
return [i for i, s in zip(index, sizes) if s is not None]
|
| 358 |
+
|
| 359 |
+
return [x for x in sizes if x is not None], reindex, prune
|
| 360 |
+
|
| 361 |
+
# Note - [On Statically Known]
|
| 362 |
+
# The statically_known_* family of functions below NEVER guard, they could return True if the
|
| 363 |
+
# asked questions can be answered without guarding otherwise they return False.
|
| 364 |
+
# Those are similar to statically_known_true in symbolic_shapes.py but operate on sympy
|
| 365 |
+
# expressions instead of symnodes.
|
| 366 |
+
def statically_known_true(self, expr: sympy.Basic | bool) -> bool:
|
| 367 |
+
"""
|
| 368 |
+
Returns true if an expression is always true (symbolically or via guards),
|
| 369 |
+
false otherwise. Never add guards, or throw data dependent errors.
|
| 370 |
+
"""
|
| 371 |
+
return statically_known_true(self.shape_env, expr)
|
| 372 |
+
|
| 373 |
+
def statically_known_equals(self, left: Expr | int, right: Expr | int) -> bool:
|
| 374 |
+
"""
|
| 375 |
+
Returns a bool indicating if it is sound to optimize as if left and right are equal.
|
| 376 |
+
"""
|
| 377 |
+
return self.statically_known_true(sympy.Eq(left, right)) # type: ignore[arg-type]
|
| 378 |
+
|
| 379 |
+
def statically_known_list_equals(
|
| 380 |
+
self, left: Sequence[Expr], right: Sequence[Expr]
|
| 381 |
+
) -> bool:
|
| 382 |
+
"""
|
| 383 |
+
Returns a bool indicating if it is sound to optimize as if left and right lists are equal.
|
| 384 |
+
"""
|
| 385 |
+
return len(left) == len(right) and all(
|
| 386 |
+
self.statically_known_equals(l, r) for l, r in zip(left, right)
|
| 387 |
+
)
|
| 388 |
+
|
| 389 |
+
def statically_known_leq(self, left: Expr, right: Expr | int) -> bool:
|
| 390 |
+
"""
|
| 391 |
+
Returns a bool indicating if it is sound to optimize as if left is less than or equal to right.
|
| 392 |
+
"""
|
| 393 |
+
expr = left <= right
|
| 394 |
+
return self.statically_known_true(expr)
|
| 395 |
+
|
| 396 |
+
def statically_known_geq(self, left: Expr, right: Expr | int) -> bool:
|
| 397 |
+
"""
|
| 398 |
+
Returns a bool indicating if it is sound to optimize as if left is greater than or equal to right.
|
| 399 |
+
"""
|
| 400 |
+
expr = left >= right
|
| 401 |
+
return self.statically_known_true(expr)
|
| 402 |
+
|
| 403 |
+
def statically_known_lt(self, left: Expr, right: Expr | int) -> bool:
|
| 404 |
+
"""
|
| 405 |
+
Returns a bool indicating if it is sound to optimize as if left is less than right.
|
| 406 |
+
"""
|
| 407 |
+
expr = left < right
|
| 408 |
+
return self.statically_known_true(expr)
|
| 409 |
+
|
| 410 |
+
def statically_known_gt(self, left: Expr, right: Expr | int) -> bool:
|
| 411 |
+
"""
|
| 412 |
+
Returns a bool indicating if it is sound to optimize as if left is greater than right.
|
| 413 |
+
"""
|
| 414 |
+
expr = left > right
|
| 415 |
+
return self.statically_known_true(expr)
|
| 416 |
+
|
| 417 |
+
def _is_multiple_of(self, numerator: Expr, denominator: int) -> bool:
|
| 418 |
+
"""
|
| 419 |
+
Structural divisibility check: returns True only if numerator is
|
| 420 |
+
provably a multiple of denominator. Recurses over sympy expression
|
| 421 |
+
structure before falling back to statically_known_true.
|
| 422 |
+
"""
|
| 423 |
+
# Rule 1 — concrete value
|
| 424 |
+
if isinstance(numerator, (int, sympy.Integer)):
|
| 425 |
+
return int(numerator) % denominator == 0
|
| 426 |
+
|
| 427 |
+
# Rule 2 — product: any factor divisible → product divisible
|
| 428 |
+
if isinstance(numerator, sympy.Mul):
|
| 429 |
+
for factor in numerator.args:
|
| 430 |
+
if self._is_multiple_of(factor, denominator):
|
| 431 |
+
return True
|
| 432 |
+
# Also check if combined constant factors are divisible
|
| 433 |
+
const = 1
|
| 434 |
+
for factor in numerator.args:
|
| 435 |
+
if isinstance(factor, (int, sympy.Integer)):
|
| 436 |
+
const *= int(factor)
|
| 437 |
+
if const != 1 and const % denominator == 0:
|
| 438 |
+
return True
|
| 439 |
+
|
| 440 |
+
# Rule 3 — sum: all terms divisible → sum divisible
|
| 441 |
+
if isinstance(numerator, sympy.Add):
|
| 442 |
+
if all(self._is_multiple_of(term, denominator) for term in numerator.args):
|
| 443 |
+
return True
|
| 444 |
+
|
| 445 |
+
# Rule 4 — FloorDiv(a, b): if a is multiple of b*n
|
| 446 |
+
if isinstance(numerator, FloorDiv):
|
| 447 |
+
a, b = numerator.args
|
| 448 |
+
if isinstance(b, (int, sympy.Integer)):
|
| 449 |
+
if self._is_multiple_of(a, int(b) * denominator):
|
| 450 |
+
return True
|
| 451 |
+
|
| 452 |
+
# Rule 5 — Mod(a, b): Mod(a,b) = a - b*floor(a/b), so if both a and b
|
| 453 |
+
# are multiples of n, then Mod(a,b) is too.
|
| 454 |
+
if isinstance(numerator, (Mod, sympy.Mod)):
|
| 455 |
+
a, b = numerator.args
|
| 456 |
+
if self._is_multiple_of(a, denominator) and self._is_multiple_of(
|
| 457 |
+
b, denominator
|
| 458 |
+
):
|
| 459 |
+
return True
|
| 460 |
+
|
| 461 |
+
# Rule 6 — axiom fallback: ask ShapeEnv
|
| 462 |
+
expr = sympy.Eq(Mod(numerator, denominator), 0)
|
| 463 |
+
return self.statically_known_true(expr)
|
| 464 |
+
|
| 465 |
+
def statically_known_multiple_of(
|
| 466 |
+
self, numerator: Expr, denominator: Expr | int
|
| 467 |
+
) -> bool:
|
| 468 |
+
"""
|
| 469 |
+
Return a bool indicating if it is sound to optimize for the numerator being a multiple of the denominator.
|
| 470 |
+
"""
|
| 471 |
+
# The reason we skip compute here is to avoid the cost of trying to eval this symbolically.
|
| 472 |
+
# see https://github.com/sympy/sympy/issues/28200
|
| 473 |
+
|
| 474 |
+
if len(free_symbols(numerator)) > 20:
|
| 475 |
+
return False
|
| 476 |
+
|
| 477 |
+
if isinstance(denominator, (int, sympy.Integer)):
|
| 478 |
+
return self._is_multiple_of(numerator, int(denominator))
|
| 479 |
+
|
| 480 |
+
# For symbolic denominators, fall back to direct sympy check
|
| 481 |
+
expr = sympy.Eq(Mod(numerator, denominator), 0)
|
| 482 |
+
return self.statically_known_true(expr) # type: ignore[arg-type]
|
| 483 |
+
|
| 484 |
+
def statically_known_power_of_2(self, expr: Expr) -> bool:
|
| 485 |
+
"""
|
| 486 |
+
Returns a bool indicating if x is known to be a power of 2.
|
| 487 |
+
"""
|
| 488 |
+
return isinstance(expr, sympy.Integer) and is_power_of_2(int(expr))
|
| 489 |
+
|
| 490 |
+
# The expect/check functions require you to ALREADY KNOW that a particular
|
| 491 |
+
# condition holds. They are similar to expect_true in symbolic_shapes.py and
|
| 492 |
+
# torch.check but operates on sympy expressions instead of symnodes.
|
| 493 |
+
def expect_true(self, expr: Expr) -> bool:
|
| 494 |
+
"""
|
| 495 |
+
Use it when you already know that expr is true or should be true and want to
|
| 496 |
+
ensure that guards/runtime assertions are in place to ensure this in compiled
|
| 497 |
+
function. Unlike check, this WON'T raise an error if expr isn't actually true.
|
| 498 |
+
check Note [expect_true].
|
| 499 |
+
"""
|
| 500 |
+
if not self.statically_known_true(expr):
|
| 501 |
+
return self.shape_env.guard_or_defer_runtime_assert(
|
| 502 |
+
expr, "sizevars.expect_true"
|
| 503 |
+
)
|
| 504 |
+
return True
|
| 505 |
+
|
| 506 |
+
def check(self, expr: Expr) -> None:
|
| 507 |
+
"""
|
| 508 |
+
Use it when you already know that expr is true or should be true and want to
|
| 509 |
+
ensure that guards/runtime assertions are in place to ensure this in compiled
|
| 510 |
+
function. Unlike expect_true, this WILL raise an error if expr isn't actually true.
|
| 511 |
+
check Note [expect_true].
|
| 512 |
+
"""
|
| 513 |
+
expr = sympy_subs(expr, self.inv_precomputed_replacements)
|
| 514 |
+
assert self.expect_true(expr)
|
| 515 |
+
|
| 516 |
+
def check_equals(self, left: Expr, right: Expr) -> None:
|
| 517 |
+
"""
|
| 518 |
+
check(sympy.Eq(left, right)).
|
| 519 |
+
|
| 520 |
+
"""
|
| 521 |
+
self.check(sympy.Eq(left, right))
|
| 522 |
+
return left
|
| 523 |
+
|
| 524 |
+
def check_equals_and_simplify(self, left: Expr, right: Expr) -> Expr:
|
| 525 |
+
"""
|
| 526 |
+
check(sympy.Eq(left, right)) and returns left after applying
|
| 527 |
+
inv_precomputed_replacements.
|
| 528 |
+
"""
|
| 529 |
+
self.check(sympy.Eq(left, right))
|
| 530 |
+
return sympy_subs(left, self.inv_precomputed_replacements)
|
| 531 |
+
|
| 532 |
+
def check_leq(self, left: Expr, right: Expr) -> None:
|
| 533 |
+
self.check(sympy.Le(left, right))
|
| 534 |
+
|
| 535 |
+
def check_lt(self, left: Expr, right: Expr) -> None:
|
| 536 |
+
self.check(sympy.Lt(left, right))
|
| 537 |
+
|
| 538 |
+
# Similar to the functions guard_or_false/guard_or_true in symbolic_shapes.py
|
| 539 |
+
# but operates on sympy expressions instead of symnodes. see Note [guard_or_].
|
| 540 |
+
def guard_or_false(self, left):
|
| 541 |
+
import torch.fx.experimental._config as exp_config
|
| 542 |
+
|
| 543 |
+
if exp_config.backed_size_oblivious:
|
| 544 |
+
static_val = self.shape_env._maybe_evaluate_static(left)
|
| 545 |
+
if static_val is not None:
|
| 546 |
+
return static_val
|
| 547 |
+
return False
|
| 548 |
+
return self.evaluate_expr(left, fallback_value=False)
|
| 549 |
+
|
| 550 |
+
def guard_or_true(self, left):
|
| 551 |
+
import torch.fx.experimental._config as exp_config
|
| 552 |
+
|
| 553 |
+
if exp_config.backed_size_oblivious:
|
| 554 |
+
static_val = self.shape_env._maybe_evaluate_static(left)
|
| 555 |
+
if static_val is not None:
|
| 556 |
+
return static_val
|
| 557 |
+
return True
|
| 558 |
+
return self.evaluate_expr(left, fallback_value=True)
|
| 559 |
+
|
| 560 |
+
# The evaluate functions evaluate some symbolic sympy expression
|
| 561 |
+
# (NB: not necessarily an Expr) and return what the concrete result
|
| 562 |
+
# is, guarding on the expression being that result
|
| 563 |
+
|
| 564 |
+
# NB: write evaluate_expr(sympy.Lt(a, b)) rather than evaluate_expr(a < b)
|
| 565 |
+
# as this will ensure that you actually have a sympy'ified expression,
|
| 566 |
+
# and will prevent you from incorrectly writing evaluate_expr(a == b)
|
| 567 |
+
# which does the wrong thing if a or b is a sympy expression
|
| 568 |
+
def evaluate_expr(
|
| 569 |
+
self,
|
| 570 |
+
left: Expr | sympy.logic.boolalg.Boolean,
|
| 571 |
+
size_oblivious: bool = False,
|
| 572 |
+
fallback_value: bool | None = None,
|
| 573 |
+
) -> bool:
|
| 574 |
+
assert isinstance(left, (Expr, sympy.logic.boolalg.Boolean)), type(left)
|
| 575 |
+
return self.shape_env.evaluate_expr(
|
| 576 |
+
sympy.sympify(left),
|
| 577 |
+
size_oblivious=size_oblivious,
|
| 578 |
+
fallback_value=fallback_value,
|
| 579 |
+
)
|
| 580 |
+
|
| 581 |
+
def is_size_one_or_false(self, size: Expr) -> bool:
|
| 582 |
+
"""Return True if size equals 1.
|
| 583 |
+
|
| 584 |
+
Unbacked symbolic sizes return False without introducing a guard.
|
| 585 |
+
"""
|
| 586 |
+
return self.guard_or_false(sympy.Eq(size, 1))
|
| 587 |
+
|
| 588 |
+
def evaluate_min(self, left: Expr, right: Expr) -> Expr:
|
| 589 |
+
"""Return the smaller of left and right, and guard on that choice."""
|
| 590 |
+
if isinstance(left, Expr):
|
| 591 |
+
left = sympy_subs(left, self.inv_precomputed_replacements) # type: ignore[arg-type]
|
| 592 |
+
if isinstance(right, Expr):
|
| 593 |
+
right = sympy_subs(right, self.inv_precomputed_replacements) # type: ignore[arg-type]
|
| 594 |
+
if self.guard_or_false(sympy.Le(left, right)):
|
| 595 |
+
return left
|
| 596 |
+
if self.guard_or_false(sympy.Le(right, left)):
|
| 597 |
+
return right
|
| 598 |
+
|
| 599 |
+
# GCD fallback: if gcd(a, b) == a then a divides b, implying a <= b.
|
| 600 |
+
#
|
| 601 |
+
# TODO: This is NOT always sound for unbacked symints. It can
|
| 602 |
+
# produce wrong results when:
|
| 603 |
+
# - inputs can be negative: gcd(u0, 10*u0) = u0, returns u0,
|
| 604 |
+
# but if u0 < 0 then u0 > 10*u0 (e.g. u0=-1: min(-1,-10) = -10)
|
| 605 |
+
# - a factor can be zero: gcd(u0, u0*u1) = u0, returns u0,
|
| 606 |
+
# but if u1=0 then u0*u1=0 < u0 (e.g. u0=5,u1=0: min(5,0) = 0)
|
| 607 |
+
# TODO shall we add a runtime assertion at least.
|
| 608 |
+
gcd = sympy.gcd(left, right)
|
| 609 |
+
if left == gcd:
|
| 610 |
+
return left
|
| 611 |
+
if right == gcd:
|
| 612 |
+
return right
|
| 613 |
+
|
| 614 |
+
# Min/Max fallback: we can prove Min(a, b) <= c when any arg <= c, but
|
| 615 |
+
# sympy doesn't simplify this yet. So, evaluate it here. Same for Max.
|
| 616 |
+
for lhs, rhs in [(left, right), (right, left)]:
|
| 617 |
+
|
| 618 |
+
def le_rhs(a: Expr) -> bool:
|
| 619 |
+
return self.guard_or_false(sympy.Le(a, rhs))
|
| 620 |
+
|
| 621 |
+
# Min(Min(a, b), c) ==> Min(a, b) if (a <= c) or (b <= c).
|
| 622 |
+
if isinstance(lhs, sympy.Min) and any(le_rhs(a) for a in lhs.args):
|
| 623 |
+
return lhs
|
| 624 |
+
# Min(Max(a, b), c) ==> Max(a, b) if (a <= c) and (b <= c).
|
| 625 |
+
if isinstance(lhs, sympy.Max) and all(le_rhs(a) for a in lhs.args):
|
| 626 |
+
return lhs
|
| 627 |
+
|
| 628 |
+
raise TypeError(
|
| 629 |
+
f"evaluate_min({left}, {right}) with unbacked symints"
|
| 630 |
+
) from None
|
| 631 |
+
|
| 632 |
+
def evaluate_max(self, left: Expr, right: Expr) -> Expr:
|
| 633 |
+
"""Return the larger of left and right, and guard on that choice."""
|
| 634 |
+
# Always choose the opposite of eval min for consistency
|
| 635 |
+
# This means min(a, b) and max(a, b) produce the same guards
|
| 636 |
+
min_val = self.evaluate_min(left, right)
|
| 637 |
+
return right if min_val is left else left
|
| 638 |
+
|
| 639 |
+
def guard_int(self, expr: Expr | int) -> int:
|
| 640 |
+
"""
|
| 641 |
+
Similar to guard_int in symbolic_shapes.py, except this function works with SymPy
|
| 642 |
+
expressions instead of SymNodes. It extracts the value represented by expr from shapeEnv
|
| 643 |
+
and specialize the compiled graph on it. Raises an error if the result cannot be
|
| 644 |
+
determined due to unhinted or unbacked symbols.
|
| 645 |
+
"""
|
| 646 |
+
if isinstance(expr, int):
|
| 647 |
+
return expr
|
| 648 |
+
val = self.guarding_hint_or_throw(expr)
|
| 649 |
+
self.check_equals(expr, sympy.Integer(val))
|
| 650 |
+
return int(val)
|
| 651 |
+
|
| 652 |
+
def guard_int_seq(self, left: Sequence[Expr | int]) -> list[int]:
|
| 653 |
+
"""
|
| 654 |
+
Apply guard_int on a sequence of inputs.
|
| 655 |
+
"""
|
| 656 |
+
return [self.guard_int(x) for x in left]
|
| 657 |
+
|
| 658 |
+
def remove_precomputed_replacements(self, expr: Expr) -> Expr:
|
| 659 |
+
if any(symbol_is_type(s, SymT.PRECOMPUTED_SIZE) for s in expr.free_symbols): # type: ignore[attr-defined]
|
| 660 |
+
return sympy_subs(expr, self.inv_precomputed_replacements) # type: ignore[arg-type]
|
| 661 |
+
return expr
|
| 662 |
+
|
| 663 |
+
def replace_backed_symbols_with_hints(
|
| 664 |
+
self,
|
| 665 |
+
expr: Expr | int,
|
| 666 |
+
) -> Expr | int:
|
| 667 |
+
"""
|
| 668 |
+
Replace all backed symbols in an expression with their concrete hint values.
|
| 669 |
+
|
| 670 |
+
This function substitutes backed symbolic variables with their known concrete
|
| 671 |
+
values from the shape environment, while leaving unbacked symbols (data-dependent
|
| 672 |
+
values) untouched. Not recommended to be used except as last resort!
|
| 673 |
+
was added for existing usages compatibility.
|
| 674 |
+
"""
|
| 675 |
+
if isinstance(expr, int):
|
| 676 |
+
return expr
|
| 677 |
+
# Substitute all hints into expr, but leave unbacked symints alone
|
| 678 |
+
expr = self.simplify(expr)
|
| 679 |
+
if not isinstance(expr, Expr):
|
| 680 |
+
assert isinstance(expr, int)
|
| 681 |
+
return expr
|
| 682 |
+
|
| 683 |
+
expr = self.remove_precomputed_replacements(expr)
|
| 684 |
+
expr = sympy_subs(expr, self.backed_var_to_val)
|
| 685 |
+
if isinstance(expr, Expr):
|
| 686 |
+
expr = expr.expand(identity=True)
|
| 687 |
+
|
| 688 |
+
free_symbols = expr.free_symbols
|
| 689 |
+
if not free_symbols:
|
| 690 |
+
try:
|
| 691 |
+
return int(expr)
|
| 692 |
+
except TypeError:
|
| 693 |
+
return expr # inf/nan/I
|
| 694 |
+
|
| 695 |
+
return expr
|
| 696 |
+
|
| 697 |
+
def to_symint_or_int(self, expr: Expr | int) -> SymInt | int:
|
| 698 |
+
"""Convert a sympy expression to SymInt, or return int as is."""
|
| 699 |
+
if isinstance(expr, int):
|
| 700 |
+
return expr
|
| 701 |
+
|
| 702 |
+
from torch.fx.experimental.sym_node import SymNode
|
| 703 |
+
|
| 704 |
+
expr = self.simplify(expr)
|
| 705 |
+
if isinstance(expr, (int, sympy.Integer)):
|
| 706 |
+
return int(expr)
|
| 707 |
+
|
| 708 |
+
# Remove precomputed replacements to get canonical form
|
| 709 |
+
# (consistent with symbolic_hint behavior)
|
| 710 |
+
expr = self.remove_precomputed_replacements(expr)
|
| 711 |
+
|
| 712 |
+
try:
|
| 713 |
+
hint = self.guarding_hint_or_throw(expr)
|
| 714 |
+
except Exception:
|
| 715 |
+
hint = None
|
| 716 |
+
node = SymNode(expr, self.shape_env, int, hint)
|
| 717 |
+
return SymInt(node)
|
| 718 |
+
|
| 719 |
+
def to_symints_or_ints(self, exprs: Sequence[Expr | int]) -> list[SymInt | int]:
|
| 720 |
+
"""Convert a sequence of sympy expressions to SymInts, or return ints as is."""
|
| 721 |
+
return [self.to_symint_or_int(e) for e in exprs]
|
| 722 |
+
|
| 723 |
+
def guarding_hint_or_throw(self, expr: Expr | int) -> int:
|
| 724 |
+
"""
|
| 725 |
+
Return a concrete integer hint for an expression that is safe to use for guarding.
|
| 726 |
+
|
| 727 |
+
This method evaluates the expression using only backed-symbols hints. Unlike
|
| 728 |
+
optimization_hint(), this method does NOT use heuristics or fallback values
|
| 729 |
+
for unbacked symbols.
|
| 730 |
+
|
| 731 |
+
Use this method when you need a hint value that will be used for guarding decision.
|
| 732 |
+
|
| 733 |
+
Args:
|
| 734 |
+
expr: A sympy expression or integer to evaluate.
|
| 735 |
+
|
| 736 |
+
Returns:
|
| 737 |
+
The concrete integer value of the expression based on backed symbol hints.
|
| 738 |
+
|
| 739 |
+
Raises:
|
| 740 |
+
GuardOnDataDependentSymNode: If the expression contains unbacked symbols
|
| 741 |
+
(data-dependent values) that cannot be resolved to concrete values.
|
| 742 |
+
|
| 743 |
+
See Also:
|
| 744 |
+
optimization_hint: For cases where fallback/heuristic values are acceptable
|
| 745 |
+
for unbacked symbols.
|
| 746 |
+
"""
|
| 747 |
+
if isinstance(expr, SymNode):
|
| 748 |
+
raise TypeError(
|
| 749 |
+
f"guarding_hint_or_throw expects a sympy Expr or int, not {type(expr)}. "
|
| 750 |
+
"Use expr.expr to extract the sympy expression from a SymNode."
|
| 751 |
+
)
|
| 752 |
+
return _guarding_hint_or_throw_base(
|
| 753 |
+
self.shape_env, expr, self.inv_precomputed_replacements
|
| 754 |
+
)
|
| 755 |
+
|
| 756 |
+
def optimization_hint(self, expr: Expr | int, fallback: int | None = None) -> int:
|
| 757 |
+
"""
|
| 758 |
+
Return a concrete integer hint for an expression.
|
| 759 |
+
|
| 760 |
+
This function should be used for non-guarding based optimizations. If you
|
| 761 |
+
want a hint that you can guard on, use the guarding_hint API instead.
|
| 762 |
+
|
| 763 |
+
This function will hint unbacked symbols using user provided optimization
|
| 764 |
+
hints. If not provided, fallback will be used along with some heuristics
|
| 765 |
+
that try to maximize consistency with the shape environment.
|
| 766 |
+
|
| 767 |
+
Special cases:
|
| 768 |
+
- Complex numbers (containing sympy.I): raises an error since tensor
|
| 769 |
+
dimensions cannot be complex.
|
| 770 |
+
- Infinity (int_oo, sympy.oo): returns sys.maxsize.
|
| 771 |
+
- NaN (sympy.nan): returns the fallback value.
|
| 772 |
+
"""
|
| 773 |
+
return _optimization_hint_base(
|
| 774 |
+
self.shape_env, expr, self.inv_precomputed_replacements, fallback
|
| 775 |
+
)
|
| 776 |
+
|
| 777 |
+
def optimization_hints(
|
| 778 |
+
self,
|
| 779 |
+
exprs: Iterable[Expr | int],
|
| 780 |
+
fallback: int | None = None,
|
| 781 |
+
) -> tuple[int, ...]:
|
| 782 |
+
"""
|
| 783 |
+
Like optimization_hint but for a sequence of expressions.
|
| 784 |
+
Returns a tuple of concrete integer hints.
|
| 785 |
+
"""
|
| 786 |
+
# Read config at call time to respect runtime patches (e.g., in tests)
|
| 787 |
+
if fallback is None:
|
| 788 |
+
fallback = config.unbacked_symint_fallback
|
| 789 |
+
return tuple(self.optimization_hint(x, fallback=fallback) for x in exprs)
|
| 790 |
+
|
| 791 |
+
def all_unbacked_explicitly_hinted(self, exprs: IterateExprs) -> bool:
|
| 792 |
+
"""
|
| 793 |
+
Return True if every unbacked symbol in *exprs* has an explicit
|
| 794 |
+
user-provided hint in var_to_hint_override. If there are no
|
| 795 |
+
unbacked symbols at all, returns True (vacuously).
|
| 796 |
+
*exprs* can be a single expression or any iterable accepted by
|
| 797 |
+
free_unbacked_symbols (list, tuple, etc.).
|
| 798 |
+
"""
|
| 799 |
+
unbacked = free_unbacked_symbols(exprs)
|
| 800 |
+
return unbacked.issubset(self.var_to_hint_override.keys())
|
| 801 |
+
|
| 802 |
+
def optimization_hint_with_override(
|
| 803 |
+
self,
|
| 804 |
+
expr: Expr | int,
|
| 805 |
+
hint_override: int | None,
|
| 806 |
+
) -> int:
|
| 807 |
+
r"""Return a concrete integer hint for an expression, with optional override.
|
| 808 |
+
This is used in dynamic dispatch scenarios where callers may want to
|
| 809 |
+
provide a specific hint value rather than computing one from the expression.
|
| 810 |
+
The resolution order is:
|
| 811 |
+
1. If ``expr`` simplifies to a static integer, return that value
|
| 812 |
+
(``hint_override`` is ignored for static shapes).
|
| 813 |
+
2. If ``expr`` is dynamic and ``hint_override`` is not ``None``,
|
| 814 |
+
return ``hint_override``.
|
| 815 |
+
3. Otherwise, compute a hint via :meth:`optimization_hint`
|
| 816 |
+
Args:
|
| 817 |
+
expr (Expr or int): The expression to get a hint for.
|
| 818 |
+
hint_override (int, optional): If provided and ``expr`` is dynamic,
|
| 819 |
+
this value is returned instead of computing a hint.
|
| 820 |
+
Returns:
|
| 821 |
+
int: A concrete integer hint for the expression.
|
| 822 |
+
"""
|
| 823 |
+
simplified = _maybe_realize_expr(self.simplify(expr), None)
|
| 824 |
+
|
| 825 |
+
if simplified is not None:
|
| 826 |
+
return simplified
|
| 827 |
+
|
| 828 |
+
# Dynamic shape: use hint_override if set,
|
| 829 |
+
# else return optimization_hint
|
| 830 |
+
if hint_override is not None:
|
| 831 |
+
return hint_override
|
| 832 |
+
|
| 833 |
+
return self.optimization_hint(expr)
|
| 834 |
+
|
| 835 |
+
def optimization_hints_with_override(
|
| 836 |
+
self,
|
| 837 |
+
exprs: Iterable[Expr | int],
|
| 838 |
+
hint_override: int | None,
|
| 839 |
+
) -> tuple[int, ...]:
|
| 840 |
+
"""
|
| 841 |
+
Like optimization_hint_with_override but for a sequence of expressions.
|
| 842 |
+
Returns a tuple of concrete integer hints.
|
| 843 |
+
"""
|
| 844 |
+
return tuple(
|
| 845 |
+
self.optimization_hint_with_override(e, hint_override) for e in exprs
|
| 846 |
+
)
|
| 847 |
+
|
| 848 |
+
def guarding_hints_or_throw(
|
| 849 |
+
self,
|
| 850 |
+
exprs: Iterable[Expr | int],
|
| 851 |
+
) -> tuple[int, ...]:
|
| 852 |
+
return tuple(self.guarding_hint_or_throw(x) for x in exprs)
|
| 853 |
+
|
| 854 |
+
def _lru_cache(self, fn, maxsize=None):
|
| 855 |
+
"""
|
| 856 |
+
Wrapper around functools.lru_cache that clears when replacements
|
| 857 |
+
has been invalidated.
|
| 858 |
+
"""
|
| 859 |
+
fn_cache = functools.lru_cache(maxsize)(fn)
|
| 860 |
+
prior_len = len(self.replacements)
|
| 861 |
+
|
| 862 |
+
@functools.wraps(fn)
|
| 863 |
+
def wrapper(*args, **kwargs):
|
| 864 |
+
nonlocal prior_len
|
| 865 |
+
if prior_len != len(self.replacements):
|
| 866 |
+
prior_len = len(self.replacements)
|
| 867 |
+
fn_cache.cache_clear()
|
| 868 |
+
return fn_cache(*args, **kwargs)
|
| 869 |
+
|
| 870 |
+
return wrapper
|
| 871 |
+
|
| 872 |
+
def make_stride_vars_cache(self):
|
| 873 |
+
cache = self._lru_cache(self._stride_vars)
|
| 874 |
+
|
| 875 |
+
def stride_vars(
|
| 876 |
+
index: Expr,
|
| 877 |
+
vars: Sequence[sympy.Symbol],
|
| 878 |
+
support_vars: Sequence[sympy.Symbol] | None = None,
|
| 879 |
+
) -> list[Expr]:
|
| 880 |
+
if not support_vars:
|
| 881 |
+
support_vars = vars
|
| 882 |
+
return cache(index, tuple(vars), tuple(support_vars))
|
| 883 |
+
|
| 884 |
+
return stride_vars
|
| 885 |
+
|
| 886 |
+
def _stride_vars(
|
| 887 |
+
self,
|
| 888 |
+
index: Expr,
|
| 889 |
+
vars: Sequence[sympy.Symbol],
|
| 890 |
+
support_vars: Sequence[sympy.Symbol],
|
| 891 |
+
) -> list[Expr]:
|
| 892 |
+
"""Convert an indexing expression back into strides
|
| 893 |
+
|
| 894 |
+
NOTE: This is only valid if the index is a standard strided offset
|
| 895 |
+
calculation. e.g. 10 * ModularIndexing(i0 + 1, 1, 2) would give a
|
| 896 |
+
stride of -10 because the index wraps around after the first element
|
| 897 |
+
|
| 898 |
+
"""
|
| 899 |
+
strides = []
|
| 900 |
+
index = self.simplify(index)
|
| 901 |
+
# remove any offset
|
| 902 |
+
index = index - sympy_subs(
|
| 903 |
+
index, {v: sympy.S.Zero for v in support_vars if v != 0}
|
| 904 |
+
)
|
| 905 |
+
for i in range(len(vars)):
|
| 906 |
+
# drop all the other dims
|
| 907 |
+
index_dim = sympy_subs(
|
| 908 |
+
index,
|
| 909 |
+
{
|
| 910 |
+
support_vars[j]: sympy.S.Zero
|
| 911 |
+
for j in range(len(support_vars))
|
| 912 |
+
if vars[i] != support_vars[j] and support_vars[j] != 0
|
| 913 |
+
},
|
| 914 |
+
)
|
| 915 |
+
v = vars[i]
|
| 916 |
+
if v == 0:
|
| 917 |
+
strides.append(sympy.S.Zero)
|
| 918 |
+
else:
|
| 919 |
+
# TODO(jansel): should we use sympy.diff here?
|
| 920 |
+
strides.append(
|
| 921 |
+
sympy_subs(index_dim, {v: sympy.S.One})
|
| 922 |
+
- sympy_subs(index_dim, {v: sympy.S.Zero})
|
| 923 |
+
)
|
| 924 |
+
return strides
|
| 925 |
+
|
| 926 |
+
def offset_var(self, index: Expr, vars: Sequence[sympy.Symbol]) -> Expr:
|
| 927 |
+
"""Extract offset part of an indexing expression"""
|
| 928 |
+
index = self.simplify(index)
|
| 929 |
+
return sympy_subs(index, {v: sympy.S.Zero for v in vars if v != 0})
|
| 930 |
+
|
| 931 |
+
# Return stride optimizaitons hints,
|
| 932 |
+
# only used for optimizations.
|
| 933 |
+
def stride_hints(
|
| 934 |
+
self,
|
| 935 |
+
index: Expr,
|
| 936 |
+
vars: Sequence[sympy.Symbol],
|
| 937 |
+
support_vars: Sequence[sympy.Symbol] | None = None,
|
| 938 |
+
) -> list[int]:
|
| 939 |
+
for v in index.free_symbols:
|
| 940 |
+
if symbol_is_type(v, SymT.INDIRECT): # type: ignore[attr-defined]
|
| 941 |
+
index = sympy_subs(index, {v: 0}) # type: ignore[dict-item]
|
| 942 |
+
result = []
|
| 943 |
+
for s in self.stride_vars(index, vars, support_vars):
|
| 944 |
+
result.append(self.optimization_hint(s, fallback=0))
|
| 945 |
+
return result
|
| 946 |
+
|
| 947 |
+
def stride_order(self, index: Expr, vars: list[sympy.Symbol]) -> list[int]:
|
| 948 |
+
strides = tuple(map(abs, self.stride_hints(index, vars)))
|
| 949 |
+
order = list(range(len(strides)))
|
| 950 |
+
order.sort(key=lambda x: (strides[x] == 0, strides[x]))
|
| 951 |
+
return order
|
| 952 |
+
|
| 953 |
+
def lookup_precomputed_size(self, expr: Expr) -> Expr:
|
| 954 |
+
if (
|
| 955 |
+
isinstance(expr, (int, sympy.Symbol, sympy.Number))
|
| 956 |
+
or expr.is_number
|
| 957 |
+
or expr.is_symbol
|
| 958 |
+
):
|
| 959 |
+
return expr
|
| 960 |
+
expr = self.remove_precomputed_replacements(expr)
|
| 961 |
+
if expr not in self.precomputed_replacements:
|
| 962 |
+
sym = sympy_index_symbol_with_prefix(
|
| 963 |
+
SymT.PRECOMPUTED_SIZE, len(self.precomputed_replacements)
|
| 964 |
+
)
|
| 965 |
+
self.precomputed_replacements[expr] = sym
|
| 966 |
+
self.inv_precomputed_replacements[sym] = expr
|
| 967 |
+
return self.precomputed_replacements[expr]
|
| 968 |
+
|
| 969 |
+
def free_symbols(self) -> OrderedSet[sympy.Symbol]:
|
| 970 |
+
return OrderedSet(self.backed_var_to_val.keys()) - OrderedSet(
|
| 971 |
+
self.replacements.keys()
|
| 972 |
+
)
|
| 973 |
+
|
| 974 |
+
def combine_modular_indexing_pairs(self, index: sympy.Expr) -> sympy.Expr:
|
| 975 |
+
"""
|
| 976 |
+
A pair of special ModularIndexing can be combined.
|
| 977 |
+
|
| 978 |
+
E.g. ModularIndexing(ModularIndexing(x, 1, a), 1, b)
|
| 979 |
+
We can simplify this to ModuleIndexing(x, 1, b), if
|
| 980 |
+
1. x is non negative integer
|
| 981 |
+
2. a and b are positive integers
|
| 982 |
+
3. a is a multiple of b.
|
| 983 |
+
"""
|
| 984 |
+
|
| 985 |
+
def _check_args(x, div, mod, is_first):
|
| 986 |
+
if not isinstance(div, sympy.Integer) or not isinstance(mod, sympy.Integer):
|
| 987 |
+
return False
|
| 988 |
+
if div != 1:
|
| 989 |
+
return False
|
| 990 |
+
if mod <= 0:
|
| 991 |
+
return False
|
| 992 |
+
|
| 993 |
+
if is_first:
|
| 994 |
+
# first ModularIndexing should contains a nested ModularIndex
|
| 995 |
+
if not isinstance(x, ModularIndexing):
|
| 996 |
+
return False
|
| 997 |
+
else:
|
| 998 |
+
# second ModularIndexing should contains a non-negative
|
| 999 |
+
# symbol
|
| 1000 |
+
if not isinstance(x, sympy.Symbol) or not self.statically_known_geq(
|
| 1001 |
+
x, 0
|
| 1002 |
+
):
|
| 1003 |
+
return False
|
| 1004 |
+
return True
|
| 1005 |
+
|
| 1006 |
+
if isinstance(index, ModularIndexing):
|
| 1007 |
+
x, div, mod = index.args
|
| 1008 |
+
|
| 1009 |
+
if not _check_args(x, div, mod, True):
|
| 1010 |
+
return index
|
| 1011 |
+
|
| 1012 |
+
x2, div2, mod2 = x.args
|
| 1013 |
+
|
| 1014 |
+
if not _check_args(x2, div2, mod2, False):
|
| 1015 |
+
return index
|
| 1016 |
+
|
| 1017 |
+
if Mod(mod2, mod) != 0:
|
| 1018 |
+
return index
|
| 1019 |
+
|
| 1020 |
+
return ModularIndexing(x2, 1, mod)
|
| 1021 |
+
|
| 1022 |
+
return index
|
| 1023 |
+
|
| 1024 |
+
def expand_floor_div(
|
| 1025 |
+
self, index: sympy.Expr
|
| 1026 |
+
) -> bool | tuple[sympy.Expr, sympy.Expr]:
|
| 1027 |
+
"""
|
| 1028 |
+
Expand the FloorDiv to the entire expression so that the expression may
|
| 1029 |
+
be simplified.
|
| 1030 |
+
|
| 1031 |
+
E.g., for a 2D contiguous tensor with shape [a, 2 * b], and index variables
|
| 1032 |
+
x1, x2, index expression 'x1 * 2b + x2' can be easily combined.
|
| 1033 |
+
But index expression 'x1 * b + x2 // 2' can not.
|
| 1034 |
+
By expanding the FloorDiv to the entire expression, we get
|
| 1035 |
+
'(x1 * 2b + x2) // 2'. This transformation allows us to merge loops
|
| 1036 |
+
for the numerator!
|
| 1037 |
+
|
| 1038 |
+
Return false if this optimization can be applied;
|
| 1039 |
+
Return the new expression and the denominator otherwise.
|
| 1040 |
+
The original expression will be equivalent to 'new_expression // denominator'
|
| 1041 |
+
"""
|
| 1042 |
+
if not isinstance(index, sympy.Add):
|
| 1043 |
+
return False
|
| 1044 |
+
terms = index.args
|
| 1045 |
+
|
| 1046 |
+
if len(terms) < 2:
|
| 1047 |
+
return False
|
| 1048 |
+
floor_div_index = -1
|
| 1049 |
+
varlist = []
|
| 1050 |
+
factorlist = []
|
| 1051 |
+
for idx, term in enumerate(terms):
|
| 1052 |
+
if isinstance(term, sympy.Mul):
|
| 1053 |
+
# For dynamic shape, term like '2*s1*x1' has 3 child nodes.
|
| 1054 |
+
# - A integer for 2
|
| 1055 |
+
# - A symbol for s1
|
| 1056 |
+
# - A symbol for x1
|
| 1057 |
+
# Skip for now.
|
| 1058 |
+
if len(term.args) != 2:
|
| 1059 |
+
return False
|
| 1060 |
+
factor, var = term.args
|
| 1061 |
+
varlist.append(var)
|
| 1062 |
+
factorlist.append(factor)
|
| 1063 |
+
if not isinstance(factor, sympy.Integer) or not isinstance(
|
| 1064 |
+
var, sympy.Symbol
|
| 1065 |
+
):
|
| 1066 |
+
return False
|
| 1067 |
+
# It's easier to reason about the correceness of the transformation
|
| 1068 |
+
# for non-negative integers.
|
| 1069 |
+
if not self.statically_known_geq(var, 0):
|
| 1070 |
+
return False
|
| 1071 |
+
elif isinstance(term, FloorDiv):
|
| 1072 |
+
var, factor = term.args
|
| 1073 |
+
if not isinstance(factor, sympy.Integer) or not isinstance(
|
| 1074 |
+
var, sympy.Symbol
|
| 1075 |
+
):
|
| 1076 |
+
return False
|
| 1077 |
+
if not self.statically_known_geq(var, 0):
|
| 1078 |
+
return False
|
| 1079 |
+
if floor_div_index >= 0:
|
| 1080 |
+
# can not handle multi FloorDiv yet
|
| 1081 |
+
return False
|
| 1082 |
+
|
| 1083 |
+
floor_div_index = idx
|
| 1084 |
+
varlist.append(var)
|
| 1085 |
+
# this factor is denominator
|
| 1086 |
+
factorlist.append(factor)
|
| 1087 |
+
else:
|
| 1088 |
+
return False
|
| 1089 |
+
|
| 1090 |
+
if floor_div_index < 0:
|
| 1091 |
+
return False
|
| 1092 |
+
|
| 1093 |
+
# Construct the new expression and remember the denominator
|
| 1094 |
+
denominator = factorlist[floor_div_index]
|
| 1095 |
+
new_index = sympy.S.Zero
|
| 1096 |
+
|
| 1097 |
+
for var, factor, idx in zip(varlist, factorlist, itertools.count()):
|
| 1098 |
+
if idx == floor_div_index:
|
| 1099 |
+
new_index += var
|
| 1100 |
+
else:
|
| 1101 |
+
new_index += (factor * denominator) * var
|
| 1102 |
+
|
| 1103 |
+
return new_index, denominator
|
| 1104 |
+
|
| 1105 |
+
|
| 1106 |
+
def join_dimensions(expr: Expr) -> Expr:
|
| 1107 |
+
if not isinstance(expr, sympy.Add) or not expr.has(ModularIndexing):
|
| 1108 |
+
return expr # fast exit path
|
| 1109 |
+
return _join_dimensions_cached(expr)
|
| 1110 |
+
|
| 1111 |
+
|
| 1112 |
+
@functools.lru_cache(256)
|
| 1113 |
+
def _join_dimensions_cached(expr: Expr) -> Expr:
|
| 1114 |
+
"""
|
| 1115 |
+
ModularIndexing(i0, 1, 32) + 32 * ModularIndexing(i0, 32, 4)
|
| 1116 |
+
becomes
|
| 1117 |
+
ModularIndexing(i0, 1, 128)
|
| 1118 |
+
ModularIndexing(i0, 1, 32) + 32 * FloorDiv(i0, 32)
|
| 1119 |
+
becomes i0
|
| 1120 |
+
|
| 1121 |
+
|
| 1122 |
+
This type of pattern can come from view operations
|
| 1123 |
+
"""
|
| 1124 |
+
assert isinstance(expr, sympy.Add)
|
| 1125 |
+
|
| 1126 |
+
scale = sympy.Wild("scale", exclude=[0], integer=True)
|
| 1127 |
+
base = sympy.Wild("base", integer=True)
|
| 1128 |
+
divisor = sympy.Wild("divisor", integer=True)
|
| 1129 |
+
mod1 = sympy.Wild("modulus", integer=True)
|
| 1130 |
+
mod2 = sympy.Wild("modulus2", integer=True)
|
| 1131 |
+
for term1 in expr.args:
|
| 1132 |
+
m1 = term1.match(scale * ModularIndexing(base, divisor, mod1))
|
| 1133 |
+
if m1:
|
| 1134 |
+
for term2 in expr.args:
|
| 1135 |
+
m2 = term2.match(
|
| 1136 |
+
m1[scale]
|
| 1137 |
+
* m1[mod1]
|
| 1138 |
+
* ModularIndexing(m1[base], m1[divisor] * m1[mod1], mod2)
|
| 1139 |
+
)
|
| 1140 |
+
if m2 and term1 != term2:
|
| 1141 |
+
expr = join_dimensions(
|
| 1142 |
+
expr
|
| 1143 |
+
- term1
|
| 1144 |
+
- term2
|
| 1145 |
+
+ m1[scale]
|
| 1146 |
+
* ModularIndexing(m1[base], m1[divisor], m1[mod1] * m2[mod2])
|
| 1147 |
+
)
|
| 1148 |
+
return expr
|
| 1149 |
+
for term1 in expr.args:
|
| 1150 |
+
m1 = term1.match(scale * ModularIndexing(base, divisor, mod1))
|
| 1151 |
+
if m1:
|
| 1152 |
+
for term2 in expr.args:
|
| 1153 |
+
m2 = term2.match(
|
| 1154 |
+
m1[scale] * m1[mod1] * FloorDiv(m1[base], m1[divisor] * m1[mod1])
|
| 1155 |
+
)
|
| 1156 |
+
if m2 is not None: # in case of success we get an empty dict here
|
| 1157 |
+
expr = join_dimensions(
|
| 1158 |
+
expr
|
| 1159 |
+
- term1
|
| 1160 |
+
- term2
|
| 1161 |
+
+ m1[scale] * FloorDiv(m1[base], m1[divisor])
|
| 1162 |
+
)
|
| 1163 |
+
return expr
|
| 1164 |
+
return expr
|
| 1165 |
+
|
| 1166 |
+
|
| 1167 |
+
class SimplifyIndexing(V.WrapperHandler): # type: ignore[name-defined]
|
| 1168 |
+
"""
|
| 1169 |
+
A wrapper around .virtualize.ops that uses var range information to
|
| 1170 |
+
simplify ModularIndexing/FloorDiv.
|
| 1171 |
+
"""
|
| 1172 |
+
|
| 1173 |
+
def __init__(self, inner, var_ranges: VarRanges) -> None:
|
| 1174 |
+
super().__init__(inner)
|
| 1175 |
+
self.name = "SimplifyIndexing"
|
| 1176 |
+
self._simplify: Callable[[Expr], Expr] = (
|
| 1177 |
+
lambda index: V.graph.sizevars.simplify_with_ranges(index, var_ranges)
|
| 1178 |
+
)
|
| 1179 |
+
|
| 1180 |
+
def load(self, name: str, index: sympy.Expr):
|
| 1181 |
+
return self._inner.load(name, self._simplify(index))
|
| 1182 |
+
|
| 1183 |
+
def store(self, name, index, value, mode=None):
|
| 1184 |
+
return self._inner.store(name, self._simplify(index), value, mode=mode)
|
| 1185 |
+
|
| 1186 |
+
def store_reduction(self, name, index, value):
|
| 1187 |
+
return self._inner.store_reduction(name, self._simplify(index), value)
|
| 1188 |
+
|
| 1189 |
+
def index_expr(self, index, dtype):
|
| 1190 |
+
return self._inner.index_expr(self._simplify(index), dtype)
|
| 1191 |
+
|
| 1192 |
+
def check_bounds(self, index, size, lower, upper):
|
| 1193 |
+
return self._inner.check_bounds(self._simplify(index), size, lower, upper)
|
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/standalone_compile.py
ADDED
|
@@ -0,0 +1,491 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import contextlib
|
| 4 |
+
import copy
|
| 5 |
+
import logging
|
| 6 |
+
import os
|
| 7 |
+
import pickle
|
| 8 |
+
import shutil
|
| 9 |
+
from abc import ABC, abstractmethod
|
| 10 |
+
from contextlib import AbstractContextManager, nullcontext
|
| 11 |
+
from typing import Any, Literal, TYPE_CHECKING
|
| 12 |
+
|
| 13 |
+
import torch.fx
|
| 14 |
+
from torch._dynamo.aot_compile_types import BundledAOTAutogradSerializableCallable
|
| 15 |
+
from torch._dynamo.utils import dynamo_timed
|
| 16 |
+
from torch._inductor.cpp_builder import normalize_path_separator
|
| 17 |
+
from torch._inductor.cudagraph_utils import BoxedDeviceIndex
|
| 18 |
+
from torch._inductor.runtime.cache_dir_utils import temporary_cache_dir
|
| 19 |
+
from torch._inductor.utils import BoxedBool, InputType
|
| 20 |
+
from torch._subclasses import FakeTensorMode
|
| 21 |
+
from torch.fx.experimental.symbolic_shapes import ShapeEnv
|
| 22 |
+
|
| 23 |
+
from . import config
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
if TYPE_CHECKING:
|
| 27 |
+
from collections.abc import Callable, Sequence
|
| 28 |
+
|
| 29 |
+
from torch.compiler._cache import CacheInfo
|
| 30 |
+
from torch.fx import GraphModule
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
log = logging.getLogger(__name__)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class CompiledArtifact(ABC):
|
| 37 |
+
"""
|
| 38 |
+
CompiledArtifact class represents the inductor cache artifacts that
|
| 39 |
+
can be invoked in order to avoid repeated compilation.
|
| 40 |
+
|
| 41 |
+
CompiledArtifact can be obtained by calling standalone_compile(gm, example_inputs)
|
| 42 |
+
to create a fresh CompiledArtifact from a GraphModule and example inputs.
|
| 43 |
+
|
| 44 |
+
Later this CompiledArtifact can be saved to disk, either as a binary or unpacked
|
| 45 |
+
into the provided folder via the CompiledArtifact.save function.
|
| 46 |
+
|
| 47 |
+
CompiledArtifact.load provides a way to create a CompiledArtifact from the
|
| 48 |
+
binary or unpacked data.
|
| 49 |
+
|
| 50 |
+
Finally, the CompiledArtifact can be invoked via the __call__ method
|
| 51 |
+
to execute the cached artifact.
|
| 52 |
+
"""
|
| 53 |
+
|
| 54 |
+
def __init__(
|
| 55 |
+
self,
|
| 56 |
+
compiled_fn: Callable[..., Any],
|
| 57 |
+
artifacts: tuple[bytes, CacheInfo] | None,
|
| 58 |
+
):
|
| 59 |
+
self._compiled_fn = compiled_fn
|
| 60 |
+
self._artifacts = artifacts
|
| 61 |
+
|
| 62 |
+
@abstractmethod
|
| 63 |
+
def __call__(self, *args: Any) -> Any: ...
|
| 64 |
+
|
| 65 |
+
@abstractmethod
|
| 66 |
+
def save(
|
| 67 |
+
self, *, path: str, format: Literal["binary", "unpacked"] = "binary"
|
| 68 |
+
) -> None: ...
|
| 69 |
+
|
| 70 |
+
@staticmethod
|
| 71 |
+
def load(
|
| 72 |
+
*, path: str, format: Literal["binary", "unpacked"] = "binary"
|
| 73 |
+
) -> CompiledArtifact:
|
| 74 |
+
if format == "unpacked":
|
| 75 |
+
# If format is unpacked, it must be a CacheCompiledArtifact
|
| 76 |
+
return CacheCompiledArtifact.load(path=path, format=format)
|
| 77 |
+
|
| 78 |
+
assert format == "binary"
|
| 79 |
+
with open(path, "rb") as file:
|
| 80 |
+
from torch.utils._appending_byte_serializer import BytesReader
|
| 81 |
+
|
| 82 |
+
from .codecache import torch_key
|
| 83 |
+
|
| 84 |
+
result_bytes = file.read()
|
| 85 |
+
reader = BytesReader(result_bytes)
|
| 86 |
+
header = reader.read_bytes()
|
| 87 |
+
if header == AOTCompiledArtifact.AOT_HEADER:
|
| 88 |
+
assert reader.read_bytes() == torch_key()
|
| 89 |
+
artifact = reader.read_bytes()
|
| 90 |
+
assert reader.is_finished()
|
| 91 |
+
return AOTCompiledArtifact.deserialize(artifact)
|
| 92 |
+
# Otherwise, it's in the CacheCompiledArtifact format
|
| 93 |
+
elif header == CacheCompiledArtifact.CACHE_HEADER:
|
| 94 |
+
assert reader.read_bytes() == torch_key()
|
| 95 |
+
key = reader.read_str()
|
| 96 |
+
artifact_bytes = reader.read_bytes()
|
| 97 |
+
assert reader.is_finished()
|
| 98 |
+
torch.compiler.load_cache_artifacts(artifact_bytes)
|
| 99 |
+
return CacheCompiledArtifact._load_impl(nullcontext(), key)
|
| 100 |
+
else:
|
| 101 |
+
raise RuntimeError(
|
| 102 |
+
"Invalid header, expected CacheCompiledArtifact or AOTCompiledArtifact, got: "
|
| 103 |
+
+ header.decode("utf-8")
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
class CacheCompiledArtifact(CompiledArtifact):
|
| 108 |
+
"""
|
| 109 |
+
CompiledArtifact that depends on torch.compiler.save_cache_artifacts
|
| 110 |
+
"""
|
| 111 |
+
|
| 112 |
+
CACHE_HEADER = bytes("CacheCompiledArtifact", "utf-8")
|
| 113 |
+
|
| 114 |
+
def __init__(
|
| 115 |
+
self,
|
| 116 |
+
compiled_fn: Callable[..., Any],
|
| 117 |
+
artifacts: tuple[bytes, CacheInfo] | None,
|
| 118 |
+
):
|
| 119 |
+
self._compiled_fn = compiled_fn
|
| 120 |
+
self._artifacts = artifacts
|
| 121 |
+
|
| 122 |
+
def __call__(self, *args: Any) -> Any:
|
| 123 |
+
return self._compiled_fn(*args)
|
| 124 |
+
|
| 125 |
+
def is_saveable(self) -> bool:
|
| 126 |
+
if self._artifacts is None:
|
| 127 |
+
return False
|
| 128 |
+
_, cache_info = self._artifacts
|
| 129 |
+
# 0 means nothing was saved
|
| 130 |
+
# >1 means multiple artifacts were saved, which is concerning
|
| 131 |
+
# (we only expect one)
|
| 132 |
+
return len(cache_info.aot_autograd_artifacts) == 1
|
| 133 |
+
|
| 134 |
+
def save(
|
| 135 |
+
self, *, path: str, format: Literal["binary", "unpacked"] = "binary"
|
| 136 |
+
) -> None:
|
| 137 |
+
with dynamo_timed("CompiledArtifact.save"):
|
| 138 |
+
if self._artifacts is None:
|
| 139 |
+
raise RuntimeError(
|
| 140 |
+
"CompiledArtifact.save failed to save since there's no artifact to save"
|
| 141 |
+
)
|
| 142 |
+
artifact_bytes, cache_info = self._artifacts
|
| 143 |
+
if len(cache_info.aot_autograd_artifacts) == 0:
|
| 144 |
+
raise RuntimeError(
|
| 145 |
+
f"CompiledArtifact.save failed to save due to no aot_autograd artifacts. "
|
| 146 |
+
f"This likely means there was something that was not serializable in the "
|
| 147 |
+
f"graph passed to standalone_compile. This can generally be fixed by "
|
| 148 |
+
f"ensuring that your model only uses constructs that are serializable. "
|
| 149 |
+
f"{cache_info}"
|
| 150 |
+
)
|
| 151 |
+
if len(cache_info.aot_autograd_artifacts) > 1:
|
| 152 |
+
raise AssertionError(
|
| 153 |
+
f"CompiledArtifact.save failed to save because there was more than one "
|
| 154 |
+
f"artifact but we only expected one. {cache_info}"
|
| 155 |
+
)
|
| 156 |
+
key = cache_info.aot_autograd_artifacts[0]
|
| 157 |
+
|
| 158 |
+
if format == "binary":
|
| 159 |
+
# can't assert that it is a file since it might not exist yet
|
| 160 |
+
assert not os.path.isdir(path)
|
| 161 |
+
|
| 162 |
+
from torch.utils._appending_byte_serializer import BytesWriter
|
| 163 |
+
|
| 164 |
+
from .codecache import torch_key
|
| 165 |
+
|
| 166 |
+
writer = BytesWriter()
|
| 167 |
+
writer.write_bytes(CacheCompiledArtifact.CACHE_HEADER)
|
| 168 |
+
writer.write_bytes(torch_key())
|
| 169 |
+
writer.write_str(key)
|
| 170 |
+
writer.write_bytes(artifact_bytes)
|
| 171 |
+
|
| 172 |
+
from torch._inductor.codecache import write_atomic
|
| 173 |
+
|
| 174 |
+
write_atomic(path, writer.to_bytes())
|
| 175 |
+
else:
|
| 176 |
+
assert format == "unpacked"
|
| 177 |
+
if os.path.exists(path):
|
| 178 |
+
assert os.path.isdir(path)
|
| 179 |
+
shutil.rmtree(path, ignore_errors=True)
|
| 180 |
+
|
| 181 |
+
from .codecache import FxGraphCache
|
| 182 |
+
|
| 183 |
+
with temporary_cache_dir(path):
|
| 184 |
+
# This function unpacks the cache artifacts to disk
|
| 185 |
+
loaded_cache_info = torch.compiler.load_cache_artifacts(
|
| 186 |
+
artifact_bytes
|
| 187 |
+
)
|
| 188 |
+
assert loaded_cache_info is not None
|
| 189 |
+
# Now write all the output_code artifacts to disk so that
|
| 190 |
+
# they can be inspected and modified
|
| 191 |
+
for key in loaded_cache_info.inductor_artifacts:
|
| 192 |
+
subdir = FxGraphCache._get_tmp_dir_for_key(key)
|
| 193 |
+
assert os.path.exists(subdir)
|
| 194 |
+
for path in sorted(os.listdir(subdir)):
|
| 195 |
+
with open(os.path.join(subdir, path), "rb") as f:
|
| 196 |
+
graph = pickle.load(f)
|
| 197 |
+
output_file = graph.write_to_disk()
|
| 198 |
+
log.info("Output code written to: %s", output_file)
|
| 199 |
+
|
| 200 |
+
@staticmethod
|
| 201 |
+
def _load_impl(
|
| 202 |
+
cache_dir_ctx: AbstractContextManager[Any], key: str
|
| 203 |
+
) -> CompiledArtifact:
|
| 204 |
+
with (
|
| 205 |
+
cache_dir_ctx,
|
| 206 |
+
config.patch(unsafe_skip_cache_dynamic_shape_guards=True),
|
| 207 |
+
):
|
| 208 |
+
with torch._functorch.config.patch(strict_autograd_cache=True):
|
| 209 |
+
from torch._functorch._aot_autograd.autograd_cache import (
|
| 210 |
+
AOTAutogradCache,
|
| 211 |
+
)
|
| 212 |
+
|
| 213 |
+
result = AOTAutogradCache._lookup(
|
| 214 |
+
key,
|
| 215 |
+
local=True,
|
| 216 |
+
remote=False,
|
| 217 |
+
args=[],
|
| 218 |
+
cache_info={},
|
| 219 |
+
aot_config=None,
|
| 220 |
+
)
|
| 221 |
+
|
| 222 |
+
assert result is not None
|
| 223 |
+
(entry, _) = result
|
| 224 |
+
|
| 225 |
+
from .compile_fx import _CompileFxKwargs
|
| 226 |
+
|
| 227 |
+
fx_config = _CompileFxKwargs(
|
| 228 |
+
cudagraphs=BoxedBool(False),
|
| 229 |
+
boxed_forward_device_index=BoxedDeviceIndex(0),
|
| 230 |
+
)
|
| 231 |
+
|
| 232 |
+
context = torch._guards.TracingContext(FakeTensorMode(shape_env=ShapeEnv()))
|
| 233 |
+
with torch._guards.tracing(context):
|
| 234 |
+
compiled_fn = entry.wrap_post_compile(
|
| 235 |
+
[], entry.sanitized_aot_config, fx_config
|
| 236 |
+
)
|
| 237 |
+
return CacheCompiledArtifact(lambda *args: compiled_fn(list(args)), None)
|
| 238 |
+
|
| 239 |
+
@staticmethod
|
| 240 |
+
def _prepare_load(
|
| 241 |
+
*, path: str, format: Literal["binary", "unpacked"] = "binary"
|
| 242 |
+
) -> tuple[str, AbstractContextManager[Any]]:
|
| 243 |
+
"""
|
| 244 |
+
Do format specific prep and loads, return a context manager and key
|
| 245 |
+
"""
|
| 246 |
+
path = normalize_path_separator(path)
|
| 247 |
+
with dynamo_timed("CompiledArtifact.load"):
|
| 248 |
+
if format == "binary":
|
| 249 |
+
# can't assert that it is a file since it might not exist yet
|
| 250 |
+
assert not os.path.isdir(path)
|
| 251 |
+
with open(path, "rb") as file:
|
| 252 |
+
artifacts = file.read()
|
| 253 |
+
from torch.utils._appending_byte_serializer import BytesReader
|
| 254 |
+
|
| 255 |
+
from .codecache import torch_key
|
| 256 |
+
|
| 257 |
+
reader = BytesReader(artifacts)
|
| 258 |
+
assert reader.read_bytes() == torch_key()
|
| 259 |
+
key = reader.read_str()
|
| 260 |
+
artifact_bytes = reader.read_bytes()
|
| 261 |
+
assert reader.is_finished()
|
| 262 |
+
|
| 263 |
+
torch.compiler.load_cache_artifacts(artifact_bytes)
|
| 264 |
+
return key, nullcontext()
|
| 265 |
+
else:
|
| 266 |
+
assert format == "unpacked"
|
| 267 |
+
assert os.path.isdir(path)
|
| 268 |
+
autograd_cache_dir = os.path.join(path, "aotautograd")
|
| 269 |
+
assert os.path.isdir(autograd_cache_dir)
|
| 270 |
+
files = list(os.listdir(autograd_cache_dir))
|
| 271 |
+
assert len(files) == 1
|
| 272 |
+
key = files[0]
|
| 273 |
+
cache_dir_ctx = temporary_cache_dir(path)
|
| 274 |
+
return key, cache_dir_ctx
|
| 275 |
+
|
| 276 |
+
@staticmethod
|
| 277 |
+
def load(
|
| 278 |
+
*, path: str, format: Literal["binary", "unpacked"] = "binary"
|
| 279 |
+
) -> CompiledArtifact:
|
| 280 |
+
key, cache_dir_ctx = CacheCompiledArtifact._prepare_load(
|
| 281 |
+
path=path, format=format
|
| 282 |
+
)
|
| 283 |
+
return CacheCompiledArtifact._load_impl(cache_dir_ctx, key)
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
class AOTCompiledArtifact(CompiledArtifact):
|
| 287 |
+
"""
|
| 288 |
+
Similar to CompiledArtifact, but the object is a single, bundled precompiled function.
|
| 289 |
+
This object is always a serializable callable function.
|
| 290 |
+
|
| 291 |
+
This object is essentially a wrapper for BundledAOTAutogradSerializableCallable, which
|
| 292 |
+
is used by torch._dynamo.aot_compile for AOT Precompilation.
|
| 293 |
+
"""
|
| 294 |
+
|
| 295 |
+
AOT_HEADER = bytes("AOTCompiledArtifact", "utf-8")
|
| 296 |
+
|
| 297 |
+
def __init__(
|
| 298 |
+
self,
|
| 299 |
+
compiled_fn: Callable[..., Any],
|
| 300 |
+
):
|
| 301 |
+
self.inner_fn = BundledAOTAutogradSerializableCallable(compiled_fn)
|
| 302 |
+
self._artifacts = (
|
| 303 |
+
None # We don't need artifacts, the inner object handles everything
|
| 304 |
+
)
|
| 305 |
+
|
| 306 |
+
@staticmethod
|
| 307 |
+
def from_bundled_callable(
|
| 308 |
+
bundled_fn: BundledAOTAutogradSerializableCallable,
|
| 309 |
+
) -> AOTCompiledArtifact:
|
| 310 |
+
return AOTCompiledArtifact(bundled_fn.compiled_fn)
|
| 311 |
+
|
| 312 |
+
def __call__(self, *args: Any) -> Any:
|
| 313 |
+
return self.inner_fn(*args)
|
| 314 |
+
|
| 315 |
+
def save(
|
| 316 |
+
self, *, path: str, format: Literal["binary", "unpacked"] = "binary"
|
| 317 |
+
) -> None:
|
| 318 |
+
if format == "unpacked":
|
| 319 |
+
raise RuntimeError(
|
| 320 |
+
"AOTCompiledArtifact does not support unpacked format yet"
|
| 321 |
+
)
|
| 322 |
+
result_bytes = self.serialize()
|
| 323 |
+
from torch.utils._appending_byte_serializer import BytesWriter
|
| 324 |
+
|
| 325 |
+
from .codecache import torch_key
|
| 326 |
+
|
| 327 |
+
writer = BytesWriter()
|
| 328 |
+
writer.write_bytes(AOTCompiledArtifact.AOT_HEADER)
|
| 329 |
+
writer.write_bytes(torch_key())
|
| 330 |
+
writer.write_bytes(result_bytes)
|
| 331 |
+
|
| 332 |
+
from torch._inductor.codecache import write_atomic
|
| 333 |
+
|
| 334 |
+
# Save a sentinel file to indicate that this is AOT
|
| 335 |
+
write_atomic(path, writer.to_bytes())
|
| 336 |
+
|
| 337 |
+
def serialize(self) -> bytes:
|
| 338 |
+
return BundledAOTAutogradSerializableCallable.serialize_compile_artifacts(
|
| 339 |
+
self.inner_fn
|
| 340 |
+
)
|
| 341 |
+
|
| 342 |
+
@staticmethod
|
| 343 |
+
def deserialize(result_bytes: bytes) -> AOTCompiledArtifact:
|
| 344 |
+
deserialized = (
|
| 345 |
+
BundledAOTAutogradSerializableCallable.deserialize_compile_artifacts(
|
| 346 |
+
result_bytes
|
| 347 |
+
)
|
| 348 |
+
)
|
| 349 |
+
assert isinstance(deserialized, BundledAOTAutogradSerializableCallable)
|
| 350 |
+
return AOTCompiledArtifact.from_bundled_callable(deserialized)
|
| 351 |
+
|
| 352 |
+
@staticmethod
|
| 353 |
+
def load(
|
| 354 |
+
*, path: str, format: Literal["binary", "unpacked"] = "binary"
|
| 355 |
+
) -> CompiledArtifact:
|
| 356 |
+
if format == "unpacked":
|
| 357 |
+
raise RuntimeError(
|
| 358 |
+
"AOTCompiledArtifact does not support unpacked format yet"
|
| 359 |
+
)
|
| 360 |
+
with open(path, "rb") as file:
|
| 361 |
+
from torch.utils._appending_byte_serializer import BytesReader
|
| 362 |
+
|
| 363 |
+
from .codecache import torch_key
|
| 364 |
+
|
| 365 |
+
result_bytes = file.read()
|
| 366 |
+
reader = BytesReader(result_bytes)
|
| 367 |
+
header = reader.read_bytes()
|
| 368 |
+
assert header == AOTCompiledArtifact.AOT_HEADER
|
| 369 |
+
assert reader.read_bytes() == torch_key()
|
| 370 |
+
artifact = reader.read_bytes()
|
| 371 |
+
assert reader.is_finished()
|
| 372 |
+
return AOTCompiledArtifact.deserialize(artifact)
|
| 373 |
+
|
| 374 |
+
|
| 375 |
+
def _resolve_ignore_shape_env(dynamic_shapes: Any):
|
| 376 |
+
# tells compile_fx to ignore the shape_envs on the ambient context
|
| 377 |
+
# and the graph_module.
|
| 378 |
+
return dynamic_shapes == "from_example_inputs"
|
| 379 |
+
|
| 380 |
+
|
| 381 |
+
def _resolve_fake_mode(gm: GraphModule, dynamic_shapes: Any) -> FakeTensorMode:
|
| 382 |
+
if dynamic_shapes == "from_example_inputs":
|
| 383 |
+
return FakeTensorMode(shape_env=ShapeEnv())
|
| 384 |
+
elif dynamic_shapes == "from_tracing_context":
|
| 385 |
+
# Reuse fake_mode from the TracingContext.
|
| 386 |
+
# NB: The TracingContext only exists if we're currently in a torch.compile backend.
|
| 387 |
+
context = torch._guards.TracingContext.get()
|
| 388 |
+
assert context.fake_mode is not None
|
| 389 |
+
return context.fake_mode
|
| 390 |
+
elif dynamic_shapes == "from_graph":
|
| 391 |
+
# Strategy: find a FakeTensor in the graph output, grab its FakeTensorMode.
|
| 392 |
+
# The graph passed to standalone_compile must be an Inductor-approved graph,
|
| 393 |
+
# which means that there is at least one Tensor output and the output node
|
| 394 |
+
# contains a flat list of Tensors.
|
| 395 |
+
last_node = next(iter(reversed(gm.graph.nodes)))
|
| 396 |
+
assert last_node.op == "output"
|
| 397 |
+
assert len(last_node.args) == 1
|
| 398 |
+
|
| 399 |
+
# If gm came from Dynamo, then last_node.args[0] is always a list,
|
| 400 |
+
# even in single-Tensor returns.
|
| 401 |
+
#
|
| 402 |
+
# It's possible to get into a situation where last_node.args[0]
|
| 403 |
+
# is a Node (and not a list!). This happens if you call split_module
|
| 404 |
+
# on the graph. We allow for this case since it is common.
|
| 405 |
+
nodes = (
|
| 406 |
+
[last_node.args[0]]
|
| 407 |
+
if isinstance(last_node.args[0], torch.fx.Node)
|
| 408 |
+
else last_node.args[0]
|
| 409 |
+
)
|
| 410 |
+
for node in nodes:
|
| 411 |
+
if "example_value" in node.meta:
|
| 412 |
+
maybe_tensor = node.meta["example_value"]
|
| 413 |
+
if isinstance(maybe_tensor, torch._subclasses.fake_tensor.FakeTensor):
|
| 414 |
+
return maybe_tensor.fake_mode
|
| 415 |
+
|
| 416 |
+
return FakeTensorMode(shape_env=ShapeEnv())
|
| 417 |
+
else:
|
| 418 |
+
raise ValueError(
|
| 419 |
+
f"standalone_compile got unsupported `dynamic_shapes` value: dynamic_shapes={dynamic_shapes}."
|
| 420 |
+
)
|
| 421 |
+
|
| 422 |
+
|
| 423 |
+
@contextlib.contextmanager
|
| 424 |
+
def _standalone_context(gm: GraphModule, dynamic_shapes: Any, aot: bool):
|
| 425 |
+
from torch.compiler._cache import CacheArtifactManager
|
| 426 |
+
|
| 427 |
+
fake_mode = _resolve_fake_mode(gm, dynamic_shapes)
|
| 428 |
+
tracing_context = torch._guards.TracingContext(fake_mode)
|
| 429 |
+
with (
|
| 430 |
+
torch._guards.tracing(tracing_context),
|
| 431 |
+
CacheArtifactManager.with_fresh_cache(),
|
| 432 |
+
config.patch("triton.autotune_at_compile_time", True),
|
| 433 |
+
torch._functorch.config.patch("bundled_autograd_cache", aot),
|
| 434 |
+
):
|
| 435 |
+
yield
|
| 436 |
+
|
| 437 |
+
|
| 438 |
+
def standalone_compile(
|
| 439 |
+
gm: GraphModule,
|
| 440 |
+
example_inputs: Sequence[InputType],
|
| 441 |
+
*,
|
| 442 |
+
dynamic_shapes: Any,
|
| 443 |
+
options: Any,
|
| 444 |
+
aot: bool = False, # AOT mode, which uses BundledAOTAutogradCache
|
| 445 |
+
donate_graph_module: bool = False,
|
| 446 |
+
) -> CompiledArtifact:
|
| 447 |
+
"""
|
| 448 |
+
Implementation of torch.inductor.standalone_compile
|
| 449 |
+
"""
|
| 450 |
+
from .compile_fx import compile_fx
|
| 451 |
+
|
| 452 |
+
ignore_shape_env = _resolve_ignore_shape_env(dynamic_shapes)
|
| 453 |
+
with _standalone_context(gm, dynamic_shapes, aot):
|
| 454 |
+
# compile_fx takes ownership of gm and may mutate it on cache miss.
|
| 455 |
+
if not donate_graph_module:
|
| 456 |
+
gm = copy.deepcopy(gm)
|
| 457 |
+
compiled_fn = compile_fx(
|
| 458 |
+
gm, example_inputs, ignore_shape_env=ignore_shape_env, **options
|
| 459 |
+
)
|
| 460 |
+
assert callable(compiled_fn)
|
| 461 |
+
if aot:
|
| 462 |
+
if not hasattr(compiled_fn, "serialize"):
|
| 463 |
+
raise RuntimeError(
|
| 464 |
+
"Compiled function should have serialize method when aot=True"
|
| 465 |
+
)
|
| 466 |
+
return AOTCompiledArtifact(compiled_fn)
|
| 467 |
+
artifacts = torch.compiler.save_cache_artifacts()
|
| 468 |
+
if artifacts is None:
|
| 469 |
+
log.warning(
|
| 470 |
+
"standalone_compile artifact generation failed, cannot save. "
|
| 471 |
+
"Run with TORCH_LOGS=+torch._inductor.codecache to identify the problem"
|
| 472 |
+
)
|
| 473 |
+
|
| 474 |
+
return CacheCompiledArtifact(compiled_fn, artifacts)
|
| 475 |
+
|
| 476 |
+
|
| 477 |
+
def autograd_cache_key(
|
| 478 |
+
graph,
|
| 479 |
+
example_inputs,
|
| 480 |
+
dynamic_shapes: Any,
|
| 481 |
+
aot: bool = False, # AOT mode, which uses BundledAOTAutogradCache
|
| 482 |
+
):
|
| 483 |
+
from . import compile_fx
|
| 484 |
+
|
| 485 |
+
ignore_shape_env = _resolve_ignore_shape_env(dynamic_shapes)
|
| 486 |
+
with _standalone_context(graph, dynamic_shapes, aot):
|
| 487 |
+
return compile_fx.autograd_cache_key(
|
| 488 |
+
graph,
|
| 489 |
+
example_inputs,
|
| 490 |
+
ignore_shape_env=ignore_shape_env,
|
| 491 |
+
)
|
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/stream_constants.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Constants for CUDA stream naming in Inductor codegen."""
|
| 2 |
+
|
| 3 |
+
DEFAULT_STREAM: str = "default_stream"
|
| 4 |
+
DEFAULT_STREAM_IDX: int = 0
|
| 5 |
+
STREAM_NAME_TEMPLATE: str = "stream{stream_idx:d}"
|
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/stream_utils.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Stream utilities for Inductor codegen."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import functools
|
| 6 |
+
|
| 7 |
+
from torch._inductor.stream_constants import (
|
| 8 |
+
DEFAULT_STREAM,
|
| 9 |
+
DEFAULT_STREAM_IDX,
|
| 10 |
+
STREAM_NAME_TEMPLATE,
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
__all__ = [
|
| 15 |
+
"DEFAULT_STREAM",
|
| 16 |
+
"DEFAULT_STREAM_IDX",
|
| 17 |
+
"STREAM_NAME_TEMPLATE",
|
| 18 |
+
"get_raw_stream_name",
|
| 19 |
+
"get_stream_name",
|
| 20 |
+
]
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
@functools.lru_cache
|
| 24 |
+
def get_stream_name(stream_idx: int) -> str:
|
| 25 |
+
"""Generate CUDA Stream name from stream index number.
|
| 26 |
+
|
| 27 |
+
Args:
|
| 28 |
+
stream_idx: Non-negative index number. 0 refers to the default stream, others refer to side
|
| 29 |
+
streams.
|
| 30 |
+
"""
|
| 31 |
+
if stream_idx == 0:
|
| 32 |
+
return DEFAULT_STREAM
|
| 33 |
+
else:
|
| 34 |
+
return STREAM_NAME_TEMPLATE.format(stream_idx=stream_idx)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def get_raw_stream_name(device_idx: int) -> str:
|
| 38 |
+
"""Generate variable name for a raw stream handle on the given device."""
|
| 39 |
+
return f"raw_stream{device_idx}"
|