Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,19 @@ void nvte_splits_to_offsets_multi(NVTETensor split_sizes, NVTETensor *outputs,
const int64_t *strides, const int *include_leading_zero,
size_t num_outputs, cudaStream_t stream);

/*! \brief Compute multiple scaled offsets and assert row split alignment on the device.
*
* This uses the same prefix-sum kernel as nvte_splits_to_offsets_multi and does not synchronize
* the host, so the validation remains active during CUDA graph replay.
*
* \param[in] split_alignment Positive required divisor of every split size.
* \see nvte_splits_to_offsets_multi for the remaining parameters.
*/
void nvte_splits_to_offsets_multi_v2(NVTETensor split_sizes, size_t split_alignment,
NVTETensor *outputs, const int64_t *strides,
const int *include_leading_zero, size_t num_outputs,
cudaStream_t stream);

/*! \brief TE Grouped Tensor type
*
* NVTEGroupedTensor is a collection of tensors with potentially different shapes
Expand Down
63 changes: 50 additions & 13 deletions transformer_engine/common/util/splits_to_offsets.cu
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ namespace transformer_engine::splits_to_offsets {
namespace {

constexpr size_t kThreadsPerBlock = 256;
constexpr size_t kSmallThreadsPerBlock = 128;

struct KernelArgs {
static constexpr size_t kMaxNumOutputs = 8;
Expand All @@ -29,6 +30,7 @@ struct KernelArgs {
int64_t strides[kMaxNumOutputs] = {};
bool include_leading_zero[kMaxNumOutputs] = {};
size_t num_outputs = 0;
size_t split_alignment = 1;
};

__device__ __forceinline__ int64_t load_split_size(const void *__restrict__ split_sizes,
Expand Down Expand Up @@ -58,7 +60,8 @@ __device__ __forceinline__ void store_output(void *__restrict__ output, DType dt
}
}

__global__ void __launch_bounds__(kThreadsPerBlock) kernel(KernelArgs args) {
template <size_t kScanThreads>
__global__ void __launch_bounds__(kScanThreads) kernel(KernelArgs args) {
const size_t tid = threadIdx.x;

// Fill leading zeros if needed
Expand All @@ -71,7 +74,7 @@ __global__ void __launch_bounds__(kThreadsPerBlock) kernel(KernelArgs args) {
}

// Workspace for prefix sum chunk
__shared__ int64_t block_scan[kThreadsPerBlock];
__shared__ int64_t block_scan[kScanThreads];

// Sum from previous chunks
__shared__ int64_t chunk_prefix;
Expand All @@ -81,19 +84,35 @@ __global__ void __launch_bounds__(kThreadsPerBlock) kernel(KernelArgs args) {
__syncthreads();

// Perform prefix sum in chunks
for (size_t chunk_start = 0; chunk_start < args.num_splits; chunk_start += kThreadsPerBlock) {
for (size_t chunk_start = 0; chunk_start < args.num_splits; chunk_start += kScanThreads) {
const size_t idx = chunk_start + tid;

// Load input from global memory into shared memory
if (idx < args.num_splits) {
block_scan[tid] = load_split_size(args.split_sizes, args.split_sizes_dtype, idx);
} else {
block_scan[tid] = 0;
// Validate before the scan. All threads leave together so no thread waits at a barrier
// after another thread has trapped on invalid split metadata.
const int64_t split =
idx < args.num_splits ? load_split_size(args.split_sizes, args.split_sizes_dtype, idx) : 0;
if (args.split_alignment > 1) {
const size_t alignment_mask = args.split_alignment - 1;
bool misaligned;
if ((args.split_alignment & alignment_mask) == 0) {
misaligned = (static_cast<size_t>(split) & alignment_mask) != 0;
} else {
misaligned = static_cast<size_t>(split) % args.split_alignment != 0;
}
const bool invalid = idx < args.num_splits && (split < 0 || misaligned);
if (__syncthreads_or(invalid)) {
if (tid == 0) {
NVTE_DEVICE_ERROR("Grouped split size is not divisible by split_alignment.");
Comment thread
vthumbe1503 marked this conversation as resolved.
Resolved
__trap(); // NVTE_DEVICE_ERROR's assert is compiled out of release builds.
}
return;
}
}
block_scan[tid] = split;
__syncthreads();
Comment on lines +90 to 112

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is fine, but bitmasking seems overkill for a kernel that's unlikely to be compute-bound. The following is a little simpler.

Suggested change
// Validate before the scan. All threads leave together so no thread waits at a barrier
// after another thread has trapped on invalid split metadata.
const int64_t split =
idx < args.num_splits ? load_split_size(args.split_sizes, args.split_sizes_dtype, idx) : 0;
if (args.split_alignment > 1) {
const size_t alignment_mask = args.split_alignment - 1;
bool misaligned;
if ((args.split_alignment & alignment_mask) == 0) {
misaligned = (static_cast<size_t>(split) & alignment_mask) != 0;
} else {
misaligned = static_cast<size_t>(split) % args.split_alignment != 0;
}
const bool invalid = idx < args.num_splits && (split < 0 || misaligned);
if (__syncthreads_or(invalid)) {
if (tid == 0) {
NVTE_DEVICE_ERROR("Grouped split size is not divisible by split_alignment.");
__trap(); // NVTE_DEVICE_ERROR's assert is compiled out of release builds.
}
return;
}
}
block_scan[tid] = split;
__syncthreads();
// Load input from global memory into shared memory
if (idx < args.num_splits) {
block_scan[tid] = load_split_size(args.split_sizes, args.split_sizes_dtype, idx);
} else {
block_scan[tid] = 0;
}
if (args.split_alignment > 1) {
// Check that splits have required alignment
if (__syncthreads_or(block_scan[tid] % args.split_alignment)) {
if (tid == 0) {
NVTE_DEVICE_ERROR("Grouped split size is not divisible by split_alignment.");
__trap(); // NVTE_DEVICE_ERROR's assert is compiled out of release builds.
}
return;
}
} else {
__syncthreads();
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I evaluated this change. And not doing the bit masking costs us 0.07us consistently for the kernel.


// Prefix sum in shared memory
for (size_t offset = 1; offset < kThreadsPerBlock; offset <<= 1) {
for (size_t offset = 1; offset < kScanThreads; offset <<= 1) {
const int64_t addend = (tid >= offset) ? block_scan[tid - offset] : 0;
__syncthreads();
block_scan[tid] += addend;
Expand All @@ -112,7 +131,7 @@ __global__ void __launch_bounds__(kThreadsPerBlock) kernel(KernelArgs args) {

// Update sum for later chunks
__syncthreads();
if (tid == kThreadsPerBlock - 1) {
if (tid == kScanThreads - 1) {
chunk_prefix += block_scan[tid];
}
__syncthreads();
Expand Down Expand Up @@ -187,7 +206,11 @@ void nvte_splits_to_offsets(const int64_t *split_sizes, int64_t *output, size_t
args.strides[0] = stride;
args.include_leading_zero[0] = true;
args.num_outputs = 1;
s2o::kernel<<<1, s2o::kThreadsPerBlock, 0, stream>>>(args);
if (num_splits <= s2o::kSmallThreadsPerBlock) {
s2o::kernel<s2o::kSmallThreadsPerBlock><<<1, s2o::kSmallThreadsPerBlock, 0, stream>>>(args);
} else {
s2o::kernel<s2o::kThreadsPerBlock><<<1, s2o::kThreadsPerBlock, 0, stream>>>(args);
}
Comment on lines +209 to +213

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is there reason to support separate kernels for these two cases? This kernel seems launch-bound, so the smaller variant doesn't give us much perf benefit.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The improves perf by about 0.2 us. I would assume this is purely warp scheduling overhead (4 vs 8 warps). But at the scale of 2-3 us for the kernel that is 10% perf benefit. And so I kept this change. But you can see that we are loosing the perf by about 0.2 us due to the introduction of alginment check.

NVTE_CHECK_CUDA(cudaGetLastError());
}

Expand Down Expand Up @@ -230,10 +253,19 @@ void nvte_splits_to_offsets_2d(const NVTETensor first_dims, const NVTETensor las
void nvte_splits_to_offsets_multi(NVTETensor split_sizes, NVTETensor *outputs,
const int64_t *strides, const int *include_leading_zero,
size_t num_outputs, cudaStream_t stream) {
NVTE_API_CALL(nvte_splits_to_offsets_multi);
nvte_splits_to_offsets_multi_v2(split_sizes, 1, outputs, strides, include_leading_zero,
num_outputs, stream);
}

void nvte_splits_to_offsets_multi_v2(NVTETensor split_sizes, size_t split_alignment,
NVTETensor *outputs, const int64_t *strides,
const int *include_leading_zero, size_t num_outputs,
cudaStream_t stream) {
NVTE_API_CALL(nvte_splits_to_offsets_multi_v2);
using namespace transformer_engine;
namespace s2o = transformer_engine::splits_to_offsets;

NVTE_CHECK(split_alignment > 0, "split_alignment must be positive.");
if (num_outputs == 0) {
return;
}
Expand Down Expand Up @@ -279,14 +311,19 @@ void nvte_splits_to_offsets_multi(NVTETensor split_sizes, NVTETensor *outputs,
args.split_sizes_dtype = split_sizes_dtype;
args.num_splits = num_splits;
args.num_outputs = chunk_size;
args.split_alignment = split_alignment;
for (size_t i = 0; i < chunk_size; ++i) {
const size_t out_idx = chunk_start + i;
args.outputs[i] = output_tensors[out_idx]->data.dptr;
args.outputs_dtype[i] = output_tensors[out_idx]->dtype();
args.strides[i] = strides[out_idx];
args.include_leading_zero[i] = include_leading_zero[out_idx] != 0;
}
s2o::kernel<<<1, s2o::kThreadsPerBlock, 0, stream>>>(args);
if (num_splits <= s2o::kSmallThreadsPerBlock) {
s2o::kernel<s2o::kSmallThreadsPerBlock><<<1, s2o::kSmallThreadsPerBlock, 0, stream>>>(args);
} else {
s2o::kernel<s2o::kThreadsPerBlock><<<1, s2o::kThreadsPerBlock, 0, stream>>>(args);
}
NVTE_CHECK_CUDA(cudaGetLastError());
}
}
2 changes: 1 addition & 1 deletion transformer_engine/pytorch/csrc/extensions.h
Original file line number Diff line number Diff line change
Expand Up @@ -586,7 +586,7 @@ at::Tensor splits_to_offsets(const at::Tensor &first_dims, int64_t logical_last_
std::tuple<at::Tensor, std::vector<at::Tensor>> splits_to_offsets_multi(
const at::Tensor &split_sizes, const c10::Device &device, const std::vector<int64_t> &strides,
const std::vector<bool> &include_leading_zero, const std::vector<at::ScalarType> &dtypes,
bool bulk_allocate_outputs);
bool bulk_allocate_outputs, size_t split_alignment);

at::Tensor copy_data_ptrs_to_device(const std::vector<at::Tensor> &tensors,
const c10::Device &device);
Expand Down
9 changes: 5 additions & 4 deletions transformer_engine/pytorch/csrc/extensions/misc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ at::Tensor splits_to_offsets(const at::Tensor &first_dims, int64_t logical_last_
std::tuple<at::Tensor, std::vector<at::Tensor>> splits_to_offsets_multi(
const at::Tensor &split_sizes, const c10::Device &device, const std::vector<int64_t> &strides,
const std::vector<bool> &include_leading_zero, const std::vector<at::ScalarType> &dtypes,
bool bulk_allocate_outputs) {
bool bulk_allocate_outputs, size_t split_alignment) {
const size_t num_outputs = strides.size();
const size_t num_splits = static_cast<size_t>(split_sizes.numel());

Expand All @@ -51,6 +51,7 @@ std::tuple<at::Tensor, std::vector<at::Tensor>> splits_to_offsets_multi(
"strides, include_leading_zero, and dtypes must have matching lengths, but got ",
strides.size(), ", ", include_leading_zero.size(), ", and ", dtypes.size(), ".");
NVTE_CHECK(device.is_cuda(), "device must be CUDA, but got ", device.str(), ".");
NVTE_CHECK(split_alignment > 0, "split_alignment must be positive.");

// Convert split sizes to int64 GPU tensor.
const at::Tensor split_sizes_i64 =
Expand Down Expand Up @@ -94,9 +95,9 @@ std::tuple<at::Tensor, std::vector<at::Tensor>> splits_to_offsets_multi(

auto split_sizes_nvte = makeTransformerEngineTensor(split_sizes_out);
NVTE_SCOPED_GIL_RELEASE({
nvte_splits_to_offsets_multi(split_sizes_nvte.data(), outputs_nvte.data(), strides.data(),
include_leading_zero_int.data(), num_outputs,
at::cuda::getCurrentCUDAStream());
nvte_splits_to_offsets_multi_v2(split_sizes_nvte.data(), split_alignment, outputs_nvte.data(),
strides.data(), include_leading_zero_int.data(), num_outputs,
at::cuda::getCurrentCUDAStream());
});

return {split_sizes_out, std::move(outputs)};
Expand Down
3 changes: 2 additions & 1 deletion transformer_engine/pytorch/csrc/extensions/pybind.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -625,7 +625,8 @@ PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
m.def("splits_to_offsets_multi", &transformer_engine::pytorch::splits_to_offsets_multi,
"Compute multiple scaled inclusive-scan offsets from a split-sizes vector",
py::arg("split_sizes"), py::arg("device"), py::kw_only(), py::arg("strides"),
py::arg("include_leading_zero"), py::arg("dtypes"), py::arg("bulk_allocate") = false);
py::arg("include_leading_zero"), py::arg("dtypes"), py::arg("bulk_allocate") = false,
py::arg("split_alignment") = 1);
m.def("get_num_cublas_streams", &nvte_get_num_compute_streams, "Get number of compute streams",
py::call_guard<py::gil_scoped_release>());

Expand Down
7 changes: 4 additions & 3 deletions transformer_engine/pytorch/ops/basic/grouped_linear.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,7 @@ class GroupedLinear(BasicOperation):
The grouped-tensor path is selected automatically where it is
supported (see ``is_op_fuser_grouped_tensor_path_supported``). With
MXFP8 or FP8 block scaling, it relies on every split size being a
multiple of 128, as ``split_alignment`` declares. This is checked only
on the device, so other split sizes can produce incorrect results.
multiple of 128, as ``split_alignment`` declares.

Parameters
----------
Expand Down Expand Up @@ -201,7 +200,8 @@ class GroupedLinear(BasicOperation):
in the forward pass. The scale tensor has shape
``(total_tokens,)`` and is split according to the split sizes.
split_alignment : int, default = ``128``
Guaranteed alignment of each input split size.
Expected alignment for input split sizes. Split sizes that do not
match alignment may produce errors or incorrect results.
"""

# Operation expects input split sizes (and optionally scales tensor)
Expand Down Expand Up @@ -1363,6 +1363,7 @@ def _fuser_forward_grouped_tensor(
include_leading_zero=[False, True, True, True],
dtypes=[torch.int32, torch.int64, torch.int64, torch.int64],
bulk_allocate=True,
split_alignment=self.split_alignment,
)
split_points = grouped_tensor_offsets[0]
base_split_offsets = grouped_tensor_offsets[1]
Expand Down
1 change: 1 addition & 0 deletions transformer_engine/pytorch/ops/fused/grouped_mlp.py
Original file line number Diff line number Diff line change
Expand Up @@ -1491,6 +1491,7 @@ def fuser_forward(
torch.int64,
],
bulk_allocate=True,
split_alignment=256,
)

# Prepare FC1 grouped weight tensor for fused kernels.
Expand Down
Loading