Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Editing Quantized Weights: Q8_0 and Q4_K

Quantization stores approximate weights in compact groups called blocks. Values within a block share encoding parameters, so changing one value can change how its neighbors are reconstructed.

secant quant patch edits Q8_0 tensors by decoding and re-encoding only affected blocks. It writes a new checkpoint by default; --in-place changes the input file. Unaffected blocks retain their stored bytes. This avoids converting the entire checkpoint back through a model framework.

This page separates two operations: reordering whole groups of columns and editing their values. “Commutation” means two operations give the same result in either order. Reordering complete Q8_0 blocks can preserve bytes exactly; changing weights introduces quantization error. Neither property guarantees model behavior.

Relation to prior work. In October 2026, Kiwimaker (apply_abliteration.py / apply_abliteration_v2.py) demonstrated in-place r+ np.memmap GGUF Q8_0 editing with SHA-256 verification of untouched byte ranges (while empirically skipping Q4_K and dequantizing/re-quantizing full 2-D Q8_0 matrices in float64 via gguf-py), and SC117 (October 2026) demonstrated byte-level in-place GGUF tensor transplantation and scale-preserving Q2_0 re-encoding. Existing .safetensors abliteration tools (jim-plus/llm-abliteration, NousResearch sharded_ablate.py, and abliterix) re-serialize modified shards from scratch via save_file(). OPLoRA, Safe LoRA, and OGPSA concern constrained adaptation or alignment; their guarantees do not automatically extend through lossy storage. Secant’s specific contributions here are in-place zero-copy mmap byte-slice surgery inside .safetensors files, sub-tensor compressed-domain surgery directly on touched 34-byte block_q8_0 structs (crates/secant-core/src/quant.rs) without full-tensor float64 materialization, and the formal block-head alignment proof between 32-weight Q8_0 blocks (128=4×32128 = 4 \times 32) and 256-weight Q4_K superblocks under Π128\Pi_{128}. See the paper for bibliographic details and limits.

Q8_0 32-element blocks vs Q4_K 256-element superblocks across 128-column ssm_out panels

Figure 1:How the stored blocks fit the 128-column ssm_out panels: four 32-element Q8_0 blocks divide each 128-column panel evenly (128=4×32128 = 4 \times 32), whereas a single 256-element Q4_K superblock spans two neighboring heads with a shared FP16 scale d and dmin.

Four-stage Secant diagram; Stage 3 shows a 256-column weight row split into two 128-column panels, eight 34-byte Q8_0 blocks aligned to the panel boundary, one 144-byte Q4_K superblock cracked at column 128, a toy panel permutation, and the byte layouts of both block types drawn to one scale

Figure 2:Where this page sits in the workflow: Stage 3. Each Q8_0 block holds 32 weights in 34 bytes (a 2-byte f16 scale d and 32 int8 codes), so a 128-column panel is exactly four whole blocks, and moving or editing a panel never touches a neighbor’s scale. A Q4_K superblock holds 256 weights in 144 bytes: f16 d and dmin, 12 bytes of packed sub-block scales, and 128 bytes of 4-bit codes. The boundary at column 128 therefore falls inside it, and Π128\Pi_{128} carries half of its weights away from the scales they share. The arrows show π(k⋅r+j)=j⋅nk+k\pi(k \cdot r + j) = j \cdot n_k + k on a toy grid (nk=r=2n_k = r = 2); Section 1 gives the Qwen3.5-4B case (nk=16n_k = 16, r=2r = 2).


1. How Column Groups Fit Into Stored Blocks

A panel is a group of 128 adjacent columns. Four Q8_0 blocks fit inside one panel. A Q4_K superblock spans two panels. That difference determines whether moving a panel also splits a set of weights that share encoding parameters.

When llama.cpp converts hybrid Qwen3.5 models with r=nv/nk>1r = n_v / n_k > 1 (such as Qwen3.5-4B, where nk=16,nv=32,r=2n_k = 16, n_v = 32, r = 2) to GGUF, it permutes the input columns of blk.N.ssm_out.weight in 128-column value-head panels (Π128\Pi_{128}). (On Qwen3.5-0.8B, nk=nv=16n_k = n_v = 16 so r=1r = 1 and the deployed column order is the identity; we evaluate Π128\Pi_{128} on blk.16.ssm_out.weight as a structural 128-tile permutation test alongside the real identity layout.)

Writing a HuggingFace 128-column panel index as h=k⋅r+jh = k \cdot r + j (k<nkk < n_k, j<rj < r), its GGUF panel position is π(k⋅r+j)=j⋅nk+k\pi(k \cdot r + j) = j \cdot n_k + k: the transpose of an (nk,r)=(16,2)(n_k, r) = (16, 2) grid into a (2,16)(2, 16) grid. On Qwen3.5-4B this is a non-trivial bijection, not an involution: because r≠nkr \neq n_k, applying it twice does not return the identity (π(1)=16\pi(1) = 16, π(16)=8\pi(16) = 8). The inverse is

π−1(g)=(g mod nk)⋅r+⌊g/nk⌋.\pi^{-1}(g) = (g \bmod n_k) \cdot r + \lfloor g / n_k \rfloor .

Whether a quantized format commutes with Π128\Pi_{128} depends on how its block size divides 128:

PropertyQ8_0 (GGML Type 8)Q4_K (GGML Type 12)
Block size & byte footprint32 weights (34 bytes: 2B f16 scale + 32B int8 codes)256 weights (144 bytes: shared f16 d, dmin + 8 sub-blocks)
Alignment with 128-col ssm_out panelsExact (128 = 4 × 32): 0 straddling blocksFails (256 = 2 × 128): straddles 2 value heads (8 per 2048-col row, 16 per 4096-col row)
Reported weight-component change (B2)98.5% removed (0.5172 → 0.0079)67% of the component retained (0.5172 → 0.346)
Secant default behaviorSupported in-place (secant quant patch)Refused (block-aligned surgery needs Q8_0)

The permutation argument assumes a deterministic, position-independent encoder, aligned rows and complete blocks. Percentages here describe projections of weights onto a supplied direction, not the percentage of prompts refused. The standalone Rust secant quant patch refuses Q4_K; experimental Q4_K re-encoding results below are not receipts from a successful invocation of that command.


2. Compare Editing Before and After Compression

Starting with already-quantized weights introduces an initial approximation error. Editing and encoding those weights adds another error. The equations below keep track of those errors separately so the two paths can be compared without assuming that rounding cancels out.

Let QB:Rdout×din→Rdout×dinQ_B : \mathbb{R}^{d_{\text{out}} \times d_{\text{in}}} \to \mathbb{R}^{d_{\text{out}} \times d_{\text{in}}} denote the block-quantize-then-dequantize operator for block size BB. We assume only that QBQ_B is deterministic and block-local: the dequantized values of block bb depend only on the input values of block bb. Suppose we apply a column-sparse behavioral edit ΔW\Delta W that touches a subset of blocks T\mathcal{T}. Define the three quantization errors

e0=QB(W)−W,e1=QB(W+ΔW)−(W+ΔW),e2=QB(QB(W)+ΔW)−(QB(W)+ΔW).\begin{aligned} e_0 &= Q_B(W) - W, \\ e_1 &= Q_B(W + \Delta W) - (W + \Delta W), \\ e_2 &= Q_B\big(Q_B(W) + \Delta W\big) - \big(Q_B(W) + \Delta W\big). \end{aligned}

The decomposition fixes the same additive ΔW\Delta W in both paths. It is exact under that assumption, but contains three quantization errors, not one. A projector-subtraction edit recomputes its delta from the current weights, so applying it after dequantization generally uses a different delta; the displayed identity alone does not describe that operator’s two paths. A uniform “half-step” bound is not valid for an arbitrary codec, so each term needs a codec-specific argument.

2.1 A Per-Weight Error Bound for Q8_0

There are two sources of rounding here: the integer weight code and the stored scale. The bound adds them, under the stated scale-range assumptions. It describes an idealized calculation; the final paragraph explains what the actual encoder adds.

The Q8_0 encoder (crates/secant-core/src/quant.rs, quantize_row_q8_0) computes, for each 32-element block xbx_b, the scale db=∥xb∥∞/127d_b = \|x_b\|_\infty / 127, the codes qi=round(xi/db)∈[−127,127]q_i = \mathrm{round}(x_i / d_b) \in [-127, 127], and stores fp16(db)\mathrm{fp16}(d_b). Dequantization returns fp16(db) qi\mathrm{fp16}(d_b)\, q_i. Assume dbd_b lies in the FP16 normal range (127⋅2−14≤∥xb∥∞≤127⋅65504127 \cdot 2^{-14} \le \|x_b\|_\infty \le 127 \cdot 65504), so that ∣fp16(db)−db∣≤2−11db|\mathrm{fp16}(d_b) - d_b| \le 2^{-11} d_b. Then each element obeys

∣Q32(x)i−xi∣  ≤  ∣dbqi−xi∣+∣fp16(db)−db∣ ∣qi∣  ≤  db2+2−11db⋅127  =  κ ∥xb∥∞,where κ=1254+2−11≈4.43×10−3,\begin{aligned} |Q_{32}(x)_i - x_i| &\;\le\; |d_b q_i - x_i| + |\mathrm{fp16}(d_b) - d_b|\,|q_i| \\ &\;\le\; \frac{d_b}{2} + 2^{-11} d_b \cdot 127 \;=\; \kappa\, \|x_b\|_\infty, \quad \text{where } \kappa = \frac{1}{254} + 2^{-11} \approx 4.43 \times 10^{-3}, \end{aligned}

up to float32 rounding in the encoder arithmetic. (For a subnormal scale, add 127⋅2−25127 \cdot 2^{-25} per element.) Write ab=∥Wb∥∞a_b = \|W_b\|_\infty and δb=∥ΔWb∥∞\delta_b = \|\Delta W_b\|_\infty on a touched block bb. Because ∣ ∥Wb+ΔWb∥∞−∥Wb∥∞∣≤δb\big|\,\|W_b + \Delta W_b\|_\infty - \|W_b\|_\infty\big| \le \delta_b, the post-edit scale drifts by at most

∣db′−db∣  ≤  δb127|d_b' - d_b| \;\le\; \frac{\delta_b}{127}

before FP16 rounding. Applying the per-element bound to each term, with ∥Q32(W)b∥∞≤(1+κ) ab\|Q_{32}(W)_b\|_\infty \le (1 + \kappa)\, a_b:

∣e0∣≤κ ab,∣e1∣≤κ (ab+δb),∣e2∣≤κ ((1+κ) ab+δb),|e_0| \le \kappa\, a_b, \quad |e_1| \le \kappa\,(a_b + \delta_b), \quad |e_2| \le \kappa\,\big((1 + \kappa)\, a_b + \delta_b\big),

so every element of a touched Q8_0 block satisfies

∣Q32(W+ΔW)−Q32(Q32(W)+ΔW)∣i  ≤  κ ((3+κ) ab+2 δb).\big|Q_{32}(W + \Delta W) - Q_{32}(Q_{32}(W) + \Delta W)\big|_i \;\le\; \kappa\,\big((3 + \kappa)\, a_b + 2\,\delta_b\big).

This is an ideal-arithmetic bound with FP16 scale rounding included. It requires the scale-range assumption for each of WbW_b, Wb+ΔWbW_b+\Delta W_b and Q32(W)b+ΔWbQ_{32}(W)_b+\Delta W_b, not only the original block. It does not include the Rust encoder’s float32 reciprocal, multiplication and rounding errors and is not a certified bound for all machine inputs. The reported max_abs_diff =2.16×10−3= 2.16 \times 10^{-3} is an empirical measurement, separate from that bound.

