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 behavior

A 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 TypeWhat It MeansImportant Limitation
Structural consistencyTensor names, shapes, types, layer counts and metadata agree with an expected representationThe underlying weight values or interpretation can still be wrong
Bitwise equivalenceCompared binary representations are exactly identical under an explicitly defined encodingIdentical bytes do not prove correct interpretation or intended behavior
Numerical fidelityWeights, activations or outputs agree within justified numerical tolerancesAgreement on tested values does not establish correctness for every input
Behavioral equivalenceThe converted model preserves intended behavior on the relevant tasks and input distributionPassing a benchmark does not guarantee numerical identity or behavior on unseen cases
Specification correctnessThe implementation satisfies an independently justified model specificationRequires 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:

  1. Binary and serialization correctness — are the bytes written and interpreted correctly?
  2. Scalar weight correctness — are the numerical values preserved exactly or within expected precision loss?
  3. Tensor correctness — are names, shapes, layouts, mappings and values correct?
  4. Architectural correctness — does the runtime reconstruct the intended network?
  5. Intermediate computational correctness — do corresponding layers calculate equivalent results?
  6. Final numerical correctness — do embeddings or logits agree with a reference?
  7. Behavioral correctness — does the model preserve useful behavior?
  8. 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 CheckEvidence ProvidedWhat It Does Not Establish
Real Hugging Face to GGUF conversionThe converter could process an actual supported model checkpointEvery weight was numerically identical or correctly transformed
24-block metadataThe expected block count was represented in the outputEvery block contained the correct tensors and operations
LAST pooling and no MTPThose architectural metadata decisions were represented as intendedEvery pooling-related numerical result matched an independent reference
llama-embedding runtime loadThe converted checkpoint was accepted by the runtimeIts outputs were numerically or behaviorally equivalent to the original
1,024-dimensional embedding outputThe runtime produced an output with the expected dimensionalityThe embedding contained the correct numerical values
Unsupported pooling rejectionAn explicitly unsupported configuration was rejected rather than silently acceptedEvery 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.gguf

This 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.1

A 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 bits

For normalized binary floating-point numbers, the represented value is approximately described by:

(−1)s×2e−bias×(1+f)(-1)^s \times 2^{e-\text{bias}} \times (1+f)

Here:

  • s is the sign bit.
  • e is the stored exponent.
  • bias adjusts the exponent.
  • f is the fractional contribution encoded by the significand bits.

Subnormal values, zeros, infinities and NaNs use special encoding rules.

FP32 vs. FP16 vs. BF16

FormatTotal BitsExponent BitsExplicit Fraction BitsMain Trade-off
FP3232823Higher precision, larger storage footprint
FP1616510Smaller representation but limited numerical range
BF161687Roughly 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.10000000149011612

In FP16:

Hex:     0x2E66

Binary:  0 | 01011 | 1001100110
         S   Exp.    Fraction

Value:   0.0999755859375

The FP16 result is different because fewer significand bits are available.

By contrast, 1.5 is exactly representable in both formats:

FP32: 0x3FC00000
FP16: 0x3E00

The 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 3C

Interpreted 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 computation

This 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:

q=round⁡(ws)q = \operatorname{round}\left(\frac{w}{s}\right)

and reconstruct:

w^=s×q\hat{w} = s \times q

where:

  • w is the original weight
  • s is a scale
  • q is 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.03

Real 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:

  1. Were the correct source values selected?
  2. Were the scales calculated as specified?
  3. Were the codes rounded and clipped correctly?
  4. Were the codes packed into bytes correctly?
  5. Does the runtime unpack them correctly?
  6. 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:  correct

This is structural evidence.

It does not prove that the actual values are correct.

For example, two matrices can have identical shape:

A.shape == B.shape

while 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_proj

The 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 == converted

It 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

Emax⁡=max⁡i∣ai−bi∣E_{\max} = \max_i |a_i-b_i|

This identifies the largest elementwise discrepancy.

It is useful for detecting isolated large errors that aggregate metrics might hide.

Relative L2 Error

