Skip to content

Commit 56f0f45

Browse files
committed
Optimize scale swizzle kernels
Use optimized rowwise full-width and columnwise chunk kernels for MXFP8 and NVFP4 scales, including multi-tensor swizzling. Keep the 16-byte scale-buffer alignment contract and cover its validation in C++ tests. Signed-off-by: Varun Thumbe <vthumbe@nvidia.com>
1 parent 96c0fb0 commit 56f0f45

5 files changed

Lines changed: 669 additions & 791 deletions

File tree

‎docs/envvars.rst‎

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -351,12 +351,6 @@ Kernel Configuration
351351
:Default: ``0``
352352
:Description: Disable NVRTC (CUDA Runtime Compilation) support. When set to ``1``, runtime kernel compilation is disabled. Existing transpose operations select their static fallback automatically. NVFP4 4over6 quantization, fused softmax, and normalization paths require their corresponding ``NVTE_BUILD_LEGACY_STATIC_NVFP4``, ``NVTE_BUILD_LEGACY_STATIC_FUSED_SOFTMAX``, or ``NVTE_BUILD_LEGACY_STATIC_NORM`` CMake option to have been enabled when the library was built; otherwise no static fallback is available.
353353

354-
.. envvar:: NVTE_MXFP8_PER_TILE_SWIZZLE
355-
356-
:Type: ``int`` (0 or 1)
357-
:Default: ``0``
358-
:Description: Swizzle MXFP8 scaling factors for GEMM with kernels that give each 128-row tile of scaling factors (row-wise) or each 128-column by 32-row block (column-wise) its own thread block, instead of batching 32 tiles in one block. This lowers the latency of swizzling the scaling factors of tensors with few tiles, such as activations.
359-
360354
.. envvar:: NVTE_USE_CUTLASS_GROUPED_GEMM
361355

362356
:Type: ``int`` (0 or 1)

‎tests/cpp/operator/test_multi_swizzle.cu‎

Lines changed: 176 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -51,18 +51,25 @@ void compute_ref_swizzle(const uint8_t *h_input, uint8_t *h_output,
5151
}
5252
}
5353

54-
static void zero_scale_inv_padding(uint8_t *buf,
55-
size_t padded_rows, size_t padded_cols,
56-
size_t orig_rows, size_t orig_cols) {
54+
static void set_scale_inv_padding(uint8_t *buf,
55+
size_t padded_rows, size_t padded_cols,
56+
size_t orig_rows, size_t orig_cols,
57+
uint8_t value) {
5758
for (size_t r = 0; r < padded_rows; ++r) {
5859
for (size_t c = 0; c < padded_cols; ++c) {
5960
if (r >= orig_rows || c >= orig_cols) {
60-
buf[r * padded_cols + c] = 0;
61+
buf[r * padded_cols + c] = value;
6162
}
6263
}
6364
}
6465
}
6566

67+
static void zero_scale_inv_padding(uint8_t *buf,
68+
size_t padded_rows, size_t padded_cols,
69+
size_t orig_rows, size_t orig_cols) {
70+
set_scale_inv_padding(buf, padded_rows, padded_cols, orig_rows, orig_cols, 0);
71+
}
72+
6673
// ===================================================================
6774
// Multi-tensor swizzle test
6875
// ===================================================================
@@ -141,6 +148,159 @@ void performTestMultiTensorSwizzle(const int num_tensors, const size_t M, const
141148
}
142149
}
143150

