Skip to content

fix: normalize float16 embeddings without NaNs - #368

Closed
xblwh wants to merge 1 commit into
MinishLab:mainfrom
xblwh:fix/float16-normalization
Closed

xblwh wants to merge 1 commit into
MinishLab:mainfrom
xblwh:fix/float16-normalization

Conversation

@xblwh

@xblwh xblwh commented Sep 15, 2026

Copy link
Copy Markdown

Normalizing float16 embeddings can produce NaNs for empty or unknown-only inputs because 1e-32 rounds to zero. Computing the norm in float16 also underflows or overflows for small or large nonzero vectors.

Compute float16 norms in float32 while preserving the embedding output dtype. Extend the empty-input regression from #114 across float16/float32/float64 and cover mixed zero/nonzero inputs at different scales with WordPiece, BPE and Unigram tokenizers.

Validation on macOS / Python 3.12:

  • The same 36 regression cases give 12 failures on main and all 36 pass with this change.
  • coverage run --source=model2vec -m pytest --ignore=tests/integration: 339 passed.
  • pytest tests/test_model.py with all CI extras installed: 105 passed.
  • pre-commit run --all-files: passed, including Ruff, pydoclint and mypy.
  • A real minishlab/potion-base-8M before/after check fixes float16 empty/whitespace outputs; float32, float64 and int8 outputs are unchanged.

@stephantul

Copy link
Copy Markdown
Contributor

@xblwh has this ever happened to you in a real model? If so, which one?

@xblwh

xblwh commented Sep 15, 2026

Copy link
Copy Markdown
Author

I reproduced this locally with minishlab/potion-base-8M, loaded with quantize_to="float16" and normalize=True, while investigating this PR. I do not have a production incident to report.

Here is a minimal example using the exact model revision I tested (macOS / Python 3.12 / NumPy 2.5.3):

import numpy as np
from huggingface_hub import snapshot_download
from model2vec import StaticModel

path = snapshot_download(
    "minishlab/potion-base-8M",
    revision="bf8b056651a2c21b8d2565580b8569da283cab23",
    allow_patterns=["config.json", "tokenizer.json", "model.safetensors"],
)
for dtype in (None, "float16"):
    model = StaticModel.from_pretrained(path, quantize_to=dtype, normalize=True)
    out = model.encode(["", "   ", "Python is a programming language."])
    print(out.dtype, "finite:", np.isfinite(out).all(axis=1).tolist())
    print("empty/whitespace zero:", (out[:2] == 0).all(axis=1).tolist())

On main at d9ac0d9:

float32 finite: [True, True, True]
empty/whitespace zero: [True, True]
float16 finite: [False, False, True]
empty/whitespace zero: [False, False]

With this PR at 69cd5d7:

float32 finite: [True, True, True]
empty/whitespace zero: [True, True]
float16 finite: [True, True, True]
empty/whitespace zero: [True, True]

The real-model failure here is empty/whitespace input after loading in float16; the default float32 model is fine in this example. The very small/large scale cases in the tests are synthetic regression coverage, not failures I observed in this model's normal text embeddings.

@stephantul

Copy link
Copy Markdown
Contributor

Uh yeah I mean I introduced this regression literally 3 hours ago. It's not even released yet, so unless you installed this today you wouldn't have been able to observe this.

@xblwh

xblwh commented Sep 15, 2026

Copy link
Copy Markdown
Author

Right — I checked out today's main at d9ac0d9 while investigating potential issues. That's where I found this and then reproduced it with the Potion checkpoint. I wasn't reporting an issue encountered in a released version. Sorry for not making that timeline clearer.

@stephantul stephantul closed this Sep 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants