Status: historical. This is the design df11pack was built from, kept as the record of why it is shaped this way. Where it differs from the code, the code and COMPATIBILITY.md win. Main departures: checkpoints, journal and resume (§6) were dropped for atomic output (a failed run leaves no partial files; PLAN Phase 4); post-hoc verification's integrity level checks structure, or opt-in
--hashes, instead of journal hashes (§7.3); and added since: adiffusers-singlelayout, per-definition key rules, the tiedlm_headunit,--hashes, and the experimentalidx8index. Also stale below: the §5.2 budget of 1.35 N bytes per worker is now a measured 6.0 (BYTES_PER_WEIGHT_HELD); §5.5's per-tensor criterion is now whole-file byte-identity (FINDINGS, "Erratum: byte-identical held for tensors"); the parallel CPU decoder and GPU path of §1.5 and §7.2 were not built (the verifier is sequential, H12 unmeasured); §8's argpartition exception became a refusal; and §11 decisions 2 and 7 are closed (H7 confirmed; ChromaRadiance ships). Measurements: FINDINGS.md.
An independent DFloat11 compressor: streaming, with crash recovery. Guiding principle: anyone must be able to compress models, including on old machines, with little RAM, without an NVIDIA GPU, or on a spinning hard drive.
Scope for v1: both layouts from the start, ComfyUI-native (single file) and
diffusers (directory with shards + config.json).
Changes from v0.1: added §1.5 (compression/decompression asymmetry), §1.6 (diffusers layout), the kernel's 32-bit limits, H9–H12, a parallel CPU decoder mirroring the kernel, and reordered phases.
Status of this document: no code. Every claim is marked [CONFIRMED] (read in the source), [HYPOTHESIS] (to be measured in Phase 0) or [DECISION] (open, to be closed before implementing).
DF11 uses the word block for three different things. They are separated here:
| Term | What it is | Typical size |
|---|---|---|
| UC (compression unit) | A module matching the pattern_dict (e.g. double_blocks.7). It has its own Huffman codebook. |
141M–340M weights in Flux |
| Kernel chunk | A group of 512 CUDA threads × 8 bytes = 4096 bytes of bitstream. output_positions stores one index per chunk. |
~12k weights |
| Thread window | 8 bytes (64 bits) of bitstream. gaps stores 5 bits per window. |
~24 weights |
| Checkpoint | A resume point persisted to disk. | 1 UC (see §6) |
Source: LeanModels/DFloat11, dfloat11/dfloat11.py::compress_model and
dfloat11/dfloat11_utils.py. The node in mingyi456/ComfyUI-DFloat11-Extended
(dfloat11_model_loader.py::DFloat11ModelCompressor) has no compressor of its
own: it instantiates the full model with comfy.sd.load_diffusion_model and
calls the official compress_model with save_single_file=True,
check_correctness=True.
For each UC, in order and on a single thread:
- Takes the
.weighttensors of the layers listed in thepattern_dict, flattens them and does atorch.cat(a full copy in RAM). get_codec: exponent histogram viatorch.unique.get_32bit_codec: builds the Huffman code with thedahuffmanlibrary. If any code exceeds 32 bits, it forces the least frequent symbols to frequency 1 and rebuilds until it fits.get_luts: generates the hierarchical byte-wise LUT (a 256-entry table per prefix, plus a final row with lengths).encode_weights→encode: converts the exponents to a Python list (.tolist()) and encodes them with a Python loop, one symbol at a time, accumulating bytes into another Python list. At the same time it generatesgaps(as 5-character strings, then a list of bits, thenpackbits) andoutput_positions.- If
check_correctness: uploads everything to the GPU, runs the realdecodekernel and compares against the original weights (another copy on the CPU). - Registers the resulting buffers on the module itself and deletes
.weight. - At the end:
save_file(model.state_dict(), 'model.safetensors')for the whole model at once.
- UCs are completely independent [CONFIRMED]: each one has its own codebook, LUTs and indices. There is no global state between UCs other than the configuration (
version,threads_per_block=(512,),bytes_per_thread=8,pattern_dict). Parallelising across UCs has no conceptual barrier whatsoever. - The slowness does not come from "being single-threaded", but from the interpreted loop. [HYPOTHESIS, consistent with the figures] Flux compresses ~11.8B weights (19 double blocks × ~340M + 38 single blocks × ~141M). At 300–600 ns per Python iteration that comes to 1–2 hours, exactly what the README describes. Parallelising that loop with multiprocessing would give ×cores; removing it with native code should give ×100 or more per core. This corrects our earlier explanation (the GIL is not the main cause).
- The RAM comes from having the whole model instantiated plus huge per-UC temporaries. [HYPOTHESIS] Estimate for a large UC (~340M weights):
cat0.7 GB, int16/uint8 temporaries ~1.3 GB,.tolist()~2.7 GB (8 B per pointer), theencodedlist ~0.9 GB, gaps as strings and bit lists ~1.3 GB. That is some 6–7 GB transient on top of the model's ~24 GB, plus whatever ComfyUI's loading costs (state_dict and model possibly coexisting) and the final serialisation. Reaching ~48 GB is plausible.
Read from the encoder and from the kernel (decode.cu):
sign_mantissa[i] = (sign bit) | (7 mantissa bits), 1 byte per weight.- Exponent = bits 7..14 of the int16 pattern. Bitstream is MSB-first within each byte.
- After the last symbol,
dahuffman's EOF code is written and padded to a byte boundary. gaps: 5 bits per 64-bit window = the offset of the first code that begins in that window; padded with zeros up to a multiple of 512 windows and packed withpackbits. Requires a maximum code length ≤ 32 bits.output_positions: index of the first element beginning in each 4096-byte chunk, plus the total element count at the end; uint32 stored as a uint8 view. Its length isceil(len(encoded_exponent) / 4096) + 1, and its final value is the weight count, equal tolen(sign_mantissa)— not the encoded byte length. [CONFIRMED by measurement, 0.1/0.2; the earlier wording "pluslen(data)" was ambiguous and was read wrongly once already.]luts: uint8, shape(n_prefixes + 1, 256). In the kernel, a value ≥ 240 means "jump to LUT 256−v". Therefore:- no real exponent may be 240–255 (enormous values, Inf or NaN), and
- the number of prefix tables is bounded at 17, not 16. Jump values 240..=255 reach targets 1..=16, and table 0 makes seventeen. An eighteenth table's jump value would fall below 240 and be read as a symbol instead. [CORRECTED, Phase 5 — derived from
get_lutsand consistent withdecode.ptx, not measured: no real unit here exceeds five tables. This section previously said 16, which would refuse a legal unit.] If a model violates this, we must abort with a clear error, never emit a file. Carry-forward leakage [CONFIRMED by measurement, 0.5]. The officialget_lutsfills each table with a carry-forward loop whose accumulator is function-scoped, so a table after the first that lacks key0begins filled with the previous table's trailing value. Verified in a real official output: 128 bytes of one row held the prior row's value. Byte-identity therefore requires reproducing this. SeeCOMPATIBILITY.mdfor the default (--luts=compat) and the gated opt-out (--luts=correct).
split_positions: int64, the internal boundaries of the concatenated tensors — for n tensors it has n−1 entries, equal tocumsum(sizes)[:-1]. The total is not stored here; it islen(sign_mantissa). Empty for a single-tensor unit (a barenn.Linearornn.Embedding). The concatenation order is the order ofattr_namesin thepattern_dict. [CONFIRMED by measurement, 0.1/0.2: a 7-tensor unit produced 6 entries.]- Kernel 32-bit limits:
n_bytesandn_elementsareintandoutput_positionsisuint32. A UC cannot exceed 2³¹−1 weights or 2³¹−1 bytes of bitstream. Flux is far from this (max ~340M), but an LLM with a huge UC (e.g. giant embeddings) could hit it. This must be checked before encoding. - Output names per UC:
<uc>.luts,.encoded_exponent,.sign_mantissa,.output_positions,.gaps,.split_positions. The compressed.weighttensors disappear; the rest of the state_dict (biases, norms, layers outside the pattern) is preserved. dahuffman's Huffman tree tie-breaking: a heap of tuples(frequency, list of (symbol, code)); ties are resolved by comparing the lists, and EOF always compares as smallest. It is deterministic and reproducible in any language.
- Its format is not DF11-compatible (canonical Huffman, 8-bit index). So the format is not reused, but three things are:
extract_real_weights.py: streaming safetensors reading without torch, tensor by tensor.bitpack.py: the idea of encoding vectorised and proving byte-for-byte identity against the reference. Note: its implementation materialises an array of 1 byte per bit, which is precisely the RAM pattern we want to avoid.- The methodological discipline: verify bit-exactness before measuring times, don't combine optimisations without measuring them separately, and don't extrapolate across architectures.
- Scope note: that repo records that DF11 is no longer the state of the art in decoding speed. This does not affect this project, whose goal is compatibility with the existing DF11 ecosystem, not a new codec.
DF11 does not use the same method in both directions. The project is designed around inference (the paper and the kernel are from April–May 2025; the compression code was published in August 2025 as a utility).
Compression (dfloat11_utils.py::encode) |
Decompression (decode.cu) |
|
|---|---|---|
| Where | CPU | GPU |
| Parallelism | None: Python loop, one symbol per iteration | 512 threads per CUDA block, thousands of blocks |
| Unit of work | The UC's entire stream | An 8-byte window per thread |
| Cost per symbol | Hundreds of ns (interpreter) | One LUT access plus a shift |
How the kernel decompresses, in three synchronised phases (__syncthreads):
- Counting. Each thread takes its 8 bytes (plus 4 of margin), uses its
gapsvalue to know at which bit the first code born in its window starts, and counts how many symbols start there. No thread depends on another. - Parallel prefix sum. With those counts it performs a Blelloch scan (up-sweep and down-sweep) in shared memory. It starts from
output_positions[block]and obtains, for each thread, the output index it writes to. - Decoding and writing. Each thread decodes its window again and directly writes the fused BF16 (decoded exponent +
sign_mantissabyte) at its position.
Why this is a good starting point for this project:
- The metadata that makes decompression parallel (
gapsandoutput_positions) is exactly what the encoder computes serially. The information needed to parallelise exists; the compressor simply does not exploit it. - The technique in §5.3 (per-chunk histogram → bit length of each chunk → prefix sum → each chunk encodes from its own offset) is the kernel's trick in reverse: the kernel counts symbols per window and prefix-sums symbols; the encoder counts bits per chunk and prefix-sums bits.
- The CPU decoder used in safe mode can copy the kernel's structure (independent windows + prefix sum) and be just as parallel with
rayon. That way verification without a GPU is not serial either. - It also confirms that the official team already knew compression is parallelisable across UCs: their
compress_flux_parallel.shlaunches separate processes over block ranges withtaskset. But each process loads the whole model (the README warns that it needs a lot of memory), so parallelising multiplies the RAM instead of dividing it. That is exactly the problem this project solves.
With save_single_file=False, compress_model does the following:
- Before processing each UC, it detaches it from the parent model (
setattr(parent, child, None)). - Saves each UC in its own shard:
<name with dots replaced by _>.safetensors(e.g.transformer_blocks_0.safetensors) with keystransformer_blocks.0.luts, etc. - At the end it calls
model.save_pretrained(save_path). Since the UCs no longer hang off the model, that saves only the remaining parameters (input embeddings, final norms, etc.) intodiffusion_pytorch_model.safetensors(diffusers shards above 10 GB by default) and writesconfig.json. - It injects
dfloat11_config(version,threads_per_block,bytes_per_thread,pattern_dict) intomodel.configbefore saving. Ifconfig.jsondoes not contain it, it rewrites the file with only that key.
How it is loaded (DFloat11Model.from_pretrained + load_and_replace_tensors):
- Reads
config.jsonand requiresdfloat11_config. - Loads all the
.safetensorsfiles in the directory, in any order, and dispatches each tensor by name. The file name is irrelevant to the loader (useful for us). - In diffusers, the BF16 model is supplied by the pipeline (
bfloat16_model=pipe.transformer).
Differences from ComfyUI-native that affect the design:
- Different names and grouping. In Flux, ComfyUI has
double_blocks.Nwith a fusedimg_attn.qkv; diffusers hastransformer_blocks.Nwith separateto_q,to_k,to_v(14 linears per double block versus 10). The weights are the same, but the concatenation order changes, so the bitstream is not interchangeable between layouts. They are two distinct outputs from two distinct sources. - There are more transformations. The Extended node converts diffusers → ComfyUI on the fly (
convert_fixed_tensors.py), including aswap_scale_shifton some norms. That does not touch the compressed UCs, but it shows that uncompressed tensors can require per-architecture transformations. config.jsonmust come out the same assave_pretrained's: the transformer's original configuration plus the fields diffusers adds (_class_name,_diffusers_version, etc.) anddfloat11_config. We do not instantiate the model, so it has to be rebuilt from the sourceconfig.json(H10).
Existing ways of consuming a diffusers DF11 model that must be supported: a
directory with one shard per UC (the default output) and a single file with
from_single_file plus an explicit pattern_dict.
Verified by reading pattern_dict.py from ComfyUI-DFloat11-Extended and
diffusers/models/transformers/transformer_chroma.py (diffusers wheel 0.40.0).
Constructor (compared to Flux): patch_size=1, in_channels=64,
num_layers=19, num_single_layers=38, attention_head_dim=128,
num_attention_heads=24, joint_attention_dim=4096, axes_dims_rope=(16,56,56)
all match. It adds approximator_num_channels=64,
approximator_hidden_dim=5120, approximator_layers=5. It removes
pooled_projection_dim and guidance_embeds. ~8.9B parameters, based on
FLUX.1-schnell.
Why the modulations disappear. The classes ChromaAdaLayerNormZeroPruned and
ChromaAdaLayerNormZeroSinglePruned contain no nn.Linear at all: they are a
LayerNorm with no affine parameters that receives the modulation already
computed from outside (emb=temb). It is produced by ChromaApproximator, a
shared network. Hence the parameter-count difference against Flux 12B.
ComfyUI-native layout (the "Chroma" entry already present in Extended; this
concatenation order must be replicated exactly):
distilled_guidance_layer\.layers\.\d+→in_layer,out_layerdouble_blocks\.\d+→img_attn.qkv,img_attn.proj,img_mlp.0,img_mlp.2,txt_attn.qkv,txt_attn.proj,txt_mlp.0,txt_mlp.2(8)single_blocks\.\d+→linear1,linear2(2)
Corrections to the earlier estimate: the native single blocks differ by one
element. Chroma's have 2 (linear1, linear2); Flux's have 3, because Extended's
Flux definition keeps modulation.lin inside the unit
(data/architectures/flux-comfyui.toml), while Chroma has no per-block modulation
(it comes from the approximator). And the approximator is not a single UC: it
is split into one UC per layer (5 UCs of 2 linears each), a much finer
granularity that is favourable to the RAM budget.
Real structure of ChromaApproximator: in_proj (Linear), layers
(5 × PixArtAlphaTextProjection, each with in_layer and out_layer), norms
(5 × RMSNorm), out_proj (Linear). In the diffusers layout in_proj and
out_proj are compressed along with the layers linears, in one unit — see
below and FINDINGS §0.9. Only the RMSNorms stay uncompressed. The ComfyUI-native
pattern in Extended makes a different choice, and it is now verified against a
published native release (mingyi456/Chroma1-HD-DF11-ComfyUI, in
phase0/fixtures/real_headers/chroma-comfyui.json): 5 units of in_layer,
out_layer, with in_proj and out_proj left uncompressed.
Diffusers layout — [CONFIRMED by measurement, 0.9]. Taken from the
dfloat11_config.pattern_dict inside the published DFloat11/Chroma-DF11 and
DFloat11/FLUX.1-dev-DF11 releases, i.e. the configuration the official
compressor actually ran with. The orders below are load-bearing: they fix the
concatenation, hence split_positions, hence every compressed byte. An earlier
draft of this section derived them from the class definitions and got the right
set with the wrong order in both patterns — see FINDINGS §0.9.
transformer_blocks\.\d+→ 12, in this order:attn.to_q,attn.to_k,attn.to_v,attn.add_k_proj,attn.add_v_proj,attn.add_q_proj(k, v, q — not q, k, v),attn.to_out.0,attn.to_add_out,ff.net.0.proj,ff.net.2,ff_context.net.0.proj,ff_context.net.2.single_transformer_blocks\.\d+→ 5, in this order:proj_mlp,proj_out,attn.to_q,attn.to_k,attn.to_v— the projections come first. There is noattn.to_out.0and there are noadd_*, because the single block buildsFluxAttentionwithpre_only=Trueand withoutadded_kv_proj_dim.distilled_guidance_layer→ one unit of 12, with no layer index:in_proj,layers.0.linear_1,layers.0.linear_2, …layers.4.linear_1,layers.4.linear_2,out_proj.
For reference, Flux diffusers is the same shape plus the modulation linears:
transformer_blocks\.\d+ → 14 (norm1.linear, norm1_context.linear, then the
12 above) and single_transformer_blocks\.\d+ → 6 (norm.linear, then the 5
above).
H13 is refuted for the diffusers layout [0.9]; it holds for ComfyUI-native
(above). This section previously claimed the approximator splits
into five units of two and that in_proj, out_proj and the RMSNorms stay
uncompressed. In fact in_proj and out_proj are compressed, inside a single
unit, and the sub-modules are named linear_1/linear_2 rather than
in_layer/out_layer. Only the RMSNorms are genuinely left uncompressed. The
structural conclusion — Chroma is Flux minus the modulations, plus the
approximator — survives; the granularity and the naming did not.
ChromaRadiance. Extended additionally defines this variant: identical to
Chroma plus nerf_blocks\.\d+ → param_generator (a single element). Adding it
is almost free, but it forces support for single-tensor UCs, an edge case the
encoder and the golden tests must cover explicitly.
Nothing gets implemented until this table is closed. Every hypothesis has a refutation criterion.
| ID | Hypothesis | How it is measured | Refuted if… |
|---|---|---|---|
| H1 | Official peak RAM ≈ 2× the model, decomposing into loading + per-UC temporaries + serialisation | /usr/bin/time -v, tracemalloc and RSS sampling per stage, on a small model and (if a machine is available) on Flux |
the peak does not scale with model size |
| H2 | >90% of the time is in the Python encode loop |
py-spy on the official compressor |
another stage dominates |
| H3 | UCs are independent | already confirmed in source | — |
| H4 | We can produce tensors byte-identical to the official ones by replicating dahuffman |
golden tests (§8) | any difference in any tensor |
| H5 | With a native encoder the bottleneck becomes the disk, not the CPU | isolated encode throughput vs sequential disk read | the CPU still limits below HDD speeds |
| H6 | The loaders (official DF11, Extended node) do not depend on the physical order of tensors inside the safetensors file | load a reordered file and compare inference output | some loader fails or differs |
| H7 | The decode.ptx kernel can be loaded without CuPy (CUDA driver API from a native binary) |
minimal load-and-run test | it only works via CuPy |
| H8 | The uncompressed tensors in the official file are identical to those in the source safetensors | compare tensor by tensor | ComfyUI adds buffers, casts dtypes or renames on instantiation |
| H9 | In diffusers, the official diffusion_pytorch_model.safetensors contains exactly the source tensors that belong to no UC, unchanged |
compare tensor by tensor | tensors are missing, extra or different |
| H10 | The output config.json can be rebuilt without diffusers: source config + save_pretrained fields + dfloat11_config |
diff against the official one | fields appear that depend on the installed diffusers version |
| H11 | Splitting into shards (one per UC) is not mandatory for the loader; a single shard or arbitrary groupings also load | load variants and compare inference | the loader depends on file names |
| H12 | A CPU decoder with an independent-window structure scales nearly linearly with cores | measure on 1, 2, 4, 8… cores | it saturates on memory bandwidth very early |
| H13 | Chroma's approximator (in_proj, out_proj, RMSNorms) stays uncompressed and the official loader expects it that way |
compress Chroma with the official tool and compare its list of compressed tensors against §1.7 | Extended's pattern differs from what the official loader assumes |
H8 is critical: the official tool works on the instantiated model, we work on the file. If ComfyUI or diffusers alter anything on instantiation, we will have to replicate it explicitly per architecture and per layout. H8 and H9 are the same question in the two ecosystems.
Goals
- Output compatible with the existing loaders, in both layouts: ComfyUI-native (original node and Extended) and diffusers (
DFloat11Model.from_pretrainedandfrom_single_file). Strong criterion: tensors byte-identical to the official compressor's for the same input, and an equivalentconfig.json. - RAM bounded by a budget the user sets, independent of model size.
- Parallelism that adapts to the machine (cores, RAM, disk type).
- Resume after interruption without losing more than one UC of work.
- Two modes: fast and safe. Plus independent post-hoc verification.
- Self-contained binary, no Python environment on the main path.
Non-goals (v1)
- New codecs or ratio improvements. The format is DF11 as it stands.
- Inference. Compression and verification only.
- dtype conversion: the input must be BF16.
- Conversion between layouts (diffusers ↔ ComfyUI). Each output is generated from a source in its own layout. This stays a possible extension, since the Extended node shows the conversion is feasible.
Single criterion: efficiency (time, RAM, energy) and absence of blockers. Familiarity does not count.
| Task | Character | Recommendation | Reason |
|---|---|---|---|
| safetensors reading (JSON header + data) | I/O, pread/mmap |
Rust | The safetensors crate is the format's reference implementation; memmap2 and direct pread. |
| Field separation (exponent / sign+mantissa) | Pure SIMD | Rust | Autovectorises well; C would be equivalent in speed. |
| Exponent histogram | SIMD, trivially parallel | Rust (rayon) | 256 counters, no contention. |
Huffman construction compatible with dahuffman + 32-bit limit + LUT |
~257 symbols, negligible cost | Rust | The cost is irrelevant; what matters is replicating the exact tie-breaking. |
| Bitstream encoder + gaps + output_positions | The hot path | Rust, CPU | See §5.3. A branchless 64-bit bit writer should be in the low single-digit ns per symbol. |
| safetensors writer + journal | Sequential I/O, fsync, rename |
Rust | Fine control over writes and atomicity. |
| Reference CPU decoder (safe mode without GPU) | Exact replica of the kernel's semantics, with its same parallel structure (§1.5) | Rust (rayon) | Implementation independent of the encoder, so a bug cannot "self-confirm". Independent 8-byte windows + prefix sum = parallel across cores (H12). |
diffusers config.json |
JSON, once per model | Rust (serde_json) |
Trivial; the hard part is knowing which fields to put in (H10), not writing them. |
| Verification with the official GPU kernel | Calling decode.ptx |
Rust if H7 is confirmed; otherwise an optional Python + CuPy shim | The real kernel is the ground truth of the format; it is best to use exactly it. |
Architecture definitions (pattern_dict, key prefixes) |
Data | Versioned TOML/JSON | Derived from Extended's pattern_dict.py; this way adding models does not require recompiling. |
Why not C, C++ or Zig? On performance, Rust and C are equivalent for this
problem. Rust wins because of the reference safetensors crate, because of rayon
for data parallelism without writing a scheduler, because of shipping a single
static binary (key for old machines with no Python environment), and because of
memory safety in bit-manipulation code, where silent errors are the main risk.
Why not GPU for encoding? [HYPOTHESIS H5] If the native encoder exceeds disk read speed, the GPU accelerates nothing, adds PCIe transfers, high idle power draw, and a dependency that excludes anyone without NVIDIA. The GPU is reserved for verification. If Phase 0 refutes H5, this reopens.
source safetensors (HDD/SSD)
│ reader (1 thread on HDD, N on SSD) — in physical offset order
▼
bounded UC queue (bounds RAM)
▼
encoding workers (rayon, N per budget)
│ histogram → Huffman → LUT → encode → gaps/positions
│ [safe mode] decode and compare before handing off
▼
in-order committer (1 thread)
│ writes UCs in order, fsync, updates journal
▼
destination DF11 safetensors + journal
For a UC with N weights, a worker needs roughly:
sign_mantissa: N bytesencoded_exponent: ~0.34 N bytes (≈ 2.7 bits per exponent)- gaps, positions, LUT: negligible
That is about 1.35 N bytes. On Flux's largest UC (~340M weights) that is ~460 MB [HYPOTHESIS, calculation]. Source data is read through the page cache, which the system frees under pressure and which does not count as our own memory.
- Workers =
min(cores, RAM_budget / (1.35 × N_max)). - With a 4 GB budget: ~6–8 workers. With 1 GB: 2. With 512 MB: 1 (still works).
- In safe mode with the CPU decoder, add ~2 N bytes for the decoded output, unless the comparison is done chunk by chunk without materialising it (preferable, see §7).
Key idea: the per-chunk histogram gives the bit offsets for free.
- Split the UC into chunks of M symbols (e.g. 1–4M).
- Pass 1 (parallel): histogram of each chunk. Their sum gives the UC's codebook. With the codebook, each chunk's bit count is
Σ hist[s] × len[s]. The cumulative sum gives the exact bit offset where each chunk starts. - Pass 2 (parallel): each chunk encodes into its own region with a 64-bit accumulator. Boundary bytes between chunks are combined with OR.
gapsandoutput_positionsare derived in each chunk's same pass, because every chunk knows its global offset.- Tail: EOF and padding, identical to the official tool.
No per-symbol offset array is ever materialised (that would be 8 B/weight).
- HDD: a single sequential reader in physical offset order. Several concurrent readers cause head seeks and destroy throughput. Automatic detection (
/sys/block/*/queue/rotationalon Linux, a latency heuristic on other systems) with a manual override. - Source and destination on the same HDD: warn. Use a large write buffer to alternate in bursts and reduce seeks.
- SSD/NVMe: concurrent readers.
- Disk space: source + destination only (~24 GB + ~16 GB on Flux). No intermediate copy space is needed (see §6).
Both layouts share the whole pipeline (reader, workers, committer, journal). Only
three pluggable pieces change: the architecture definition (key prefixes and
pattern_dict), the writer, and the config generation.
ComfyUI-native
- Source: a single safetensors with ComfyUI keys (
double_blocks.N...), possibly with a prefix (model.diffusion_model.) that is detected and stripped per architecture. - Output: a single safetensors with UCs + uncompressed tensors. No
config.json(the Extended node discards it).
Diffusers
- Source: the
transformer/directory of a diffusers repo (config.json+ one or morediffusion_pytorch_model-*.safetensorsshards with theirindex.json). The reader walks the shards following the index, so a sharded source changes nothing in the pipeline. - Output, two variants:
- Directory (default, like the official tool): one shard per UC with the same name the official tool generates,
diffusion_pytorch_model.safetensorswith the uncompressed tensors, andconfig.jsonwithdfloat11_config. - Single file, for
from_single_file.
- Directory (default, like the official tool): one shard per UC with the same name the official tool generates,
- Advantage of the directory variant for this project: one shard per UC is also a natural checkpoint, because each file is written once and closed.
Common
- The file is written sequentially with the header reserved up front (the safetensors format allows padding the header with spaces) and filled in at the end. Tensors end up contiguous, with no holes.
- The physical order of tensors may differ from the official one (official safetensors sorts by dtype alignment and name). The compatibility criterion is per tensor, not per file, pending H6.
The original proposal was "checkpoint every n blocks depending on the machine". With the architecture above, the natural checkpoint is every UC:
- A UC is the idempotent unit: the same input produces the same bytes.
- Each UC is hundreds of MB of output, so the cost of an
fsync+ journal update is negligible against its work. - The most that is lost in a crash is the workers' in-flight work (≤ N UCs in parallel).
The "n" parameter still exists, but only as an fsync policy (useful on very slow disks or SD cards), not as recovery granularity.
- Diffusers as a directory: no ordering needed. Each UC is written to
<name>.safetensors.tmp,fsync, then an atomicrename. The journal only records hashes. Resuming = skip the complete and valid shards. The uncompressed tensors andconfig.jsonare written at the end, also atomically. - Single file (both layouts): workers finish UCs in any order; the committer writes only in order. UCs finished out of order wait in memory (bounded by the budget) or, if they do not fit, in a temporary spill file.
- Journal (separate file, append-only), one entry per committed UC:
- UC identifier
- offset and length written in the destination
- hash of the source tensors (xxh3) and of the output
- names, dtypes and shapes of the emitted tensors
- mode (fast/safe) and verification result
- Job fingerprint at the head of the journal: hash of the source header, file size, df11pack version and DF11 format version,
pattern_dictused.
- Read the journal and validate the fingerprint. If the source or the configuration changed, refuse to resume.
- Validate the last entry (hash of the bytes on disk). A corrupt or incomplete entry is discarded.
- Truncate the destination to the end of the last valid UC.
- Continue from the next UC.
Compresses only. Even so it performs cheap checks that should never be disabled: out-of-range exponents (§1.3), maximum code length, number of LUTs, symbol counts, and hashes in the journal.
Before committing each UC, it is decoded and compared bit for bit against the source.
- With an NVIDIA GPU: the official
decodekernel (what the real loader will use). VRAM needed ≈ the UC's compressed size + 2 N bytes, about 1.2 GB on Flux's largest UC. - Without a GPU: an independent CPU decoder that replicates the kernel's semantics, including gaps and output_positions (not just sequential decoding, because those indices are exactly what an encoder bug can break). It uses the kernel's same structure: each 8-byte window starts from its
gapand is decoded in parallel. If a window starts wrong, its symbols do not match the prefix sum and the error shows up localised. - The comparison is done per kernel chunk against the source as read, without materialising the whole decoded UC.
Over the already-generated file and the source, three levels:
| Level | What it detects | Cost |
|---|---|---|
| Integrity (journal hashes) | On-disk corruption, incomplete writes | Read the destination, no decoding |
| Semantic sampling | Systematic encoder errors | Seconds |
| Full sweep | Everything | One complete decoding pass |
On the 1k random samples. The minimum useful unit of random access is the
kernel chunk (~12k weights): output_positions says where it starts in
symbols and gaps where it starts in bits, so a single chunk can be decoded and
compared against the equivalent stretch of the source (accessed with pread).
1000 chunks ≈ 12M weights.
But we have to be clear about what that detects:
- A systematic error (a bug affecting a fraction f of the chunks) is detected with probability 1 − (1 − f)^1000. At f = 0.5% that is already above 99%.
- An isolated error (one corrupt chunk out of ~1M) is detected with probability ~0.1%. That is what the integrity level is for, not sampling.
Hence stratified sampling is proposed instead of uniform:
- At least one chunk per UC (Flux: 57).
- First and last chunk of each UC, and the chunks containing each
split_positionsboundary, which is where boundary errors concentrate. - The rest uniform, up to 1000.
- Seed recorded in the report, so the verification can be reproduced exactly.
Minimum corpus:
- Qwen3-0.6B (a small real LLM; an official example
pattern_dictexists). - Reduced synthetic Flux, in both layouts (same name structure, few layers, small hidden size, weights ~ normal) to validate both
pattern_dicts,split_positions,config.jsonand the shard split without needing 48 GB. In diffusers it is generated by instantiatingFluxTransformer2DModelwith a reduced config; in ComfyUI, with its equivalent class. - Sharded diffusers source: the same synthetic model saved across several shards with
index.json, to validate the multi-file reader. - Adversarial synthetic distributions:
- codes that force 2, 3 and 4 LUT levels
- a histogram that triggers the 32-bit limit path
- a UC with a single distinct exponent
- a UC whose weight count is not a multiple of anything convenient
- exponents 240–255 (must abort)
- single-tensor UC (ChromaRadiance's
param_generatorcase, §1.7) - very small UC: Chroma's 5 approximator UCs are orders of magnitude smaller than a double block; verify that chunk distribution and the prefix sum behave when there is less work than threads
- Real Flux and Chroma models in both layouts. For Chroma both sources exist:
lodestones/Chroma1-HD(single file) andimnotednamode/Chroma-v36-dc-diffusers(subfolder="transformer"), so the diffusers layout is no longer a blocker on a machine with enough RAM to run the official tool as a final proof.
Criterion: for every case, all tensors in df11pack's result must be byte-identical
to the official compressor's, and the official loader must produce identical
inference output, both in ComfyUI (Extended node) and in diffusers
(DFloat11Model).
Delicate case: the 32-bit limit path uses np.argpartition, whose ordering
among ties depends on the implementation. If it cannot be replicated exactly, a
different but valid result is accepted only in that case, documented and
verified with the kernel. (Superseded in Phase 1: df11pack refuses such a unit
with AmbiguousLimiterTie instead of emitting a different result; FINDINGS 0.5,
erratum.)
Always measured with the same protocol (warm-up, median of several runs, environment recorded):
- Total time and time per stage (reading, encode, verification, writing).
- Peak RSS.
- Energy: RAPL (
/sys/class/powercap) on Intel/AMD CPUs;nvidia-smifor the GPU in safe mode. - Isolated encode throughput (symbols/s per core), to confirm or refute H5.
- Reference machines: a modern one, an old one (e.g. 4 cores, 8 GB, HDD) and one without a GPU.
Diffusers is in from Phase 1: the per-layout pieces (architecture definition, writer, config) are designed from the start, not bolted on afterwards.
| Phase | Deliverable | Exit gate |
|---|---|---|
| 0 | Measurements of the official compressor in both layouts; results for H1–H13 | Table §2 closed |
| 1 | Native single-threaded in-memory encoder over one UC; Flux and Chroma architecture definitions for ComfyUI and diffusers | Byte-for-byte identity on corpus §8 (cases 1, 2 and 4) |
| 2 | Readers (single file, and shards with index), writers for both layouts, config.json |
The official loaders load our output; H9–H11 confirmed |
| 3 | Streaming, intra- and inter-UC parallelism, RAM budget, I/O scheduler | Bounded RAM measured; throughput limited by disk (H5) |
| 4 | Journal, ordered committer, atomic shards, resume | Kill the process at random points × 100 with no differences in the output |
| 5 | Safe mode: parallel CPU decoder and GPU kernel | Detects deliberately injected errors in bitstream, gaps and positions; H12 measured |
| 6 | Post-hoc verification: integrity, stratified sampling, full sweep | Detects corruption injected on disk |
| 7 | The remaining architectures from Extended's pattern_dict.py and the official examples (Wan2.1, LLMs) |
Golden tests per architecture and layout |
v1 architectures and layoutsClosed: Flux and Chroma, both layouts from the start.- [DECISION] Python dependency for GPU verification, if H7 is not confirmed: an optional shim, or CPU decoder only?
- [DECISION] Licence. The official code is Apache-2.0; replicating its format and its construction algorithm is compatible with any permissive licence, but if
decode.ptxis redistributed its licence and attribution must be respected. Decided (2026-09-23): MIT.decode.ptxis not redistributed. - [DECISION] ComfyUI integration: CLI binary only, or also a thin node that invokes it?
- [DECISION] Name (
df11packis provisional). Chroma in diffusersClosed:ChromaTransformer2DModelis stable (diffusers ≥0.34) and both layout definitions are derived in §1.7. It stays in Phase 1.- [DECISION] Does ChromaRadiance ship in v1? The cost is one more definition, but it drags in the single-tensor UC edge case (§1.7).