Skip to content

fix(cluster): Scorer.Route accepts non-finite and zero-norm embeddings #780

Description

@rohith500

Summary

After a successful Embed call, Scorer.Route verifies only the embedding dimension. A dimensionally valid zero, NaN, or Inf vector proceeds into topPNearest, so the router can select a model from centroid similarities that are not numerically meaningful instead of returning ErrClusterUnavailable.

Why it matters

  • Routing correctness (and potentially cost) whenever the inference runtime returns numerically corrupted output without an error.
  • Violates the package fail-closed contract (AGENTS.md / CLAUDE.md: new failure modes return ErrClusterUnavailable, not silent degradation) and the Embedder contract (successful call returns an L2-normalized vector).
  • Production incidence is unproven — do not claim current customer impact without telemetry or an ONNX reproduction. Severity: medium correctness, low-probability path.

Failure modes if accepted

  • Zero vector: all centroid similarities tie at zero; stable selection falls to lowest-index centroids — artifact order becomes an unintended routing feature.
  • NaN/Inf vector: the topPNearest comparator no longer defines a meaningful strict ordering over similarities, so selected centroids are not semantically valid. Go need not panic; routing can still succeed.
  • Availability: correct behavior is a visible 503 — fixing converts silent bad routing into a detectable failure.

Where

  • internal/router/cluster/scorer.goScorer.Route (immediately after the embedding dimension check) — trust boundary; put validation here
  • internal/router/cluster/scorer.gotopPNearest (no finite checks before sort)
  • internal/router/cluster/embedder_onnx.goonnxEmbedder.Embed (validates count/dim only)

Hugot’s WithNormalization does not convert these into errors: zero vectors remain zero (denominator floored at 1e-12); NaNs remain NaN. So this is not merely an impossible test-double case.

Recommended fix

Validate in Scorer.Route (not only in the ONNX adapter — Scorer can receive alternative embedders/wrappers). Reject non-finite and zero-norm vectors only; map every failure to ErrClusterUnavailable with an error log that includes failure category, embedder ID, expected dim, and request correlation metadata — not vector values or prompt content.

func validateEmbedding(vec []float32, wantDim int) error {
	if len(vec) != wantDim {
		return fmt.Errorf("embedding dim %d != expected %d", len(vec), wantDim)
	}
	var normSq float64
	for i, v := range vec {
		x := float64(v)
		if math.IsNaN(x) || math.IsInf(x, 0) {
			return fmt.Errorf("embedding contains non-finite value at index %d", i)
		}
		normSq += x * x
	}
	if normSq == 0 {
		return errors.New("embedding has zero norm")
	}
	return nil
}

Use exact normSq == 0 (or a deliberately documented minimal threshold). Do not pick an arbitrary epsilon without evidence: a tiny positive finite vector still has meaningful cosine ordering up to scale; zero and non-finite categorically do not. Do not enforce norm ≈ 1 until real Jina/Qwen output norms are measured.

Observability (ship with the fix)

Count rejected embeddings by reason: zero_norm, nan, positive_inf, negative_inf, wrong_dimension. Prefer a short shadow/measure window in production before relying on the metric for severity, but the validation itself is correct regardless.

Tests

Table-driven Route tests:

  • All-zero vector
  • NaN at beginning / middle / end
  • ±Inf
  • Valid finite vector
  • Very small but nonzero finite vector (must still route if using exact zero-norm)

Invalid cases: errors.Is(err, ErrClusterUnavailable).

Optional integration smoke against production embedders (empty / whitespace / long / unicode / normal coding prompt): record finiteness + norm only, never snapshot the full vector.

Confidence

High that the validation gap exists; Medium on operational importance (rises with telemetry or ONNX reproduction).

Scope

File and fix independently of the dial-calibration / AlphaFloor work. Do not bundle into that PR.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions