-
Notifications
You must be signed in to change notification settings - Fork 22
[Frontend] Lower togsim.transfer directly to Gemmini (drop memref.dma_start) #286
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
YWHyuk
merged 11 commits into
feature/indirect-offset-descriptor
from
feature/transfer-direct-lowering
Jul 7, 2026
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
b0bc612
[Frontend] Drop memref.dma_start: lower togsim.transfer directly to G…
YWHyuk 9884d20
[Frontend] Port the trace path (build_tog) to togsim.transfer/togsim.…
YWHyuk 76c8a31
[Frontend] Detect the indirect offset operand by the `indirect` attr,…
YWHyuk c2032ea
[Frontend] Fix stale build_skeleton docstrings (memref.dma_wait -> to…
YWHyuk e7379db
[Frontend] Lower togsim.transfer to a DMA descriptor + CONFIG_DESC (n…
YWHyuk dd3143e
[Build] Bump spike to v1.0.4 (DMA descriptor / CONFIG_DESC)
YWHyuk debd1e8
[Build] Bump spike to v1.0.5 (masked-DMA clamp + MVOUT accumulate)
YWHyuk b52c37b
[Frontend] Masked-DMA dynamic clamp: drop tile divisibility, fill non…
YWHyuk 8f10c4c
[Frontend] Extend the masked-DMA clamp to conv (explicit extents + fi…
YWHyuk f019a9c
[Frontend] Clamp the fused-epilogue output store on non-dividing shapes
YWHyuk 4f8221d
[Test] Golden unit test for the lower_transfer_to_gemmini pass
YWHyuk File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -591,8 +591,10 @@ def load(self, name: str, index: sympy.Expr): | |
| sram_var, sram_index_var = self.get_scratchpad_buffer(dtype, name, local_tile_desc, index) | ||
| compute_index_var = ",".join(sram_index_var.split(",")[:-1] + [f"%{self.compute_idx}"]) | ||
|
|
||
| masked_bounds = self._masked_bounds(name, index, dram_stride, local_tile_desc, is_load=True, buffer=self.dma_loads) | ||
| code = self.emit_transfer("MVIN", vlane_split_axis, vlane_stride, mlir_dtype, dram_var, index_var, sram_var, sram_index_var, | ||
| dram_shape, tile_shape, dram_stride, tile_stride, int(padding), offset=offset_desc) | ||
| dram_shape, tile_shape, dram_stride, tile_stride, int(padding), offset=offset_desc, | ||
| masked_bounds=masked_bounds, masked_fill=self._masked_fill_bits(dtype, index)) | ||
| self.cse.generate(self.dma_loads, code, assignment = False) # FIXME: assignment = False does not support caching | ||
|
|
||
| with self.override_buffer_cse(buffer=self.loads): | ||
|
|
@@ -606,15 +608,18 @@ def store(self, name: str, index: sympy.Expr, value, mode=None, *args, **kwargs) | |
| offset_desc = None | ||
|
|
||
| # Handle scatter store | ||
| accumulate = False | ||
| if self._has_indirect(index): | ||
| # Convert the output buffer type to the inplace buffer | ||
| arg_name = V.graph.scheduler.mutation_real_name.get(name, name) | ||
| if arg_name not in self.kernel_group.args.inplace_buffers: | ||
| self.kernel_group.args.make_inplace(arg_name, arg_name) | ||
|
|
||
| if mode == "atomic_add": | ||
| loaded_value = ops.load(name, index) | ||
| value = ops.add(loaded_value, value) | ||
| # index_add: let the MVOUT do out[idx] += val. The DMA processes positions | ||
| # sequentially, so duplicate indices accumulate correctly regardless of tile | ||
| # size -- unlike the compute gather-add-overwrite, which loses duplicates that | ||
| # land in the same (non-dividing) tile. | ||
| accumulate = (mode == "atomic_add") | ||
| index, offset_desc = self.convert_indirect_indexing(index) | ||
| dram_var = self.kernel_group.args.output(name) | ||
|
|
||
|
|
@@ -656,8 +661,10 @@ def store(self, name: str, index: sympy.Expr, value, mode=None, *args, **kwargs) | |
| sram_index_var = self.spad_buffer_dict[str(value)][3] | ||
|
|
||
| # Generate DMA instruction | ||
| masked_bounds = self._masked_bounds(name, index, dram_stride, local_tile_desc, is_load=False, buffer=self.dma_stores) | ||
| code = self.emit_transfer("MVOUT", vlane_split_axis, vlane_stride, mlir_dtype, dram_var, index_var, sram_var, sram_index_var, | ||
| dram_shape, tile_shape, dram_stride, tile_stride, 0, offset=offset_desc) | ||
| dram_shape, tile_shape, dram_stride, tile_stride, 0, offset=offset_desc, masked_bounds=masked_bounds, | ||
| accumulate=accumulate, acc_float=dtype.is_floating_point) | ||
| self.dma_stores.writeline(common.DeferredLine(name, code)) | ||
|
|
||
| def reduction(self, dtype, src_dtype, reduction_type, value): | ||
|
|
@@ -796,14 +803,6 @@ def _has_indirect(self, expr): | |
| return any(s.name in self.indirect_symbols for s in expr.free_symbols) | ||
|
|
||
| def _index_expr(self, tile_desc, renamed_expression, index, base_vector_index): | ||
| # In case of index expr, dimension size should be divisible by tile size | ||
| if not self.kernel_group.tile_desc.is_dim_dividable(self.ranges): | ||
| new_tile_size = self.kernel_group.tile_desc.adjust_tile_to_divisible(self.ranges) | ||
| prior_tile_size, prior_ranges = self.kernel_group.tile_desc.get_tile_size(), self.ranges | ||
| self.kernel_group.tile_desc.set_tile_size(new_tile_size) | ||
| self.reset("recompile") | ||
| raise mlir_common.RecompileSignal(f"Index access (tile size {prior_tile_size} is not divisible by {prior_ranges})") | ||
|
|
||
| tile_size_per_lane = tile_desc.get_tile_size_per_lane() | ||
| compute_vec_size = tile_desc.get_compute_vec_size() | ||
| strides = tile_desc.get_tile_stride_per_lane() | ||
|
|
@@ -1359,12 +1358,111 @@ def get_dma_info(self, name, index, broadcast=True, store_reduction=False, buffe | |
| if len(self.itervars) == 1 and self.reduction_depth == 0: | ||
| # In case of reduction loop only case, we will add dummy loop so shift it once | ||
| dram_stride = [0] + dram_stride[:-1] | ||
|
|
||
| # Stash the tile-axis -> loop-dim map so load()/store() can build the masked-DMA | ||
| # per-dim [low, high) clamp (needs the loop iv of each tile axis). See _masked_bounds. | ||
| self._last_local_dims = local_dims | ||
| return local_tile_desc, index_var, dram_stride | ||
|
|
||
| _FILL_BITVIEW = {torch.float32: torch.int32, torch.float16: torch.int16, | ||
| torch.bfloat16: torch.int16, torch.float64: torch.int64} | ||
|
|
||
| def _masked_fill_bits(self, dtype, index): | ||
| """Raw bits for the masked-DMA tail fill = the consuming reduction's identity | ||
| (reduction_init: sum->0, prod->1, max->-inf, min->+inf), in the LOAD dtype; 0 for | ||
| a non-reduction load. So a non-dividing reduction's tail is the reduction identity, | ||
| not garbage/0. See Reduction.default_accumulator (PyTorch's canonical table). | ||
|
|
||
| Log-sum-exp exception (softmax): a sum whose node came from exp reduces exp(input), | ||
| so the PRIMARY input's tail must be -inf (exp(-inf)=0), not the sum identity 0. | ||
| Only the primary reduced tensor (indexed by a reduction itervar) is overridden; | ||
| broadcast operands (e.g. the subtracted max) keep their finite identity, else | ||
| input - (-inf) = +inf -> exp = inf -> NaN.""" | ||
| node = getattr(self, "current_node", None) | ||
| if node is None or getattr(node, "node", None) is None or not node.node.get_reduction_type(): | ||
| return 0 | ||
| rtype = node.node.get_reduction_type() | ||
| is_primary = bool(set(self.itervars[self.reduction_depth:]) & index.free_symbols) | ||
| if rtype == "sum" and is_primary and any("exp" in o.name for o in node.node.origins): | ||
| init = "-inf" | ||
| else: | ||
| init = reduction_init(rtype, dtype) | ||
| val = {"-inf": float("-inf"), "inf": float("inf")}.get(init, init) | ||
| if isinstance(val, str): # e.g. welford_reduce -> "0.0" | ||
| val = float(val) | ||
| t = torch.tensor(val, dtype=dtype) | ||
| view = self._FILL_BITVIEW.get(dtype) | ||
| bits = int(t.view(view).item()) if view is not None else int(t.item()) | ||
| return bits & ((1 << (t.element_size() * 8)) - 1) | ||
|
|
||
| def _masked_bounds(self, name, index, dram_stride, local_tile_desc, is_load, buffer): | ||
| """Per tile-axis dynamic [low, high) clamp for a masked DMA. | ||
|
|
||
| For tile axis d (loop dim k, loop iv %index_k = the tile base), the valid GLOBAL | ||
| range is [glo, ghi): MVOUT writes [0, output_extent); MVIN reads the padded input, | ||
| valid where the input coord is in-bounds -> [pad_d, pad_d + input_extent_d). The | ||
| tile-LOCAL clamp is then low = max(0, glo - base), high = min(tile, ghi - base), | ||
| emitted as affine.max/affine.min of the loop iv so the last (partial) tile and the | ||
| pad borders fall out naturally. Returns [(tile_axis, low_var, high_var), ...] for | ||
| the non-trivial axes; low/high are index SSA vars. | ||
| """ | ||
| local_dims = self._last_local_dims | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this method can support dynamic dim? We need to support dynamic dim |
||
| tile_size = local_tile_desc.get_tile_size() | ||
| const = int(index.as_coeff_Add()[0]) if not index.is_number else int(index) | ||
| # index const = -sum(pad_d * dram_stride[d]); recover per-dim pad greedily (desc stride). | ||
| pad = [0] * len(tile_size) | ||
| if is_load and const < 0: | ||
| rem = -const | ||
| for d in sorted(range(len(dram_stride)), key=lambda x: -dram_stride[x]): | ||
| s = dram_stride[d] | ||
| if s > 0 and d < len(pad): | ||
| pad[d] = rem // s | ||
| rem -= pad[d] * s | ||
| in_shape, in_stride = self.buffer_types[name][2], self.buffer_types[name][3] | ||
| axes = [] | ||
| for d, k in enumerate(local_dims): | ||
| if d >= len(tile_size) or k >= len(self.ranges): | ||
| continue | ||
| iv = str(self.itervars[k]) | ||
| # A padded load (const < 0, i.e. index shifted by -pad) reads a smaller input: | ||
| # clamp per-dim to [pad_d, pad_d + input_extent_d). Every other DMA (stores and | ||
| # non-padded/contiguous loads, including collapsed tensors) is valid over the | ||
| # whole loop extent -> only the trailing tail needs clamping. | ||
| if is_load and const < 0: | ||
| ext = next((int(in_shape[j]) for j, st in enumerate(in_stride) | ||
| if int(st) == int(dram_stride[d])), None) | ||
| glo, ghi = (pad[d], pad[d] + ext) if ext is not None else (0, int(self.ranges[k])) | ||
| else: | ||
| glo, ghi = 0, int(self.ranges[k]) | ||
| axes.append((d, iv, glo, ghi, int(tile_size[d]))) | ||
| return self._emit_clamp(axes, buffer) | ||
|
|
||
| def _emit_clamp(self, axes, buffer): | ||
| """Emit the per-axis dynamic clamp. axes: [(tile_axis, base_iv, glo, ghi, tile), ...] | ||
| -- low = max(0, glo - base), high = min(tile, ghi - base) as affine.max/affine.min of | ||
| the loop iv so the last partial tile and the pad borders fall out per iteration. | ||
| Returns [(tile_axis, low_var, high_var), ...] for the non-trivial axes only.""" | ||
| result = [] | ||
| for d, iv, glo, ghi, tile in axes: | ||
| if glo == 0 and ghi % tile == 0: # every tile fully valid -> no clamp | ||
| continue | ||
| high_var = self.apply_cse.generate( | ||
| buffer, f"affine.min affine_map<(d0) -> ({tile}, {ghi} - d0)>(%{iv})") | ||
| self.register_var_info(high_var, [1, "index"]) | ||
| if glo > 0: | ||
| low_var = self.apply_cse.generate( | ||
| buffer, f"affine.max affine_map<(d0) -> (0, {glo} - d0)>(%{iv})") | ||
| self.register_var_info(low_var, [1, "index"]) | ||
| else: | ||
| low_var = self.get_const_cse(0) | ||
| result.append((d, low_var, high_var)) | ||
| return result | ||
|
|
||
| def emit_transfer(self, dma_type_name, vlane_split_axis, vlane_stride, mlir_dtype, | ||
| dram_var, dram_index_var, sram_var, sram_index_var, | ||
| dram_shape, tile_shape, dram_stride, tile_stride, padding, | ||
| subtile_size=None, async_type=None, offset=None): | ||
| subtile_size=None, async_type=None, offset=None, masked_bounds=None, masked_fill=0, | ||
| accumulate=False, acc_float=False): | ||
| """Emit a generic togsim.transfer op for a DMA whose access exceeds the | ||
| 4D Gemmini descriptor limit. Carries the full N-D access (dram/tile | ||
| strides + shapes) plus the SSA operands a memref.dma_start needs | ||
|
|
@@ -1405,15 +1503,31 @@ def emit_transfer(self, dma_type_name, vlane_split_axis, vlane_stride, mlir_dtyp | |
| if subtile_size: | ||
| av = int(async_type) if async_type is not None else 1 | ||
| attrs += f', subtile_size = {list(subtile_size)}, async = {av} : i64' | ||
| # operands: dram, dram_idx, sram, sram_idx, tag, dma_type, vlane_stride [, offset spad] | ||
| if accumulate: # index_add: MVOUT does out[idx] += val (float or integer add) | ||
| attrs += f', accumulate = true' | ||
| if acc_float: | ||
| attrs += f', acc_float = true' | ||
| # operands: dram, dram_idx, sram, sram_idx, tag, tag_idx, dma_type, vlane_stride [, offset spad] | ||
| operands = (f'%{dram_var}, %{dram_index_var}, %{sram_var}, %{zero_cse}, ' | ||
| f'%{tag}, %{dma_type}, %{vst}') | ||
| optypes = f'{dram_shape}, index, {tile_shape}, index, memref<1xi32>, index, index' | ||
| f'%{tag}, %{zero_cse}, %{dma_type}, %{vst}') | ||
| optypes = f'{dram_shape}, index, {tile_shape}, index, memref<1xi32>, index, index, index' | ||
| if offset is not None: # indirect: per-position offset spad (decompose lifts it to a symbol attr) | ||
| offset_buf, offset_type, offset_stride = offset | ||
| operands += f', %{offset_buf}' | ||
| optypes += f', {offset_type}' | ||
| attrs += f', indirect = true, offset_stride = {int(offset_stride)} : i64' | ||
| # masked-DMA dynamic clamp: append (low, high) index operands per clamped tile axis; | ||
| # masked_axes names the tile axis of each pair so the lowering writes the runtime | ||
| # values into the descriptor's dim_low/dim_high before the DMA. See _masked_bounds. | ||
| if masked_bounds: | ||
| axes = [d for d, _lo, _hi in masked_bounds] | ||
| for _d, lo, hi in masked_bounds: | ||
| operands += f', %{lo}, %{hi}' | ||
| optypes += ', index, index' | ||
| attrs += f', masked_axes = {axes}' | ||
| # box-excluded positions are filled with the consuming reduction's identity | ||
| # (0/1/-inf/+inf, per dtype); 0 for non-reduction loads. See _masked_fill_bits. | ||
| attrs += f', masked_fill = {int(masked_fill)} : i64' | ||
| return f'"togsim.transfer"({operands}) {{{attrs}}} : ({optypes}) -> ()' | ||
|
|
||
| def allocate_sram_buffer(self, dtype, dram_name, tile_desc, raw_index, buffer=None, forced_name=None): | ||
|
|
@@ -1498,13 +1612,6 @@ def convert_indirect_indexing(self, index :sympy.Expr): | |
| if not self._has_indirect(index): | ||
| return index, None | ||
|
|
||
| # Note: In case of indirect indexing, dimensions should be divisible by tile size | ||
| if not self.kernel_group.tile_desc.is_dim_dividable(self.ranges): | ||
| new_tile_size = self.kernel_group.tile_desc.adjust_tile_to_divisible(self.ranges) | ||
| self.kernel_group.tile_desc.set_tile_size(new_tile_size) | ||
| self.reset("recompile") | ||
| raise mlir_common.RecompileSignal(f"Indirect access (tile size {self.kernel_group.tile_desc.get_tile_size()} is not divisible by {self.ranges})") | ||
|
|
||
| # Process start | ||
| indirect_dims = [str(dim) for dim in index.free_symbols if str(dim) in self.indirect_symbols] | ||
| indirect_dims.sort() | ||
|
|
||
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
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
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
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
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it best?