Skip to content

feat: fpz compressed pack format with batched GPU decode - #32

Open
AlperenKonukbay wants to merge 8 commits into
speed/fleet-hardening-v1from
speed/fpz-gpu-decode-v1
Open

feat: fpz compressed pack format with batched GPU decode#32
AlperenKonukbay wants to merge 8 commits into
speed/fleet-hardening-v1from
speed/fpz-gpu-decode-v1

Conversation

@AlperenKonukbay

@AlperenKonukbay AlperenKonukbay commented Jul 25, 2026

Copy link
Copy Markdown

Stacked on #31; once it merges this diff is fpz-only.

Format

pack_to_file(..., compress="fpz-bf16") stores bf16 macroblocks as split byte-planes: the mantissa plane raw (incompressible in practice), the sign/exponent plane zstd-compressed. 1.401x smaller on a real 38 GB transformer pack, byte-exact on read. The v2 layout splits the compressed plane into independent 16-byte-aligned chunks — the shape a batched GPU decoder needs. Opt-in; plain packs are unaffected. The encoder streams, so a 38 GB repack needs no uncompressed scratch file (50 min → ~3.5 min).

Read paths

  • CPU decode: thread-pooled zstd (GIL released), any device target.
  • GPU decode (FLASHPACK_FPZ_GPU_DECODE=1): ctypes bindings to libnvcomp's batched Zstd decompressor (_nvcomp_ll.py). The pybind nvcomp wrapper doesn't expose the batched C API and its per-chunk Python objects cost ~7x, so the batched path is the only GPU path shipped. Per batch: one pinned chunk table, one H2D copy, one foreign call — Python overhead independent of chunk count. Selection is automatic, with warn-once CPU fallback when libnvcomp is missing, the pack is v1, or its alignment doesn't satisfy the decompressor.
  • fpz_gpu_warmup(): pays nvcomp's one-time CUDA module load off the hot path (worst observed 26.7 s lazy → ~1 s).

Measurements (38 GB pack, byte parity verified on every run)

leg result
GPU decode, page-hot, H200 1.72–1.73 s (~22 GB/s logical)
GPU decode, page-hot, B200 1.78–1.85 s
NVMe-cold decode 0.03 s — fully hidden under the reads
8× H200, rank0 fpz read + broadcast 2.02 s to all ranks

Stable across 5+ boots on 2 GPU classes (B200 is a single boot).

