7 · The 12-bit lossless weight format (pack_z and decz)

At m ≤ 16 the GEMV runs at nearly full memory bandwidth, so it can only go faster by reading fewer bytes. This chapter shows how each BF16 weight is stored in 12 bits with no loss of information. It reads the compressor pack_z (lines 650–708) and the 12-instruction decoder decz (lines 246–273). Lab 3 checks every claim bit for bit.

ON THIS PAGE

7.1 Anatomy of a BF16 number

1514131211109876543210
SE7E6E5E4E3E2E1E0M6M5M4M3M2M1M0
high byte: sign + top 7 exponent bits (“e7”) → 4 bits (sign + 3-bit code)low byte: lowest exponent bit + 7 mantissa bits → kept as is
A BF16 number: 1 sign bit, 8 exponent bits, 7 mantissa bits. The 12-bit format keeps the low byte and replaces the high byte by the sign plus a 3-bit index into a window of 8 consecutive e7 values.

A BF16 value is (−1)^sign × 2^(exponent − 127) × 1.mantissa. Our weights are drawn like the judge's: normal with standard deviation 1/√K ≈ 0.011. Such numbers span only a few powers of two, so their exponents are highly repetitive. The mantissa bits, by contrast, look random and cannot be compressed.

So the format compresses only the high byte: the sign plus the top 7 exponent bits, which we call e7.

Here is the distribution of e7 over 8.4 million weights drawn like the judge's (lab 3, part 1):

  e7= 53  |w| in [2^-21, 2^-19)    0.0098%
  e7= 54  |w| in [2^-19, 2^-17)    0.0416%   <- window
  e7= 55  |w| in [2^-17, 2^-15)    0.1659%   <- window
  e7= 56  |w| in [2^-15, 2^-13)    0.6512%   <- window
  e7= 57  |w| in [2^-13, 2^-11)    2.6323%   <- window
  e7= 58  |w| in [2^-11, 2^-9)    10.4717%   <- window
  e7= 59  |w| in [2^-9, 2^-7)     37.9717%   <- window
  e7= 60  |w| in [2^-7, 2^-5)     47.5746%   <- window
  e7= 61  |w| in [2^-5, 2^-3)      0.4774%   <- window
  window: e7 in [54, 61]  covers 99.9865% of weights

Eight consecutive values of e7 (a 3-bit code) cover 99.987 % of the weights. Only tiny weights, below 2⁻¹⁹, and exact zeros fall outside.

7.2 The format

part bits content
low byte 8 stored unchanged: the lowest exponent bit and the 7 mantissa bits
nibble 4 the sign, plus code = e7 − base (0…7), where base is chosen per matrix
total 12 25 % fewer bytes than BF16
exceptions rare weights with e7 outside [base, base+7]: their code is clamped to 0 or 7, and the exact difference (original − decoded) is stored as an FP32 value in a per-row list

Decoding never needs a branch: rebuild the high byte from the sign and base + code, and glue it onto the low byte. For the 0.013 % of weights outside the window, that gives a slightly wrong value. A separate warp adds correction × x[k] to the output (chapter 8). The sum is exactly the original weight times x[k], so the format is lossless.

Here is one weight at a time (lab 3, part 1b, base = 54):

  weight +0.0123 -> BF16 bits 0011110001001010 (0x3c4a) = +0.0123291
    sign 0   e7 (bits 14-8)  60 = 0111100   low byte (bits 7-0) 0x4a = 01001010
    stored: low byte 0x4a + nibble (sign 0, code e7-base = 60-54 = 6)  = 12 bits
    rebuilt: sign | (base+code)<<8 | low = 0x3c4a = +0.0123291  exact
  weight +3.1e-07 -> BF16 bits 0011010010100110 (0x34a6) = +3.09199e-07
    sign 0   e7 (bits 14-8)  52 = 0110100   low byte (bits 7-0) 0xa6 = 10100110
    stored: low byte 0xa6 + nibble (sign 0, code e7-base = 52-54 = -2 -> clamped to 0)  = 12 bits
    rebuilt: sign | (base+code)<<8 | low = 0x36a6 = +4.94719e-06  -> needs an exception: -4.63799e-06

How the bytes are laid out. The weights keep exactly the fragment order of chapter 5, but the 16 rows × 64 k of one "group" are split into three 512-byte sections:

section (32 lanes × 16 B)bytes of one laneholds
0 (A0)16low bytes of fragment 0 (8 weights) + fragment 1 (8 weights)
1 (A1)16low bytes of fragments 2 and 3
2 (B)16four 32-bit words, one per fragment: 8 × (3-bit code + sign bit)
One 16-row × 64-k group of compressed weights = 1,536 bytes = 96 × 16-byte units: each lane reads 48 bytes and decodes them into 4 mma A fragments (32 weights). Uncompressed it would be 64 bytes.

Within each fragment's 32-bit code word Q, the bits are placed so the decoder needs as few instructions as possible:

