Codegen and Python Perf optimizations - #2447
Draft
ThrudPrimrose wants to merge 59 commits into
Draft
Conversation
Property.__get__ is the hottest read in the stack (>200k calls in a single validate_sdfg) and probed hasattr(self, 'attr_name') then rebuilt "_" + attr_name on every access. make_properties now precomputes private_name once, and class-level defaults replace the probe. validate_state tested hasattr(context, 'in_gpu'), but context is a dict, so the probe was always False: the device-level scope walk was recomputed for every state and the value the caller threaded down was ignored. The validate_undefs config lookup also ran per memlet; hoisted out of the loop. validate_sdfg guarded the transient stride/total_size checks with two hasattr probes. Only descriptors that are really allocated have those attributes, so test for the allocated kinds positively; DistributedDescriptor (ProcessGrid/ SubArray/RedistrArray) describes an MPI communicator and has neither. The positive form also fails safe: an unknown Data subclass is skipped rather than raising AttributeError out of validation. NestedDict.__getitem__/__contains__ split the key on '.' for every sdfg.arrays[...] lookup; an unqualified name now takes a direct dict hit.
…canning Two hot spots in code generation re-derive per node or per descriptor what is invariant across the whole SDFG. symbols_defined_at replays the entire chain per node -- SDFG symbols, every array's free symbols, the interstate edges, then the scope entries down to the node. Only the last step depends on the node, so each call redoes an O(#arrays) sympy walk. Symbols are defined going down (map parameters and dynamic-range connectors, consume PE index), so one top-down pass computes each scope's table once by inheriting its parent's. That is also what correctness wants: new_symbols receives the accumulated table because it types a map range against the symbols already in scope. determine_allocation_lifetime scans per descriptor, so each re-walks the SDFG: which states hold the container, whether it appears in an interstate edge or a block condition, and every state's scope dictionary. The condition scan is the worst -- cfg.used_symbols() parses through sympy/ast and does not depend on the descriptor at all, only the `name in` test does, so it ran D*|CFGs| times to answer |CFGs| questions. Both modules build only dictionaries; every decision stays in framecode. The allocation tables preserve sdfg.states() order, not topological order, because that is what the replaced scans used and it decides which state emits an allocation. The two distinct predicates are kept apart: `n.data == name` for the State branch, `node.root_data == name` for the Scope branch. defined_at falls back to symbols_defined_at on a miss, since _generate_ConsumeEntry still rewrites dataflow during codegen despite the documented freeze. Verified by hashing generated code for six programs (nested maps, scope transient, nested SDFG, gemm, LoopRegion body, multi-state transients) against unmodified main: all six identical. tests/perf/ adds a benchmark that times simplify and generate_code over the npbench/polybench corpus plus loop unrolling on cloudsc, and a script that turns two CSVs into markdown speedup tables and a plot.
…ables Backport of 3657ef4 from the extended branch. symbols_defined_at folded in global symbols, inter-state-edge assignments and dataflow-scope (Map/Consume) symbols, but never walked up the control-flow hierarchy, so the loop variable of an enclosing LoopRegion could be reported as undefined for a node in the loop body. That false "undefined" tripped propagate_memlets_nested_sdfg's widening fallback and replaced a nested-SDFG connector dim indexed by the loop variable with the whole array (arr[*, jk-1, *] -> arr[*, 0:klev, *]), hiding per-iteration uniqueness and blocking LoopToMap. Walk parent_graph up the CFG, collect each enclosing LoopRegion, and fold in its loop variable via LoopRegion.new_symbols, outermost first and before the scope-symbol walk. symbol_scopes mirrors the same step so the two stay equivalent. Caveat on verification: on this branch I could not construct a graph where the loop variable is actually missing without the walk -- it is reported as defined either way, so some other route already supplies it here. The observable difference in the tests is key ORDER. The scenario the original fix targets is ICON-scale (nested SDFG plus memlet propagation plus LoopToMap) and is not reproduced by the unit tests added here.
The cloudsc workload timed loop unrolling and codegen but not the initial simplify. Each stage now feeds the next -- simplify on the freshly built graph, unroll on the simplified one, codegen on the unrolled one -- which is the order a real compile runs them in. Prefers build_cloudsc_sdfg(), the maintained entry point, over calling to_sdfg on the raw program.
Builds a worktree per ref, links dace/external/{cub,moodycamel} into it (git
worktree does not populate submodules, and without them everything that
compiles C++ fails), runs the benchmark in each, then emits the markdown tables
and plot.
Kernel corpora sit at tests/corpus/<name> on extended and tests/<name> upstream, and only the former are Python packages. Hardcoding one layout (and using a dotted module name) discovered zero kernels here and wrote an empty CSV that looked like a clean run. Search both roots, load by file path, key kernels by file stem so a base/new pair across layouts lines up, and exit non-zero rather than writing an empty CSV. cloudsc exists only where tests/corpus/cloudsc does; say so explicitly instead of dropping three workloads without comment.
The kernel corpus and the benchmark scripts are inputs, not the thing under test. Letting them vary per branch compares two different benchmarks, and a base ref that predates tests/perf cannot run at all. The driver now checks tests/corpus and tests/perf out of <harness-ref> into every worktree, so both arms measure the identical workload with identical code. The benchmark reads a single tests/corpus root, loads files by path rather than by dotted module name (the corpus dirs are not packages everywhere), and exits non-zero rather than writing an empty CSV that would look like a clean run. cloudsc unavailability warns and continues instead of discarding the npbench/polybench results already collected.
…stance
symbols_defined_at special-cased LoopRegion to pick up the loop iterator, but
AbstractControlFlowRegion.new_symbols is already the hook for 'what does this
region bind', returning {} for regions that bind nothing. Walking every
enclosing region and folding in new_symbols drops the isinstance test, treats
control-flow scope exactly like the dataflow scope resolved just below it, and
lets any future region type bind symbols without touching this function.
Dispatch is also stricter than the test it replaces: LoopRegion.new_symbols
additionally requires an init statement and skips a loop variable that appears
in its own init RHS.
symbol_scopes mirrors the same walk. Generated code is byte-identical to
unmodified main across the six-program corpus.
SymbolScopes and AllocationScopes were plain functions under dace/codegen, but they are exactly what ppl.Pass models: pure analysis over the whole SDFG returning dictionaries keyed by cfg_id. determine_allocation_lifetime already calls StateReachability().apply_pass right next to them, so the neighbours were already passes and these two were the odd ones out. As passes they declare Modifies.Nothing and a real should_reapply, which replaces an informal "valid while the SDFG is frozen" docstring with the invalidation contract the pipeline already understands. They also become reusable outside codegen -- validate_state calls symbols_defined_at per edge, which is the same quadratic this removes -- and their results can be shared through pipeline_results rather than recomputed per consumer. AllocationScopes no longer needs the frame codegen's memoized free_symbols helper: reading isedge.data.free_symbols directly gives the same set and drops the dependency on the caller. Generated code is byte-identical to unmodified main across the six-program corpus.
Codegen constructed StateReachability, SymbolScopes and AllocationScopes separately. StateReachability depends_on ControlFlowBlockReachability, so running them apart recomputes that dependency per consumer; a Pipeline resolves depends_on once and shares every result through pipeline_results. CodegenAnalysisPipeline names the group, matching GPUCodegenPreprocessPipeline next door. All three declare Modifies.Nothing, so it is safe on a frozen SDFG and the results stay valid while nothing mutates. Generated code is byte-identical to unmodified main across the six-program corpus.
…ces pass determine_allocation_lifetime still opened with an inline walk collecting shared transients and, per container, every state that uses it -- through an access node, through a code node naming it as a free symbol with no memlet, or through a surrounding interstate edge. That is pure analysis producing cfg_id-keyed dictionaries, i.e. the same shape as the passes beside it, so it belongs in the pipeline rather than in the routine that consumes it. AccessInstances joins CodegenAnalysisPipeline, leaving determine_allocation_lifetime with decision logic only: 33 lines of gathering become 8 lines of lookups. The block-topological order is preserved and called out in the docstring -- the consumer takes the FIRST and LAST entry per container to choose which state declares and which frees a transient, so reordering would move allocations. symbols_and_constants stays in framecode -- it is memoized on the code generator, not derivable from the SDFG alone. Generated code is byte-identical to unmodified main across the six-program corpus.
Runs code generation on the same cloudsc SDFG under the old analysis path and the new one, reporting the three stages that feed it: initial simplify, loop unroll, then codegen on the unrolled graph. Both arms take tests/corpus and tests/perf from <harness-ref>, so they run the identical workload with identical measurement code -- which is also what makes an upstream base ref runnable, since neither directory exists there. --cloudsc-only skips the kernel corpus for a focused run. SBATCH headers are inert outside SLURM, so the script is submittable or directly runnable.
tests/corpus lives on extended; tests/perf lives on the branch under test. A single harness-ref forced both from one place, so pointing it at extended failed with a bare 'pathspec tests/perf did not match any file(s)'. Each checkout is now preceded by a cat-file existence test naming the missing ref and path. generate_data_for_cloudsc imports tests.corpus.cloudsc.cloudsc absolutely, which needs the repo root on sys.path, while 'cloudsc.*' resolves against tests/corpus; only one was inserted.
rm -rf deletes the directory but leaves git's worktree registration, so a second run failed with 'missing but already registered worktree'. Prune before adding and force the add, which is what a rerun after any failure hits.
…one clone run_perf_ab.sh runs the npbench/polybench kernel sweep and the cloudsc stages in one pass, old analysis path vs new, replacing the two near-identical scripts it supersedes. submit_perf_ab.sh sbatches it on one exclusive normal node with a 2h limit. Also fixes two things that only bite at cloudsc scale (2687 arrays, 5890 states): timed() held every rep's deepcopy alive at once (gigabytes), now one at a time outside the timer; and the ~10 min frontend build, identical for every ref, is cached via DACE_BENCH_SDFG_CACHE so the second arm loads it.
submit_perf_ab.sh is the sbatch entry point: #SBATCH account g34, partition normal, 1 exclusive node, 2h. It spack-loads llvm@22.1.5 and gcc@16.1.0 (sourcing spack setup-env first if the function is not defined in the batch shell), makes perf_ab/ for the --output path, then execs run_perf_ab.sh (a plain worker, no SLURM headers). sbatch tests/perf/submit_perf_ab.sh [args...]
The class docstrings ran to multi-paragraph rationales; the codebase uses a single line (cf. AccessSets, FindAccessStates). Trimmed each to one line, kept only the load-bearing notes -- the ported start-state bug, the load-bearing block order, and why the loop-variable walk exists -- as short inline comments.
Three reuse fixes from an audit of what the passes reimplemented: * sdfg_symbols/state_symbols were verbatim copies of symbols_defined_at's body. Extracted as sdfg_scope_symbols/enclosing_region_symbols in state.py; symbols_defined_at and SymbolScopes now call the same functions, so the two cannot drift. * AllocationScopes.scope_dicts re-cached what SDFGState.scope_dict() already caches on the state. Dropped the table; the consumer calls state.scope_dict() directly (self-caching, frozen SDFG -> identical). * AccessInstances' inline `free_symbols & array_names` on interstate edges is exactly InterstateEdge.used_arrays(sdfg.arrays); use it and drop the intermediate set. symbols_defined_at behavior preserved (LoopRegion loop variable still visible), generated code byte-identical to unmodified main across the six-program corpus.
393 -> 318 call sites across sdfg, codegen, libraries, transformation and the frontends. Each removal was checked against the declaring class: guards for attributes every subclass defines are dropped, kind discrimination becomes isinstance, and lazy per-instance caches become class-level defaults. Reflection that has no static equivalent is kept: serialization and property machinery, visit_<TypeName> dispatch, attribute lookup by a runtime string, duck-typing on sympy/numpy/ast/user objects, ONNX schema-driven attributes, and markers other passes attach at runtime. Three defects surfaced on the way: - condition_fusion assigned the containing SDFG into NestedSDFG.sdfg, i.e. over the nested graph itself. Restricted to ControlFlowBlock; set_nested_sdfg_parent_references already covers the nested case. - validate_state tested a **kwargs dict with hasattr, which is always False, so the in_gpu value validate_sdfg computes was discarded and recomputed for every state. is_in_scope ignores the state argument when the node is None, so the threaded value is the same one; it is now reused. - _layernorm_axis guarded node.axis with hasattr and then dereferenced it in the else branch. Defaults to ONNX's -1, deduplicated into one helper. cpu.py's ArrayStreamView guard looks dead but is not: sdfg/utils.py attaches the attribute at runtime. Its comment now says so.
``used_symbols``, ``used_symbols_within_scope`` and ``ReferenceToView`` all called ``new_symbols(...).keys()``, discarding the types. Producing them costs two ``infer_expr_type`` calls per map parameter, and each of those runs a sympy preorder traversal, a pycode print and an ``ast.parse`` -- 13% of code generation on the npbench and polybench corpus. Add ``new_symbol_names``, the names-only counterpart of ``new_symbols``, and resolve it by dispatch on the node. Also fold the four copies of the dynamic scope input expression into ``EntryNode.dynamic_input_connectors``. Generated code is byte-identical on the corpus, and the names agree with ``new_symbols().keys()`` node for node over 1675 scope entries.
``typeclass.__init__`` read ``compiler.default_data_types`` for every type it wrapped, even though only ``int``, ``float`` and ``complex`` consult it. That made it the single hottest configuration lookup in code generation -- 27886 of the 31113 ``Config.get`` calls over the npbench and polybench corpus, each one probing the environment. Read it behind the branch that needs it, and drive the three identical width tables from one mapping instead of repeating the dispatch per type.
…scalar ``dtypes`` rebinds ``bool`` to a typeclass so that ``dace.bool`` names a DaCe type. ``typeclass.__init__`` resolves its globals at call time, so the ``wrapped_type is bool`` branch compared the argument against that typeclass rather than the builtin and could never be taken: ``typeclass(bool).type`` stayed ``builtins.bool`` while ``int``, ``float`` and ``complex`` all normalized to their numpy counterparts. Name the builtin explicitly so the branch applies. Beyond the inconsistency, this repairs the hash contract: ``typeclass(bool)`` and ``typeclass(numpy.bool_)`` compared equal but hashed differently, so both could occupy one dictionary. Every other observable -- ctype, size, ``to_string``, ``as_ctypes``, ``as_numpy_dtype``, ``to_json`` -- is unchanged, and generated code is byte-identical on the npbench and polybench corpus.
Dropping the ``hasattr(desc, 'strides')`` guard made ``validate_sdfg`` reach for ``strides`` and ``total_size`` on every transient descriptor. A ``DistributedDescriptor`` -- ProcessGrid, SubArray, RedistrArray -- describes an MPI communicator and has neither, so validating any SDFG holding a process grid raised ``AttributeError``. That is what test_process_grid_bcast hit. Test for the allocated kinds positively instead of probing for the attribute, so an unknown Data subclass is skipped rather than crashing.
Left behind once the getattr call it supported was replaced; ruff fails on it.
The names-only path is only safe while it agrees with ``new_symbols``, so assert that over frontend-built maps, nested maps and an expanded reduction, before and after simplify, plus a dynamic scope input and a Consume PE index. Also cover the typeclass rules the same change relies on: Python's scalars follow ``compiler.default_data_types``, ``bool`` normalizes like the rest, and equal typeclasses hash alike.
Takes the repo-wide sweep from #2449 so this branch carries it plus the code generation work on top. The only conflict was the wording of one comment in validate_state; both sides had reached the same code.
Ported back from extended, where this side won the merge. `analysis_results['AllocationScopes']` and friends are silent on a rename: the pass class moves, the literal keeps compiling, and the lookup KeyErrors at codegen time instead of failing at import. Keying on the class's own __name__ ties the lookup to the class it names, so a rename is caught by the import.
test_allocated_kinds_are_exactly_the_ones_with_strides asserted that a Data subclass has strides/total_size if and only if it is in validate_sdfg's allocation-checked allowlist. That held when DistributedDescriptor had neither attribute, but 9c37398 ("Address review: empty descriptor properties") gave it both as constant stubs -- [] and 0, i.e. a declaration that a communicator allocates nothing -- and did not update the test, so the tripwire fired on the one subtree that provably needs no check. Having the attributes is not what should exempt it; the stubs being constant is. So the subtree is exempted, and what makes the exemption sound is pinned instead: the stub values, and that no subclass overrides either property. A DistributedDescriptor that grows a real buffer still trips the check.
…ght instances SymPy's global @cacheit LRUs key on _hashable_content. dace.symbolic.symbol kept dtype as a plain attribute and left it out, so symbol('i', int32) and symbol('i', int64) aliased in those caches and a cached entry handed back an expression rebuilt around the foreign symbol -- the order-dependent 'Mod(symbol($i, dtype=dace.int64), 3i16)' round-trip failure. TypedConstant already includes its dtype for the same reason. With dtype in the identity, propagation could no longer mint scope parameters from bare names: a map over 0:N gets an int64 iterator (result_type_of over the range bounds) while pystr_to_symbolic('i') mints int32, so no Wild-exclude, match or membership test hit and the iteration variable leaked into the propagated subset. Parameters now resolve to the instances the analysed subsets themselves carry, and defined variables -- which are only ever membership-tested -- stay bare names, which is what comparing freshly minted symbols amounted to before.
The dtype-aliasing bug only showed up as a round-trip mismatch in a test that ran after an unrelated one had built the same expression around an int64 symbol, so it was invisible under the fixed collection order every workflow uses. Two additions: - Two tests that assert the invariant directly, without depending on order: a same-name symbol of another dtype is a different symbol, and SymPy's global caches never substitute one for the other. - A workflow that feeds pytest a seeded permutation of the test files, with --dist loadfile so each worker still runs many files in one process, which is where the cache carry-over happens. The seed comes from the commit SHA, so the order varies across commits but a failure always reproduces, and it is echoed as a GitHub annotation on both the run and the failure.
SymPy orders expressions by comparing _hashable_content tuples element-wise, and typeclasses define equality but no ordering, so a raw typeclass in there raised TypeError from Basic.compare. ctype is what typeclass equality is defined on anyway.
Symbol identity now carries the dtype, and DaCe routinely holds the same name at two widths on the two sides of a nested SDFG boundary, or mints a scope parameter from a bare name and compares it against an expression that already holds one. Neither is a statement about the value: - shape/stride equality in both inliners goes through symbolic.same_value, which ignores dtypes, - Memlet.get_stride and SubgraphFusion take the parameter instance the subset carries, so the shifted and unshifted expressions cancel again, - the vectorizers ask whether the parameter *name* occurs, which is what they always meant.
A list never equals a tuple, and the inliners rely on that: an outer descriptor keeps its strides in a list where the inner one has a tuple, which blocks inlining. Only the symbol dtype may be ignored.
Retyping to the default dtype dropped assumptions such as nonnegative, so two symbols that differ in their assumptions could compare equal. Substituting a plain SymPy symbol carrying the same assumptions leaves the dtype as the only thing ignored.
Symbol identity now includes the dtype, so a name parsed on its own is no longer the instance an expression carries: WCR conflict detection stopped matching and every reduction went atomic. An UndefinedSymbol reaching the defined-variable set by name also let affine propagation run on a range that has no value, which never terminates.
Same regression as the propagation path, unobserved only because no test drives an undefined range through the underapproximation.
…arguments The scan stepped one hop out of the exit node. A thread-block map inserted inside a GPU_Device map adds a second exit whose edge still carries the inner Memlet, so the array being written was never named and the kernel referenced a parameter it did not have.
…ames A symbol's identity includes its dtype, so the keys ``replace_dict`` builds from bare names are not the instances a memlet volume carries. Intersecting the two sets of instances comes out empty exactly when there is something to replace, and the volume keeps symbols the surrounding SDFG no longer defines -- which the frontend then reports as missing on the nested SDFG it has just built. The two subset guards above already compare names; do the same here. The symbol mapping of a nested SDFG also went through ``str`` on its way to ``subs``, which re-minted every symbol in it at the default dtype. Hand the value over as it is.
…-copy The view memlet's subsets are parsed from a string, so the symbols in them carry the default dtype while the write range carries the declared one. Identity includes the dtype, so the two spellings of the same range compared as unrelated, ``intersects`` could not decide, and a staging transient went in to be safe. The redundant-array cleanup then collapsed that chain with the subsets in swapped roles, and ``p[:, -1] = p[:, -2]`` copied backwards. Put both sides on one instance per symbol name first. Nothing that reaches the SDFG changes: keeping the subsets themselves instead of their string spelling was tried and reverted, because the round trip also canonicalizes the expressions, and the passes that pattern-match a memlet subset depend on that. The two indirection memlets move off the deprecated string constructor while here.
Both re-parse what they have just printed, which loses the dtype every symbol carries. ``pystr_to_symbolic`` passes a symbolic expression through untouched, so the argument can be handed over as it is.
These pieces are spliced into the emitted C++, so they need the C++ spelling of an expression rather than whatever ``str`` happens to print. Drops a ternary whose two branches were identical while there.
An exact comparison followed by a comparison of the two string spellings was standing in for a comparison that ignores how a symbol is spelled. ``same_value`` is that comparison, so six copies of the pair -- and the TODO each one carried -- come out.
``free_symbols`` already yields names, so re-minting them only produced instances that compare unequal to the loop variable once the dtype is part of identity, and the check silently stopped firing.
``loop_variable`` is a plain string, so ``in`` let an ``i`` claim the bounds of the nearest enclosing ``idx`` loop and size the stored buffer from the wrong range.
The two tests added alongside it pin the same invariant without depending on collection order, which is the cheaper guard and needs no runner of its own.
ThrudPrimrose
force-pushed
the
rm-redundant-getattr-hasattr
branch
from
August 4, 2026 15:53
adf8459 to
42a2d1e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three pieces. The first two are behavior-preserving; the third is a bug fix.
Extra
In many places subsets are copied via
str(symbolic_expr)and there are many risky and wrong uses of otherstr(symbolic_expr)in many places. I am also fixing those in this PR.1. Hot-path attribute overhead (
65f48c1aa)Property.__get__is the hottest read in the stack (>200k calls in a singlevalidate_sdfg) and rebuilt"_" + attr_nameon every access.make_propertiesnow precomputes it.validate_statetestedin_gpuwithhasattron a dict, which is always False — so the device-level scope walk was recomputed for every state and the value the caller threaded down was ignored.validate_undefsconfig lookup ran per memlet; hoisted out of the loop.NestedDict.__getitem__/__contains__split every name on.; unqualified names now take a direct dict hit.2. Two codegen scope passes (
79674add6)symbols_defined_atreplays a chain that is invariant across the whole SDFG for every node — SDFG symbols, every array's free symbols, interstate edges — when only the final scope step depends on the node. Symbols are defined going down (map parameters and dynamic-range connectors, consume PE index, loop iterator), sosymbol_scopescomputes each scope's table once by inheriting its parent's. That is also what correctness wants:new_symbolsreceives the accumulated table because it types a map range against the symbols already in scope.determine_allocation_lifetimescans per descriptor, so each scan re-walks the SDFG. The worst is loop-invariant in a stronger sense:cfg.used_symbols()parses conditions through sympy/ast and does not depend on the descriptor at all — only thename intest does — so it ranD x |CFGs|times to answer|CFGs|questions.allocation_scopesbuilds only dictionaries; every decision stays inframecode.Two details that matter for correctness: the allocation tables preserve
sdfg.states()order, not topological order, because that is the order the replaced scans used and it decides which state emits an allocation; and the two distinct predicates are kept apart (n.data == namefor the State branch,node.root_data == namefor the Scope branch).defined_atfalls back tosymbols_defined_aton a miss, because_generate_ConsumeEntrystill rewrites dataflow during codegen despite the documented freeze inpreprocess.3.
symbols_defined_atincludes enclosing LoopRegion loop variables (a806c7ae1)Backport of
3657ef490. The walk never went up the control-flow hierarchy, so an enclosingLoopRegion's loop variable could be reported undefined for a node in the body, trippingpropagate_memlets_nested_sdfg's widening fallback (arr[*, jk-1, *]->arr[*, 0:klev, *]) and blockingLoopToMap.