Skip to content

Add distribution of thread blocks to chiplets in old codegen - #2527

Merged
tbennun merged 22 commits into
spcl:mainfrom
GridTools:chiplet_old_codegen_updateddace
Sep 11, 2026
Merged

tbennun merged 22 commits into
spcl:mainfrom
GridTools:chiplet_old_codegen_updateddace

Conversation

@iomaganaris

Copy link
Copy Markdown
Contributor

The goal of this PR is to distribute the thread blocks of the GPU backend to AMD GPU chiplets in such way that the X dimension of the grid is divided by the number of chiplets, so each one of them gets a continuous range of the domain. The domains that execute on the same chiplet can use then the same L2 cache, improving cache hits when there are data that can be reused between thread blocks in the same region or data that live in one dimension in two dimensional kernels (i.e. ICON neighbor tables and other vertical level independent fields).
This change has a ~7% performance improvement in the icon4py dycore.
We have already discussed about the necessity of this solution with @ThrudPrimrose I was interested to implement it in the old codegen though because we still haven't switched to the new one

iomaganaris and others added 6 commits April 29, 2026 13:36
Drop two changes that came along with the chiplet commits but are
unrelated to the chiplet mapping and active regardless of it:

* The `threadIdx.y` -> `threadIdx.z` remap for 2D thread-block maps.
  Block sizes are never remapped by the chiplet patch, so the second
  index of a 2D thread-block map would be read from `threadIdx.z`,
  which is 0 for a (bx, by, 1) block. Since `AddThreadBlockMap` is now
  applied everywhere, this fired on nearly every kernel.
* The `/*block*/` and `/*threads*/` markers in the generated code.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdEk8Xdh2bd53mE3tf5Thy
… generator

Distributing the thread-blocks of a kernel over the chiplets of the GPU moves the
second dimension of the grid to `blockIdx.z`, which the merged implementation did
not account for in every case:

* Grids using all three dimensions had their third dimension silently dropped, or
  were folded into an index expression asking for a fourth `blockIdx`, which
  raises. The distribution is now only applied when the third grid dimension is 1,
  and kernels that cannot use it keep their grid and report why.
* Nested device maps and dynamic thread-block maps read `blockIdx` outside of
  `generate_kernel_scope`, where the chiplet ID would be taken for a work index.
  Both are excluded as well.
* The first grid dimension is padded to a multiple of the number of chiplets, but
  the condition masking out the padded thread-blocks was elided whenever the map
  divided evenly by the block size, and was not generated at all for kernels with
  an inner thread-block map. It is now always generated.

The default of `compiler.cuda.chiplet_number` is 1, i.e. the grid is left alone
unless the distribution is asked for (`DACE_compiler_cuda_chiplet_number=6` on
MI300A). The generated code with the default is identical to the code generated
before the merge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdEk8Xdh2bd53mE3tf5Thy
@ThrudPrimrose

ThrudPrimrose commented Aug 26, 2026 •

Copy link
Copy Markdown
Collaborator

The assignment of thread-blocks to physical locations is a matter of codegen, yet right now the choice is too implicit. It is also not providing the user the option to choose between options. (This change would force all kernels to be generated either the old way or the new chiplet-aware way)

I think it would make more sense to have a map-level attribute (to also not break the existing assumption of "GPU_Device" is a GPU kernel and no a new schedule type to update everything)

I see that, through the tests the existence of tblock maps it should be fine.

For this pattern (and new codegen enforces it):

for i,j in dace.map[0:D0:16, 0:D1:16] @ GPU_Device:
  for bi, bj in dace.map[0:16, 0:16] @ GPU_ThreadBlock: 
     <body>

Codegen only changes the tblocks are assigned to chiplets so it seems fine to me.

@ThrudPrimrose

Copy link
Copy Markdown
Collaborator

For test quality we should have a test where we check numerical equivalence between the two strategies (e.g. maybe a very small icon loopnest snippet written using the python frontend?). Also please add a test involving persistent schedule (so that we dont break it unknowingly)

@ThrudPrimrose ThrudPrimrose left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All previous comments are addressed.

The codegen is opt-in and I like it. We also have enough numerical correctness checks.

Only one question? Would it make sense, and justify the effort to auto-detect chiplet count if a value like 0 is provided?
We could have a simple dictionary between compute arch/model to chiplet count to make it simpler to run on multiple amd devises? What do you think?

@iomaganaris

Copy link
Copy Markdown
Contributor Author

All previous comments are addressed.

The codegen is opt-in and I like it. We also have enough numerical correctness checks.

Only one question? Would it make sense, and justify the effort to auto-detect chiplet count if a value like 0 is provided? We could have a simple dictionary between compute arch/model to chiplet count to make it simpler to run on multiple amd devises? What do you think?

Thank you very much for the review.
I have some logic added here in gt4py to detect the number of XCDs based on the amdsmi python package if it exists. I would be okay moving this logic to DaCe or use a look up table. Whatever you prefer