byte of Q bits 0–2 bit 3 bits 4–6 bit 7
0 code of weight 0 sign of weight 4 code of weight 1 sign of weight 0
1 code of weight 2 sign of weight 5 code of weight 3 sign of weight 1
2 code of weight 4 sign of weight 6 code of weight 5 sign of weight 2
3 code of weight 6 sign of weight 7 code of weight 7 sign of weight 3

The codes of weights 0–3 fill the low 16 bits and those of weights 4–7 the high 16 bits. The signs of weights 0–3 sit at bit 7 of each byte, which is the BF16 sign position of each decoded high byte.

7.3 pack_z, line by line

submission.py, lines 650–657
650
651
652
653
654
655
656
657
def pack_z(w: torch.Tensor, nwin: int = 8):
    """bf16 [N, K] -> dict(w=packed uint8 [RB, K/64, 3, 32, 16], esc=int32 [N, capr, 2] (k, fp32 delta bits),
    capr, TL, TH, base, nesc). Lossless: decoded + delta == w exactly."""
    assert w.dtype == torch.bfloat16 and nwin == 8
    n, k = w.shape
    assert n % 16 == 0 and k % 64 == 0
    rb, kb = n // 16, k // 32
    dev = w.device
submission.py, lines 658–666 · choose the window
658
659
660
661
662
663
664
665
666
    bits = w.contiguous().view(torch.int16).to(torch.int32) & 0xFFFF
    e7 = (bits >> 8) & 0x7F
    h = torch.bincount(e7.flatten(), minlength=128)
    c = torch.cat([torch.zeros(1, dtype=h.dtype, device=dev), torch.cumsum(h, 0)])
    cover = c[nwin:] - c[:-nwin]
    base = int(torch.argmax(cover))
    code = (e7 - base).clamp(0, nwin - 1)
    sign = bits >> 15
    low = bits & 0xFF
submission.py, lines 667–670 · what the kernel will rebuild
667
668
669
670
    dbits = (sign << 15) | ((base + code) << 8) | low
    dbits = torch.where(dbits >= 32768, dbits - 65536, dbits).to(torch.int16)
    dec = dbits.view(torch.bfloat16)
    del dbits, e7, bits
submission.py, lines 671–680 · the exact corrections
671
672
673
674
675
676
677
678
679
680
    # exception lists: per row, (k, fp32 value) entries summing to w - decoded. The fp32 difference is exact
    # unless |w| is ~2^-16 below the window; then a second entry carries the fp32 residual.
    d64 = w.double() - dec.double()
    d32 = d64.float()
    res = (d64 - d32.double()).float()
    i1, i2 = (d32 != 0).nonzero(), (res != 0).nonzero()
    rows = torch.cat([i1[:, 0], i2[:, 0]])
    ks = torch.cat([i1[:, 1], i2[:, 1]])
    vals = torch.cat([d32[i1[:, 0], i1[:, 1]], res[i2[:, 0], i2[:, 1]]])
    del d64, d32, res
submission.py, lines 681–689 · per-row exception lists
681
682
683
684
685
686
687
688
689
    order = torch.argsort(rows * k + ks, stable=True)
    rows, ks, vals = rows[order], ks[order], vals[order]
    cnt = torch.bincount(rows, minlength=n)
    capr = max(1, int(cnt.max()) if len(rows) else 1)
    start = torch.cumsum(cnt, 0) - cnt
    pos = torch.arange(len(rows), device=dev) - start[rows]
    esc = torch.zeros(n, capr, 2, dtype=torch.int32, device=dev)
    esc[rows, pos, 0] = ks.to(torch.int32)
    esc[rows, pos, 1] = vals.contiguous().view(torch.int32)
