Repository navigation
[OMNIML-5899] Add Q8_0 CUDA packing kernel #2515
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3af9b00
c7f0fb9
4c8e5cc
7d3f375
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,6 +24,7 @@ | |
| at::Tensor iq1_s_pack_cuda(at::Tensor input, at::Tensor grid); | ||
| at::Tensor iq2_xs_pack_cuda(at::Tensor input, at::Tensor grid, at::Tensor scales); | ||
| at::Tensor iq2_xxs_pack_cuda(at::Tensor input, at::Tensor grid, at::Tensor scales); | ||
| at::Tensor q8_0_pack_cuda(at::Tensor input); | ||
|
|
||
| namespace { | ||
|
|
||
|
|
@@ -71,6 +72,12 @@ at::Tensor iq2_xxs_pack(at::Tensor input, at::Tensor grid, at::Tensor scales) { | |
| return iq2_xxs_pack_cuda(input.contiguous(), grid.contiguous(), scales.contiguous()); | ||
| } | ||
|
|
||
| at::Tensor q8_0_pack(at::Tensor input) { | ||
| TORCH_CHECK(input.is_cuda(), "Q8_0 packing requires a CUDA input"); | ||
| modelopt::ggml::check_scalar_pack_input("Q8_0", input, 32); | ||
| return q8_0_pack_cuda(input.contiguous()); | ||
| } | ||
|
|
||
| } // namespace | ||
|
|
||
| PYBIND11_MODULE(TORCH_EXTENSION_NAME, module) { | ||
|
|
@@ -95,4 +102,9 @@ PYBIND11_MODULE(TORCH_EXTENSION_NAME, module) { | |
| "Returns uint8 [numel / 256, 66] on the input device. Non-finite input elements are " | ||
| "treated as zero during packing, and finite elements outside the float32 range " | ||
| "saturate."); | ||
| module.def("q8_0_pack", &q8_0_pack, | ||
| "Pack a non-empty float32, float64, float16, or bfloat16 CUDA tensor whose innermost " | ||
| "dimension is a multiple of 32. Returns uint8 [numel / 32, 34] on the input device. " | ||
| "Non-finite input elements are treated as zero during packing, and finite elements " | ||
| "outside the float32 range saturate."); | ||
|
Comment on lines
+105
to
+109
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [SUGGESTION] The documented saturation contract is incomplete for Q8_0.
Since this docstring is the user-facing contract for the packer, consider stating the scale limit explicitly, e.g. "...blocks whose absolute maximum exceeds ~8.3e6 saturate, because the per-block scale is stored as float16." A matching note next to the |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| /* | ||
| * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| #include "common.cuh" | ||
|
|
||
| namespace { | ||
|
|
||
| using namespace modelopt::ggml; | ||
|
|
||
| // This CUDA encoder was independently implemented for ModelOpt. It follows the Q8_0 packed | ||
| // payload and scalar quantization formula defined by GGML; no llama.cpp implementation code is | ||
| // incorporated here. Format reference: | ||
| // https://github.com/ggml-org/llama.cpp/blob/9b05354ec6fb58b4e665e9a39ebc40285c015638/ggml/src/ggml-common.h | ||
| // Scalar-formula reference: | ||
| // https://github.com/ggml-org/llama.cpp/blob/9b05354ec6fb58b4e665e9a39ebc40285c015638/ggml/src/ggml-quants.c | ||
| constexpr int kQ8BlockSize = 32; | ||
| constexpr int kPayloadBytes = kScaleBytes + kQ8BlockSize; | ||
| constexpr float kMaxQuant = 127.0f; | ||
|
|
||
| template <typename scalar_t> | ||
| __global__ void encode(const scalar_t *input, int64_t num_blocks, uint8_t *output) { | ||
| const int lane = threadIdx.x; | ||
| const int64_t block = blockIdx.x; | ||
| if (block >= num_blocks) | ||
| return; | ||
|
|
||
| const float value = load_float(input + block * kQ8BlockSize + lane); | ||
| float amax = fabsf(value); | ||
| #pragma unroll | ||
| for (int delta = 16; delta > 0; delta >>= 1) | ||
| amax = fmaxf(amax, __shfl_down_sync(0xffffffff, amax, delta)); | ||
|
|
||
| float d = 0.0f; | ||
| uint8_t *payload = output + block * kPayloadBytes; | ||
| if (lane == 0) { | ||
| d = fminf(amax / kMaxQuant, 65504.0f); | ||
| const uint16_t d_bits = __half_as_ushort(__float2half_rn(d)); | ||
| payload[0] = static_cast<uint8_t>(d_bits); | ||
| payload[1] = static_cast<uint8_t>(d_bits >> 8); | ||
| } | ||
| d = __shfl_sync(0xffffffff, d, 0); | ||
| const float rounded = d > 0.0f ? roundf(value / d) : 0.0f; | ||
| const int quant = static_cast<int>(fminf(fmaxf(rounded, -kMaxQuant), kMaxQuant)); | ||
| payload[kScaleBytes + lane] = static_cast<uint8_t>(static_cast<int8_t>(quant)); | ||
| } | ||
|
|
||
| } // namespace | ||
|
|
||
| at::Tensor q8_0_pack_cuda(at::Tensor input) { | ||
| TORCH_CHECK(input.is_contiguous(), "input must be contiguous"); | ||
| modelopt::ggml::check_scalar_pack_input("Q8_0", input, kQ8BlockSize); | ||
| const int64_t num_blocks = input.numel() / kQ8BlockSize; | ||
| c10::cuda::CUDAGuard guard(input.device()); | ||
| auto output = at::empty({num_blocks, kPayloadBytes}, input.options().dtype(at::kByte)); | ||
| const auto stream = c10::cuda::getCurrentCUDAStream(); | ||
|
|
||
| AT_DISPATCH_FLOATING_TYPES_AND2( | ||
| at::ScalarType::Half, at::ScalarType::BFloat16, input.scalar_type(), "q8_0_pack", [&] { | ||
| encode<scalar_t><<<static_cast<int>(num_blocks), kQ8BlockSize, 0, stream>>>( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [SUGGESTION] One CUDA block per GGML block means 32 threads — a single warp — per block here, unlike the IQ kernels where the same convention yields a healthy Impact: hardware caps resident blocks per SM (32 on Hopper/Ada), so 1 warp/block caps occupancy at ~50% of the warp slots, and a 4096×4096 tensor launches ~524k blocks each doing 5 dependent Suggestion: have each CUDA block encode |
||
| input.data_ptr<scalar_t>(), num_blocks, output.data_ptr<uint8_t>()); | ||
| C10_CUDA_KERNEL_LAUNCH_CHECK(); | ||
| }); | ||
| return output; | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -80,11 +80,12 @@ def get_cuda_ext_mx(raise_if_failed: bool = False): | |||||
|
|
||||||
|
|
||||||
| def get_cuda_ext_ggml(raise_if_failed: bool = False): | ||||||
| """Return the GGML-compatible IQ packing extension, exposing one packer per IQ format. | ||||||
| """Return the GGML-compatible packing extension, exposing one packer per format. | ||||||
|
|
||||||
| The formats share their packing helpers, CUDA version requirement, and build flags, so they | ||||||
| build as a single extension: ``iq1_s_pack(input, grid)`` and | ||||||
| ``iq2_xs_pack(input, grid, scales)``. | ||||||
| build as a single extension: ``iq1_s_pack(input, grid)``, | ||||||
| ``iq2_xs_pack(input, grid, scales)``, ``iq2_xxs_pack(input, grid, scales)``, and | ||||||
| ``q8_0_pack(input)``. | ||||||
| """ | ||||||
| if not hasattr(get_cuda_ext_ggml, "extension") or ( | ||||||
| raise_if_failed and get_cuda_ext_ggml.extension is None | ||||||
|
|
@@ -96,6 +97,7 @@ def get_cuda_ext_ggml(raise_if_failed: bool = False): | |||||
| kernels_ggml / "iq1_s.cu", | ||||||
| kernels_ggml / "iq2_xs.cu", | ||||||
| kernels_ggml / "iq2_xxs.cu", | ||||||
| kernels_ggml / "q8_0.cu", | ||||||
| ], | ||||||
| cuda_version_specifiers=">=11.8", | ||||||
| fail_msg="GGML IQ CUDA packing extension is unavailable.", | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [SUGGESTION] The docstring above was deliberately de-IQ'd in this diff ("the GGML-compatible packing extension, exposing one packer per format"), but this user-facing failure message still says "IQ". A user who only requested Q8_0 packing now gets told the IQ extension is unavailable, which sends them looking in the wrong place.
Suggested change
|
||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[SUGGESTION] The block size is a bare
32here while the kernel's authority for it,kQ8BlockSize, lives inq8_0.cu's anonymous namespace and so cannot be referenced from this file.This is exactly the drift the surrounding code takes pains to avoid: the IQ wrappers get
kBlockSizefromcommon.cuh, and the comment oncheck_pack_inputsexplains that validation is shared so "the enforced rule and the message it reports are written once." With two independent literals, a future change to the Q8_0 block size inq8_0.culeaves this wrapper validating the old value, and the mismatch surfaces as out-of-bounds writes rather than aTORCH_CHECKfailure (the.cuentry point re-validates with its own constant, so the two checks would disagree silently).Suggestion: move
kQ8BlockSize(and ideallykPayloadBytes) intocommon.cuhnext tokBlockSize/kScaleBytes, and use it in both places.