Erel=∥A−B∥2max⁡(∥A∥2,ϵ)E_{\mathrm{rel}} = \frac{\|A-B\|_2}{\max(\|A\|_2,\epsilon)}

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:

  1. How much did the weights change relative to the original?
  2. 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 GB

Loading 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:

  1. Inventory all source and destination tensors.
  2. Establish an explicit mapping.
  3. Compare one tensor or tensor chunk at a time.
  4. Accumulate error statistics.
  5. Release temporary arrays before proceeding.
  6. 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
ConversionExpected FidelityRecommended ValidationNuance
FP32 → FP32Exact values when only lossless serialization and layout transformations occurBitwise comparison after restoring a common layoutNaN payloads, signed zeros and deliberate arithmetic transformations need explicit policy
FP32 → FP16Expected rounding to FP16Compare against independently computed FP16 casts and report loss relative to FP32Large and tiny values can overflow or underflow
FP32 → BF16Expected BF16 roundingCompare against the specified BF16 conversion rulesWider range than FP16, but lower significand precision
FP16 → FP32Exact preservation of finite FP16 numerical valuesVerify lossless numerical wideningThe original information lost before FP16 storage cannot be recovered
FP32/FP16 → low-bit quantizationLossy approximation according to the quantizerCompare reconstructed weights, quantization error and downstream behaviorErrors 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 embedding

The 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 StageIllustrative Relative L2 ErrorInterpretation
Input embeddings0.00001Excellent numerical agreement
Block 4 output0.00004Small difference consistent with plausible precision effects
Block 8 input0.00006Still close
Block 8 attention projection0.12First major divergence; investigate the projection and its inputs
Block 9 output0.18Difference has propagated
Final embedding0.21Final 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 AA and BB, cosine similarity is:

cosine⁡(A,B)=A⋅B∥A∥2∥B∥2\operatorname{cosine}(A,B) = \frac{A \cdot B} {\|A\|_2 \|B\|_2}

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:

A⋅B=10+40+90=140A \cdot B = 10 + 40 + 90 = 140

And:

∥A∥2=14\|A\|_2 = \sqrt{14} ∥B∥2=1400=1014\|B\|_2 = \sqrt{1400} = 10\sqrt{14}

Therefore:

cosine⁡(A,B)=14014⋅1014=1\operatorname{cosine}(A,B) = \frac{140}{\sqrt{14}\cdot10\sqrt{14}} =1

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.99999973

This 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

MetricWhat It MeasuresMain Limitation
Cosine similarityDirectional agreement between nonzero vectorsInsensitive to positive uniform scaling
Maximum absolute errorLargest elementwise discrepancySensitive to scale; does not summarize overall error
Relative L2 errorTotal numerical discrepancy relative to a reference normCan underemphasize isolated differences and needs care near zero norms
Norm comparisonDifference in vector magnitudeCannot establish directional or elementwise agreement
Recall@k / NDCG / MRRDownstream retrieval qualityCannot 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:

∥A∥2=∥B∥2=1\|A\|_2 = \|B\|_2 = 1

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.10000000149011612

Convert it to FP16:

FP16: 0.0999755859375

Now convert that FP16 value back to FP32:

FP32: 0.0999755859375

Convert it again to FP16:

FP16: 0.0999755859375

And 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:

FP16⁡(FP32⁡(FP16⁡(x)))=FP16⁡(x)\operatorname{FP16}( \operatorname{FP32}( \operatorname{FP16}(x) ) ) = \operatorname{FP16}(x)

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.99999973

Assume:

  • 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.99999973

These 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 scheme

Each 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 weights

is different from:

Inference:
    Stored weights + inputs → numerical computation → outputs

Repeatedly 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:

(a+b)+c≠a+(b+c)(a+b)+c \ne a+(b+c)

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.812000

A small numerical perturbation may reverse their order:

Document A: 0.811999
Document B: 0.812001

The 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

MetricWhat It EvaluatesNuance
Recall@kWhether relevant items appear among the top k resultsDoes not fully capture ranking order or false positives
MRRHow highly the first relevant result ranksLess informative when many relevant documents matter
NDCGRanking quality with graded relevanceRequires reliable relevance judgments
Top-k overlapHow similar converted and reference retrieval results areReference agreement is not necessarily correctness against ground truth
Threshold error rateFalse positives and false negatives around decision thresholdsThresholds must be calibrated to the model and domain

