Repository navigation
ple: read the PLE table at full BF16 precision (opt-in, alongside FP8) - #586
Closed
constantindjonkam wants to merge 1 commit into
Closed
constantindjonkam wants to merge 1 commit into
constantindjonkam wants to merge 1 commit into
Conversation
The table reaches the engine as IQ4_NL (ISTA-DASLab's shard 2), which is
about 8% off per row against the checkpoint's own values. tools/ple_table_pack
already recovers Qwen's F8 E4M3 release exactly; the full checkpoint stores the
same shards as BF16, which is the source of record and 102.4 GB rather than
51.2 GB.
So accept it rather than re-quantize it. A bfloat16 IS the top half of a
float32, so widening is a shift: exact for every value, no rounding and no
scale, where casting BF16 to FP8 would carry a 3-bit mantissa and land within a
few percent of the IQ4_NL table this is meant to replace.
- ngram: PLE_ROW_BYTES_BF16 (320) and bf16_dequant_row, widening little-endian
bytes explicitly so the result does not depend on host endianness.
- PleTable::open: a BF16 tensor sets the row width; it needs no scale and
trusts no metadata, because the values carry themselves.
- The direct reader takes the table's own row size, so BF16 reads unbuffered
like IQ4_NL and FP8 instead of falling back to the mapped path Q5_0 needs.
The comment claiming fixed 90-byte rows was stale from before FP8.
- tools/ple_fp8_pack.py -> tools/ple_table_pack.py: reads whichever dtype the
checkpoint holds and copies the bytes through. FP8 output is unchanged, byte
for byte; nothing imports or tests the old path, so the rename is free.
- ple_reader_test: the widening is exact over the patterns a lossy path breaks
on (subnormals, +-inf, both NaNs), and a synthetic BF16 GGUF reads back
identically through Direct and Mmap. Both were mutation-checked - widening by
>> instead of <<, and a 160 B row width, both fail the selftest.
Host build and ple_reader_selftest pass. No GPU, model or engine run: this says
the reader widens BF16 exactly, not that a 102 GB table serves prompts well.
(cherry picked from commit 33a2775a6a9ff912d90054bd6e40b878dbe5bc05)
(cherry picked from commit 4ae5478)
Niko1221
pushed a commit
that referenced
this pull request
Oct 6, 2026
The full checkpoint stores the table as BF16 (320-byte rows, 102.4 GB), the source of record; the engine gets it as IQ4_NL (about 8% off per row) or FP8. A bfloat16 is the top half of a float32, so widening is a shift: exact for every value, no rounding and no scale. One entry in the PLE format list (type BF16, self-describing: no scale, no metadata) and bf16_dequant_row; the direct reader takes the 320-byte row like any other width, and PLE_ROW_BYTES_MAX is 320. ple_reader_test checks the widening is exact over the patterns a lossy path breaks on (subnormals, +-inf, both NaNs); the generic every-format test covers a synthetic BF16 GGUF through both readers. Not in this commit: the PR's rename of tools/ple_fp8_pack.py (nothing needs it) and any setup option. Whether a table format other than IQ4_NL is worth offering is a quality question (see the measurement in the 0.1.40 notes). Ported by the maintainer from constantindjonkam's PR #586. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Owner
|
Fixed in 0.1.40 (reader only): a BF16 PLE table is read exactly, as one entry in the PLE format table, with your widening test. The tools/ple_fp8_pack.py rename is not taken, and setup does not offer a BF16 table. On the Q2_0 model the table's precision is hard to see (teacher-forced, 700 tokens, KL against the BF16 table 0.0117-0.0146 for Q5_0, Q5_1, Q8_0, FP8, Q4_0 and IQ4_NL, perplexity within 1%), while BF16 is 102 GB. |
Niko1221
pushed a commit
that referenced
this pull request
Oct 6, 2026
The full checkpoint stores the table as BF16 (320-byte rows, 102.4 GB), the source of record; the engine gets it as IQ4_NL (about 8% off per row) or FP8. A bfloat16 is the top half of a float32, so widening is a shift: exact for every value, no rounding and no scale. One entry in the PLE format list (type BF16, self-describing: no scale, no metadata) and bf16_dequant_row; the direct reader takes the 320-byte row like any other width, and PLE_ROW_BYTES_MAX is 320. ple_reader_test checks the widening is exact over the patterns a lossy path breaks on (subnormals, +-inf, both NaNs); the generic every-format test covers a synthetic BF16 GGUF through both readers. Not in this commit: the PR's rename of tools/ple_fp8_pack.py (nothing needs it) and any setup option. Whether a table format other than IQ4_NL is worth offering is a quality question (see the measurement in the 0.1.40 notes). Ported by the maintainer from constantindjonkam's PR #586.
anon761
pushed a commit
to anon761/Strata-upstream
that referenced
this pull request
Oct 6, 2026
The full checkpoint stores the table as BF16 (320-byte rows, 102.4 GB), the source of record; the engine gets it as IQ4_NL (about 8% off per row) or FP8. A bfloat16 is the top half of a float32, so widening is a shift: exact for every value, no rounding and no scale. One entry in the PLE format list (type BF16, self-describing: no scale, no metadata) and bf16_dequant_row; the direct reader takes the 320-byte row like any other width, and PLE_ROW_BYTES_MAX is 320. ple_reader_test checks the widening is exact over the patterns a lossy path breaks on (subnormals, +-inf, both NaNs); the generic every-format test covers a synthetic BF16 GGUF through both readers. Not in this commit: the PR's rename of tools/ple_fp8_pack.py (nothing needs it) and any setup option. Whether a table format other than IQ4_NL is worth offering is a quality question (see the measurement in the 0.1.40 notes). Ported by the maintainer from constantindjonkam's PR Niko1221#586.
sskver
pushed a commit
to sskver/Strata
that referenced
this pull request
Oct 7, 2026
The full checkpoint stores the table as BF16 (320-byte rows, 102.4 GB), the source of record; the engine gets it as IQ4_NL (about 8% off per row) or FP8. A bfloat16 is the top half of a float32, so widening is a shift: exact for every value, no rounding and no scale. One entry in the PLE format list (type BF16, self-describing: no scale, no metadata) and bf16_dequant_row; the direct reader takes the 320-byte row like any other width, and PLE_ROW_BYTES_MAX is 320. ple_reader_test checks the widening is exact over the patterns a lossy path breaks on (subnormals, +-inf, both NaNs); the generic every-format test covers a synthetic BF16 GGUF through both readers. Not in this commit: the PR's rename of tools/ple_fp8_pack.py (nothing needs it) and any setup option. Whether a table format other than IQ4_NL is worth offering is a quality question (see the measurement in the 0.1.40 notes). Ported by the maintainer from constantindjonkam's PR Niko1221#586.
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.
What
Add a BF16 PLE table as an opt-in alternative to the FP8 one. This is an addition, not a
replacement: FP8 stays the default path, nothing about its behaviour changes, and the existing
tools/ple_fp8_pack.pyentry point keeps working.A bfloat16 is the top half of a float32, so widening one is a shift — exact for every value, normal or
not. The checkpoint ships the table in F8_E4M3 (2.66% off per row) and in BF16 (the source of record), so
this reads the source instead of an approximation of it. Disk is the price: 102 GB against 51 GB.
What it does not do
--ple-ggufis unchanged; a table that is not a BF16 one takesexactly the path it took before. The only edit outside the reader and the packer is a comment.
ple_fp8_pack.py. That file is kept as a shim over the renamedple_table_pack.py, because it is the only way to get a non-IQ4_NL table and command lines alreadyexist against it.
The measurement the previous review asked for
I built the FP8 table from
Qwen/Qwen3.8-Flash-Next-FP8with the tool inmain— 33 shards, 52.3 GB ofsafetensors, packed to 51.2 GB
f8_e4m3— and compared it against BF16 and against the IQ4_NL default.Teacher-forcing, decided from the dumps rather than from the text. Window row 0 is
P(next token | everything before pos), so two arms' rows at the same position are comparable only whileboth have emitted the same tokens. That is decidable without a reference transcript: walk the windows in
order and stop at the first position where the greedy argmax differs. Every earlier row provably saw an
identical context.
An earlier version of this harness tried to align on the generated text and was wrong in an instructive
way: the engine writes timing lines to stdout too, so it compared log lines, called a bit-identical
control arm divergent, and compared post-divergence rows at equal positions — producing a KL of 19, which
reports that two contexts differ, not that two tables are 19 nats apart.
Control first
8 prompts, 585 to 37K tokens,
--pcie-frac 0 --adapt-swaps 0so residency is fixed and the table is theonly variable:
The control is exactly zero at the median and the 95th percentile, with a single window out of 1701 above
it. So the BF16 row is a real difference — its median is about twice the control's worst window — and so
is IQ4_NL's, at roughly twice BF16's.
The number that matters for a table choice
Verify windows of identical greedy output before the first disagreement, and how many of the 8 prompts
produced byte-identical output:
FP8 and BF16 are not interchangeable. They disagree within about four verify windows — roughly 16
generated tokens — on every prompt tried. This is not a tail-logits curiosity; the table choice changes
the text.
And the practical result for a stock install: IQ4_NL is about twice as far from FP8 as BF16 is, in both
median and p95. Going IQ4_NL → FP8 recovers more than going FP8 → BF16 does.
What this does not show, stated plainly
Different is not better. This measures how far apart the tables are, not which answers are better:
BF16 is the checkpoint's exact values, so it is "more correct" only by construction, and nothing here is a
quality benchmark. A perplexity number against a labelled set would answer the actual question and is not
included. On the previous review's evidence — BF16-vs-FP8 median KL 0.00087 against a 0.00080 noise floor,
for twice the disk — FP8 remains the sensible default, and I am not claiming otherwise.
Two limits on the numbers themselves: the teacher-forced sample is small (30 and 53 windows) precisely
because the arms diverge early, so the p95 rests on few points; and one control window of 1701 differs
(0.00577), so the floor is not a hard zero.
Cost
None measurable. Same sweep, same card, 256 output tokens per cell, cold — IQ4_NL 90 B rows against BF16
320 B rows:
Within ±2.3% over a 25x range with the sign flipping. 16 rows/token is dwarfed by streaming 47 GB of
experts over PCIe.
The reader
BF16 rows are 320 B rather than 160, and widening assembles the little-endian bytes explicitly rather than
reinterpreting memory, so it does not depend on host endianness. No scale is read: the values carry
themselves. The direct reader already takes the table's own row size, so BF16 reads unbuffered like FP8
instead of falling back to the mapped path.
Tested in
ple_reader_test --selftestfor the patterns a lossy path breaks on — subnormals, ±inf, both NaNencodings — plus a synthetic BF16 GGUF round-tripping identically through Direct and Mmap. Both claims
were mutation-checked after the 0.1.38 rebase: widening by
>>instead of<<fails on the firstsubnormal, and a 160 B row width fails the round-trip.
Which to use
tools/ple_fp8_pack.py(orple_table_pack.py).approximation of them, and has the disk.
disk allows.
The table streams (
--ple-io direct, never resident; the row cache is ~90 MB at the default), so this isdisk and not RAM — but 51 GB or 102 GB has to exist.