| 1 | /* |
| 2 | * This file contains the CUDA kernels for the fused quantized layernorm. |
| 3 | * The kernels correspond to the kernels in layernorm_kernels.cu, except they |
| 4 | * also produce quantized output directly. |
| 5 | * Currently, only static fp8 quantization is supported. |
| 6 | */ |
| 7 | |
| 8 | #include <numeric> |
| 9 | |
| 10 | #include "torch_utils.h" |
| 11 | |
| 12 | #include "cub_helpers.h" |
| 13 | #include "../core/batch_invariant.hpp" |
| 14 | #include "../quantization/w8a8/fp8/common.cuh" |
| 15 | #include "type_convert.cuh" |
| 16 | #include "dispatch_utils.h" |
| 17 | #include "quantization/vectorization_utils.cuh" |
| 18 | |
| 19 | namespace vllm { |
| 20 | |
| 21 | // TODO(woosuk): Further optimize this kernel. |
| 22 | template <typename scalar_t, typename fp8_type, int VEC_SIZE> |
| 23 | __global__ void rms_norm_static_fp8_quant_kernel( |
| 24 | fp8_type* __restrict__ out, // [..., hidden_size] |
| 25 | const scalar_t* __restrict__ input, // [..., hidden_size] |
| 26 | const int input_stride, |
| 27 | const scalar_t* __restrict__ weight, // [hidden_size] |
| 28 | const float* __restrict__ scale, // [1] |
| 29 | const float epsilon, const int num_tokens, const int hidden_size) { |
| 30 | __shared__ float s_variance; |
| 31 | float variance = 0.0f; |
| 32 | |
| 33 | const scalar_t* input_row = input + blockIdx.x * input_stride; |
| 34 | |
| 35 | auto vec_op = [&variance](const vec_n_t<scalar_t, VEC_SIZE>& vec) { |
| 36 | #pragma unroll |
| 37 | for (int i = 0; i < VEC_SIZE; ++i) { |
| 38 | float x = static_cast<float>(vec.val[i]); |
| 39 | variance += x * x; |
| 40 | } |
| 41 | }; |
| 42 | auto scalar_op = [&variance](const scalar_t& val) { |
| 43 | float x = static_cast<float>(val); |
| 44 | variance += x * x; |
| 45 | }; |
| 46 | vllm::vectorize_read_with_alignment<VEC_SIZE>( |
| 47 | input_row, hidden_size, threadIdx.x, blockDim.x, vec_op, scalar_op); |
| 48 | |
| 49 | using BlockReduce = cub::BlockReduce<float, 1024>; |
| 50 | __shared__ typename BlockReduce::TempStorage reduceStore; |
| 51 | variance = BlockReduce(reduceStore).Reduce(variance, CubAddOp{}, blockDim.x); |
| 52 | |
| 53 | if (threadIdx.x == 0) { |
| 54 | s_variance = rsqrtf(variance / hidden_size + epsilon); |
| 55 | } |
| 56 | __syncthreads(); |
| 57 | |
| 58 | // invert scale to avoid division |
| 59 | float const scale_inv = 1.0f / *scale; |
| 60 | |
| 61 | auto* v_in = reinterpret_cast<const vec_n_t<scalar_t, VEC_SIZE>*>(input_row); |
| 62 | auto* v_w = reinterpret_cast<const vec_n_t<scalar_t, VEC_SIZE>*>(weight); |
| 63 | for (int idx = threadIdx.x; idx < hidden_size / VEC_SIZE; idx += blockDim.x) { |
| 64 | vec_n_t<scalar_t, VEC_SIZE> src1 = v_in[idx]; |
| 65 | vec_n_t<scalar_t, VEC_SIZE> src2 = v_w[idx]; |
| 66 | #pragma unroll |
| 67 | for (int j = 0; j < VEC_SIZE; j++) { |
| 68 | float x = static_cast<float>(src1.val[j]); |
| 69 | float w = static_cast<float>(src2.val[j]); |
| 70 | // Round normalized result through scalar_t to match the precision of the |
| 71 | // unfused composite (rms_norm writes scalar_t, then |
| 72 | // static_scaled_fp8_quant re-loads it as float before FP8 conversion). |
| 73 | // Without this round, the fused path is strictly more accurate and |
| 74 | // disagrees with the composite at exact E4M3 quantization tie boundaries. |
| 75 | scalar_t out_norm = static_cast<scalar_t>(x * s_variance * w); |
| 76 | out[blockIdx.x * hidden_size + idx * VEC_SIZE + j] = |
| 77 | scaled_fp8_conversion<true, fp8_type>(static_cast<float>(out_norm), |
| 78 | scale_inv); |
| 79 | } |
| 80 | } |
| 81 | } |
| 82 | |
| 83 | /* Function specialization in the case of FP16/BF16 tensors. |
| 84 | Additional optimizations we can make in this case are |
| 85 | packed and vectorized operations, which help with the |
| 86 | memory latency bottleneck. */ |
| 87 | template <typename scalar_t, int width, typename fp8_type> |
| 88 | __global__ std::enable_if_t<(width > 0) && _typeConvert<scalar_t>::exists> |
| 89 | fused_add_rms_norm_static_fp8_quant_kernel( |
| 90 | fp8_type* __restrict__ out, // [..., hidden_size] |
| 91 | scalar_t* __restrict__ input, // [..., hidden_size] |
| 92 | const int input_stride, |
| 93 | scalar_t* __restrict__ residual, // [..., hidden_size] |
| 94 | const scalar_t* __restrict__ weight, // [hidden_size] |
| 95 | const float* __restrict__ scale, // [1] |
| 96 | const float epsilon, const int num_tokens, const int hidden_size) { |
| 97 | // Sanity checks on our vector struct and type-punned pointer arithmetic |
| 98 | static_assert(std::is_pod_v<_f16Vec<scalar_t, width>>); |
| 99 | static_assert(sizeof(_f16Vec<scalar_t, width>) == sizeof(scalar_t) * width); |
| 100 | |
| 101 | const int vec_hidden_size = hidden_size / width; |
| 102 | const int vec_input_stride = input_stride / width; |
| 103 | __shared__ float s_variance; |
| 104 | float variance = 0.0f; |
| 105 | /* These and the argument pointers are all declared `restrict` as they are |
| 106 | not aliased in practice. Argument pointers should not be dereferenced |
| 107 | in this kernel as that would be undefined behavior */ |
| 108 | auto* __restrict__ input_v = |
| 109 | reinterpret_cast<_f16Vec<scalar_t, width>*>(input); |
| 110 | auto* __restrict__ residual_v = |
| 111 | reinterpret_cast<_f16Vec<scalar_t, width>*>(residual); |
| 112 | auto* __restrict__ weight_v = |
| 113 | reinterpret_cast<const _f16Vec<scalar_t, width>*>(weight); |
| 114 | |
| 115 | for (int idx = threadIdx.x; idx < vec_hidden_size; idx += blockDim.x) { |
| 116 | int stride_id = blockIdx.x * vec_input_stride + idx; |
| 117 | int id = blockIdx.x * vec_hidden_size + idx; |
| 118 | _f16Vec<scalar_t, width> temp = input_v[stride_id]; |
| 119 | temp += residual_v[id]; |
| 120 | variance += temp.sum_squares(); |
| 121 | residual_v[id] = temp; |
| 122 | } |
| 123 | |
| 124 | using BlockReduce = cub::BlockReduce<float, 1024>; |
| 125 | __shared__ typename BlockReduce::TempStorage reduceStore; |
| 126 | variance = BlockReduce(reduceStore).Reduce(variance, CubAddOp{}, blockDim.x); |
| 127 | |
| 128 | if (threadIdx.x == 0) { |
| 129 | s_variance = rsqrtf(variance / hidden_size + epsilon); |
| 130 | } |
| 131 | __syncthreads(); |
| 132 | |
| 133 | // invert scale to avoid division |
| 134 | float const scale_inv = 1.0f / *scale; |
| 135 | |
| 136 | for (int idx = threadIdx.x; idx < vec_hidden_size; idx += blockDim.x) { |
| 137 | int id = blockIdx.x * vec_hidden_size + idx; |
| 138 | _f16Vec<scalar_t, width> res = residual_v[id]; |
| 139 | _f16Vec<scalar_t, width> w = weight_v[idx]; |
| 140 | using Converter = _typeConvert<scalar_t>; |
| 141 | using HipT = typename Converter::hip_type; |
| 142 | #pragma unroll |
| 143 | for (int i = 0; i < width; ++i) { |
| 144 | float x = Converter::convert(res.data[i]); |
| 145 | float wf = Converter::convert(w.data[i]); |
| 146 | // See note in rms_norm_static_fp8_quant_kernel: round through scalar_t |
| 147 | // to match the unfused composite path at FP8 boundaries. We use the |
| 148 | // backend's hip_type for the intermediate since c10::Half/BFloat16 has |
| 149 | // ambiguous conversions on CUDA and no implicit conversion on ROCm. |
| 150 | HipT out_norm_h = Converter::convert(x * s_variance * wf); |
| 151 | out[id * width + i] = scaled_fp8_conversion<true, fp8_type>( |
| 152 | Converter::convert(out_norm_h), scale_inv); |
| 153 | } |
| 154 | } |
| 155 | } |
| 156 | |
| 157 | /* Generic fused_add_rms_norm_kernel |
| 158 | The width field is not used here but necessary for other specializations. |
| 159 | */ |
| 160 | template <typename scalar_t, int width, typename fp8_type> |
| 161 | __global__ std::enable_if_t<(width == 0) || !_typeConvert<scalar_t>::exists> |
| 162 | fused_add_rms_norm_static_fp8_quant_kernel( |
| 163 | fp8_type* __restrict__ out, // [..., hidden_size] |
| 164 | scalar_t* __restrict__ input, // [..., hidden_size] |
| 165 | const int input_stride, |
| 166 | scalar_t* __restrict__ residual, // [..., hidden_size] |
| 167 | const scalar_t* __restrict__ weight, // [hidden_size] |
| 168 | const float* __restrict__ scale, // [1] |
| 169 | const float epsilon, const int num_tokens, const int hidden_size) { |
| 170 | __shared__ float s_variance; |
| 171 | float variance = 0.0f; |
| 172 | |
| 173 | for (int idx = threadIdx.x; idx < hidden_size; idx += blockDim.x) { |
| 174 | scalar_t z = input[blockIdx.x * input_stride + idx]; |
| 175 | z += residual[blockIdx.x * hidden_size + idx]; |
| 176 | float x = (float)z; |
| 177 | variance += x * x; |
| 178 | residual[blockIdx.x * hidden_size + idx] = z; |
| 179 | } |
| 180 | |
| 181 | using BlockReduce = cub::BlockReduce<float, 1024>; |
| 182 | __shared__ typename BlockReduce::TempStorage reduceStore; |
| 183 | variance = BlockReduce(reduceStore).Reduce(variance, CubAddOp{}, blockDim.x); |
| 184 | |
| 185 | if (threadIdx.x == 0) { |
| 186 | s_variance = rsqrtf(variance / hidden_size + epsilon); |
| 187 | } |
| 188 | __syncthreads(); |
| 189 | |
| 190 | // invert scale to avoid division |
| 191 | float const scale_inv = 1.0f / *scale; |
| 192 | |
| 193 | for (int idx = threadIdx.x; idx < hidden_size; idx += blockDim.x) { |
| 194 | float x = (float)residual[blockIdx.x * hidden_size + idx]; |
| 195 | float w = (float)weight[idx]; |
| 196 | // See note in rms_norm_static_fp8_quant_kernel: round through scalar_t |
| 197 | // to match the unfused composite path at FP8 boundaries. |
| 198 | scalar_t out_norm = static_cast<scalar_t>(x * s_variance * w); |
| 199 | out[blockIdx.x * hidden_size + idx] = scaled_fp8_conversion<true, fp8_type>( |
| 200 | static_cast<float>(out_norm), scale_inv); |
| 201 | } |
| 202 | } |
| 203 | |
| 204 | } // namespace vllm |
| 205 | |
| 206 | void rms_norm_static_fp8_quant( |
| 207 | torch::stable::Tensor& out, // [..., hidden_size] |
| 208 | torch::stable::Tensor& input, // [..., hidden_size] |
| 209 | torch::stable::Tensor& weight, // [hidden_size] |
| 210 | torch::stable::Tensor& scale, // [1] |
| 211 | double epsilon) { |
| 212 | STD_TORCH_CHECK(out.is_contiguous()); |
| 213 | int hidden_size = input.size(-1); |
| 214 | int input_stride = input.stride(-2); |
| 215 | int num_tokens = input.numel() / hidden_size; |
| 216 | |
| 217 | // For large num_tokens, use smaller blocks to increase SM concurrency. |
| 218 | const bool batch_invariant_launch = vllm::vllm_is_batch_invariant(); |
| 219 | const int max_block_size = |
| 220 | batch_invariant_launch ? 1024 : ((num_tokens < 256) ? 1024 : 256); |
| 221 | dim3 grid(num_tokens); |
| 222 | const torch::stable::accelerator::DeviceGuard device_guard( |
| 223 | input.get_device_index()); |
| 224 | const cudaStream_t stream = get_current_cuda_stream(); |
| 225 | VLLM_STABLE_DISPATCH_FLOATING_TYPES( |
| 226 | input.scalar_type(), "rms_norm_kernel_scalar_type", [&] { |
| 227 | VLLM_STABLE_DISPATCH_FP8_TYPES( |
| 228 | out.scalar_type(), "rms_norm_kernel_fp8_type", [&] { |
| 229 | const int calculated_vec_size = |
| 230 | std::gcd(16 / sizeof(scalar_t), hidden_size); |
| 231 | const int block_size = |
| 232 | std::min(hidden_size / calculated_vec_size, max_block_size); |
| 233 | dim3 block(block_size); |
| 234 | VLLM_STABLE_DISPATCH_VEC_SIZE(calculated_vec_size, [&] { |
| 235 | vllm::rms_norm_static_fp8_quant_kernel<scalar_t, fp8_t, |
| 236 | vec_size> |
| 237 | <<<grid, block, 0, stream>>>( |
| 238 | out.mutable_data_ptr<fp8_t>(), |
| 239 | input.const_data_ptr<scalar_t>(), input_stride, |
| 240 | weight.const_data_ptr<scalar_t>(), |
| 241 | scale.const_data_ptr<float>(), epsilon, num_tokens, |
| 242 | hidden_size); |
| 243 | }); |
| 244 | }); |
| 245 | }); |
| 246 | } |
| 247 | |
| 248 | #define LAUNCH_FUSED_ADD_RMS_NORM(width) \ |
| 249 | VLLM_STABLE_DISPATCH_FLOATING_TYPES( \ |
| 250 | input.scalar_type(), "fused_add_rms_norm_kernel_scalar_type", [&] { \ |
| 251 | VLLM_STABLE_DISPATCH_FP8_TYPES( \ |
| 252 | out.scalar_type(), "fused_add_rms_norm_kernel_fp8_type", [&] { \ |
| 253 | vllm::fused_add_rms_norm_static_fp8_quant_kernel<scalar_t, \ |
| 254 | width, fp8_t> \ |
| 255 | <<<grid, block, 0, stream>>>( \ |
| 256 | out.mutable_data_ptr<fp8_t>(), \ |
| 257 | input.mutable_data_ptr<scalar_t>(), input_stride, \ |
| 258 | residual.mutable_data_ptr<scalar_t>(), \ |
| 259 | weight.const_data_ptr<scalar_t>(), \ |
| 260 | scale.const_data_ptr<float>(), epsilon, num_tokens, \ |
| 261 | hidden_size); \ |
| 262 | }); \ |
| 263 | }); |
| 264 | void fused_add_rms_norm_static_fp8_quant( |
| 265 | torch::stable::Tensor& out, // [..., hidden_size], |
| 266 | torch::stable::Tensor& input, // [..., hidden_size] |
| 267 | torch::stable::Tensor& residual, // [..., hidden_size] |
| 268 | torch::stable::Tensor& weight, // [hidden_size] |
| 269 | torch::stable::Tensor& scale, // [1] |
| 270 | double epsilon) { |
| 271 | STD_TORCH_CHECK(out.is_contiguous()); |
| 272 | STD_TORCH_CHECK(residual.is_contiguous()); |
| 273 | STD_TORCH_CHECK(residual.scalar_type() == input.scalar_type()); |
| 274 | STD_TORCH_CHECK(weight.scalar_type() == input.scalar_type()); |
| 275 | int hidden_size = input.size(-1); |
| 276 | int input_stride = input.stride(-2); |
| 277 | int num_tokens = input.numel() / hidden_size; |
| 278 | |
| 279 | dim3 grid(num_tokens); |
| 280 | /* This kernel is memory-latency bound in many scenarios. |
| 281 | When num_tokens is large, a smaller block size allows |
| 282 | for increased block occupancy on CUs and better latency |
| 283 | hiding on global mem ops. */ |
| 284 | const bool batch_invariant_launch = vllm::vllm_is_batch_invariant(); |
| 285 | const int max_block_size = |
| 286 | batch_invariant_launch ? 1024 : ((num_tokens < 256) ? 1024 : 256); |
| 287 | dim3 block(std::min(hidden_size, max_block_size)); |
| 288 | const torch::stable::accelerator::DeviceGuard device_guard( |
| 289 | input.get_device_index()); |
| 290 | const cudaStream_t stream = get_current_cuda_stream(); |
| 291 | /*If the tensor types are FP16/BF16, try to use the optimized kernel |
| 292 | with packed + vectorized ops. |
| 293 | Max optimization is achieved with a width-8 vector of FP16/BF16s |
| 294 | since we can load at most 128 bits at once in a global memory op. |
| 295 | However, this requires each tensor's data to be aligned to 16 |
| 296 | bytes. |
| 297 | */ |
| 298 | auto inp_ptr = reinterpret_cast<std::uintptr_t>(input.data_ptr()); |
| 299 | auto res_ptr = reinterpret_cast<std::uintptr_t>(residual.data_ptr()); |
| 300 | auto wt_ptr = reinterpret_cast<std::uintptr_t>(weight.data_ptr()); |
| 301 | bool ptrs_are_aligned = |
| 302 | inp_ptr % 16 == 0 && res_ptr % 16 == 0 && wt_ptr % 16 == 0; |
| 303 | if (ptrs_are_aligned && hidden_size % 8 == 0 && input_stride % 8 == 0 && |
| 304 | !batch_invariant_launch) { |
| 305 | LAUNCH_FUSED_ADD_RMS_NORM(8); |
| 306 | } else { |
| 307 | LAUNCH_FUSED_ADD_RMS_NORM(0); |
| 308 | } |
| 309 | } |