submission.py, lines 691–693 · fragment order (as in chapter 5)
691
692
693
    def perm(a):  # [N, K] -> [RB, G, f(4), lane(32), i(8)] in mma A-fragment order
        p = a.reshape(rb, 2, 8, kb, 4, 2, 2, 2).permute(0, 3, 5, 2, 4, 6, 1, 7)  # RB, KB, j, g, t, h, rh, e
        return p.reshape(rb, kb // 2, 4, 32, 8)
submission.py, lines 695–696 · sections 0 and 1: the low bytes
695
696
    lowp = perm(low.to(torch.uint8))
    A = lowp.permute(0, 1, 3, 2, 4).reshape(rb, kb // 2, 32, 2, 16).permute(0, 1, 3, 2, 4)  # RB, G, sec, lane, 16
submission.py, lines 697–703 · section 2: codes and signs
697
698
699
700
701
702
703
    codp = perm(code.to(torch.int64))
    sgnp = perm(sign.to(torch.int64))
    csh = torch.tensor([0, 4, 8, 12, 16, 20, 24, 28], device=dev, dtype=torch.int64)
    ssh = torch.tensor([7, 15, 23, 31, 3, 11, 19, 27], device=dev, dtype=torch.int64)
    q = (codp << csh).sum(-1) + (sgnp << ssh).sum(-1)  # [RB, G, 4, 32]
    q = torch.where(q >= 2 ** 31, q - 2 ** 32, q).to(torch.int32)
    B = q.permute(0, 1, 3, 2).contiguous().view(torch.uint8).reshape(rb, kb // 2, 1, 32, 16)
submission.py, lines 704–708 · assemble
704
705
706
707
708
    packed = torch.cat([A, B], 2).contiguous()
    lut = [base + j for j in range(8)]
    TL = sum(lut[j] << (8 * j) for j in range(4))
    TH = sum(lut[4 + j] << (8 * j) for j in range(4))
    return {"w": packed, "esc": esc.contiguous(), "capr": capr, "TL": TL, "TH": TH, "base": base, "nesc": int(len(rows))}

7.4 The decoder, line by line

submission.py, lines 246–262
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
__device__ __forceinline__ uint4 ldz_stream(const uint4* p) {
  uint4 r;
  asm volatile("ld.global.nc.L1::no_allocate.v4.u32 {%0,%1,%2,%3}, [%4];"
               : "=r"(r.x), "=r"(r.y), "=r"(r.z), "=r"(r.w) : "l"(p));
  return r;
}
__device__ __forceinline__ void mmaz(float (&d)[4], uint4 a, uint32_t b0, uint32_t b1) {
  asm volatile("mma.sync.aligned.m16n8k16.row.col.f32.bf16.bf16.f32 {%0,%1,%2,%3}, {%4,%5,%6,%7}, {%8,%9}, {%0,%1,%2,%3};"
               : "+f"(d[0]), "+f"(d[1]), "+f"(d[2]), "+f"(d[3])
               : "r"(a.x), "r"(a.y), "r"(a.z), "r"(a.w), "r"(b0), "r"(b1));
}
__device__ __forceinline__ uint32_t prmtz(uint32_t a, uint32_t b, uint32_t sel) {
  uint32_t r; asm("prmt.b32 %0, %1, %2, %3;" : "=r"(r) : "r"(a), "r"(b), "r"(sel)); return r;
}
__device__ __forceinline__ uint32_t or_and(uint32_t a, uint32_t b, uint32_t c) {  // a | (b & c)
  uint32_t r; asm("lop3.b32 %0, %1, %2, %3, 0xF8;" : "=r"(r) : "r"(a), "r"(b), "r"(c)); return r;
}
submission.py, lines 263–273 · decz: 8 weights in, one 16-byte mma fragment out
263
264
265
266
267
268
269
270
271
272
273
// S0/S1: low bytes of weights 0-3 / 4-7 of one A fragment; Q: their codes + signs (layout in the module doc)
__device__ __forceinline__ uint4 decz(uint32_t S0, uint32_t S1, uint32_t Q, uint32_t TL, uint32_t TH) {
  uint32_t H0 = or_and(prmtz(TL, TH, Q & 0x7777u), Q, 0x80808080u);
  uint32_t H1 = or_and(prmtz(TL, TH, (Q >> 16) & 0x7777u), Q << 4, 0x80808080u);
  uint4 a;
  a.x = prmtz(S0, H0, 0x5140u);
  a.y = prmtz(S0, H0, 0x7362u);
  a.z = prmtz(S1, H1, 0x5140u);
  a.w = prmtz(S1, H1, 0x7362u);
  return a;
}

The total is about 12 integer instructions per 8 weights per lane: 2 lookups, 2 merges, 4 interleaves and 4 shifts or masks. The memory system needs roughly the same time to deliver those 12 bytes, so the decoding hides under the loads.

Key idea. Put each bit where the decoder needs it, at pack time.

Try it · lab3_compress.py — Lab 3: the lossless 12-bit weight format (pack_z + decz), emulated bit for bit in numpy.
cd ~/projects/okc/muse_bf16_proj/learn/labs
python3 lab3_compress.py
The file is already on your Mac at ~/projects/okc/muse_bf16_proj/learn/labs/lab3_compress.py; you can also view or download it here.

What the lab does:

Exercises.

  1. Look at the window-size sweep. Why is 8 the sweet spot? (4 values leave 1.35 % of weights outside, which is far too many exceptions; 16 values cost a 13th bit.)
  2. Replace the weights with a heavier-tailed distribution, rng.standard_t(3, (N, K)) / np.sqrt(K), and watch the exception rate change.
  3. In decz, change 0x5140 to 0x4051. Which weights come out swapped?

Check yourself

Why keep the low byte raw instead of compressing the mantissa too?

The mantissa bits of these weights are essentially random. Random bits cannot be compressed, so all the compressible information is in the narrow range of exponents.

What happens to a weight that is exactly zero?

Its e7 is 0, far below the window, so its code is clamped to 0. The decoder rebuilds +2^(2·base−127) × (1 + mantissa bits), which is 1.9e-6 for base = 54 (lab 3, part 1b). The exception list stores the exact negative of that, and the extra warp subtracts it, giving an exact zero contribution.

The format needs 12 bits per weight. Why not 11, with a 2-bit code?

Four consecutive e7 values cover only about 98.65 % of the weights. 1.35 % exceptions would mean about 450,000 corrections for the pooler, far too many to apply cheaply. With 8 values, 0.014 % remain, about 4,600 corrections, and they cost almost nothing.