/* * turboquant_cuda_kernel.cu -- TQ3 (3-bit) KV Cache Quantization for CUDA * * Copyright (c) 2026 Elyan Labs. All rights reserved. * Licensed under the MIT License. * * CUDA implementation of TurboQuant TQ3, matching the POWER8 VSX version * in ggml-turboquant-vsx.h. 128-float blocks quantized to 56 bytes * (9.1x compression vs FP32). * * Algorithm (quantize): * 1. L2 norm of 128-float block, normalize to unit vector * 2. Deterministic sign flips (xoshiro256** PRNG, seed-based) * 3. Fast Walsh-Hadamard Transform (FWHT) -- 7 butterfly stages * 4. Absmax of transformed vector, scale to [-1, +1] * 5. Lloyd-Max codebook quantize (8 centroids, 3 bits) * 6. Pack 128 x 3-bit indices into 48 bytes * Store: norm (4B) + scale (4B) + packed (48B) = 56 bytes * * Dequantize is the exact reverse. * * Target GPUs: V100 (SM_70), RTX 5070 (SM_100), RTX 3060 (SM_86), M40 (SM_52) * * Build: * nvcc -arch=sm_70 -O2 -o tq3_cuda_test turboquant_cuda_kernel.cu \ * -DTURBOQUANT_SELF_TEST */ #include #include #include #include #include #include /* ------------------------------------------------------------------ */ /* Constants */ /* ------------------------------------------------------------------ */ #define TQ3_BLOCK_SIZE 128 #define TQ3_PACKED_BYTES 48 /* 128 * 3 / 8 */ #define TQ3_FWHT_STAGES 7 /* log2(128) */ #define TQ3_NUM_CENTROIDS 8 /* ------------------------------------------------------------------ */ /* Block layout: 56 bytes per 128 floats */ /* ------------------------------------------------------------------ */ typedef struct { float norm; /* 4 bytes */ float scale; /* 4 bytes */ unsigned char packed_indices[TQ3_PACKED_BYTES]; /* 48 bytes */ } block_tq3_t; /* 56 bytes */ /* ------------------------------------------------------------------ */ /* Constant memory: Lloyd-Max codebook and decision boundaries */ /* Matches ggml-turboquant-vsx.h exactly for bit-compatible output. */ /* ------------------------------------------------------------------ */ __constant__ float d_TQ3_CENTROIDS[TQ3_NUM_CENTROIDS] = { -0.9816f, -0.6168f, -0.3479f, -0.1129f, 0.1129f, 0.3479f, 0.6168f, 0.9816f }; __constant__ float d_TQ3_BOUNDARIES[7] = { -0.79920f, -0.48235f, -0.23040f, 0.00000f, 0.23040f, 0.48235f, 0.79920f }; /* Host-side centroids available if needed for CPU-side verification */ /* static const float h_TQ3_CENTROIDS[TQ3_NUM_CENTROIDS] = { -0.9816f, -0.6168f, -0.3479f, -0.1129f, 0.1129f, 0.3479f, 0.6168f, 0.9816f }; */ /* ------------------------------------------------------------------ */ /* xoshiro256** PRNG -- device side */ /* Used to generate deterministic sign-flip mask per seed. */ /* ------------------------------------------------------------------ */ __device__ __forceinline__ uint64_t tq3_rotl(uint64_t x, int k) { return (x << k) | (x >> (64 - k)); } /* * Generate next xoshiro256** value and advance state. * Matches the VSX header implementation exactly. */ __device__ __forceinline__ uint64_t tq3_xoshiro_next(uint64_t s[4]) { const uint64_t result = tq3_rotl(s[1] * 5, 7) * 9; const uint64_t t = s[1] << 17; s[2] ^= s[0]; s[3] ^= s[1]; s[1] ^= s[2]; s[0] ^= s[3]; s[2] ^= t; s[3] = tq3_rotl(s[3], 45); return result; } /* * Seed xoshiro256** state from a 64-bit seed via splitmix64. * Identical to the VSX header so sign masks match bit-for-bit. */ __device__ __forceinline__ void tq3_xoshiro_seed(uint64_t s[4], uint64_t seed) { for (int i = 0; i < 4; i++) { seed += 0x9E3779B97F4A7C15ULL; uint64_t z = seed; z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ULL; z = (z ^ (z >> 27)) * 0x94D049BB133111EBULL; z = z ^ (z >> 31); s[i] = z; } } /* ================================================================== */ /* CUDA Kernels */ /* ================================================================== */ /* * Shared memory layout per block (1 block = 1 TQ3 vector): * [0..511] float work[128] -- 512 bytes, working buffer * [512..639] int8_t signs[128] -- 128 bytes, +1/-1 as bytes * * Block size: 128 threads, one per vector coordinate. */ /* ------------------------------------------------------------------ */ /* tq3_quantize_kernel */ /* One CUDA block per 128-float TQ3 vector. */ /* ------------------------------------------------------------------ */ __global__ void tq3_quantize_kernel( const float * __restrict__ src, block_tq3_t * __restrict__ dst, int n_vectors, uint64_t seed) { const int vec_id = blockIdx.x; if (vec_id >= n_vectors) return; const int tid = threadIdx.x; /* 0..127 */ /* Shared memory: working buffer + sign mask */ __shared__ float s_work[TQ3_BLOCK_SIZE]; __shared__ int8_t s_sign[TQ3_BLOCK_SIZE]; /* ---- Step 0: Generate sign-flip mask (thread 0 only, tiny cost) ---- */ if (tid == 0) { uint64_t rng[4]; tq3_xoshiro_seed(rng, seed); for (int i = 0; i < TQ3_BLOCK_SIZE; i++) { uint64_t r = tq3_xoshiro_next(rng); s_sign[i] = (r & 1) ? -1 : 1; } } __syncthreads(); /* ---- Step 1: Load source value ---- */ const float *block_src = src + (int64_t)vec_id * TQ3_BLOCK_SIZE; float val = block_src[tid]; /* ---- Step 2: Compute L2 norm ---- */ /* * Reduction order is part of the format here, not an implementation * detail: `norm` scales the vector the codebook indices are derived * from, so a last-bit disagreement changes the packed indices for a * percent-scale fraction of blocks. A warp-shuffle tree sums in a * different order than the 4-lane vec_madd loop in * ggml-turboquant-vsx.h, which is why the two backends did not agree * bit-for-bit. Mirror the VSX lane layout instead: accumulator j * takes elements j, j+4, j+8, ... with a fused multiply-add, then the * four accumulators are combined left to right. */ __shared__ float s_lane[4]; s_work[tid] = val; /* staged here; overwritten in Step 3 */ __syncthreads(); if (tid < 4) { float acc = 0.0f; for (int i = tid; i < TQ3_BLOCK_SIZE; i += 4) { acc = fmaf(s_work[i], s_work[i], acc); } s_lane[tid] = acc; } __syncthreads(); __shared__ float s_norm; __shared__ float s_inv_norm; if (tid == 0) { float total = s_lane[0] + s_lane[1] + s_lane[2] + s_lane[3]; s_norm = sqrtf(total); s_inv_norm = (s_norm > 1e-12f) ? (1.0f / s_norm) : 0.0f; } __syncthreads(); float norm = s_norm; /* Handle zero-norm block: write zeros and exit */ if (norm < 1e-12f) { if (tid == 0) { dst[vec_id].norm = 0.0f; dst[vec_id].scale = 0.0f; } /* Each thread clears part of packed_indices (48 bytes / 128 threads) */ if (tid < TQ3_PACKED_BYTES) { dst[vec_id].packed_indices[tid] = 0; } return; } /* ---- Step 3: Normalize to unit vector, then apply the sign flips ---- */ /* * D of the randomized Hadamard transform H*D must be applied *before* * H. Applied after the transform the flips are inert: absmax is * invariant under a sign flip and the codebook is symmetric about zero, * so the reconstruction is identical to using no flips at all. */ val *= s_inv_norm; s_work[tid] = val * (float)s_sign[tid]; __syncthreads(); /* ---- Step 4: In-place FWHT (7 butterfly stages) ---- */ /* * Each stage s: stride = 2^(s+1), half = 2^s * Pair (base + j, base + j + half): a' = a + b, b' = a - b * Each thread handles its own index based on butterfly pattern. */ for (int stage = 0; stage < TQ3_FWHT_STAGES; stage++) { int half = 1 << stage; int stride = half << 1; /* Determine which pair this thread belongs to */ int group = tid / stride; /* which group of size 'stride' */ int within = tid % stride; /* position within group */ int base = group * stride; if (within < half) { /* This thread is in the 'a' position */ int idx_a = base + within; int idx_b = base + within + half; float a = s_work[idx_a]; float b = s_work[idx_b]; s_work[idx_a] = a + b; s_work[idx_b] = a - b; } __syncthreads(); } /* ---- Step 5: Compute absmax via warp reduction ---- */ /* absmax is order independent (fmaxf is associative and exact), so the warp-shuffle tree here is safe to keep. */ __shared__ float s_partial[4]; float absval = fabsf(s_work[tid]); for (int offset = 16; offset > 0; offset >>= 1) { float other = __shfl_down_sync(0xFFFFFFFF, absval, offset); absval = fmaxf(absval, other); } if ((tid & 31) == 0) { s_partial[tid >> 5] = absval; } __syncthreads(); __shared__ float s_scale; __shared__ float s_inv_scale; if (tid == 0) { float amax = fmaxf(fmaxf(s_partial[0], s_partial[1]), fmaxf(s_partial[2], s_partial[3])); s_scale = amax; s_inv_scale = (amax > 1e-12f) ? (1.0f / amax) : 0.0f; } __syncthreads(); /* ---- Step 6: Normalize to [-1, +1] for codebook ---- */ float normalized = s_work[tid] * s_inv_scale; /* ---- Step 7: Quantize to nearest Lloyd-Max centroid ---- */ /* * Binary search through 7 boundaries for 8 centroids. * Matches _tq3_quantize_scalar() in the VSX header exactly. */ uint8_t idx; if (normalized < d_TQ3_BOUNDARIES[3]) { if (normalized < d_TQ3_BOUNDARIES[1]) { idx = (normalized < d_TQ3_BOUNDARIES[0]) ? 0 : 1; } else { idx = (normalized < d_TQ3_BOUNDARIES[2]) ? 2 : 3; } } else { if (normalized < d_TQ3_BOUNDARIES[5]) { idx = (normalized < d_TQ3_BOUNDARIES[4]) ? 4 : 5; } else { idx = (normalized < d_TQ3_BOUNDARIES[6]) ? 6 : 7; } } /* ---- Step 8: Pack 128 x 3-bit indices into 48 bytes ---- */ /* * Each thread owns one 3-bit index. We compute the byte position * and bit offset, then use atomicOr to write without conflicts. * This is safe because at most 2 threads share a byte, and their * 3-bit fields never overlap on the same bits. * * For throughput we first zero the packed buffer, then OR in bits. */ unsigned char *packed = dst[vec_id].packed_indices; /* Zero the packed buffer (48 bytes, use first 48 threads) */ if (tid < TQ3_PACKED_BYTES) { packed[tid] = 0; } __syncthreads(); { int bit_pos = tid * 3; int byte_idx = bit_pos >> 3; int bit_off = bit_pos & 7; /* Low part always fits in one byte */ atomicOr((unsigned int *)(packed + (byte_idx & ~3)), (unsigned int)((idx << bit_off) << ((byte_idx & 3) * 8))); /* High part: if bit_off > 5, 3 bits spill into next byte */ if (bit_off > 5) { int next_byte = byte_idx + 1; atomicOr((unsigned int *)(packed + (next_byte & ~3)), (unsigned int)(((idx >> (8 - bit_off)) & 0x07) << ((next_byte & 3) * 8))); } } __syncthreads(); /* ---- Step 9: Write norm and scale (thread 0) ---- */ if (tid == 0) { dst[vec_id].norm = norm; dst[vec_id].scale = s_scale; } } /* ------------------------------------------------------------------ */ /* tq3_dequantize_kernel */ /* One CUDA block per 128-float TQ3 vector. */ /* ------------------------------------------------------------------ */ __global__ void tq3_dequantize_kernel( const block_tq3_t * __restrict__ src, float * __restrict__ dst_out, int n_vectors, uint64_t seed) { const int vec_id = blockIdx.x; if (vec_id >= n_vectors) return; const int tid = threadIdx.x; /* 0..127 */ __shared__ float s_work[TQ3_BLOCK_SIZE]; __shared__ int8_t s_sign[TQ3_BLOCK_SIZE]; /* ---- Step 0: Generate sign-flip mask ---- */ if (tid == 0) { uint64_t rng[4]; tq3_xoshiro_seed(rng, seed); for (int i = 0; i < TQ3_BLOCK_SIZE; i++) { uint64_t r = tq3_xoshiro_next(rng); s_sign[i] = (r & 1) ? -1 : 1; } } __syncthreads(); float norm = src[vec_id].norm; float scale = src[vec_id].scale; /* Handle zero-norm block */ if (norm < 1e-12f) { dst_out[(int64_t)vec_id * TQ3_BLOCK_SIZE + tid] = 0.0f; return; } /* ---- Step 1: Unpack this thread's 3-bit index ---- */ const unsigned char *packed = src[vec_id].packed_indices; uint8_t idx; { int bit_pos = tid * 3; int byte_idx = bit_pos >> 3; int bit_off = bit_pos & 7; uint16_t two_bytes = (uint16_t)packed[byte_idx]; if (byte_idx + 1 < TQ3_PACKED_BYTES) { two_bytes |= ((uint16_t)packed[byte_idx + 1]) << 8; } idx = (uint8_t)((two_bytes >> bit_off) & 0x07); } /* ---- Step 2: Dequantize: centroid * absmax_scale ---- */ s_work[tid] = d_TQ3_CENTROIDS[idx] * scale; __syncthreads(); /* ---- Step 3: Inverse FWHT (same as forward, then /N) ---- */ for (int stage = 0; stage < TQ3_FWHT_STAGES; stage++) { int half = 1 << stage; int stride = half << 1; int group = tid / stride; int within = tid % stride; int base = group * stride; if (within < half) { int idx_a = base + within; int idx_b = base + within + half; float a = s_work[idx_a]; float b = s_work[idx_b]; s_work[idx_a] = a + b; s_work[idx_b] = a - b; } __syncthreads(); } /* Divide by N (= 128) */ float result = s_work[tid] * (1.0f / (float)TQ3_BLOCK_SIZE); /* ---- Step 4: Undo sign flips (D is its own inverse) ---- */ result *= (float)s_sign[tid]; /* ---- Step 5: Rescale by original L2 norm ---- */ result *= norm; /* ---- Write output ---- */ dst_out[(int64_t)vec_id * TQ3_BLOCK_SIZE + tid] = result; } /* ================================================================== */ /* Host Wrappers */ /* ================================================================== */ void tq3_cuda_quantize( const float *d_src, block_tq3_t *d_dst, int n_vectors, uint64_t seed, cudaStream_t stream) { if (n_vectors <= 0) return; dim3 grid(n_vectors); dim3 block(TQ3_BLOCK_SIZE); tq3_quantize_kernel<<>>(d_src, d_dst, n_vectors, seed); } void tq3_cuda_dequantize( const block_tq3_t *d_src, float *d_dst, int n_vectors, uint64_t seed, cudaStream_t stream) { if (n_vectors <= 0) return; dim3 grid(n_vectors); dim3 block(TQ3_BLOCK_SIZE); tq3_dequantize_kernel<<>>(d_src, d_dst, n_vectors, seed); } /* ================================================================== */ /* Self-Test */ /* ================================================================== */ #ifdef TURBOQUANT_SELF_TEST #define CUDA_CHECK(call) \ do { \ cudaError_t err = (call); \ if (err != cudaSuccess) { \ fprintf(stderr, "CUDA error at %s:%d: %s\n", \ __FILE__, __LINE__, cudaGetErrorString(err)); \ exit(EXIT_FAILURE); \ } \ } while (0) /* * tq3_cuda_self_test -- round-trip accuracy test on GPU * * 1. Generate pseudo-Gaussian test data (central limit theorem, 12 uniforms) * 2. Quantize on GPU * 3. Dequantize on GPU * 4. Copy back and check MSE + cosine similarity * * Returns 0 on PASS, 1 on FAIL. */ int tq3_cuda_self_test(void) { const uint64_t seed = 0x456C79616E4C6162ULL; /* "ElyanLab" */ const int n_vectors = 64; /* test 64 blocks for good coverage */ const int n_floats = n_vectors * TQ3_BLOCK_SIZE; /* ---- Allocate host memory ---- */ float *h_original = (float *)malloc(n_floats * sizeof(float)); float *h_recovered = (float *)malloc(n_floats * sizeof(float)); if (!h_original || !h_recovered) { fprintf(stderr, "Host allocation failed\n"); return 1; } /* ---- Generate test data: pseudo-Gaussian via CLT ---- */ { uint32_t lcg = 0xDEADBEEF; for (int i = 0; i < n_floats; i++) { float sum = 0.0f; for (int j = 0; j < 12; j++) { lcg = lcg * 1664525u + 1013904223u; sum += (float)(lcg & 0xFFFF) / 65535.0f; } h_original[i] = (sum - 6.0f) * 0.5f; /* ~N(0, 0.25) */ } } /* ---- Allocate device memory ---- */ float *d_src = NULL; float *d_dst = NULL; block_tq3_t *d_blocks = NULL; CUDA_CHECK(cudaMalloc(&d_src, n_floats * sizeof(float))); CUDA_CHECK(cudaMalloc(&d_dst, n_floats * sizeof(float))); CUDA_CHECK(cudaMalloc(&d_blocks, n_vectors * sizeof(block_tq3_t))); /* ---- Upload source data ---- */ CUDA_CHECK(cudaMemcpy(d_src, h_original, n_floats * sizeof(float), cudaMemcpyHostToDevice)); /* ---- Quantize on GPU ---- */ tq3_cuda_quantize(d_src, d_blocks, n_vectors, seed, 0); CUDA_CHECK(cudaGetLastError()); /* ---- Dequantize on GPU ---- */ tq3_cuda_dequantize(d_blocks, d_dst, n_vectors, seed, 0); CUDA_CHECK(cudaGetLastError()); /* ---- Copy results back ---- */ CUDA_CHECK(cudaMemcpy(h_recovered, d_dst, n_floats * sizeof(float), cudaMemcpyDeviceToHost)); CUDA_CHECK(cudaDeviceSynchronize()); /* ---- Compute error metrics ---- */ double mse_total = 0.0; double mean_total = 0.0; double var_total = 0.0; double dot_or = 0.0, dot_oo = 0.0, dot_rr = 0.0; for (int i = 0; i < n_floats; i++) { mean_total += (double)h_original[i]; } mean_total /= (double)n_floats; for (int i = 0; i < n_floats; i++) { double diff = (double)h_recovered[i] - (double)h_original[i]; mse_total += diff * diff; double dev = (double)h_original[i] - mean_total; var_total += dev * dev; dot_or += (double)h_original[i] * (double)h_recovered[i]; dot_oo += (double)h_original[i] * (double)h_original[i]; dot_rr += (double)h_recovered[i] * (double)h_recovered[i]; } double mse = mse_total / (double)n_floats; double var = var_total / (double)n_floats; double nmse = (var > 1e-12) ? (mse / var) : 0.0; double cos_sim = (dot_oo > 1e-12 && dot_rr > 1e-12) ? dot_or / (sqrt(dot_oo) * sqrt(dot_rr)) : 0.0; /* ---- Also verify one block's packed data via host unpack ---- */ block_tq3_t h_block; CUDA_CHECK(cudaMemcpy(&h_block, d_blocks, sizeof(block_tq3_t), cudaMemcpyDeviceToHost)); int pack_ok = 1; { /* Unpack and verify each index is in range [0, 7] */ int bit_pos = 0; for (int i = 0; i < TQ3_BLOCK_SIZE; i++) { int byte_idx = bit_pos >> 3; int bit_off = bit_pos & 7; uint16_t two_bytes = (uint16_t)h_block.packed_indices[byte_idx]; if (byte_idx + 1 < TQ3_PACKED_BYTES) { two_bytes |= ((uint16_t)h_block.packed_indices[byte_idx + 1]) << 8; } uint8_t idx = (uint8_t)((two_bytes >> bit_off) & 0x07); if (idx > 7) { pack_ok = 0; break; } bit_pos += 3; } } /* ---- Print GPU info ---- */ int device; cudaDeviceProp prop; cudaGetDevice(&device); cudaGetDeviceProperties(&prop, device); printf("TQ3 CUDA Self-Test (Elyan Labs TurboQuant)\n"); printf(" GPU: %s (SM_%d%d)\n", prop.name, prop.major, prop.minor); printf(" Test vectors: %d (%d floats)\n", n_vectors, n_floats); printf(" Block size: %d floats\n", TQ3_BLOCK_SIZE); printf(" Packed size: %d bytes (%.1fx compression vs FP32)\n", (int)sizeof(block_tq3_t), (float)(TQ3_BLOCK_SIZE * sizeof(float)) / (float)sizeof(block_tq3_t)); printf(" Block[0] norm: %.6f\n", h_block.norm); printf(" Block[0] scale: %.6f\n", h_block.scale); printf(" MSE: %.8f\n", mse); printf(" Variance: %.8f\n", var); printf(" NMSE: %.6f (%.2f%%)\n", nmse, nmse * 100.0); printf(" Cosine sim: %.6f\n", cos_sim); printf(" Pack check: %s\n", pack_ok ? "PASS" : "FAIL"); /* * Pass criteria (same as VSX header): * - NMSE < 15%: TQ3 with Lloyd-Max achieves ~3-12% on typical activations. * - Cosine similarity > 0.90: directional preservation is critical for * KV cache -- attention depends on vector direction. */ int nmse_ok = (nmse < 0.15); int cos_ok = (cos_sim > 0.90); int pass = nmse_ok && cos_ok && pack_ok; printf(" NMSE check: %s (%.2f%% < 15%%)\n", nmse_ok ? "PASS" : "FAIL", nmse * 100.0); printf(" Cosine check: %s (%.4f > 0.90)\n", cos_ok ? "PASS" : "FAIL", cos_sim); printf(" Result: %s\n", pass ? "PASS" : "FAIL"); /* ---- Cleanup ---- */ cudaFree(d_src); cudaFree(d_dst); cudaFree(d_blocks); free(h_original); free(h_recovered); return pass ? 0 : 1; } int main(void) { int device_count = 0; CUDA_CHECK(cudaGetDeviceCount(&device_count)); if (device_count == 0) { fprintf(stderr, "No CUDA devices found.\n"); return 1; } return tq3_cuda_self_test(); } #endif /* TURBOQUANT_SELF_TEST */