Comment thread dace/sdfg/nodes.py Outdated
default=True,
desc="Allow the thread-blocks of this kernel to be distributed over the chiplets of the GPU "
"(see the `compiler.cuda.chiplet_number` configuration entry)",
serialize_if=lambda m: m.schedule in dtypes.GPU_SCHEDULES)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should only be for GPU_Device and GPU_Persistent, right? not GPU_Threadblock[Dynamic]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If GPU_Persistent is enabled then based on the chiplet_count function nothing happens. I thought it would actually make sense probably to set this for the GPU_Threadblock schedule maybe?

Comment thread dace/config_schema.yml Outdated
Comment thread dace/codegen/targets/cuda.py Outdated
# contiguous chunk of ``ceil(grid_size[0] / chiplets)`` blocks of the first dimension,
# together with the full second dimension, which moves to ``blockIdx.z``.
original_grid_size = grid_size
grid_size = [self._kernel_chiplet_count, int_ceil(grid_size[0], self._kernel_chiplet_count), grid_size[1]]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure why we should change the grid size; that means that the grid sizes are effectively minimized because blockdim x is the one that can be the largest of the three.
I'd consider a straightforward code generation approach (changing the blocks based on a modulo operator rather than changing the actual map dimensions.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I deliberately wanted to avoid the module operations and the thread ID translation because they add extra resource usage and complicates further the generated code. Unless there is actual need to support 3D maps I would keep this simpler approach

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since I realized that different grid dimensions have different allowed ranges I have updated the logic to be codegen only with % and / usage

@tbennun tbennun left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am a big fan of the idea, but I am not sure we should change the map dimensions to provide that. It should be a purely generated-code approach as it changes the mapping of logical thread-blocks to physical thread-block numbers.

Comment thread dace/codegen/targets/cuda.py Outdated
# together with the full second dimension, which moves to ``blockIdx.z``.
original_grid_size = grid_size
grid_size = [self._kernel_chiplet_count, int_ceil(grid_size[0], self._kernel_chiplet_count), grid_size[1]]
warnings.warn(f'Distributing the grid of kernel "{kernelmap_entry.map.label}" over '

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This warning prints a lot of warnings like:

/capstor/scratch/cscs/ioannmag/cycle38/icon4py-benchmarks/venv_mi300/lib/python3.12/site-packages/dace/codegen/targets/cuda.py:2321: UserWarning: Distributing the grid of kernel "map_0_fieldop" over 6 chiplets, adjusting its size from [649, 1, 1] to [6, 109, 1].

when the option is enabled.
I am not really sure if this is beneficial or not

@ThrudPrimrose
ThrudPrimrose self-requested a review September 9, 2026 07:17
@ThrudPrimrose

Copy link
Copy Markdown
Collaborator

@tbennun I think the latest stage is good can re-review?

@tbennun

tbennun commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

cscs-ci run

@tbennun

tbennun commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Thanks, will review

@tbennun

tbennun commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

@iomaganaris looks like the test is failing

iomaganaris and others added 5 commits September 10, 2026 09:31
A map body carried into a nested SDFG got a local descriptor sized from
the number of elements of the outer subset, while the inner memlet was
only shifted by the outer start. For a strided outer subset such as
A[15*i:15*i+28:3] this left an inner range of 0:28:3 against a
shape-(10,) array, and parsing failed with "Memlet subset
out-of-bounds".
`StripMining._create_strided_range` over-approximated a
non-divides-evenly tile as `tile_size` indices wide while its exact
bound spans `tile_size * td_step`, so `Range.size()` reported a strided
map's tile as `tile_size / td_step` iterations -- tiling a step-2 map by
128 produced a thread-block map that `InferGPUGridAndBlockSize` measured
at 64 threads and rejected against the kernel's declared
`gpu_block_size`. The approximation now carries the step, matching the
exact bound and the `divides_evenly` branch one line above, and is
identical for `td_step == 1` so no existing caller changes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@iomaganaris

Copy link
Copy Markdown
Contributor Author

cscs-ci run

@iomaganaris

Copy link
Copy Markdown
Contributor Author

@tbennun the test is now fixed. Could you please have another look?

@tbennun
tbennun added this pull request to the merge queue Sep 11, 2026
Merged via the queue into spcl:main with commit 7e4b35e Sep 11, 2026
17 checks passed
SkyWorld117 pushed a commit to SkyWorld117/dace that referenced this pull request Sep 24, 2026
The goal of this PR is to distribute the thread blocks of the GPU
backend to AMD GPU chiplets in such way that the X dimension of the grid
is divided by the number of chiplets, so each one of them gets a
continuous range of the domain. The domains that execute on the same
chiplet can use then the same L2 cache, improving cache hits when there
are data that can be reused between thread blocks in the same region or
data that live in one dimension in two dimensional kernels (i.e. ICON
neighbor tables and other vertical level independent fields).
This change has a ~7% performance improvement in the `icon4py` dycore.
We have already discussed about the necessity of this solution with
@ThrudPrimrose I was interested to implement it in the old codegen
though because we still haven't switched to the new one

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Tal Ben-Nun <tbennun@gmail.com>
Co-authored-by: Yakup Koray Budanaz <budanaz.yakup@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants