LLM Model Conversion Correctness
November 01, 2026
Overview
Converting a large language model from one format to another is not simply a matter of copying weights into a new file.
A converter may need to translate tensor names, reorganize matrices, change numerical precision, preserve architectural metadata, represent attention mechanisms correctly, and produce a checkpoint that a different inference engine can execute.
This raises a deceptively difficult question:
How do we know that a converted model is actually correct?
A successful conversion command is insufficient. A file that loads successfully may still contain incorrect weights. Correct tensor names and dimensions do not guarantee correct numerical values. Even an embedding that closely matches the original does not prove that every layer, input, or behavior is correct.
The central principle is:
Model conversion correctness is not a single property established by one passing test. It is a collection of independently testable properties, spanning serialized bits, scalar weights, tensors, architecture, numerical computation, and observable behavior.
We will examine these properties through a recurring example: converting a Qwen3.5 embedding model from Hugging Face into GGUF for inference with llama.cpp.
The example is grounded in llama.cpp pull request #27920, which introduced support for Qwen3.5 embedding models using the Hugging Face Qwen3_5TextModel architecture. The pull request was merged on October 8, 2026.
We will also explore a particularly useful thought experiment: what happens if the same model is converted between FP32 and FP16 one hundred million times?
The Model Conversion Pipeline
A typical conversion pipeline looks like this:
Original model repository
|
|-- Configuration and architecture
|-- Tokenizer and vocabulary
|-- Model weights (often Safetensors)
|-- Embedding/pooling configuration
|
v
Model converter
|
|-- Read and interpret source tensors
|-- Map tensor names
|-- Reshape, permute or reorder values
|-- Convert numerical representations
|-- Write architectural metadata
|
v
Converted GGUF checkpoint
|
|-- Binary header and tensor directory
|-- Architecture and tokenizer metadata
|-- Serialized tensor values
|
v
llama.cpp inference runtime
|
|-- Reconstruct tensors
|-- Execute attention and feed-forward operations
|-- Apply pooling and normalization
|
v
Embeddings or logits
|
v
Observable behaviorA bug at any stage can produce a model that appears structurally valid while behaving incorrectly.
For example, imagine a GGUF file that:
- declares 24 transformer blocks
- contains the expected tensor names
- has the correct tensor dimensions
- loads successfully
- produces a 1,024-dimensional embedding
Those are useful observations. They do not establish that the scalar values inside every tensor match the source model.
A converter could have transposed a matrix incorrectly, mapped one projection into another, or silently changed the values of an entire layer.
We need validation at multiple levels.
The Different Meanings of Correctness
Before examining those levels, it helps to distinguish several kinds of correctness that engineers sometimes use interchangeably.
| Correctness Type | What It Means | Important Limitation |
|---|---|---|
| Structural consistency | Tensor names, shapes, types, layer counts and metadata agree with an expected representation | The underlying weight values or interpretation can still be wrong |
| Bitwise equivalence | Compared binary representations are exactly identical under an explicitly defined encoding | Identical bytes do not prove correct interpretation or intended behavior |
| Numerical fidelity | Weights, activations or outputs agree within justified numerical tolerances | Agreement on tested values does not establish correctness for every input |
| Behavioral equivalence | The converted model preserves intended behavior on the relevant tasks and input distribution | Passing a benchmark does not guarantee numerical identity or behavior on unseen cases |
| Specification correctness | The implementation satisfies an independently justified model specification | Requires confidence that the specification itself is authoritative and applicable |
These are related, but they are not interchangeable.
Two models can be behaviorally similar without being numerically identical. Two checkpoints can contain equivalent numbers without containing identical bytes. And two implementations can agree perfectly while sharing the same underlying architectural mistake.
Eight Complementary Levels of Verification
A useful validation framework consists of eight dimensions:
- Binary and serialization correctness — are the bytes written and interpreted correctly?
- Scalar weight correctness — are the numerical values preserved exactly or within expected precision loss?
- Tensor correctness — are names, shapes, layouts, mappings and values correct?
- Architectural correctness — does the runtime reconstruct the intended network?
- Intermediate computational correctness — do corresponding layers calculate equivalent results?
- Final numerical correctness — do embeddings or logits agree with a reference?
- Behavioral correctness — does the model preserve useful behavior?
- Reference or specification correctness — is the original implementation or configuration itself correct?
These dimensions are not a rigid mathematical hierarchy.
For example, a deliberately quantized model cannot preserve every original weight bit, yet it may retain excellent retrieval behavior. Conversely, a model might produce the expected output on several inputs despite an incorrect internal transformation.
The strongest validation combines complementary evidence rather than relying on a single result.
Case Study: Qwen3.5 Embedding Conversion
The Qwen3.5 embedding conversion PR is a useful concrete example.
It extended convert_hf_to_gguf.py support for the Hugging Face Qwen3_5TextModel architecture, including models such as Rebine/Qwen3.5-Embedding-0.8B.
The change addressed several model-specific details:
- registering the text-model architecture with the converter
- recognizing tensor names used by the standalone text-model checkpoint
- mapping linear-attention components into GGUF tensor conventions
- preventing inappropriate Multi-Token Prediction (MTP) block-count extension
- propagating last-token pooling configuration
- rejecting unsupported explicitly declared embedding pooling modes
The tensor mappings include components such as linear-attention QKV projections, gating projections, convolution weights and state-related projections. These are not merely generic transformer tensor names.
The model repository describes a 24-layer hybrid architecture, consisting of 18 linear-attention and 6 full-attention layers, with a hidden size of 1,024. These are properties of that particular example model, not universal properties of Qwen3.5 embedding models.
What the Published PR Validated
The pull request documents the following manual checks:
| Published Check | Evidence Provided | What It Does Not Establish |
|---|---|---|
| Real Hugging Face to GGUF conversion | The converter could process an actual supported model checkpoint | Every weight was numerically identical or correctly transformed |
| 24-block metadata | The expected block count was represented in the output | Every block contained the correct tensors and operations |
| LAST pooling and no MTP | Those architectural metadata decisions were represented as intended | Every pooling-related numerical result matched an independent reference |
| llama-embedding runtime load | The converted checkpoint was accepted by the runtime | Its outputs were numerically or behaviorally equivalent to the original |
| 1,024-dimensional embedding output | The runtime produced an output with the expected dimensionality | The embedding contained the correct numerical values |
| Unsupported pooling rejection | An explicitly unsupported configuration was rejected rather than silently accepted | Every supported configuration behaved correctly |
These are meaningful conversion and integration checks. However, the PR description does not publish a full scalar-weight comparison or an independently reproduced end-to-end numerical-equivalence report.
That distinction matters: we should credit the evidence actually provided without treating unreported tests as though they passed.
The following sections demonstrate how to extend such validation.
Level 1: Binary and Serialization Correctness
The lowest-level question is whether the checkpoint is a valid and correctly interpreted binary artifact.
Modern model formats contain more than floating-point values.
A GGUF file includes:
- a file identifier and format version
- metadata entries
- tensor names
- tensor dimensions
- tensor storage types
- offsets into the tensor-data region
- alignment and padding
- serialized tensor data
According to the GGUF specification, tensor data offsets are relative to the tensor-data region and must satisfy alignment requirements.
This means that even a converter with correct numerical weights can generate an unusable or incorrect checkpoint if the metadata describing where those weights live is wrong.
What Can Go Wrong?
Examples include:
- incorrect endianness
- wrong tensor offsets
- overlapping tensor regions
- truncated serialized data
- invalid alignment
- incorrect element widths
- unsupported type identifiers
- incorrectly calculated tensor sizes
- corrupted or incomplete file transfers
Suppose a 32-bit floating-point number is written using the correct four bytes, but the reader interprets those bytes in the wrong byte order.
The bytes may be intact, but the reconstructed number can be completely different.
Similarly, a tensor may have the correct data bytes but the wrong declared dimensions. The loader could then interpret the intended rows as columns.
Binary Integrity Is Not Numerical Fidelity
A cryptographic checksum verifies the identity of a byte sequence.
sha256sum model-f16.ggufThis is useful for detecting accidental modification or transfer corruption when compared against a trusted expected digest.
However:
A SHA-256 match does not prove that the original conversion was correct.
If a converter writes incorrect values and then calculates a checksum over that incorrect file, the checksum still verifies successfully.
For binary-level validation, useful checks include:
- parsing the file using an independent or well-tested reader
- verifying format version and magic bytes
- checking tensor offsets and alignment
- verifying each tensor fits within the file
- checking expected tensor data lengths
- comparing file hashes against trusted artifacts
These tests establish serialization integrity. They do not establish the intended meaning of the stored values.
Level 2: What Exists Below an Individual Weight?
Before comparing billions of weights, we should understand what one weight actually is.
Suppose a neural-network parameter is mathematically:
w = 0.1A real number is an abstract mathematical object. A computer must store a finite representation of it.
For ordinary floating-point formats, that representation contains:
Sign | Exponent | Fraction / Significand bitsFor normalized binary floating-point numbers, the represented value is approximately described by:
Here:
sis the sign bit.eis the stored exponent.biasadjusts the exponent.fis the fractional contribution encoded by the significand bits.
Subnormal values, zeros, infinities and NaNs use special encoding rules.
FP32 vs. FP16 vs. BF16
| Format | Total Bits | Exponent Bits | Explicit Fraction Bits | Main Trade-off |
|---|---|---|---|---|
| FP32 | 32 | 8 | 23 | Higher precision, larger storage footprint |
| FP16 | 16 | 5 | 10 | Smaller representation but limited numerical range |
| BF16 | 16 | 8 | 7 | Roughly FP32-like exponent range, but less significand precision than FP16 |
FP32 and FP16 are IEEE 754 binary floating-point formats. BF16 is a related 16-bit format designed to retain FP32's exponent range while reducing significand precision.
This distinction matters when converting model weights:
- FP16 offers more significand precision than BF16.
- BF16 supports a much wider magnitude range than FP16.
- FP32 provides greater precision than either.
- None of these formats represents every decimal number exactly.
Worked Example: Encoding 0.1
The decimal number 0.1 has no finite binary fractional representation.
In FP32, it is stored approximately as:
Hex: 0x3DCCCCCD
Binary: 0 | 01111011 | 10011001100110011001101
S Exponent Fraction
Value: 0.10000000149011612In FP16:
Hex: 0x2E66
Binary: 0 | 01011 | 1001100110
S Exp. Fraction
Value: 0.0999755859375The FP16 result is different because fewer significand bits are available.
By contrast, 1.5 is exactly representable in both formats:
FP32: 0x3FC00000
FP16: 0x3E00The important insight is that precision-changing conversion can introduce numerical differences even when the converter is perfectly correct.
A correct FP32-to-FP16 converter should usually preserve the appropriately rounded FP16 value, not the impossible requirement of retaining every FP32 bit.
Storing the Correct Bytes vs. Interpreting Them Correctly
The byte sequence itself is only one part of the representation.
For example, consider the bytes:
00 3CInterpreted as little-endian FP16, they represent 1.0.
Interpreted as a little-endian unsigned 16-bit integer, they represent 15360.
The bytes are identical. Their numerical interpretation is not.
The same principle applies to tensor layouts:
Correct scalar values + wrong matrix ordering = incorrect computationThis is why binary integrity and tensor correctness must be tested separately.
Quantized Weights: More Than Reduced Precision
FP16 and BF16 are floating-point formats. Low-bit model quantization can use a different representation.
A simplified uniform quantizer might store:
and reconstruct:
where:
wis the original weightsis a scaleqis an integer codeŵis the reconstructed approximation
For an illustrative scale of 0.1:
Original weight: 0.37
Quantized integer: 4
Reconstructed weight: 0.40
Absolute error: 0.03Real GGUF quantization formats can be considerably more complicated.
They may include:
- packed integer values
- per-block scales
- multiple scale levels
- offsets or alternative codebooks
- different block sizes
- specialized treatment of some weight groups
In a packed four-bit representation, two four-bit codes can share one byte. That byte is not itself a floating-point weight; the runtime must unpack the codes and apply the format-specific reconstruction rules.
The llama.cpp GGUF quantization implementation contains concrete examples of these block-level encodings.
Why Quantization Makes Correctness Harder
For an FP32-to-FP16 conversion, we have a relatively direct numerical expectation.
For a quantized representation, correctness must be defined against the intended quantizer:
- Were the correct source values selected?
- Were the scales calculated as specified?
- Were the codes rounded and clipped correctly?
- Were the codes packed into bytes correctly?
- Does the runtime unpack them correctly?
- Are the reconstructed weights within the expected error budget?
A quantized checkpoint can therefore be binary-correct and quantizer-correct while intentionally losing some numerical fidelity relative to its source.
That loss must be evaluated for its effect on model computation and behavior.
Level 3: Tensor Correctness Goes Beyond Metadata
Consider a converter that declares:
Transformer blocks: 24
Hidden size: 1024
Tensor names: correct
Tensor shapes: correct
Tensor data types: correctThis is structural evidence.
It does not prove that the actual values are correct.
For example, two matrices can have identical shape:
A.shape == B.shapewhile containing completely different values:
A = [[1, 2],
[3, 4]]
B = [[100, 200],
[300, 400]]A metadata-only validator would not detect this difference.
Enumerate the Source Checkpoint
Hugging Face models frequently store weights in Safetensors files.
The Safetensors format records tensor names, dtypes, shapes and offsets in a header, followed by the tensor data. Its documented encoding uses little-endian storage and row-major tensor ordering.
For large or sharded checkpoints, begin by inspecting the source metadata rather than immediately loading every parameter.
from pathlib import Path
from safetensors import safe_open
for path in sorted(Path("./hf-model").glob("*.safetensors")):
with safe_open(path, framework="pt", device="cpu") as f:
for name in f.keys():
tensor_slice = f.get_slice(name)
shape = tuple(tensor_slice.get_shape())
count = 1
for dimension in shape:
count *= dimension
print(path.name, name, shape, count)For selected tensors, inspect additional properties such as the source tensor's dtype and strides:
with safe_open("model.safetensors", framework="pt", device="cpu") as f:
tensor = f.get_tensor("some.weight")
print("Shape:", tuple(tensor.shape))
print("Stride:", tensor.stride())
print("Dtype:", tensor.dtype)
print("Elements:", tensor.numel())The second example materializes the selected tensor.
Strides matter because two tensors with identical logical dimensions need not have identical memory layout. A transpose can change how the logical elements correspond to stored bytes.
For sharded checkpoints, use the shard index to locate the appropriate source tensor.
Inspect the Converted GGUF
The gguf Python package includes a reader for GGUF metadata and tensor data.
from gguf import GGUFReader
reader = GGUFReader("model-f16.gguf")
for tensor in reader.tensors:
print(
tensor.name,
tuple(tensor.shape),
tensor.tensor_type.name,
tensor.n_elements,
tensor.n_bytes,
)A useful inventory comparison should establish:
- which source tensors were retained
- which tensors were intentionally omitted
- which tensors were renamed
- which were merged, split or reordered
- whether the expected logical parameter counts agree
- whether the intended storage types were used
Total parameter counts need interpretation. A converter may intentionally exclude auxiliary heads, replace tied weights, or produce derived tensors.
A numerical mismatch in total stored elements is therefore a reason to investigate, not automatically proof of a bug.
Establish the Exact Tensor Mapping
Source and destination tensor names often differ.
For example, the Qwen3.5 conversion PR added mappings for source patterns such as:
layers.{bid}.linear_attn.in_proj_qkv
layers.{bid}.linear_attn.in_proj_z
layers.{bid}.linear_attn.conv1d
layers.{bid}.linear_attn.dt_proj
layers.{bid}.linear_attn.out_projThe converter must translate these into the GGUF tensor conventions expected by the runtime.
This translation can involve more than renaming.
Depending on the architecture, a converter might need to:
- transpose a matrix
- permute attention heads
- reorder Q, K and V projections
- reshape fused projection tensors
- rearrange weights associated with grouped attention
- convert weights into a different storage layout
- split or combine logically related tensors
The mapping itself should be an independently reviewable artifact.
Why Raw Array Comparison Can Produce False Failures
Imagine the source matrix:
source = [
[1, 2, 3],
[4, 5, 6],
]The destination runtime might expect the transpose:
converted = [
[1, 4],
[2, 5],
[3, 6],
]These arrays differ in shape and byte order.
However:
converted == transpose(source)They can still represent the same intended mathematical transformation when the runtime interprets them accordingly.
Therefore, the correct comparison is not necessarily:
source == convertedIt is:
expected = documented_conversion_transform(source)
actual = decoded_gguf_tensor
compare(expected, actual)The transformation must be established from the conversion specification or implementation, not invented afterward merely to make a failing comparison pass.
An additional subtlety is that square matrices can be incorrectly transposed without changing their shape.
For example, a 1024 × 1024 matrix remains 1024 × 1024 after transposition. Shape validation alone cannot detect that mistake.
Compare the Actual Weight Values
Once the mapping is known, numerical comparisons can establish whether the weights were preserved.
Useful metrics include maximum absolute error and relative L2 error.
Maximum Absolute Error
This identifies the largest elementwise discrepancy.
It is useful for detecting isolated large errors that aggregate metrics might hide.
Relative L2 Error
This measures the total difference relative to the reference magnitude. The denominator must be handled carefully for zero or near-zero reference tensors.
Neither metric should be used without finite-value checks and dtype-specific expectations.
Practical Python Comparison
The following example compares one mapped tensor from a Safetensors checkpoint against a non-quantized F16 or F32 tensor in GGUF.
It reads the source tensor in row chunks and uses the GGUF reader's memory-mapped tensor data.
Assumption: the chosen tensor pair already has the same logical layout. For transposed or reordered tensors, first implement and validate the expected mapping.
import numpy as np
import torch
from safetensors import safe_open
from gguf import GGUFReader
hf_file = "model.safetensors"
gguf_file = "model-f16.gguf"
# Replace these with a verified source-to-destination mapping.
hf_name = "some.weight"
gguf_name = "mapped.weight"
reader = GGUFReader(gguf_file)
target = next(t for t in reader.tensors if t.name == gguf_name)
assert target.tensor_type.name in {"F16", "F32"}
with safe_open(hf_file, framework="pt", device="cpu") as checkpoint:
source = checkpoint.get_slice(hf_name)
shape = tuple(source.get_shape())
assert tuple(target.data.shape) == shape
assert len(shape) >= 1
max_abs_error = 0.0
squared_error = 0.0
squared_reference = 0.0
cast_mismatches = 0
for start in range(0, shape[0], 64):
end = min(start + 64, shape[0])
source_tensor = source[start:end]
reference = source_tensor.float().numpy()
actual = np.ascontiguousarray(
target.data[start:end],
dtype=np.float32,
)
# Expected result of the declared precision conversion.
if target.tensor_type.name == "F16":
expected = source_tensor.to(torch.float16).float().numpy()
else:
expected = reference
# Detect differences beyond the intended dtype conversion.
cast_mismatches += np.count_nonzero(
actual.view(np.uint32) !=
np.ascontiguousarray(expected).view(np.uint32)
)
ref64 = reference.astype(np.float64)
actual64 = actual.astype(np.float64)
assert np.isfinite(ref64).all()
assert np.isfinite(actual64).all()
difference = actual64 - ref64
max_abs_error = max(
max_abs_error,
float(np.max(np.abs(difference))),
)
squared_error += float(np.sum(difference * difference))
squared_reference += float(np.sum(ref64 * ref64))
relative_l2 = (
(squared_error / squared_reference) ** 0.5
if squared_reference > 0
else squared_error ** 0.5
)
print("Max absolute error:", max_abs_error)
print("Relative L2 error:", relative_l2)
print("Unexpected cast mismatches:", cast_mismatches)This distinguishes two questions:
- How much did the weights change relative to the original?
- Did the destination contain precisely the result expected from the declared precision conversion?
A nonzero numerical error relative to FP32 is expected when converting to FP16.
However, unexpected mismatches against the independently calculated FP16 representation deserve investigation.
Important Limitations of This Example
This code is a validation template, not a reported test result for the Qwen3.5 PR.
It also deliberately excludes packed quantization formats and nontrivial layout transformations.
For quantized tensors, the process must account for the destination's block encoding and dequantize its values using the correct format rules.
For tensors requiring transposition or reordering, both sides must be transformed into a common logical representation before comparison.
When checking actual checkpoints, also test for NaNs, infinities, signed-zero differences where relevant, missing tensors, duplicate mappings and unexpected target tensors.
Streaming Comparisons for Billion-Parameter Models
A full model comparison should not require loading two complete model copies into memory.
Consider one billion parameters.
Approximate raw weight storage is:
FP32: 4 GB
FP16: 2 GBLoading the source and destination into memory and converting both to FP64 for comparison can require substantially more memory than either model alone.
A better strategy is:
- Inventory all source and destination tensors.
- Establish an explicit mapping.
- Compare one tensor or tensor chunk at a time.
- Accumulate error statistics.
- Release temporary arrays before proceeding.
- Record results per tensor, rather than only one aggregate score.
Memory-mapped formats help because the operating system can load the necessary file pages on demand.
For quantized GGUF tensors, chunk boundaries must respect the actual quantization-block representation. An arbitrary byte offset may start in the middle of a packed block.
For heavily permuted or fused tensors, streaming may require a mapping-specific traversal instead of simple row slicing.
Why Per-Tensor Reporting Matters
Suppose an entire model has 800 million parameters, and one small but important normalization tensor is incorrect.
A single global average error might be small because the incorrect tensor represents only a tiny fraction of all weights.
Per-tensor reporting makes such failures visible:
Token embeddings: PASS
Block 0 projections: PASS
Block 1 projections: PASS
Block 2 normalization: FAIL
Block 3 projections: PASS
...You can additionally prioritize sensitive architectural components, including normalization, gating, attention and output projection tensors.
A good validator makes failures localizable, not merely detectable.
Choosing Numerical Tolerances
There is no universally correct floating-point tolerance.
Tolerance selection depends on:
- source dtype
- destination dtype
- magnitude and distribution of values
- whether conversion involves arithmetic
- whether quantization is applied
- whether the comparison targets weights, activations or outputs
- the numerical requirements of the application
| Conversion | Expected Fidelity | Recommended Validation | Nuance |
|---|---|---|---|
| FP32 → FP32 | Exact values when only lossless serialization and layout transformations occur | Bitwise comparison after restoring a common layout | NaN payloads, signed zeros and deliberate arithmetic transformations need explicit policy |
| FP32 → FP16 | Expected rounding to FP16 | Compare against independently computed FP16 casts and report loss relative to FP32 | Large and tiny values can overflow or underflow |
| FP32 → BF16 | Expected BF16 rounding | Compare against the specified BF16 conversion rules | Wider range than FP16, but lower significand precision |
| FP16 → FP32 | Exact preservation of finite FP16 numerical values | Verify lossless numerical widening | The original information lost before FP16 storage cannot be recovered |
| FP32/FP16 → low-bit quantization | Lossy approximation according to the quantizer | Compare reconstructed weights, quantization error and downstream behavior | Errors depend heavily on block scaling and quantization format |
For exact serialization transformations, a tolerance may be inappropriate: you should verify equality directly.
For lossy conversion, set tolerances from the numerical format's expected behavior and observed error distributions, then validate whether those differences affect model outputs.
Do not choose a loose tolerance simply to make a test pass.
Level 4: Architectural Correctness
Correct weights do not guarantee that a model executes the intended architecture.
The runtime must also reconstruct:
- the correct number and ordering of transformer blocks
- the intended attention mechanisms
- projection dimensions and head arrangements
- positional encodings
- normalization placement and parameters
- feed-forward and gating operations
- residual connections
- auxiliary architectural branches
- output pooling and normalization
Why the Qwen3.5 Case Is Interesting
The example embedding model uses a hybrid architecture containing both linear-attention and full-attention blocks.
That means conversion correctness involves more than identifying a generic transformer.
The converter and runtime must agree on which blocks implement which operations and how their associated tensors are represented.
The PR also accounts for MTP, or Multi-Token Prediction.
Some Qwen3.5 model configurations contain auxiliary MTP-related structure. However, the standalone Qwen3_5TextModel embedding checkpoint does not require the converter to invent that auxiliary extension.
Incorrectly adding such a block could lead to an architectural mismatch despite apparently reasonable base metadata.
Pooling Is Part of the Model
For an embedding model, the final token hidden states are not necessarily the published embedding.
A typical sequence might be:
Input text
|
Tokenization
|
Transformer execution
|
Final token hidden states
|
Last-token pooling
|
L2 normalization
|
Final embeddingThe example model uses last-token pooling and L2 normalization.
If the converter or runtime mistakenly uses mean pooling, it may still produce a 1,024-dimensional embedding.
The output dimension would be correct, but the values and retrieval behavior could differ significantly.
Another subtlety is selecting the last actual token, not an unintended padding position.
Embedding-model prompting also matters. The example model card describes a query instruction prefix, while corpus passages are embedded without that same prefix.
A numerical comparison that changes the input protocol is not testing only conversion correctness.
Level 5: Validate Intermediate Computations
Suppose every weight tensor passes comparison, but the final embeddings still differ substantially.
What should we test next?
The most informative approach is often to compare intermediate activations between the reference and converted runtimes.
Instead of examining only the final output, inspect the computational pipeline.
Intermediate Values Worth Comparing
- input token IDs and masks
- token embedding lookup results
- positional encoding inputs and outputs
- attention projection outputs
- attention or recurrent-state computations
- feed-forward projections and activation functions
- normalization inputs and outputs
- residual connections
- transformer-block outputs
- final hidden states
- pooled vectors
- normalized embeddings
The idea is to locate the earliest stage where numerical agreement becomes unexpectedly poor.
Hypothetical Debugging Example
The following results are illustrative, not published Qwen3.5 test measurements.
| Computation Stage | Illustrative Relative L2 Error | Interpretation |
|---|---|---|
| Input embeddings | 0.00001 | Excellent numerical agreement |
| Block 4 output | 0.00004 | Small difference consistent with plausible precision effects |
| Block 8 input | 0.00006 | Still close |
| Block 8 attention projection | 0.12 | First major divergence; investigate the projection and its inputs |
| Block 9 output | 0.18 | Difference has propagated |
| Final embedding | 0.21 | Final mismatch is a consequence of an earlier problem |
The numbers are not universal error thresholds.
Their purpose is to illustrate localization of a divergence.
If the inputs to block 8 agree but a projection output differs sharply, investigate that projection:
- Is the correct tensor being loaded?
- Was its shape interpreted correctly?
- Was a fused projection reordered incorrectly?
- Is the runtime using the expected matrix orientation?
- Is the numerical kernel implementing the same operation?
- Is there an unexpected precision difference?
If all transformer outputs agree but the final embedding differs, attention shifts toward pooling and output normalization.
This can dramatically reduce the debugging search space.
Practical Challenges
Comparing intermediate activations is harder than comparing static weights.
Different inference engines may:
- fuse multiple operations into one kernel
- store tensors in different layouts
- use different accumulation precision
- use different reduction orders
- reuse memory buffers
- expose different execution boundaries
- represent recurrent attention states differently
For example, a reference implementation may expose separate matrix multiplication, bias and normalization outputs, while an optimized runtime fuses those operations.
You must identify mathematically corresponding boundaries rather than comparing buffers that merely have similar names.
A practical workflow is to instrument the Hugging Face model with forward hooks and add targeted debug outputs to the converted runtime. Not every llama.cpp model exposes equivalent intermediate tensors through an ordinary public API, so runtime instrumentation may be required.
Normalize the Test Conditions First
Before blaming the converter, confirm both executions use:
- identical token IDs
- identical attention masks
- equivalent padding behavior
- equivalent positional encoding
- compatible precision settings
- disabled stochastic training behavior
- comparable batching and sequence lengths
- the same pooling specification
A hidden-state mismatch caused by different tokenization is not evidence that the weight converter is defective.
PyTorch's numerical accuracy documentation also cautions that floating-point operation order, backend choice and accumulation precision can produce differences even for mathematically equivalent computations.
Level 6: Final Numerical Correctness
Once the model can execute, we can compare its final outputs.
For embedding models, cosine similarity is a common starting point.
For generation models, relevant comparisons may include:
- logits
- probability distributions
- next-token rankings
- token-level losses
- generated outputs under controlled decoding
The central question is whether the converted model computes approximately the same function as the reference.
Understanding Cosine Similarity
For two nonzero vectors and , cosine similarity is:
It measures the angle between the vectors.
Values near 1 indicate strong directional agreement.
However, cosine similarity is not the same as numerical equality.
A Simple Counterexample
Consider:
A = [1, 2, 3]
B = [10, 20, 30]Here:
And:
Therefore:
The vectors point in exactly the same direction.
But every element of B is ten times the corresponding element of A.
Their relative L2 error, using A as the reference, is 9, or 900%.
So:
Cosine similarity of 1 does not prove that two unnormalized vectors contain the same numerical values.
What Does 0.99999973 Mean?
Consider a hypothetical experiment where the original model and converted model produce embeddings with cosine similarity:
0.99999973This is extremely strong directional agreement.
However, it does not establish:
- bitwise equality
- equal vector magnitudes
- equal values in every dimension
- correct weights in every layer
- correct behavior on every input
- absence of shared bugs
Nor does it necessarily indicate that the converter is wrong. Small differences can arise from numerical precision and runtime implementation.
The value 0.99999973 is illustrative here, not a published result from PR #27920.
Even very high cosine similarity can coexist with isolated element errors, especially in high-dimensional vectors.
Complementary Output Metrics
| Metric | What It Measures | Main Limitation |
|---|---|---|
| Cosine similarity | Directional agreement between nonzero vectors | Insensitive to positive uniform scaling |
| Maximum absolute error | Largest elementwise discrepancy | Sensitive to scale; does not summarize overall error |
| Relative L2 error | Total numerical discrepancy relative to a reference norm | Can underemphasize isolated differences and needs care near zero norms |
| Norm comparison | Difference in vector magnitude | Cannot establish directional or elementwise agreement |
| Recall@k / NDCG / MRR | Downstream retrieval quality | Cannot establish numerical equivalence of internal computations |
Practical Python Comparison
import numpy as np
def compare_embeddings(a, b):
a = np.asarray(a, dtype=np.float64)
b = np.asarray(b, dtype=np.float64)
if a.shape != b.shape:
raise ValueError("Embedding shapes differ")
if not np.isfinite(a).all() or not np.isfinite(b).all():
raise ValueError("Embeddings contain non-finite values")
norm_a = np.linalg.norm(a)
norm_b = np.linalg.norm(b)
if norm_a == 0 or norm_b == 0:
raise ValueError("Cosine similarity is undefined for zero vectors")
cosine = np.dot(a, b) / (norm_a * norm_b)
return {
"cosine": float(cosine),
"max_abs_error": float(np.max(np.abs(a - b))),
"relative_l2": float(np.linalg.norm(a - b) / norm_a),
"norm_a": float(norm_a),
"norm_b": float(norm_b),
}
print(compare_embeddings(
[1, 2, 3],
[10, 20, 30],
))The function demonstrates why a complete output comparison needs more than cosine similarity.
It also clarifies a separate distinction:
Comparing final embedding vectors is not equivalent to comparing weight tensors.
Final embeddings are products of the entire inference pipeline. Weight comparisons examine the checkpoint representation before execution.
Both tests are valuable, but they answer different questions.
Normalization Changes the Interpretation
Many embedding models normalize their output vectors to unit length.
If:
then cosine similarity is their dot product.
In this special case, cosine similarity of exactly 1 mathematically implies the two nonzero vectors are identical.
But measured floating-point values are rarely exact, and a value merely near 1 does not establish elementwise equality.
Moreover, normalization may hide an upstream scaling error.
For example:
Before normalization:
A = [1, 2, 3]
B = [10, 20, 30]
After L2 normalization:
normalize(A) = normalize(B)That may be acceptable for a system whose specification requires normalized embeddings.
It does not mean the intermediate vectors were numerically equivalent.
When validating a model conversion, compare vectors both before and after normalization when those intermediate values are available.
The Repeated-Conversion Thought Experiment
Now consider the following question:
If we repeatedly convert a model from FP32 to FP16 and back again one hundred million times, will its accuracy progressively deteriorate?
The intuitive answer might be yes.
After all, FP16 has less precision than FP32, so each conversion appears to risk losing information.
But for a pure deterministic FP32/FP16 round trip, something important happens:
The weight generally stops changing after the first lossy conversion.
Walk Through One Weight
Start with:
Original FP32: 0.10000000149011612Convert it to FP16:
FP16: 0.0999755859375Now convert that FP16 value back to FP32:
FP32: 0.0999755859375Convert it again to FP16:
FP16: 0.0999755859375And again:
FP32: 0.0999755859375
FP16: 0.0999755859375
FP32: 0.0999755859375
...The initial conversion changed the numerical value.
The subsequent conversions do not keep subtracting precision.
Why?
Every finite FP16 value can be represented exactly in FP32.
Therefore, widening an FP16 value into FP32 does not require rounding.
When the exact widened value is converted back to FP16 using ordinary, deterministic IEEE-compatible conversion rules, it produces the same FP16 value.
Mathematically:
This is an idempotence property of that particular round trip.
It is not a claim that all model conversions are reversible.
Python Demonstration
import numpy as np
original = np.array([0.1, 1.5, -3.14159], dtype=np.float32)
first_fp16 = original.astype(np.float16)
current = first_fp16.copy()
for _ in range(100):
current = current.astype(np.float32).astype(np.float16)
assert np.array_equal(
current.view(np.uint16),
first_fp16.view(np.uint16),
)
print("Original FP32:", original)
print("After first FP16:", first_fp16)
print("After round trips:", current)The code demonstrates one hundred round trips. Under the stated conditions, extending that process to one hundred million cycles does not cause progressive numerical degradation.
The result can also be established from the representability argument without executing one hundred million conversions.
What Happens to Cosine Similarity?
Suppose the first converted model produces an embedding whose cosine similarity to the original FP32 reference is:
0.99999973Assume:
- the model weights remain unchanged after the first FP16 conversion
- the model architecture and tokenizer remain identical
- pooling and normalization remain identical
- the inference runtime and precision settings remain identical
- inference is deterministic
- no extra arithmetic is performed during checkpoint conversion
Then repeated FP16-to-FP32-to-FP16 conversions do not create new weight errors.
If the same deterministic inference computation is repeated, its output and its similarity to the original reference should remain stable.
Conceptually:
Original reference: 1.00000000
After first conversion: 0.99999973
After 100 conversions: 0.99999973
After 1,000,000: 0.99999973
After 100,000,000: 0.99999973These numbers illustrate the expected stability; they are not results from a real 100-million-conversion experiment.
The critical insight is that precision loss is not automatically cumulative when repeatedly applying the same idempotent conversion.
When Repeated Conversions Really Can Degrade a Model
The preceding property applies to a narrow class of transformations.
Other conversions are genuinely lossy on repeated application.
Examples include:
Repeated Quantization With Different Schemes
FP32
↓
Q8
↓
Dequantized FP32
↓
Q4
↓
Dequantized FP32
↓
Different Q8 schemeEach quantizer may use different scales and rounding rules.
The reconstructed values may change repeatedly.
Recalculated Quantization Scales
Suppose a quantizer calculates scales from the maximum magnitude or distribution of each tensor block.
After quantization and dequantization, the values may differ from the original.
Recalculating scales on that altered distribution can produce different codes on subsequent passes.
This can cause further changes, though degradation is not guaranteed to be monotonic.
Non-Invertible Transformations
Some model transformations discard information:
- reducing dimensions
- merging weights without retaining the inverse mapping
- clipping outliers
- pruning parameters
- lossy compression
Such transformations generally cannot be perfectly undone.
Extra Arithmetic Between Conversions
Consider:
x = (x * scale).astype(np.float16)
x = (x / scale).astype(np.float32)Even if the intended net transformation is the identity, intermediate rounding can change values.
This is different from simply casting FP16 values to FP32 and back.
Subnormal Handling and Hardware Differences
Some hardware or kernels handle very small floating-point values using flush-to-zero behavior.
Other implementations preserve subnormals.
NaN payload handling, rounding modes and nonstandard conversion implementations may also differ.
Thus, bitwise stability requires clearly specified conversion semantics—not just format names.
Repeated Conversion Is Not Repeated Inference
Another important distinction:
Checkpoint conversion:
Stored weights → representation conversion → stored weightsis different from:
Inference:
Stored weights + inputs → numerical computation → outputsRepeatedly executing inference on different machines can yield slightly different results even when the stored weights are identical.
For example:
- CPU and GPU matrix multiplication may use different reduction orders.
- GPU kernels may use different accumulation precision.
- Some hardware uses TensorFloat-32 or other reduced-precision computation modes.
- Fused operations may round at different points.
- Parallel reductions may not reproduce identical addition orders.
Floating-point addition is not generally associative:
in finite-precision arithmetic.
Consequently, mathematically equivalent computations may not produce bitwise-identical results.
PyTorch explicitly documents that CPU and GPU computations can differ even with identical inputs and controlled randomness.
So the correct statement is:
Stable checkpoint weights imply stable outputs only when the relevant inference conditions are themselves held stable.
Running the same model on different hardware may produce different numerical outputs without any additional checkpoint-conversion loss.
Level 7: Behavioral Correctness
A model can pass numerical comparisons while still failing important behavioral tests.
For an embedding model, relevant behavior includes:
- retrieving semantically relevant passages
- distinguishing relevant results from hard negatives
- preserving ranking quality
- handling short and long text
- supporting the intended languages
- maintaining behavior with special tokens and unusual inputs
Numerical similarity is useful, but the real application may care about ranking.
Why Tiny Numerical Changes Can Matter
Suppose an original embedding model ranks two documents:
Document A: 0.812001
Document B: 0.812000A small numerical perturbation may reverse their order:
Document A: 0.811999
Document B: 0.812001The embeddings might still exhibit extremely high cosine similarity to their reference counterparts.
Yet the top-ranked search result has changed.
Whether this matters depends on the application.
For a general search system, a rare swap between nearly identical documents may be acceptable.
For an application where a strict threshold controls access or triggers an expensive action, even small numerical changes near that threshold can matter substantially.
Build a Representative Evaluation Set
A useful behavioral test corpus should include:
- exact or near-duplicate passages
- paraphrases
- semantically related but incorrect answers
- long documents near the context limit
- very short queries
- multilingual examples
- unusual Unicode and punctuation
- model-specific prompt formatting
- empty or boundary inputs where supported
- queries whose relevant results have very close scores
For the Qwen3.5 example, the model card also describes Matryoshka representation dimensions.
If validating a truncated embedding dimension, apply truncation and normalization in the intended order. That is a separate evaluation condition from comparing the full 1,024-dimensional output.
Retrieval Evaluation Metrics
| Metric | What It Evaluates | Nuance |
|---|---|---|
| Recall@k | Whether relevant items appear among the top k results | Does not fully capture ranking order or false positives |
| MRR | How highly the first relevant result ranks | Less informative when many relevant documents matter |
| NDCG | Ranking quality with graded relevance | Requires reliable relevance judgments |
| Top-k overlap | How similar converted and reference retrieval results are | Reference agreement is not necessarily correctness against ground truth |
| Threshold error rate | False positives and false negatives around decision thresholds | Thresholds must be calibrated to the model and domain |
One strong test strategy is to evaluate both:
- Reference agreement — how often does the converted model produce the same rankings as the original?
- Ground-truth quality — how often do those rankings actually satisfy the task's relevance labels?
Those are not the same measurement.
Level 8: What If the Source Model Is Wrong?
There is a deeper correctness problem that metadata comparisons cannot resolve.
Suppose the Hugging Face configuration declares:
num_hidden_layers = 24The converter reads that configuration and writes:
GGUF block_count = 24A test compares the two values:
assert gguf_block_count == hf_config["num_hidden_layers"]The test passes.
What have we established?
Only that the converter represented this configuration value consistently.
We have not established that the model genuinely contains the architecture described by that source value.
Shared-Reference Errors
Imagine that:
- the source configuration is wrong
- the converter trusts it
- the GGUF metadata copies it
- the validation test compares the two
All three components agree.
They can still all be wrong relative to the intended architecture.
This is a shared-reference failure.
The problem becomes more subtle when the reference implementation also reads the same incorrect configuration.
Two implementations are not fully independent merely because they run in different programming languages or frameworks.
If both rely on the same mistaken assumption, they may produce mutually consistent but incorrect results.
Triangulating the Evidence
Stronger validation combines several sources:
Actual checkpoint tensors
Inspect real tensor names, counts, dimensions and values. Infer which structures are actually present rather than assuming the metadata is correct.
Architecture documentation
Compare the model against independent architecture descriptions and published specifications.
Hugging Face implementation
Inspect the source code that constructs the model and executes its forward pass.
Expected tensor layout
Check whether the observed shapes and mappings are consistent with the intended mathematical operations.
Independent execution
Compare against an implementation that does not simply reuse the same converter logic or assumptions.
Published golden outputs
When the model provider supplies trusted test vectors, compare outputs under the exact prescribed preprocessing and execution conditions.
Each additional independent source reduces the likelihood of a common-mode error.
Independence Is Not Binary
There are degrees of independence.
For example:
Validator A:
Compares GGUF block_count with HF config.
Validator B:
Counts block-associated tensors in the checkpoint.
Validator C:
Inspects the model's architecture implementation.
Validator D:
Executes a separately implemented reference forward pass.Validator A checks metadata translation.
Validators B, C and D introduce progressively different evidence about architecture and computation.
No individual test guarantees correctness, but their agreement is much more compelling.
A Practical End-to-End Verification Workflow
For a real model-conversion change, a systematic process might look like this:
Step 1: Freeze the Reference
Record:
- source model revision or commit
- checkpoint hashes
- tokenizer revision
- architecture configuration
- embedding configuration
- reference framework version
- converter commit
- inference runtime version
Without version pinning, reproducibility becomes difficult.
Step 2: Validate Binary Integrity
Parse the source and destination files and verify:
- file structures
- tensor offset ranges
- data lengths
- endianness
- alignment
- hashes against trusted manifests
Step 3: Inventory and Map Tensors
Enumerate every source tensor.
Classify each as:
- copied
- renamed
- transformed
- merged
- split
- intentionally omitted
- used to construct a derived tensor
Ensure the destination tensor inventory is also accounted for.
Step 4: Verify Numerical Weights
For every mapped tensor:
- compare the expected shape and dtype
- apply the documented transformation
- check finite-value invariants
- compare exact values where lossless conversion is expected
- measure numerical error where loss is intentional
- report unexpected mismatches per tensor
Step 5: Validate Architectural Metadata
Verify:
- block counts and types
- attention configuration
- normalization
- positional encoding
- optional auxiliary structures
- pooling
- tokenization and special tokens
Step 6: Compare Intermediate Computations
Execute matching reference and converted inference inputs.
Find the first stage whose numerical difference exceeds its justified error budget.
Step 7: Compare Final Outputs
For embeddings, record:
- cosine similarity
- maximum absolute error
- relative L2 error
- output norms
- dimensionality
For logits, compare the full numerical vectors as well as task-relevant distributions and rankings.
Step 8: Evaluate Behavioral Quality
Run representative and adversarial inputs.
Measure both:
- agreement with the original model
- performance against independently labeled ground truth
Step 9: Test Platform and Precision Variations
Repeat appropriate comparisons across relevant hardware, quantization modes, context lengths and batching conditions.
Differences between CPU and GPU execution should be measured, not automatically attributed to the converter.
Step 10: Record the Evidence
A useful report should distinguish:
PASS: GGUF parsed correctly.
PASS: Expected architecture represented.
PASS: Tensor mappings accounted for.
PASS: Weight comparison within declared error budget.
PASS: Intermediate activations within expected tolerance.
PASS: Final embedding comparison within expected tolerance.
PASS: Retrieval benchmarks within acceptance criteria.Every statement should have a corresponding test, input set, threshold and reproducible execution environment.
Do not collapse all those properties into a single unqualified label such as “conversion passed.”
Common Pitfalls and Trade-offs
| Pitfall | Why It Happens | Better Approach |
|---|---|---|
| Checking only tensor names and shapes | Metadata is easy to inspect, while numerical values are expensive to validate | Compare actual scalar values after documented tensor transformations |
| Comparing raw arrays without layout mapping | Valid transpositions or permutations appear as numerical failures | Map both representations into common logical coordinates |
| Requiring exact equality after intentional quantization | Lossy encoding cannot preserve all original values | Verify quantizer behavior, numerical error and downstream quality |
| Using cosine similarity as the only correctness metric | Directional agreement can hide magnitude and elementwise differences | Also compare norms, absolute error and relative L2 error |
| Comparing outputs with different tokenization or pooling | Input and postprocessing differences are confused with conversion errors | Align the complete preprocessing and inference protocol first |
| Treating CPU/GPU differences as conversion bugs | Floating-point kernels differ across hardware and execution paths | Control the environment and compare against hardware-specific baselines |
| Assuming repeated casts continually degrade precision | Lossy conversion is confused with cumulative numerical loss | Analyze whether the round trip is idempotent |
| Trusting source metadata without independent checks | The source, converter and test may share the same assumption | Triangulate tensor structure, architecture specifications and independent execution |
| Using one aggregate weight-error metric | Small but critical tensors can disappear inside the global average | Report per-tensor errors and inspect sensitive layers separately |
| Assuming a model that loads is correct | Runtime compatibility is mistaken for numerical or behavioral validation | Validate intermediate computation, final outputs and task behavior |
Final Verification Matrix
The following matrix summarizes the complete validation framework.
| Verification Level | Test Method | Failure Detected | What a Passing Result Proves | Remaining Limitations |
|---|---|---|---|---|
| 1. Binary and serialization | Validate file structure, checksums, offsets, alignment, endianness and encoded types | Corrupted files, invalid offsets, incorrect byte interpretation, malformed serialization | The artifact satisfies the tested format and integrity constraints | Stored values and architecture can still be semantically incorrect |
| 2. Scalar weights | Compare numerical values exactly or within justified precision tolerances | Incorrect rounding, altered weights, unexpected numerical loss | Tested scalar values agree with the expected conversion | Correct values may still be assigned to the wrong tensors or operations |
| 3. Tensor structure and mapping | Compare tensor inventories, shapes, dtypes, strides, layouts, mappings and transformed values | Wrong mapping, transposition, missing tensors, unexpected layouts | Tested tensors match the intended representation and transformation | The runtime may still execute the architecture incorrectly |
| 4. Architectural correctness | Inspect block structure, attention, normalization, auxiliary components and pooling configuration | Incorrect block counts, incompatible operations, wrong pooling or missing architectural components | The declared and reconstructed architecture agrees with the tested specification | Individual computations may still produce incorrect numerical results |
| 5. Intermediate computations | Compare corresponding activations and layer outputs under controlled inputs | Incorrect kernels, tensor interpretation, precision behavior or computation ordering | Selected internal computations agree within declared error budgets | Untested operations or inputs may still diverge |
| 6. Final numerical outputs | Compare embeddings or logits using cosine, absolute error, relative L2 and norm checks | Final numerical discrepancies, normalization errors, output mismatches | Outputs agree within measured tolerances for tested inputs | High agreement does not guarantee weight identity or universal behavioral correctness |
| 7. Behavioral correctness | Evaluate retrieval, ranking, generation or other tasks using representative and difficult cases | Loss of intended task performance, ranking regressions, edge-case failures | The model meets tested application-quality requirements | Untested distributions and rare failures remain possible |
| 8. Reference or specification correctness | Triangulate checkpoint structure, independent specifications, reference code and golden outputs | Shared configuration mistakes, incorrect reference assumptions, self-consistent but invalid implementations | Independent evidence supports the intended interpretation and behavior | Specifications and tests can still be incomplete or share hidden assumptions |
This matrix illustrates why a passing test at one level should not automatically be promoted into a claim about every other level.
Conclusion
Model conversion correctness is a multi-layer engineering problem.
At the lowest level, bytes must be serialized and interpreted correctly. Above that, numerical weights must be preserved or transformed according to justified precision rules. Tensors must map to the correct logical components, and the runtime must reconstruct and execute the intended architecture.
Beyond those structural properties, corresponding intermediate computations should agree, final embeddings or logits should demonstrate numerical fidelity, and the converted model should preserve its intended behavior on representative workloads.
The Qwen3.5 embedding conversion example shows why this distinction matters. Successfully registering the architecture, mapping the relevant tensor families, preserving the intended block count and pooling configuration, and loading the model into llama.cpp are meaningful achievements. They are not automatically equivalent to having performed exhaustive weight-level, activation-level or behavioral validation.
The repeated-conversion experiment reveals another important principle: numerical precision loss is not necessarily cumulative. Under ordinary deterministic FP16-to-FP32-to-FP16 conversion, values stabilize after the initial rounding because finite FP16 values are exactly representable in FP32. Further degradation requires some additional lossy or non-idempotent transformation.
Ultimately:
A converted model should be trusted because multiple independent tests support its correctness—not because one file loaded successfully or one cosine similarity happened to be close to 1.
Key Takeaways
- Correctness is multidimensional. Binary integrity, numerical fidelity, architecture, computation and behavior answer different questions.
- Metadata is insufficient. Matching tensor names, dimensions and block counts does not establish correct weight values.
- Precision loss must be interpreted correctly. FP32-to-FP16 rounding is expected; unexpected errors beyond the intended conversion are not.
- Tensor mappings matter. Transposition, reordering and packed representations must be accounted for before comparing values.
- Cosine similarity is valuable but incomplete. A score extremely close to 1 demonstrates directional agreement, not necessarily equal magnitudes or scalar values.
- Intermediate activations help localize errors. Find the first meaningful divergence rather than debugging only the final output.
- Pure FP16/FP32 round trips stabilize. Repeating the same deterministic cast does not continually reduce the precision of an already-representable FP16 value.
- Different hardware can produce different inference results. Numerical differences do not automatically imply checkpoint corruption.
- The reference can also be wrong. Independent architectural and numerical evidence matters because shared assumptions can produce mutually consistent failures.
- The strongest validation is evidence-driven. Record exactly what was tested, what passed, what tolerances were used and what remains unverified.
References and Further Reading
- llama.cpp PR #27920 — Qwen3.5 Embedding Model Conversion Support
- Qwen3.5 Embedding 0.8B — Model Card
- GGUF File Format Specification
- Hugging Face Safetensors Format
- llama.cpp GGUF Reader Implementation
- llama.cpp Quantization Implementation
- PyTorch Numerical Accuracy Documentation

