Swizzle scales (#2979)

This commit is contained in:
Anastasiia Filippova
2026-01-10 15:32:54 -08:00
committed by GitHub
parent 1596839256
commit 43341e8d53
3 changed files with 138 additions and 82 deletions
+4 -6
View File
@@ -46,7 +46,7 @@ inline array ensure_row_contiguous_matrix(
return x_copy;
}
array pad_and_repack_scales(
array pad_and_swizzle_scales(
const array& scale,
cu::CommandEncoder& encoder,
const Stream& s) {
@@ -64,14 +64,12 @@ array pad_and_repack_scales(
cu::malloc_async(pad_outer * pad_inner, encoder),
Shape{pad_outer, pad_inner},
scale.dtype());
repack_scales(scale, scale_tiled, encoder, s);
swizzle_scales(scale, scale_tiled, encoder, s);
encoder.add_temporary(scale_tiled);
return scale_tiled;
}
} // namespace
namespace {
void qqmm_impl(
cu::CommandEncoder& encoder,
int M,
@@ -176,8 +174,8 @@ void QQMatmul::eval_gpu(const std::vector<array>& inputs, array& out) {
int K = K_packed * (32 / bits_);
// Repack scales from linear to tiled layout for tensor cores
array scale_x = pad_and_repack_scales(scale_x_pre, encoder, s);
array scale_w = pad_and_repack_scales(scale_w_pre, encoder, s);
array scale_x = pad_and_swizzle_scales(scale_x_pre, encoder, s);
array scale_w = pad_and_swizzle_scales(scale_w_pre, encoder, s);
bool x_transposed = false;
bool w_transposed = true; // always transposed
+133 -75
View File
@@ -10,6 +10,11 @@ namespace mlx::core {
namespace cg = cooperative_groups;
constexpr int TILE_ROWS = 128;
constexpr int TILE_COLS = 4;
constexpr int TILES_PER_LANE = 1;
constexpr int LANES_PER_BLOCK = 32;
// To pass scales to tensor cores, they need to be repacked into a tiled layout
// https://docs.nvidia.com/cuda/cublas/index.html#d-block-scaling-factors-layout
// Tiled layout for scale factors is very well described in CUTLASS
@@ -43,118 +48,171 @@ namespace cg = cooperative_groups;
// [252, 253, 254, 255],
// [380, 381, 382, 383],
// [508, 509, 510, 511]]]]],
__device__ size_t
scale_tiled_offset(size_t scale_index, size_t num_rows, size_t num_scale_cols) {
// Compute the tiled layout offset for scale factors used in tensor cores
// This function maps from a linear scale index to the tiled layout expected
// by tensor cores (and cublaslt).
//
// Input: linear scale index (e.g., for a matrix M x K with group_size,
// scale_index ranges from 0 to (M * K/group_size - 1))
//
// The tiled layout organizes scales into tiles of 128 rows x 4 columns,
// where each tile is subdivided into 4 sub-blocks of 32 rows x 4 columns.
size_t row = scale_index / num_scale_cols;
size_t col = scale_index % num_scale_cols;
constexpr size_t rows_per_tile = 128;
constexpr size_t rows_per_sub_block = 32;
constexpr size_t cols_per_sub_block = 4;
constexpr size_t sub_blocks_per_tile = 4; // Vertically stacked
inline std::tuple<dim3, dim3> get_swizzle_launch_args(
size_t M_swizzled,
size_t K_swizzled) {
constexpr int tiles_per_block = LANES_PER_BLOCK * TILES_PER_LANE;
constexpr int warps_per_block = TILE_ROWS / 4; // 128 / 4 = 32
// Decompose row position
size_t tile_row = row / rows_per_tile; // Which tile row
size_t row_in_tile = row % rows_per_tile; // Row within tile
size_t sub_block_row =
row_in_tile / rows_per_sub_block; // Sub-block within tile
size_t row_in_sub_block =
row_in_tile % rows_per_sub_block; // Row in sub-block
const int num_tiles_k = K_swizzled / TILE_COLS;
const int num_tiles_m = M_swizzled / TILE_ROWS;
// Decompose column position
size_t col_tile = col / cols_per_sub_block; // Which column tile
size_t col_in_sub_block = col % cols_per_sub_block; // Column within sub-block
dim3 grid;
grid.x = cuda::ceil_div(num_tiles_k, tiles_per_block);
grid.y = num_tiles_m;
grid.z = 1;
// Block is always (32, 32) = 1024 threads
dim3 block(LANES_PER_BLOCK, warps_per_block, 1);
// Compute tile index and offset within tile
size_t num_col_tiles = cuda::ceil_div(num_scale_cols, cols_per_sub_block);
size_t tile_idx = tile_row * num_col_tiles + col_tile;
size_t offset_in_tile =
(row_in_sub_block * sub_blocks_per_tile * cols_per_sub_block) +
(sub_block_row * cols_per_sub_block) + col_in_sub_block;
constexpr size_t tile_size = rows_per_tile * cols_per_sub_block;
return tile_idx * tile_size + offset_in_tile;
return std::make_tuple(grid, block);
}
namespace cu {
__global__ void repack_scales(
__global__ void swizzle_scales(
const uint8_t* scales_linear,
uint8_t* scales_tiled,
size_t input_rows,
size_t input_cols,
size_t output_rows,
size_t output_cols) {
uint8_t* scales_swizzled,
const size_t M,
const size_t K,
const size_t M_swizzled,
const size_t K_swizzled) {
constexpr int tile_size = TILE_ROWS * TILE_COLS;
constexpr int num_tile_rows_per_thread = 4;
constexpr int max_tiles_per_block = LANES_PER_BLOCK * TILES_PER_LANE;
constexpr int tile_stride = tile_size / 16; // 32 int4s per tile
// Each thread loads 16 scales from 4 rows (stride 32) and packs them into
// int4. For example: thread (0, 0) loads scales at rows 0,32,64,96 of tile 0,
// thread (1, 0) loads rows 0,32,64,96 of of tile 1, etc.
// The store is strided within a warp (stride 32 int4s), so we first
// write to shared memory, then do a coalesced store from shared to global
auto block_size = cg::this_thread_block().dim_threads();
auto block_idx = cg::this_thread_block().group_index();
auto idx_in_block = cg::this_thread_block().thread_index();
auto tidx = block_idx.x * block_size.x + idx_in_block.x;
auto tidy = block_idx.y * block_size.y + idx_in_block.y;
auto tidx = idx_in_block.x;
auto tidy = idx_in_block.y;
auto linear_tid = tidy * block_size.x + tidx;
auto grid_dim_x =
cg::this_grid().dim_blocks().x * cg::this_grid().block_index().x;
const int bid_x = block_idx.x;
const int bid_y = block_idx.y;
size_t output_index = tidx + grid_dim_x * size_t(tidy);
size_t output_size = output_rows * output_cols;
const int K_int = K_swizzled / 4;
if (output_index >= output_size) {
return;
const size_t output_offset = static_cast<size_t>(bid_y) * TILE_ROWS * K_int +
static_cast<size_t>(bid_x) * max_tiles_per_block * tile_size / 4;
int* output_block = reinterpret_cast<int*>(scales_swizzled) + output_offset;
const int grid_dim_x = cg::this_grid().dim_blocks().x;
const int grid_dim_y = cg::this_grid().dim_blocks().y;
int remaining = K_int - bid_x * max_tiles_per_block;
int tiles_in_block = min(remaining, max_tiles_per_block);
bool valid_tile = tidx * TILES_PER_LANE < tiles_in_block;
__shared__ int4 strided_scales_thread[max_tiles_per_block * tile_stride];
// Initialize to zero for padding
int thread_tile_rows[num_tile_rows_per_thread] = {0};
if (valid_tile) {
const size_t col_base =
static_cast<size_t>(bid_x) * max_tiles_per_block * TILE_COLS +
tidx * TILE_COLS;
const bool aligned_k = (K % 4 == 0);
if (aligned_k) {
// fast path: K is aligned, use vectorized loads with stride K/4
const int K_stride = K / 4;
const size_t block_offset =
static_cast<size_t>(bid_y) * TILE_ROWS * K_stride +
static_cast<size_t>(bid_x) * max_tiles_per_block;
const int* input_block =
reinterpret_cast<const int*>(scales_linear) + block_offset;
// load
#pragma unroll
for (int i = 0; i < num_tile_rows_per_thread; i++) {
const size_t row =
static_cast<size_t>(bid_y) * TILE_ROWS + i * block_size.x + tidy;
const int thread_offset =
(i * block_size.x + tidy) * K_stride + tidx * TILES_PER_LANE;
if (row < M && col_base + TILE_COLS <= K) {
thread_tile_rows[i] = __ldg(input_block + thread_offset);
} else if (row < M) {
// partial tile at K boundary: load byte-by-byte
#pragma unroll
for (int c = 0; c < TILE_COLS; c++) {
if (col_base + c < K) {
reinterpret_cast<uint8_t*>(&thread_tile_rows[i])[c] =
scales_linear[row * K + col_base + c];
}
}
}
}
} else {
#pragma unroll
for (int i = 0; i < num_tile_rows_per_thread; i++) {
const size_t row =
static_cast<size_t>(bid_y) * TILE_ROWS + i * block_size.x + tidy;
if (row < M) {
const size_t row_start = row * K;
#pragma unroll
for (int c = 0; c < TILE_COLS; c++) {
if (col_base + c < K) {
reinterpret_cast<uint8_t*>(&thread_tile_rows[i])[c] =
scales_linear[row_start + col_base + c];
}
}
}
}
}
// store to shared with XOR swizzle to avoid bank conflicts
int base_idx = tidx * tile_stride + tidy;
int xor_bits = (tidy >> 3) & 0x3;
int swizzled_idx = base_idx ^ xor_bits;
strided_scales_thread[swizzled_idx] =
*reinterpret_cast<int4*>(thread_tile_rows);
}
size_t tiled_offset =
scale_tiled_offset(output_index, output_rows, output_cols);
cg::thread_block block = cg::this_thread_block();
cg::sync(block);
size_t row = output_index / output_cols;
size_t col = output_index % output_cols;
// Probably this can be done better with 2 separated paths for valid and
// padding
if (row < input_rows && col < input_cols) {
size_t input_index = row * input_cols + col;
scales_tiled[tiled_offset] = scales_linear[input_index];
} else {
// Zero-fill padding region
scales_tiled[tiled_offset] = 0;
const int total_int4s = tiles_in_block * tile_stride;
#pragma unroll
for (int i = linear_tid; i < total_int4s; i += block_size.x * block_size.y) {
int tile_idx = i / tile_stride;
int row_idx = i % tile_stride;
int base_idx = tile_idx * tile_stride + row_idx;
int xor_bits = (row_idx >> 3) & 0x3;
int swizzled_idx = base_idx ^ xor_bits;
reinterpret_cast<int4*>(output_block)[i] =
strided_scales_thread[swizzled_idx];
}
}
} // namespace cu
void repack_scales(
void swizzle_scales(
const array& scales,
array& scales_tiled,
cu::CommandEncoder& enc,
const Stream& s) {
enc.set_input_array(scales);
enc.set_output_array(scales_tiled);
// Note: scales_tiled is padded to full tiles so if num_rows or num_cols
// are not multiples of tile sizes, the extra space is filled with zeros
// are not multiples of tile sizes
size_t input_rows = scales.shape(-2);
size_t input_cols = scales.shape(-1);
size_t output_rows = scales_tiled.shape(-2);
size_t output_cols = scales_tiled.shape(-1);
size_t output_size = output_rows * output_cols;
bool large = output_size > UINT_MAX;
auto [num_blocks, block_dims] = get_launch_args(
output_size, scales_tiled.shape(), scales_tiled.strides(), large);
auto [num_blocks, block_dims] =
get_swizzle_launch_args(output_rows, output_cols);
enc.add_kernel_node(
cu::repack_scales,
cu::swizzle_scales,
num_blocks,
block_dims,
0,
+1 -1
View File
@@ -21,7 +21,7 @@ inline std::pair<int, int> get_padded_scale_dims(int num_rows, int num_cols) {
return {padded_rows, padded_cols};
}
void repack_scales(
void swizzle_scales(
const array& scales,
array& scales_tiled,
cu::CommandEncoder& enc,