151+
void performTestMultiTensorSwizzleNVFP4(const int num_tensors, const size_t M, const size_t K,
152+
bool rowwise) {
153+
using namespace test;
154+
constexpr size_t BLOCK_SIZE = 16;
155+
std::vector<std::unique_ptr<Tensor>> inputs;
156+
std::vector<std::unique_ptr<Tensor>> outputs;
157+
std::vector<NVTETensor> input_handles;
158+
std::vector<NVTETensor> output_handles;
159+
160+
for (int i = 0; i < num_tensors; ++i) {
161+
auto input = std::make_unique<Tensor>("input_" + std::to_string(i),
162+
std::vector<size_t>{M, K}, DType::kFloat4E2M1, rowwise,
163+
!rowwise, NVTE_NVFP4_1D_SCALING);
164+
auto output = std::make_unique<Tensor>("output_" + std::to_string(i),
165+
std::vector<size_t>{M, K}, DType::kFloat4E2M1, rowwise,
166+
!rowwise, NVTE_NVFP4_1D_SCALING);
167+
output->set_with_gemm_swizzled_scales(true);
168+
if (rowwise) {
169+
input->fill_uniform_rowwise_scale_inv();
170+
} else {
171+
input->fill_uniform_columnwise_scale_inv();
172+
}
173+
const NVTEShape scale_shape =
174+
rowwise ? input->rowwise_scale_inv_shape() : input->columnwise_scale_inv_shape();
175+
uint8_t *input_scales = rowwise ? input->rowwise_cpu_scale_inv_ptr<uint8_t>()
176+
: input->columnwise_cpu_scale_inv_ptr<uint8_t>();
177+
const size_t original_rows = rowwise ? M : K;
178+
const size_t original_cols =
179+
rowwise ? (K + BLOCK_SIZE - 1) / BLOCK_SIZE : (M + BLOCK_SIZE - 1) / BLOCK_SIZE;
180+
set_scale_inv_padding(input_scales, scale_shape.data[0], scale_shape.data[1], original_rows,
181+
original_cols, 0xFF);
182+
input->from_cpu();
183+
input_handles.push_back(input->data());
184+
output_handles.push_back(output->data());
185+
inputs.emplace_back(std::move(input));
186+
outputs.emplace_back(std::move(output));
187+
}
188+
189+
nvte_multi_tensor_swizzle_scaling_factors(input_handles.data(), output_handles.data(),
190+
num_tensors, 0);
191+
NVTE_CHECK_CUDA(cudaDeviceSynchronize());
192+
for (int i = 0; i < num_tensors; ++i) {
193+
outputs[i]->to_cpu();
194+
const NVTEShape scale_shape =
195+
rowwise ? inputs[i]->rowwise_scale_inv_shape() : inputs[i]->columnwise_scale_inv_shape();
196+
const uint8_t *input_scales = rowwise ? inputs[i]->rowwise_cpu_scale_inv_ptr<uint8_t>()
197+
: inputs[i]->columnwise_cpu_scale_inv_ptr<uint8_t>();
198+
const uint8_t *actual = rowwise ? outputs[i]->rowwise_cpu_scale_inv_ptr<uint8_t>()
199+
: outputs[i]->columnwise_cpu_scale_inv_ptr<uint8_t>();
200+
std::vector<uint8_t> reference_input(
201+
input_scales, input_scales + scale_shape.data[0] * scale_shape.data[1]);
202+
const size_t original_rows = rowwise ? M : K;
203+
const size_t original_cols =
204+
rowwise ? (K + BLOCK_SIZE - 1) / BLOCK_SIZE : (M + BLOCK_SIZE - 1) / BLOCK_SIZE;
205+
zero_scale_inv_padding(reference_input.data(), scale_shape.data[0], scale_shape.data[1],
206+
original_rows, original_cols);
207+
std::vector<uint8_t> reference(scale_shape.data[0] * scale_shape.data[1]);
208+
compute_ref_swizzle<128, 4, true>(reference_input.data(), reference.data(), scale_shape.data[0],
209+
scale_shape.data[1]);
210+
compareResults("multi_nvfp4_swizzle", actual, reference.data(), reference.size());
211+
}
212+
}
213+
214+
TEST(MultiTensorSwizzleTest, MXFP8ColumnwiseBatchIgnoresUnusedRowwiseBuffer) {
215+
using namespace test;
216+
constexpr int num_tensors = 2;
217+
const std::vector<size_t> shape{256, 128};
218+
std::vector<std::unique_ptr<Tensor>> inputs;
219+
std::vector<std::unique_ptr<Tensor>> outputs;
220+
std::vector<NVTETensor> input_handles;
221+
std::vector<NVTETensor> output_handles;
222+
223+
for (int i = 0; i < num_tensors; ++i) {
224+
const bool has_rowwise_scales = i == 0;
225+
auto input = std::make_unique<Tensor>("input_" + std::to_string(i), shape,
226+
DType::kFloat8E4M3, has_rowwise_scales, true,
227+
NVTE_MXFP8_1D_SCALING);
228+
auto output = std::make_unique<Tensor>("output_" + std::to_string(i), shape,
229+
DType::kFloat8E4M3, has_rowwise_scales, true,
230+
NVTE_MXFP8_1D_SCALING);
231+
if (has_rowwise_scales) input->fill_uniform_rowwise_scale_inv();
232+
input->fill_uniform_columnwise_scale_inv();
233+
output->set_with_gemm_swizzled_scales(true);
234+
input->from_cpu();
235+
input_handles.push_back(input->data());
236+
output_handles.push_back(output->data());
237+
inputs.emplace_back(std::move(input));
238+
outputs.emplace_back(std::move(output));
239+
}
240+
241+
nvte_multi_tensor_swizzle_scaling_factors(input_handles.data(), output_handles.data(),
242+
num_tensors, 0);
243+
NVTE_CHECK_CUDA(cudaDeviceSynchronize());
244+
for (int i = 0; i < num_tensors; ++i) {
245+
outputs[i]->to_cpu();
246+
const NVTEShape scale_shape = inputs[i]->columnwise_scale_inv_shape();
247+
std::vector<uint8_t> reference(scale_shape.data[0] * scale_shape.data[1]);
248+
compute_ref_swizzle<128, 4, false>(
249+
inputs[i]->columnwise_cpu_scale_inv_ptr<uint8_t>(), reference.data(), scale_shape.data[1],
250+
scale_shape.data[0]);
251+
compareResults("multi_mixed_buffers_columnwise_" + std::to_string(i),
252+
outputs[i]->columnwise_cpu_scale_inv_ptr<uint8_t>(), reference.data(),
253+
reference.size());
254+
}
255+
}
256+
257+
TEST(MultiTensorSwizzleTest, NVFP4FullWidthChunkLaunchSplitting) {
258+
for (const bool rowwise : {false, true}) {
259+
performTestMultiTensorSwizzleNVFP4(4, 160, 96, rowwise);
260+
performTestMultiTensorSwizzleNVFP4(4, 128, 128, rowwise);
261+
performTestMultiTensorSwizzleNVFP4(65, 128, 128, rowwise);
262+
}
263+
}
264+
265+
TEST(MultiTensorSwizzleTest, RowwiseMixedSharedMemoryFallback) {
266+
using namespace test;
267+
// The first tensor fits the full-width shared-memory limit, but the second does not.
268+
const std::vector<std::pair<size_t, size_t>> shapes{{128, 7168}, {128, 8064}};
269+
std::vector<std::unique_ptr<Tensor>> inputs;
270+
std::vector<std::unique_ptr<Tensor>> outputs;
271+
std::vector<NVTETensor> input_handles;
272+
std::vector<NVTETensor> output_handles;
273+
for (size_t i = 0; i < shapes.size(); ++i) {
274+
const auto [M, K] = shapes[i];
275+
auto input = std::make_unique<Tensor>("input_" + std::to_string(i),
276+
std::vector<size_t>{M, K}, DType::kFloat8E4M3, true,
277+
false, NVTE_MXFP8_1D_SCALING);
278+
auto output = std::make_unique<Tensor>("output_" + std::to_string(i),
279+
std::vector<size_t>{M, K}, DType::kFloat8E4M3, true,
280+
false, NVTE_MXFP8_1D_SCALING);
281+
output->set_with_gemm_swizzled_scales(true);
282+
input->fill_uniform_rowwise_scale_inv();
283+
input->from_cpu();
284+
input_handles.push_back(input->data());
285+
output_handles.push_back(output->data());
286+
inputs.emplace_back(std::move(input));
287+
outputs.emplace_back(std::move(output));
288+
}
289+
290+
nvte_multi_tensor_swizzle_scaling_factors(
291+
input_handles.data(), output_handles.data(), static_cast<int>(inputs.size()), 0);
292+
NVTE_CHECK_CUDA(cudaDeviceSynchronize());
293+
for (size_t i = 0; i < inputs.size(); ++i) {
294+
outputs[i]->to_cpu();
295+
const NVTEShape scale_shape = inputs[i]->rowwise_scale_inv_shape();
296+
std::vector<uint8_t> reference(scale_shape.data[0] * scale_shape.data[1]);
297+
compute_ref_swizzle<128, 4, true>(inputs[i]->rowwise_cpu_scale_inv_ptr<uint8_t>(),
298+
reference.data(), scale_shape.data[0], scale_shape.data[1]);
299+
compareResults("multi_rowwise_fallback", outputs[i]->rowwise_cpu_scale_inv_ptr<uint8_t>(),
300+
reference.data(), reference.size());
301+
}
302+
}
303+
144304
// ===================================================================
145305
// Multi-tensor unswizzle test (uses single-tensor swizzle to prepare)
146306
// ===================================================================
@@ -300,22 +460,11 @@ void performTestMultiTensorRoundtrip(const int num_tensors, const size_t M, cons
300460
// Test suites
301461
// ===================================================================
302462

303-
// Sets NVTE_MXFP8_PER_TILE_SWIZZLE while it lives, so that MXFP8 swizzles use the per-tile
304-
// kernels (enable) or the default ones.
305-
class PerTileSwizzle {
306-
public:
307-
explicit PerTileSwizzle(bool enable) {
308-
setenv("NVTE_MXFP8_PER_TILE_SWIZZLE", enable ? "1" : "0", 1);
309-
}
310-
~PerTileSwizzle() { unsetenv("NVTE_MXFP8_PER_TILE_SWIZZLE"); }
311-
};
312-
313463
class MultiTensorSwizzleTestSuite
314-
: public ::testing::TestWithParam<std::tuple<std::tuple<int, size_t, size_t, bool>, bool>> {};
464+
: public ::testing::TestWithParam<std::tuple<int, size_t, size_t, bool>> {};
315465

316466
TEST_P(MultiTensorSwizzleTestSuite, TestMultiTensorSwizzle) {
317-
const auto& [num_tensors, M, K, rowwise] = std::get<0>(GetParam());
318-
const PerTileSwizzle per_tile_swizzle(std::get<1>(GetParam()));
467+
const auto& [num_tensors, M, K, rowwise] = GetParam();
319468
performTestMultiTensorSwizzle(num_tensors, M, K, rowwise);
320469
}
321470

@@ -343,17 +492,7 @@ TEST_P(MultiTensorRoundtripTestSuite, TestMultiTensorRoundtrip) {
343492

344493
namespace {
345494

346-
// Shapes that exercise the narrow_k kernel (rowwise) / narrow_m kernel (colwise):
347-
// Narrow-K fires when ALL tensors have scale num_tiles_k < TB_DIM (32),
348-
// i.e. padded ceil(K/32) < 128.
349-
// Narrow-M fires analogously for colwise when padded K < 4096
350-
// (since colwise m = K padded to 128, num_tiles_m = m / 128 < 32).
351-
//
352-
// Shapes that bypass narrow and use the regular multi_tensor kernel:
353-
// K >= 4096 makes num_tiles_k >= 32 (rowwise) and num_tiles_m >= 32 (colwise).
354-
355495
std::vector<std::tuple<int, size_t, size_t, bool>> multi_tensor_test_cases = {
356-
// --- Narrow path cases (K small → narrow_k for row, narrow_m for col) ---
357496
// M and K both aligned to 128
358497
{3, 256, 256, true},
359498
{3, 256, 256, false},
@@ -365,7 +504,7 @@ std::vector<std::tuple<int, size_t, size_t, bool>> multi_tensor_test_cases = {
365504
{3, 192, 256, false},
366505
{2, 64, 256, true},
367506
{2, 64, 256, false},
368-
// Larger narrow K (num_tiles_k = 8, shared mem = 128 KB)
507+
// Larger K
369508
{2, 128, 1024, true},
370509
{2, 128, 1024, false},
371510
// K not divisible by 128
@@ -380,13 +519,16 @@ std::vector<std::tuple<int, size_t, size_t, bool>> multi_tensor_test_cases = {
380519
{4, 32, 64, true},
381520
{4, 32, 64, false},
382521

383-
// --- Non-narrow path cases (K >= 4096 → regular multi_tensor kernel) ---
384522
{3, 256, 4096, true},
385523
{3, 256, 4096, false},
386524
{2, 128, 8192, true},
387525
{2, 128, 8192, false},
388526
// Rowwise num_tiles_k = 34 selects vec_load_size = 2 (int2 kernel)
389527
{2, 128, 4352, true},
528+
// Wide row-wise scales fall back for an underfilled grid and use the full-width kernel once
529+
// the aggregate grid is large enough.
530+
{2, 2048, 7168, true}, // 32 aggregate row tiles: regular fallback.
531+
{16, 2048, 7168, true}, // 256 aggregate row tiles: rowwise full-width kernel.
390532
// Colwise num_tiles_k = 512 / 32 / 4 = 4 selects vec_load_size = 4 (int4 kernel)
391533
{2, 512, 4096, false},
392534
// MXFP8 swizzle: more tensors than one launch takes, several row-wise tiles per CTA,
@@ -404,14 +546,12 @@ std::vector<std::tuple<int, size_t, size_t, bool>> multi_tensor_test_cases = {
404546
INSTANTIATE_TEST_SUITE_P(
405547
OperatorTest,
406548
MultiTensorSwizzleTestSuite,
407-
::testing::Combine(::testing::ValuesIn(multi_tensor_test_cases), ::testing::Values(false, true)),
549+
::testing::ValuesIn(multi_tensor_test_cases),
408550
[](const testing::TestParamInfo<MultiTensorSwizzleTestSuite::ParamType>& info) {
409-
const auto& test_case = std::get<0>(info.param);
410-
return "n" + std::to_string(std::get<0>(test_case)) +
411-
"_M" + std::to_string(std::get<1>(test_case)) +
412-
"_K" + std::to_string(std::get<2>(test_case)) +
413-
(std::get<3>(test_case) ? "_row" : "_col") +
414-
(std::get<1>(info.param) ? "_per_tile" : "");
551+
return "n" + std::to_string(std::get<0>(info.param)) +
552+
"_M" + std::to_string(std::get<1>(info.param)) +
553+
"_K" + std::to_string(std::get<2>(info.param)) +
554+
(std::get<3>(info.param) ? "_row" : "_col");
415555
});
416556

417557
INSTANTIATE_TEST_SUITE_P(

0 commit comments

Comments
 (0)