Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions mlx/backend/cuda/device/gather_front.cuh
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright © 2025 Apple Inc.

#include "mlx/backend/cuda/device/indexing.cuh"
#include "mlx/backend/cuda/device/utils.cuh"

namespace mlx::core::cu {

template <typename T, typename IdxT, typename LocT, int N>
__global__ void gather_front(
const T* src,
const IdxT* indices,
T* out,
int64_t stride,
int32_t size) {
LocT row = blockIdx.x;
int n_vec = (stride + N - 1) / N;
int vec = blockIdx.y * blockDim.x + threadIdx.x;
if (vec >= n_vec) {
return;
}

auto idx = absolute_index(indices[row], size);
const T* src_row = src + static_cast<LocT>(idx) * static_cast<LocT>(stride);
T* out_row = out + static_cast<LocT>(row) * static_cast<LocT>(stride);

auto v = load_vector<N>(src_row, vec, stride, static_cast<T>(0));
store_vector<N>(out_row, vec, v, stride);
}

} // namespace mlx::core::cu
61 changes: 61 additions & 0 deletions mlx/backend/cuda/indexing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,67 @@ void Gather::eval_gpu(const std::vector<array>& inputs, array& out) {
uint32_t slice_size = std::accumulate(
slice_sizes_.begin(), slice_sizes_.end(), 1, std::multiplies<uint32_t>());

if (nidx == 1 && axes_[0] == 0 && src.flags().row_contiguous &&
inputs[1].flags().row_contiguous && slice_size == src.strides()[0] &&
inputs[1].size() <= static_cast<size_t>(INT32_MAX)) {

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.

It seems that the kernel is capable of handling the large size?

constexpr int vec_bytes = 16;
int vec_size = vec_bytes / out.dtype().size();
if (vec_size < 1) {
vec_size = 1;
}

std::string module_name = fmt::format(
"gather_front_{}_{}",
dtype_to_string(out.dtype()),
dtype_to_string(idx_dtype));

cu::JitModule& mod =
cu::get_jit_module(encoder.device(), module_name, [&]() {
std::vector<std::string> kernel_names;
for (int l = 0; l <= 1; ++l) {
kernel_names.push_back(
fmt::format(
"mlx::core::cu::gather_front<{}, {}, {}, {}>",
dtype_to_cuda_type(out.dtype()),
dtype_to_cuda_type(idx_dtype),
l ? "int64_t" : "int32_t",
vec_size));
}
return std::make_tuple(
false, jit_source_gather_front, std::move(kernel_names));
});

cu::KernelArgs args;
args.append(src);
args.append(inputs[1]);
args.append(out);
args.append<int64_t>(slice_size);
args.append<int32_t>(src.shape(0));

encoder.set_input_array(src);
encoder.set_input_array(inputs[1]);
encoder.set_output_array(out);

std::string kernel_name = fmt::format(
"mlx::core::cu::gather_front<{}, {}, {}, {}>",
dtype_to_cuda_type(out.dtype()),
dtype_to_cuda_type(idx_dtype),
large ? "int64_t" : "int32_t",
vec_size);
auto kernel = mod.get_kernel(kernel_name);

uint32_t n_vec = (slice_size + vec_size - 1) / vec_size;
uint32_t max_block_dim = 256;
uint32_t block_dim = n_vec < max_block_dim ? n_vec : max_block_dim;
dim3 block(block_dim, 1, 1);
dim3 grid(
Comment on lines +133 to +137

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.

Some nitpickings:

Suggested change
uint32_t n_vec = (slice_size + vec_size - 1) / vec_size;
uint32_t max_block_dim = 256;
uint32_t block_dim = n_vec < max_block_dim ? n_vec : max_block_dim;
dim3 block(block_dim, 1, 1);
dim3 grid(
uint32_t n_vec = cuda::ceil_div(slice_size, vec_size);
uint32_t max_block_dim = 256;
uint32_t block_dim = std::min(n_vec, max_block_dim);
dim3 block_dims(block_dim, 1, 1);
dim3 num_blocks(

We don't have standards on what names to call block and grid dims but we should use the same names in the same file. (We did not use the more common grid_dims as name because it means a different thing in Metal unfortunately.)

static_cast<uint32_t>(inputs[1].size()),
(n_vec + block_dim - 1) / block_dim,
Comment on lines +138 to +139

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.

Have you tried swapping grid.x and grid.y? It seems that currently the blocks would read in this sequence:

block0: src[0, 0:1024]
block1: src[1, 0:1024]
block2: src[2, 0:1024]
...
block8: src[0, 1024:2048]
block9: src[1, 1024:2048]

we might get better coalesce by swapping:

block0: src[0, 0:1024]
block1: src[0, 1024:2048]
block2: src[0, 2048:4096]
...
block8: src[1, 0:1024]
block9: src[1, 1024:2048]

For benchmarking you can use env var MLX_USE_CUDA_GRAPHS=0 to remove the overhead of cuda graph, there would still be overhead of lazy evaluation though.

1);
encoder.add_kernel_node_raw(kernel, grid, block, {}, 0, args.args());
return;
}

std::string module_name = fmt::format(
"gather_{}_{}_{}",
dtype_to_string(out.dtype()),
Expand Down
Loading