2.2 What the Q4_K Experiment Measured

Sharing scale parameters across panels can make an edit affect more weights than intended. The measurements below describe one experiment; they are not a general error bound or a supported Q4_K patch workflow.

Q4_K is not a single uniform scalar quantizer: each 256-element superblock has an FP16 super-scale d and super-minimum dmin, plus eight 6-bit sub-block scales and minimums. On Qwen3.5 ssm_out, each superblock straddles two 128-column value heads, so an edit to one head can change the shared fit of the neighboring head. We do not claim a proved per-element bound for Q4_K. Its behavior is characterized empirically: observed Path 1 vs. Path 2 max_abs_diff =2.38×10−2= 2.38 \times 10^{-2} (about 11×11\times the Q8_0 value), 8 straddling superblocks per 2048-column row (16 per 4096-column row), and about 67% of the original weight-direction component retained in experimental Q4_K re-encoding (0.5172 →\to 0.346, Section 1). The implemented policy is that secant quant patch accepts only Q8_0 tensors and refuses Q4_K (and every other dtype) with tensor <name> is <dtype>, block-aligned surgery needs Q8_0.


3. Compare the Retained Error Measurements

All rows below come from one identified experiment. Reconstruction error measures the distance from the intended float result; the directional residual measures the component left along the supplied direction. Those are different quantities.

The banked exp2_receipt.json (secant.phase3.exp2.v1, SHA-256 1c437338d697ccfb6b24286ac60c84fa0c0a73f3a0120e3aefd920c5d608b0c7) records a single Qwen3.5-0.8B ssm_out experiment with a projector edit of Frobenius norm 0.5172394742. Under commutation.identity_real, both codecs commute with the identity permutation. Their reconstruction errors still differ:

Metric under identity orderingQ8_0Q4_K
Path 1 relative Frobenius reconstruction error0.5836%7.5863%
Path 2 relative Frobenius reconstruction error0.7992%8.2210%
Path 2 residual along the supplied direction, L20.00785910.3457191
Path 1 versus Path 2 maximum absolute difference0.00215720.0238073

These are retained experimental measurements, not fresh model runs or outputs of the current Rust CLI. The separate whole-model Q8_0 byte-preservation experiment remains in Section 4.


4. Check Which Blocks and Tensors Changed

A block can be touched by the logical edit yet keep the same encoded bytes after rounding. The table distinguishes touched blocks, rewritten blocks, and blocks outside the edit. Whole-file verification separately checks the tensors that were not selected.

Comparing Path 1 (schematically, secant patch on BF16 followed by external Q8_0 quantization) against Path 2 (secant quant patch directly on Qwen3.5-0.8B.Q8_0.gguf) shows that direct 34-byte block surgery leaves all untouched blocks 100% bit-identical and matches Path 1 within 0.179% to 0.923% per-tensor relative Frobenius error (note that blocks where the float delta rounds to the same int8 code remain byte-identical even when inside the touched column support):

Q8_0 block coverage

Tensor / directionTotal blocksModified (Path 2)Touched, unchanged
blk.12.ffn_down.weight (B2)114,688108,9495,739
blk.13.ffn_down.weight (B3)114,68848,703449
blk.15.ffn_down.weight (B1a)114,68831,104640
blk.19.ffn_down.weight (B1b)114,68821,842686
All other 331 tensors in filen/a00

Untouched support and numerical difference

Tensor / directionUntouched, byte-identicalRelative Frobenius difference
blk.12.ffn_down.weight (B2)0 (dense row projector)0.923%
blk.13.ffn_down.weight (B3)65,536 (100% identical)0.251%
blk.15.ffn_down.weight (B1a)82,944 (100% identical)0.225%
blk.19.ffn_down.weight (B1b)92,160 (100% identical)0.179%
All other 331 tensors in file100% (0 bytes differing)0.000%