Repository navigation
Conversation
Split MlirCompiler.compile() into two phases: - Phase 1 (compile_pre_binary): all MLIR passes except gpu-module-to-binary - Phase 2 (compile_binary): only gpu-module-to-binary with waves_per_eu/maxnreg This allows the autotuner to compile Phase 1 once and reuse the pre-binary module across different compile_hints configs, avoiding redundant full recompilations (~30s each). Also adds compile_hints to the JIT cache key so different compiler options produce distinct cache entries. Addresses review feedback from PR #265: - _pre_binary_fragments derived from _pipeline_fragments (no duplication) - _make_cache_key builds on shared _make_base_cache_key helper - _pre_binary_cache guarded by use_cache flag - chip included in pre-binary cache key for multi-arch safety Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR optimizes FlyDSL autotuning by splitting JIT compilation into a reusable “pre-binary” phase (all MLIR lowering passes) and a per-config “binary” phase (final gpu-module-to-binary), reducing redundant recompilation when only backend compile hints change.
Changes:
- Adds
MlirCompiler.compile_pre_binary()andMlirCompiler.compile_binary()to support two-phase compilation driven byCompilationContextcompile hints. - Introduces
_make_base_cache_key()and extends_make_cache_key()to include__compile_hints__, enabling correct artifact caching across different tuning configurations. - Adds an in-memory
_pre_binary_cachekeyed by(chip, base_cache_key)to reuse phase-1 results across autotuning configs when runtime caching is enabled.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| all_frags = MlirCompiler._pipeline_fragments(chip=chip) | ||
| if all_frags and isinstance(all_frags[-1], str) and all_frags[-1].startswith('gpu-module-to-binary'): | ||
| return all_frags[:-1] | ||
| return all_frags |
There was a problem hiding this comment.
_pre_binary_fragments silently falls back to returning the full pipeline when the last fragment isn’t gpu-module-to-binary. That would make “pre-binary” compilation accidentally include the binary pass (and then Phase 2 would run it again), which is hard to debug. Consider raising (or at least logging) if the expected last fragment isn’t gpu-module-to-binary, mirroring _binary_fragment’s strictness.
| return all_frags | |
| raise RuntimeError('Last pipeline fragment is not gpu-module-to-binary') |
| # Two-phase compilation: reuse pre-binary module for different compile_hints. | ||
| # This avoids redundant full recompilations during autotuning when only | ||
| # waves_per_eu/maxnreg differ. | ||
| _hints = CompilationContext.get_compile_hints() |
There was a problem hiding this comment.
The two-phase path in call bypasses MlirCompiler.compile(), so debug behaviors implemented there (e.g. FLYDSL_DUMP_IR stage dumps and PassManager.enable_ir_printing via env.debug.print_after_all) won’t run when compile_hints are set. If those debug flags are expected to work during autotuning/debugging, consider disabling two-phase compilation when env.debug.dump_ir/print_after_all is enabled, or extending compile_pre_binary/compile_binary to honor the same debug pathways.
| def compile_binary(cls, pre_binary_asm: str, *, chip: str = None) -> ir.Module: | ||
| """Phase 2: Run gpu-module-to-binary on a pre-binary module ASM.""" | ||
| if chip is None: | ||
| chip = env.compile.arch or get_rocm_arch() | ||
| module = ir.Module.parse(pre_binary_asm) | ||
| frag = cls._binary_fragment(chip=chip) | ||
| pipeline = f"builtin.module({frag})" | ||
| pm = PassManager.parse(pipeline) | ||
| pm.enable_verifier(env.debug.enable_verifier) | ||
| pm.run(module.operation) |
There was a problem hiding this comment.
compile_binary parses pre_binary_asm and runs passes without verifying the parsed module first. Since pre_binary_asm may come from an in-memory cache (or could be corrupted/unsupported across versions), adding module.operation.verify() after parsing would make failures deterministic and easier to diagnose.
| self._call_state_cache = {} # cache_key -> CallState | ||
| self._sig = None # lazy: set on first call | ||
| self._mem_cache = {} | ||
| self._pre_binary_cache = {} # (chip, base_cache_key) -> (pre_binary_asm, chip) |
There was a problem hiding this comment.
_pre_binary_cache stores (pre_binary_asm, chip) as the value, but chip is already part of the dict key. Keeping only pre_binary_asm would avoid redundant state and prevent inconsistencies if these ever diverge.
| def _make_cache_key(self, bound_args): | ||
| """Cache key including compile_hints for full artifact caching.""" | ||
| key_parts = list(self._make_base_cache_key(bound_args)) | ||
| from .kernel_function import CompilationContext | ||
| hints = CompilationContext.get_compile_hints() | ||
| if hints: | ||
| key_parts.append(('__compile_hints__', tuple(sorted(hints.items())))) | ||
| return tuple(key_parts) |
There was a problem hiding this comment.
This PR changes cache key semantics by adding compile_hints and introduces a new “base key” used for pre-binary reuse. There doesn’t appear to be a unit test asserting (1) base key is unchanged by CompilationContext.compile_hints, and (2) full cache key changes when hints change. Adding a small pytest for these invariants would help prevent regressions without requiring a GPU run.
Motivation
During autotuning, FlyDSL recompiles the entire MLIR pipeline for each
waves_per_eu/maxnregconfiguration. Most passes (layout lowering, SCF-to-CF, GPU-to-ROCDL, etc.) produce identical IR regardless of these backend flags — only the finalgpu-module-to-binarypass differs. This wastes ~30s per redundant full compilation.Split out from #265 per review feedback.
Technical Details
Two-phase compilation
Splits
MlirCompiler.compile()into two reusable phases:compile_pre_binary): all MLIR passes exceptgpu-module-to-binary. Result is cached as ASM text in_pre_binary_cache.compile_binary): onlygpu-module-to-binarywith the specificwaves_per_eu/maxnregflags fromcompile_hints.Both
_pre_binary_fragmentsand_binary_fragmentare derived from_pipeline_fragments()(slice off last element) — no pass list duplication.Cache key changes
_make_cache_keynow includes__compile_hints__so different compiler options produce distinct cache entries._make_base_cache_key(new) produces the key WITHOUTcompile_hints— used for pre-binary module lookup across autotuning configs._pre_binary_cacheuses(chip, base_key)composite key for multi-arch safety._pre_binary_cacheis guarded byuse_cache— respectsFLYDSL_RUNTIME_ENABLE_CACHE=0.Review feedback addressed (from #265)
_pre_binary_fragments = _pipeline_fragments()[:-1]_make_base_cache_keyis a copy_make_cache_keybuilds on_make_base_cache_key_pre_binary_cachehas nouse_cacheguardif use_cachebase_keydoesn't includechip(chip, base_key)composite keyTest Plan
waves_per_euconfigs: Phase 1 compiles once, Phase 2 runs per configFLYDSL_RUNTIME_ENABLE_CACHE=0: no pre-binary caching, full recompilation each callFLYDSL_RUNTIME_ENABLE_CACHE=1(default): pre-binary cached, fast Phase 2 only for subsequent configscompile_hints(standard path): falls through to originalMlirCompiler.compile()Submission Checklist