One strong test strategy is to evaluate both:

  1. Reference agreement — how often does the converted model produce the same rankings as the original?
  2. 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 = 24

The converter reads that configuration and writes:

GGUF block_count = 24

A 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

PitfallWhy It HappensBetter Approach
Checking only tensor names and shapesMetadata is easy to inspect, while numerical values are expensive to validateCompare actual scalar values after documented tensor transformations
Comparing raw arrays without layout mappingValid transpositions or permutations appear as numerical failuresMap both representations into common logical coordinates
Requiring exact equality after intentional quantizationLossy encoding cannot preserve all original valuesVerify quantizer behavior, numerical error and downstream quality
Using cosine similarity as the only correctness metricDirectional agreement can hide magnitude and elementwise differencesAlso compare norms, absolute error and relative L2 error
Comparing outputs with different tokenization or poolingInput and postprocessing differences are confused with conversion errorsAlign the complete preprocessing and inference protocol first
Treating CPU/GPU differences as conversion bugsFloating-point kernels differ across hardware and execution pathsControl the environment and compare against hardware-specific baselines
Assuming repeated casts continually degrade precisionLossy conversion is confused with cumulative numerical lossAnalyze whether the round trip is idempotent
Trusting source metadata without independent checksThe source, converter and test may share the same assumptionTriangulate tensor structure, architecture specifications and independent execution
Using one aggregate weight-error metricSmall but critical tensors can disappear inside the global averageReport per-tensor errors and inspect sensitive layers separately
Assuming a model that loads is correctRuntime compatibility is mistaken for numerical or behavioral validationValidate intermediate computation, final outputs and task behavior

Final Verification Matrix

The following matrix summarizes the complete validation framework.

Verification LevelTest MethodFailure DetectedWhat a Passing Result ProvesRemaining Limitations
1. Binary and serializationValidate file structure, checksums, offsets, alignment, endianness and encoded typesCorrupted files, invalid offsets, incorrect byte interpretation, malformed serializationThe artifact satisfies the tested format and integrity constraintsStored values and architecture can still be semantically incorrect
2. Scalar weightsCompare numerical values exactly or within justified precision tolerancesIncorrect rounding, altered weights, unexpected numerical lossTested scalar values agree with the expected conversionCorrect values may still be assigned to the wrong tensors or operations
3. Tensor structure and mappingCompare tensor inventories, shapes, dtypes, strides, layouts, mappings and transformed valuesWrong mapping, transposition, missing tensors, unexpected layoutsTested tensors match the intended representation and transformationThe runtime may still execute the architecture incorrectly
4. Architectural correctnessInspect block structure, attention, normalization, auxiliary components and pooling configurationIncorrect block counts, incompatible operations, wrong pooling or missing architectural componentsThe declared and reconstructed architecture agrees with the tested specificationIndividual computations may still produce incorrect numerical results
5. Intermediate computationsCompare corresponding activations and layer outputs under controlled inputsIncorrect kernels, tensor interpretation, precision behavior or computation orderingSelected internal computations agree within declared error budgetsUntested operations or inputs may still diverge
6. Final numerical outputsCompare embeddings or logits using cosine, absolute error, relative L2 and norm checksFinal numerical discrepancies, normalization errors, output mismatchesOutputs agree within measured tolerances for tested inputsHigh agreement does not guarantee weight identity or universal behavioral correctness
7. Behavioral correctnessEvaluate retrieval, ranking, generation or other tasks using representative and difficult casesLoss of intended task performance, ranking regressions, edge-case failuresThe model meets tested application-quality requirementsUntested distributions and rare failures remain possible
8. Reference or specification correctnessTriangulate checkpoint structure, independent specifications, reference code and golden outputsShared configuration mistakes, incorrect reference assumptions, self-consistent but invalid implementationsIndependent evidence supports the intended interpretation and behaviorSpecifications 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