The default chunk size is now 1 MiB, matching the receipted configuration. It originally shipped at 64 KiB (nvcomp's default); a pre-merge regression gate (same-boot interleaved A/B, 38 GB pack) measured 64 KiB decoding 2.6x slower than 1 MiB on the threaded CPU path (39.5 vs 15.3 s hot — the path every consumer without the GPU extra gets) and 11% slower on the batched GPU path, for a compression-ratio difference under 1% (1.412x vs 1.401x). The same gate also live-verified the warn-once CPU fallback: with FLASHPACK_FPZ_GPU_DECODE=1 and libnvcomp absent, every read fell back cleanly with the install hint, parity intact. Trade-off: after an explicit page-cache drop, the fpz buffered refill can be slower than the raw path's O_DIRECT read on some nodes; the wire-cold tier still favors fpz on bytes moved. An O_DIRECT option for fpz reads is a natural follow-up.

nvidia-libnvcomp-cu12 lives in a separate fpz-gpu extra (NVIDIA-proprietary; kept out of default deps pending license review). 291 tests green.

Alperen Konukbay and others added 5 commits July 24, 2026 17:18
The fpz format: bf16 macroblocks stored as split byte-plane zstd (low/
mantissa plane raw — measured incompressible — high/sign-exponent plane
compressed), 1.401x smaller on the real 38GB LTX transformer pack,
byte-exact on read. v2 stores the high plane as many independent 16B-
aligned zstd chunks — the shape a GPU decoder needs.

Read paths:
- CPU decode (thread-pooled, GIL-released zstd) for any device target.
- Batched GPU decode (FLASHPACK_FPZ_GPU_DECODE=1 [+ FLASHPACK_FPZ_GPU_LL=1
  for the batched C path]): ctypes bindings to libnvcomp's batched Zstd
  decompressor (flashpack/_nvcomp_ll.py; the pybind nvcomp wrapper never
  exposes the batched API and its per-chunk Python objects are a measured
  7x floor). Per batch: one pinned int64 chunk table, one H2D, two
  on-device base-address adds, one foreign call. Measured: 38GB restored
  in 1.67-1.85s page-hot (H200 AND B200, byte parity everywhere), decode
  fully hidden under NVMe reads when cold.
- fpz_gpu_warmup(): pays nvcomp's one-time CUDA kernel-load off the hot
  path (26.7s worst-case observed lazy -> ~1s residual with warmup+EAGER).
- Optional fused interleave Triton kernel (default off; ~2% at best — HBM
  absorbs the strided writes — kept as dormant infrastructure).

Streaming encoder writes packs without an uncompressed scratch file
(38GB repack: 50min -> ~3.5min). Everything is opt-in via
pack_to_file(compress="fpz-bf16"); plain packs are byte-for-byte
unaffected.

Built on the affinity-clamp + distributed-hardening branch; the combined
tree is byte-identical to the gated speed/fpz-v1 head (f2f1bf0), on which
every receipt in the PR body was measured. Full development history:
https://github.com/fal-ai/flashpack/tree/speed/fpz-v1

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review cleanup — no functional additions:

- The batched libnvcomp path is now THE GPU decode; the pybind-wrapper
  path (7x slower, only ever a comparison baseline) is removed along with
  its per-chunk Array objects, DecompressConfig cache, torch-allocator
  adapter and the FLASHPACK_FPZ_GPU_LL selection gate. Path selection is
  automatic: FLASHPACK_FPZ_GPU_DECODE=1 uses the batched decoder when
  libnvcomp is present, the pack is v2 and its chunk alignment satisfies
  the decompressor; otherwise it falls back to the threaded CPU decode
  with a warn-once explaining why.
- The optional fused-interleave Triton kernel is removed (measured ~2%;
  the strided copies run at HBM speed).
- Per-thread timing trace now goes through logging.debug (was print) with
  compacted buckets; FLASHPACK_FPZ_GPU_TORCH_ALLOC and the now-dead
  _env_flag_default helper are gone.
- Tests updated accordingly: wrapper/allocator tests replaced by coverage
  of the new automatic fallback; docstrings rewritten as contracts.
- fpz-gpu extra now depends only on nvidia-libnvcomp-cu12 (the pybind
  wheel is no longer used).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The assertions carry the contract; the capsys-disabled prints were
bench instrumentation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jfischoff

Copy link
Copy Markdown
Contributor

Review by fal-9000 (GPT-5.6 Sol, xhigh) — reviewed at head 9601e5f4a9d88a6ee1f253eecf2d52782fa9b208, posted on the agent's behalf while its write access to this repo is being enabled (see FEA-8199).

Two blockers:

  1. High — compressed artifacts still advertise flashpack_v4. Released v4 readers accept that footer but ignore fpz, then mmap the logical uncompressed length from compressed offsets. Depending on layout this fails late or silently reads compressed/adjacent/footer bytes as weights. Compressed packs need a new top-level format version so old readers fail closed, plus an old-reader compatibility test.

  2. Medium — FPZ reads unconditionally call os.preadv, which is absent on Windows, while all FPZ tests are skipped there. The package advertises OS-independent support and runs Windows CI, so it can write an FPZ pack on Windows and then fail to read it. Keep the preadv fast-path and add a portable seek/read fallback, then run one Windows round-trip.

Alperen Konukbay and others added 2 commits July 31, 2026 13:00
Pre-merge regression gate (same-boot interleaved A/B on a 38 GB pack):
the shipped 64 KiB default -- chosen to match nvcomp's default chunk
size -- decodes 2.6x slower than 1 MiB on the threaded CPU path (39.5 s
vs 15.3 s hot; per-chunk overhead x 512 chunks per frame) for a
compression-ratio difference under 1% (1.412x vs 1.401x). Every GPU
batched-decode receipt (1.67-1.85 s per 38 GB) was earned on 1 MiB
packs, so the default now matches the receipted configuration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants