[Kernel] Add source-sized IQ2_S gated-pair reader

Signed-off-by: yangzhuxinyzx <153831768+yangzhuxinyzx@users.noreply.github.com>
This commit is contained in:
yangzhuxinyzx committed 2026-10-05 17:52:28 +08:00
1 parent b72c4618aa
commit 74ea7cf330
7 files changed
+325 -5

No files matched your search

@@ -33,6 +33,7 @@ def main():
)
parser.add_argument("--prototype-q4-k", action="store_true")
parser.add_argument("--prototype-iq3xxs-iq4", action="store_true")
parser.add_argument("--prototype-iq2-s", action="store_true")
parser.add_argument(
"--check-only",
action="store_true",
@@ -40,7 +41,14 @@ def main():
)
args = parser.parse_args()
assert (
sum((args.prototype_iq3_xxs, args.prototype_q4_k, args.prototype_iq3xxs_iq4))
sum(
(
args.prototype_iq3_xxs,
args.prototype_q4_k,
args.prototype_iq3xxs_iq4,
args.prototype_iq2_s,
)
)
<= 1
)
torch.backends.cuda.matmul.allow_fp16_reduced_precision_reduction = False
@@ -53,7 +61,9 @@ def main():
names = [f"blk.{layer}.ffn_{role}.weight" for role in ("gate", "up")]
types = [int(tensors[name].tensor_type) for name in names]
allowed = (
((18, 23),)
((22, 21), (21, 22), (22, 18), (18, 22))
if args.prototype_iq2_s
else ((18, 23),)
if args.prototype_iq3xxs_iq4
else ((12, 23), (23, 12))
if args.prototype_q4_k
@@ -70,7 +80,12 @@ def main():
layer_module = torch.nn.Module()
layer_module.prefix = f"model.layers.{layer}.mlp.gate_up_proj"
layer_module.gguf_tm_projections = torch.nn.ModuleList(projections)
if args.prototype_iq3_xxs or args.prototype_q4_k or args.prototype_iq3xxs_iq4:
if (
args.prototype_iq3_xxs
or args.prototype_q4_k
or args.prototype_iq3xxs_iq4
or args.prototype_iq2_s
):
from vllm.model_executor.layers.quantization.gguf_iq3_records import (
signed_index_records,
)
@@ -79,6 +94,12 @@ def main():
)
packers = {18: pack_iq3_xxs_records, 21: signed_index_records}
if args.prototype_iq2_s:
from vllm.model_executor.layers.quantization.gguf_iq2_s_records import (
pack_iq2_s_records,
)
packers[22] = pack_iq2_s_records
if args.prototype_q4_k or args.prototype_iq3xxs_iq4:
from vllm.model_executor.layers.quantization.gguf_iq4_native import (
pack_iq4_xs_records,
@@ -170,6 +170,76 @@ struct NativePairReader<18> {
}
};
template <>
struct NativePairReader<22> {
using Decoder = LatticeRawDecoder<22>;
static constexpr int kBlockBytes = 82;
static constexpr int kBookId = 22;
static constexpr int kBookBytes = Decoder::kCodebookBytes;
struct Record {
uint32_t indices[4];
uint32_t signs[4];
uint32_t high;
uint32_t scales;
float d;
};
const uint8_t* payload;
const uint8_t* metadata;
const half* original_d;
float cached_d;
int half_block;
bool first;
__device__ NativePairReader(const uint8_t* source, int tile, int blocks_k,
int first_part, int col) {
const uint8_t* macro = source + int64_t{tile} * blocks_k * 32 * kBlockBytes;
payload = macro + first_part * 1280 + col * 16;
metadata = macro + first_part * 1280 + 1024 + col * 8;
original_d = reinterpret_cast<const half*>(macro + blocks_k * 2560 +
(first_part / 2) * 64 + col * 2);
cached_d = 0;
half_block = first_part & 1;
first = true;
}
__device__ static void initialize(uint8_t* book) {
Decoder::initialize(book);
}
__device__ Record load() {
if (first || half_block == 0) cached_d = __half2float(*original_d);
const uint4 indices = *reinterpret_cast<const uint4*>(payload);
const uint4 signs = *reinterpret_cast<const uint4*>(payload + 512);
const uint2 aux = *reinterpret_cast<const uint2*>(metadata);
Record record{{indices.x, indices.y, indices.z, indices.w},
{signs.x, signs.y, signs.z, signs.w},
aux.x,
aux.y,
cached_d};
payload += 1280;
metadata += 1280;
original_d += half_block * 32;
half_block ^= 1;
first = false;
return record;
}
template <int Segment, int Fragment>
__device__ static turbomind::Array<half, 8> fragment(const Record& record,
const uint8_t* book) {
static_assert(Segment >= 0 && Segment < 8 && Fragment >= 0 && Fragment < 2);
constexpr int octet = 2 * Segment + Fragment;
const uint32_t low = (record.indices[octet / 4] >> (8 * (octet & 3))) & 255;
const uint32_t index = low | (((record.high >> (2 * octet)) & 3) << 8);
const uint32_t signs = (record.signs[octet / 4] >> (8 * (octet & 3))) & 255;
const uint64_t packed =
*reinterpret_cast<const uint64_t*>(book + index * 8);
// Reuse the original decoder's exact final FP16 operand formation.
return Decoder::table_fragment<half>(packed, signs, record.d,
(record.scales >> (4 * Segment)) & 15);
}
};
template <>
struct NativePairReader<21> {
using Decoder = Iq3NibbleBookDecoder;
@@ -11,6 +11,7 @@
using R12 = vllm::sm70_gguf::NativePairReader<12>;
using R18 = vllm::sm70_gguf::NativePairReader<18>;
using R21 = vllm::sm70_gguf::NativePairReader<21>;
using R22 = vllm::sm70_gguf::NativePairReader<22>;
using R23 = vllm::sm70_gguf::NativePairReader<23>;
namespace {
template <class Gate, class Up>
@@ -33,6 +34,8 @@ void gguf_native_pair_sm70_out(torch::Tensor output, torch::Tensor input,
TORCH_CHECK(
(gate_type == 21 && (up_type == 23 || up_type == 18)) ||
((gate_type == 23 || gate_type == 18) && up_type == 21) ||
(gate_type == 22 && (up_type == 18 || up_type == 21)) ||
((gate_type == 18 || gate_type == 21) && up_type == 22) ||
(gate_type == 18 && up_type == 23) ||
(gate_type == 12 && up_type == 23) ||
(gate_type == 23 && up_type == 12),
@@ -55,7 +58,11 @@ void gguf_native_pair_sm70_out(torch::Tensor output, torch::Tensor input,
k % 1024 == 0 && n <= INT_MAX && k <= INT_MAX,
"GGUF native pair requires M8/N32/K1024");
const auto block_bytes = [](int64_t type) {
return type == 12 ? 144 : type == 18 ? 98 : type == 21 ? 110 : 136;
return type == 12 ? 144
: type == 18 ? 98
: type == 21 ? 110
: type == 22 ? 82
: 136;
};
TORCH_CHECK(gate.numel() == n * (k / 256) * block_bytes(gate_type) &&
up.numel() == n * (k / 256) * block_bytes(up_type),
@@ -65,7 +72,15 @@ void gguf_native_pair_sm70_out(torch::Tensor output, torch::Tensor input,
TORCH_CHECK(properties->major == 7 && properties->minor == 0,
"GGUF native pair requires SM70");
const auto stream = at::cuda::getCurrentCUDAStream();
if (gate_type == 12)
if (gate_type == 22 && up_type == 21)
launch_pair<R22, R21>(output, input, gate, up, n, k, stream);
else if (gate_type == 21 && up_type == 22)
launch_pair<R21, R22>(output, input, gate, up, n, k, stream);
else if (gate_type == 22)
launch_pair<R22, R18>(output, input, gate, up, n, k, stream);
else if (up_type == 22)
launch_pair<R18, R22>(output, input, gate, up, n, k, stream);
else if (gate_type == 12)
launch_pair<R12, R23>(output, input, gate, up, n, k, stream);
else if (up_type == 12)
launch_pair<R23, R12>(output, input, gate, up, n, k, stream);
+25
View File
@@ -0,0 +1,25 @@
# Original IQ2_S shared-activation pair reader
The next two source-byte priorities pair IQ2_S with IQ3_S in layers5/9/17
and IQ3_XXS in layers7/18/33. All six are TP4 N4352/K5120. Both gate/up
orientations occur for each combination.
Preserve each original 82-byte K256 block as two K128 packet sets:
16-byte indices, 16-byte signs and eight bytes of high bits/scales per
column. Original d follows once per K256 block. Packet planes interleave
N32 columns with aligned 16/8-byte loads; no field expands or changes.
The reader caches original d across halves, including odd split-K starts,
and increments pointers in the K loop.
Reuse `LatticeRawDecoder<22>` codebook initialization and exact final FP16
operand formation, with the existing llama.cpp MIT attribution. No expanded
FP16 coefficient or second decode formula is introduced. The shared-A
skeleton, FP32 dot/reduction and gated epilogue are unchanged. Raw operators
compile both orientations; model capabilities remain canonical pending
actual-weight numerical and matched speed checks.
Ten CPU inverse/cursor checks pass, covering N32/64/96, complete K256 blocks,
strided inputs and all eight split-K starts at K5120. Six real IQ2_S tensors,
each sampled at N64/K5120, preserve 629,760 source bytes and 1,966,080 official
FP32 dequantized values bitwise. Native build, graph and speed checks are
pending; no end-to-end result is claimed.
@@ -0,0 +1,67 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import runpy
from pathlib import Path
import numpy as np
pack_iq2_s_records = runpy.run_path(
str(
Path(__file__).parents[3]
/ "vllm/model_executor/layers/quantization/gguf_iq2_s_records.py"
)
)["pack_iq2_s_records"]
def test_all_split_k_starts_reconstruct_original_decoder_fields():
rng = np.random.default_rng(1290)
source = rng.integers(0, 256, (64, 20, 82), dtype=np.uint8)
d = rng.uniform(-1, 1, (64, 20)).astype("<f2")
source[..., :2] = d.view(np.uint8).reshape(64, 20, 2)
records = pack_iq2_s_records(source.reshape(64, 1640))
for tile in range(2):
for col in range(32):
for first_part in range(0, 40, 5):
macro = tile * 20 * 32 * 82
cursor = macro + first_part * 1280
d_cursor = macro + 20 * 2560 + (first_part // 2) * 64 + col * 2
half_block = first_part & 1
cached_d = None
for part in range(first_part, first_part + 5):
if part == first_part or half_block == 0:
cached_d = records[d_cursor : d_cursor + 2].view("<f2")[0]
indices = records[cursor + col * 16 : cursor + col * 16 + 16]
signs = records[cursor + 512 + col * 16 : cursor + 528 + col * 16]
metadata = records[
cursor + 1024 + col * 8 : cursor + 1032 + col * 8
]
high = int.from_bytes(metadata[:4], "little")
scales = int.from_bytes(metadata[4:], "little")
block = source[tile * 32 + col, part // 2]
assert cached_d == block[:2].view("<f2")[0]
for segment in range(8):
for fragment in range(2):
octet = 2 * segment + fragment
base = half_block * 128 + segment * 16 + fragment * 8
index = int(indices[octet]) | (
((high >> (2 * octet)) & 3) << 8
)
original = int(block[2 + base // 8]) | (
(
(
int(block[66 + base // 32])
>> (2 * ((base // 8) % 4))
)
& 3
)
<< 8
)
assert index == original
assert int(signs[octet]) == int(block[34 + base // 8])
assert (scales >> (4 * segment)) & 15 == (
int(block[74 + base // 32]) >> (4 * ((base // 16) % 2))
) & 15
cursor += 1280
d_cursor += half_block * 64
half_block ^= 1
@@ -0,0 +1,73 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import importlib.util
from pathlib import Path
import numpy as np
import pytest
# The layout needs no native module or CUDA context, even in a source checkout.
_spec = importlib.util.spec_from_file_location(
"iq2_s_records",
Path(__file__).parents[3]
/ "vllm/model_executor/layers/quantization/gguf_iq2_s_records.py",
)
assert _spec is not None and _spec.loader is not None
_module = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(_module)
pack_iq2_s_records = _module.pack_iq2_s_records
@pytest.mark.parametrize("n,blocks", [(32, 1), (64, 3), (96, 20)])
def test_every_original_bit_survives_independent_reader(n, blocks):
rng = np.random.default_rng(1290)
raw = rng.integers(0, 256, (n, blocks * 82), dtype=np.uint8)
before = raw.copy()
records = pack_iq2_s_records(raw)
assert records.dtype == np.uint8 and records.flags.c_contiguous
assert records.nbytes == raw.nbytes
np.testing.assert_array_equal(raw, before)
recovered = np.empty_like(raw)
for tile in range(n // 32):
macro = tile * blocks * 32 * 82
for block in range(blocks):
for col in range(32):
row = recovered[tile * 32 + col, block * 82 : (block + 1) * 82]
d = macro + blocks * 2560 + block * 64 + col * 2
row[:2] = records[d : d + 2]
for half in range(2):
cursor = macro + (block * 2 + half) * 1280
index = cursor + col * 16
sign = cursor + 512 + col * 16
metadata = cursor + 1024 + col * 8
assert index % 16 == 0 and sign % 16 == 0 and metadata % 8 == 0
row[2 + half * 16 : 18 + half * 16] = records[index : index + 16]
row[34 + half * 16 : 50 + half * 16] = records[sign : sign + 16]
row[66 + half * 4 : 70 + half * 4] = records[
metadata : metadata + 4
]
row[74 + half * 4 : 78 + half * 4] = records[
metadata + 4 : metadata + 8
]
np.testing.assert_array_equal(recovered, raw)
def test_noncontiguous_tp_rows_keep_original_bytes():
backing = np.arange(64 * 3 * 82 * 2, dtype=np.uint32).astype(np.uint8)
raw = backing.reshape(64, 3 * 82 * 2)[:, ::2]
assert not raw.flags.c_contiguous
np.testing.assert_array_equal(
pack_iq2_s_records(raw), pack_iq2_s_records(raw.copy())
)
@pytest.mark.parametrize("shape", [(31, 82), (32, 99), (0, 82), (32, 0)])
def test_rejects_partial_quant_blocks_or_column_tiles(shape):
with pytest.raises(ValueError):
pack_iq2_s_records(np.zeros(shape, dtype=np.uint8))
def test_rejects_converted_weights():
with pytest.raises(TypeError):
pack_iq2_s_records(np.zeros((32, 82), dtype=np.float16))
@@ -0,0 +1,49 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Source-sized IQ2_S records for shared-activation gated-pair readers."""
import numpy as np
def pack_iq2_s_records(raw: np.ndarray) -> np.ndarray:
"""Interleave original K128 index, sign and high-bit/scale packets.
Each column retains two 16-byte planes and one 8-byte metadata packet
per K128. Original d follows the payload, once per K256. Every source
bit is preserved; scales and codebook indices are never expanded.
"""
if raw.dtype != np.uint8:
raise TypeError("IQ2_S records require original uint8 blocks")
if (
raw.ndim != 2
or raw.shape[0] == 0
or raw.shape[0] % 32
or raw.shape[1] == 0
or raw.shape[1] % 82
):
raise ValueError("IQ2_S records require N32 and complete 82-byte blocks")
n, row_bytes = raw.shape
tiles, blocks = n // 32, row_bytes // 82
source = raw.reshape(tiles, 32, blocks, 82).transpose(0, 2, 1, 3)
indices = (
source[..., 2:34].reshape(tiles, blocks, 32, 2, 16).transpose(0, 1, 3, 2, 4)
)
signs = (
source[..., 34:66].reshape(tiles, blocks, 32, 2, 16).transpose(0, 1, 3, 2, 4)
)
high = source[..., 66:74].reshape(tiles, blocks, 32, 2, 4).transpose(0, 1, 3, 2, 4)
scales = (
source[..., 74:82].reshape(tiles, blocks, 32, 2, 4).transpose(0, 1, 3, 2, 4)
)
payload = np.concatenate(
(
indices.reshape(tiles, blocks, 2, 512),
signs.reshape(tiles, blocks, 2, 512),
np.concatenate((high, scales), axis=4).reshape(tiles, blocks, 2, 256),
),
axis=3,
).reshape(tiles, blocks * 2560)
original_d = source[..., :2].reshape(tiles, blocks * 64)
records = np.concatenate((payload, original_d), axis=1).reshape(-1)
assert records.nbytes == raw.nbytes
return records