feat(quant): packed Q5_0/Q5_1 converter path + shared block packer & pre-transpose marker (#170, #184 (2)(3)) - #294
Merged
Conversation
…nspose marker (#170, #184) Three pieces, pairing with engine PR SKaiNET#951 (native Q5_0/Q5_1 kernels) for the 0.40.0 lock-step train, all verified against the published engine 0.39.0 (the default build): #170 - packed Q5_1/Q5_0 converter path: GemmaMemSegConverter keeps the legacy 32-elem Q5 formats packed (Q5_xBlockTensorData after the row-major -> block-major relayout) instead of the #169 dequant-to-FP32 fallback. Gated on actual kernel AVAILABILITY, not engine version: GGMLQuantizationType.hasPackedMatmulKernel() (llm-core) queries KernelRegistry (JVM: after an idempotent ServiceLoader install), and the FP32 dequant fallback remains for kernel-less environments - so develop stays releasable against 0.39.0 (scalar/Panama tiers serve the packed path today; #951's native FFM/K-N/JNI tiers light up on the pin bump with zero changes here). The same gate covers the commonMain board path (packGemmaKQuant) and llama (packLlamaKQuant) for Q4_0/Q5_0/Q5_1; Q4_K/Q5_K/Q6_K/Q8_0 pack unconditionally as before. #184 (2) - shared GGUF-block packer: the relayout+wrap logic gemma, llama and apertus each carried privately is hoisted to sk.ainet.lang.nn.quant.BlockQuantPacking (transformer-core, commonMain, lang-core-only), keyed by the engine TensorEncoding and covering all seven packed-kernel formats; toBlockEncoding() (llm-core) is the ggml-keyed front door. Old per-model internal entry points remain as @deprecated shims (deprecate-don't-delete); production call sites now use the shared packer. #184 (3) - pre-transpose marker ("Solution C"): PreTransposedWeight + BlockQuantPacking.packPreTransposed produce a weight already in the transposed [in, out] logical shape over the same block-major bytes (exactly what the engine's lazy packed ops.transpose would yield, zero copy), and linearProject reads the marker to skip ops.transpose, dispatching straight into the packed matmul path. Converters do not emit pre-transposed weights by default yet - that flip rides the 0.40.0 train. Verification (published 0.39.0): - BlockQuantPackingTest (commonTest, jvm + linuxX64): geometry vs physicalBytes, relayout block-transpose, pack round-trip, and pre-transposed shape-swap + marker for all seven encodings. - LinearProjectionPreTransposedTest (transformer-core jvmTest): marked Q5_1 path bit-identical to the classic packed path and matching the analytic FP32 reference on a real CPU backend. - GemmaQ5xPackedParityTest: synthetic byte-level packed-vs-dequant parity for Q5_1 AND Q5_0 (the checkpoint has no Q5_0 tensor, so Q5_0 parity is synthetic by necessity); real functiongemma-physical-ai-v10 Q5_K_M checkpoint: all 81 Q5_1 tensors are Q5_1BlockTensorData after conversion. - GemmaQ5KPackedParityTest (integration): full-model greedy decode of NATIVE_OPTIMIZED (now packing Q5_1 too) token-for-token equal to the DEQUANTIZE_TO_FP32 baseline on both the MemSeg and the wired load(NATIVE_OPTIMIZED) paths; 2.20 tok/s packed on this host. - jvmTest suites of llm-core/gemma/llama/apertus + linuxX64Test of llm-core/gemma/llama/transformer-core green; apiCheck green with additions only (no removals or signature changes). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Aug 12, 2026
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #170 (transformers side); implements #184 items (2) and (3). Pairs with the engine PR SKaiNET-developers/SKaiNET#951 (native Q5_0/Q5_1 kernels, closes engine #708) for the 0.40.0 lock-step train — but everything here runs and is verified against the published engine 0.39.0 (the default build).
What
1. Packed Q5_1 / Q5_0 converter path (#170)
GemmaMemSegConverter.convertOnegainsQ5_1andQ5_0cases that keep the weights packed (Q5_1BlockTensorData/Q5_0BlockTensorDataafter the row-major → block-major relayout) instead of the #169 dequant-to-FP32 fallback. FunctionGemma-270M "Q5_K_M" ships 81 of 236 tensors as Q5_1 (attn_q / attn_k / ffn_gate / ffn_up) — these now run the in-kernel dequant matmul like Q4_K/Q5_K/Q6_K/Q8_0 already did.Gated on kernel availability, not engine version: a new
GGMLQuantizationType.hasPackedMatmulKernel()(llm-core) asks the engine'sKernelRegistrywhether a registered provider actually carries anFP32 × packed-Q5_xmatmul kernel (installing ServiceLoader providers first on JVM, mirroring the engine's own lazy wiring). If not, the converter takes the #169 FP32-dequant fallback — still correct, never garbage. The same gate covers the board path (packGemmaKQuant) and llama (packLlamaKQuant) for all three "legacy" 32-elem formats (Q4_0/Q5_0/Q5_1); the four long-packed formats (Q4_K/Q5_K/Q6_K/Q8_0) behave exactly as before.2. Shared GGUF-block packer (#184 (2))
The GGUF-block → engine-
*BlockTensorDatapacking that gemma, llama and apertus each carried privately (relayout + wrap) is hoisted tosk.ainet.lang.nn.quant.BlockQuantPacking(transformer-core, commonMain, lang-core-only deps), keyed by the engine'sTensorEncodingand covering all seven packed-kernel formats.sk.ainet.apps.llm.weights.toBlockEncoding()(llm-core) is the ggml-keyed front door. Model modules keep only weight selection + naming. The old per-modelrelayoutKSeriesRowMajorToBlockMajorentry points remain as@Deprecatedinternal shims (deprecate-don't-delete); all production call sites now use the shared packer.3. Pre-transpose marker (#184 (3), "Solution C")
sk.ainet.lang.nn.quant.PreTransposedWeight— marker interface: tensor data whose logical shape is already the transposed[in, out].BlockQuantPacking.packPreTransposed(...)— packs a weight with the logical shape already swapped (same block-major bytes the engine's lazy packedops.transposewould produce — zero copy) and attaches the marker via internal delegating views that still satisfy every engine dispatch check (is Q*TensorData,PackedBlockStorage).linearProject(transformer-core) reads the marker and skipsops.transpose, dispatchingops.matmul(x, W)straight into the packed kernel path. Unmarked weights keep the classic transpose path (engine lazy packed transpose remains the fallback).Converters do not yet emit pre-transposed weights by default — that flip ("enable-packed-default") is deliberately left for after the engine 0.40.0 train lands (see below), with the marker infrastructure fully tested now.
Verification (default build = published engine 0.39.0)
:transformer-core:jvmTest—LinearProjectionPreTransposedTest: synthetic Q5_1 weight, marked pre-transposed path is bit-identical to the classic packed path and matches the analytic FP32 reference; marker also proven on plain FP32 data.:transformer-core:linuxX64Test—BlockQuantPackingTest(all seven encodings: geometry vsphysicalBytes, relayout block-transpose, pack round-trip, pre-transposed shape-swap + marker) green on Kotlin/Native too.:llm-inference:gemma:jvmTest—GemmaQ5xPackedParityTest:[3, 64]weight (the checkpoint has no Q5_0 tensor — Q5_0 parity is synthetic-only, by necessity);functiongemma-physical-ai-v10-Q5_K_M.gguf): all 81 Q5_1 tensors areQ5_1BlockTensorDataafter conversion — none FP32-inflated.GemmaQ5KPackedParityTest(existing,-PincludeIntegration): full-model greedy decode of NATIVE_OPTIMIZED — which now packs Q5_1 too — token-for-token equal to theDEQUANTIZE_TO_FP32baseline, on both the MemSeg converter path and the wiredload(NATIVE_OPTIMIZED)board path: all three decode[262146, 236769, 3255, 718, 498, 1373, 262152, 106]→<tool_0>(state="on")<end><end_of_turn>for the "Turn the light on." prompt; packed throughput on this host 2.20 tok/s incl. prefill (vs the ~0.67 tok/s Packed SIMD matmul kernel for Q5_1 / Q5_0 (keep NATIVE_OPTIMIZED fast, not dequant-to-FP32) #170 reported for the Q5_1 dequant-fallback build).jvmTestsuites of llm-core / llama / apertus / gemma +linuxX64Testof llm-core / gemma / llama: green.apiCheckgreen;llm-core.api/transformer-core.apigain additions only (toBlockEncoding,hasPackedMatmulKernel,BlockQuantPacking,PreTransposedWeight) — no removals, no signature changes.What lights up with engine 0.40.0 / SKaiNET#951
Under 0.39.0 the Q5_x packed path dispatches to the scalar/Panama kernels (priority ≤ 50). #951 adds the priority-100 native tier (FFM on JVM, Kotlin/Native cinterop, Android JNI) for Q5_0/Q5_1. Because the converter gates on kernel availability and the dispatch resolves by provider priority, bumping the engine pin to 0.40.0 requires zero changes in this repo: the same weights automatically ride the NEON-bodied native kernels, closing the native/Android performance gap for Q5_1-heavy checkpoints (e.g. FunctionGemma's remaining ~0.67→packed tok/s gap from #170).
Remaining steps to close #170 / #184
skainet = "0.40.0"ingradle/libs.versions.toml(one line).BlockQuantPacking.packPreTransposed+ re-run the real-checkpoint parity suite, completing Hoist quant packing + RowDequantSource out of sk.ainet.models.gemma into shared layers #184 (3) end-to-end (the marker +linearProjectsupport land here, already tested).RowDequantSource→ engineops.gather) was completed by feat(gemma): engine-backed row-dequant token_embd — re-home RowDequantSource, keep JVM eager embed packed #289; maintainer may close Hoist quant packing + RowDequantSource out of sk.ainet.models.gemma into shared layers #184 once (2)/(3) merge and the default flip lands.🤖 Generated with Claude Code
Co-Authored-By: Claude Fable 5 noreply@anthropic.com