Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions examples/vlm_finetune/mistral4/mistral4_medpix.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,10 @@ ci:
checkpoint_robustness:
# PP=4 with pp_microbatch_size=1 requires at least four pipeline microbatches.
step_scheduler.local_batch_size: 4
# The original source and AutoModel reload pass standard. Only the PP4/EP8
# post-training HF reload has measured long-context BF16 drift.
# Independent-process AutoModel and HF reloads show measured long-context
# BF16 drift for this PP4/EP8 routed-MoE topology; source parity stays standard.
parity_tolerance_profile_overrides:
automodel_reload: relaxed
hf_reload: relaxed
hf_device_map_auto: true
# Transformers' load-time dequantization leaves Mistral4 expert weights in
Expand Down
14 changes: 8 additions & 6 deletions tests/ci_tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,11 +151,13 @@ harness fails with an actionable error instead of repeating content if a request
document. Pipeline-parallel runs resize their stage activation buffers to the configured parity length; reduce the
length only when a model has a documented memory limit.

Every comparison reports mean, p95, and max per-token `KL(reference || candidate)`; whole-tensor cosine similarity;
and mean/max absolute logit difference. The full record is printed as `CHECKPOINT_PARITY_METRICS <json>` and saved
under `<checkpoint_dir>/.checkpoint_robustness/parity_metrics/`. Named profiles gate mean KL, p95 KL, and cosine
similarity. Max KL and absolute logit differences remain diagnostics, allowing a single extreme token to remain
visible without making the default gate as unstable as max KL.
Every comparison reports mean, p95, and max per-token `KL(reference || candidate)`; mean, p95, and max per-token
Jensen-Shannon divergence (natural log, bounded by `ln(2)`); whole-tensor cosine similarity; and mean/max absolute
logit difference. The full record is printed as `CHECKPOINT_PARITY_METRICS <json>` and saved under
`<checkpoint_dir>/.checkpoint_robustness/parity_metrics/`. Named profiles gate mean KL, p95 KL, and cosine similarity.
JSD, max KL, and absolute logit differences remain diagnostics, allowing their usefulness to be evaluated without
changing pass/fail policy. Record schema version 2 adds `mean_jsd`, `p95_jsd`, and `max_jsd` under `metrics`; existing
version 1 fields retain their meaning.

Each vanilla-HF reference is forwarded twice through the same loaded model. The resulting `hf_source_self_repeat`
or `hf_export_self_repeat` record is informational and distinguishes cross-framework drift from an unstable reference.
Expand Down Expand Up @@ -214,7 +216,7 @@ parity_threshold_overrides:
```

Supported metric names are `mean_kl`, `p95_kl`, and `cosine_similarity`. Numeric overrides are exceptional calibration
escape hatches, not additional profiles. Max KL remains diagnostic and cannot be overridden.
escape hatches, not additional profiles. JSD and max KL remain diagnostic and cannot be overridden.

Legacy positive `check_*` controls, generic numeric cosine fields, and max-KL threshold fields are no longer accepted.
All live recipes use default-on phases, semantic `skip_*` controls, and named profiles. The optional structured
Expand Down
4 changes: 2 additions & 2 deletions tests/ci_tests/scripts/config_templates/ci_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,8 @@ fixture_keys:
- hf_reload_timeout_seconds
- hf_source_post_load_dequantize
# Full-logit parity defaults to 2048 tokens from the repository's fixed
# long-form document. Named profiles gate mean and p95 KL; all metrics are
# emitted for calibration.
# long-form document. Named profiles gate mean KL, p95 KL, and cosine.
# Max KL, JSD, and absolute-difference metrics are report-only.
- parity_sequence_length
- parity_tolerance_profile
# Optional source_load, automodel_reload, hf_reload, or cross_tp profile
Expand Down
18 changes: 18 additions & 0 deletions tests/functional_tests/checkpoint_robustness/parity_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ class _ParityMetrics:
mean_kl: float
p95_kl: float
max_kl: float
mean_jsd: float
p95_jsd: float
max_jsd: float
cosine_similarity: float
mean_absolute_logit_difference: float
max_absolute_logit_difference: float
Expand Down Expand Up @@ -139,6 +142,7 @@ def _compute_parity_metrics(
raise ValueError("Cannot compare empty logit tensors")

kl_chunks: list[torch.Tensor] = []
jsd_chunks: list[torch.Tensor] = []
absolute_difference_sum = 0.0
max_absolute_difference = 0.0
dot_product = 0.0
Expand All @@ -160,6 +164,14 @@ def _compute_parity_metrics(
token_kl = (reference_probs * (reference_log_probs - candidate_log_probs)).sum(dim=-1)
kl_chunks.append(token_kl.cpu())

mixture_log_probs = torch.logaddexp(reference_log_probs, candidate_log_probs) - math.log(2.0)
candidate_probs = candidate_log_probs.exp()
token_jsd = 0.5 * (
(reference_probs * (reference_log_probs - mixture_log_probs)).sum(dim=-1)
+ (candidate_probs * (candidate_log_probs - mixture_log_probs)).sum(dim=-1)
)
jsd_chunks.append(token_jsd.clamp(min=0.0, max=math.log(2.0)).cpu())

absolute_difference = (reference_chunk - candidate_chunk).abs()
absolute_difference_sum += absolute_difference.sum(dtype=torch.float64).item()
max_absolute_difference = max(max_absolute_difference, absolute_difference.max().item())
Expand All @@ -170,6 +182,9 @@ def _compute_parity_metrics(
per_token_kl = torch.cat(kl_chunks)
if not bool(torch.isfinite(per_token_kl).all()):
raise ValueError("KL divergence contains non-finite values")
per_token_jsd = torch.cat(jsd_chunks)
if not bool(torch.isfinite(per_token_jsd).all()):
raise ValueError("Jensen-Shannon divergence contains non-finite values")

norm_product = math.sqrt(reference_squared_norm * candidate_squared_norm)
if norm_product == 0.0:
Expand All @@ -183,6 +198,9 @@ def _compute_parity_metrics(
mean_kl=per_token_kl.mean().item(),
p95_kl=torch.quantile(per_token_kl, 0.95).item(),
max_kl=per_token_kl.max().item(),
mean_jsd=per_token_jsd.mean().item(),
p95_jsd=torch.quantile(per_token_jsd, 0.95).item(),
max_jsd=per_token_jsd.max().item(),
cosine_similarity=cosine_similarity,
mean_absolute_logit_difference=absolute_difference_sum / reference_logits.numel(),
max_absolute_logit_difference=max_absolute_difference,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -528,7 +528,7 @@ def _compare_logits(
active_failures = _parity_failures(metrics, active_profile_thresholds)
threshold_mode = "profile_with_numeric_overrides" if uses_threshold_overrides else "profile"
payload = {
"schema_version": 1,
"schema_version": 2,
"parity_document_sha256": _PARITY_DOCUMENT_SHA256,
"phase": policy.phase,
"comparison": policy.comparison,
Expand Down
34 changes: 33 additions & 1 deletion tests/unit_tests/ci_tests/test_checkpoint_parity_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import math

import pytest
import torch
import torch.nn.functional as F
Expand All @@ -38,6 +40,9 @@ def test_identical_logits_have_zero_divergence_and_unit_cosine():
assert metrics.mean_kl == pytest.approx(0.0, abs=1e-8)
assert metrics.p95_kl == pytest.approx(0.0, abs=1e-8)
assert metrics.max_kl == pytest.approx(0.0, abs=1e-8)
assert metrics.mean_jsd == pytest.approx(0.0, abs=5e-8)
assert metrics.p95_jsd == pytest.approx(0.0, abs=5e-8)
assert metrics.max_jsd == pytest.approx(0.0, abs=5e-8)
assert metrics.cosine_similarity == pytest.approx(1.0)
assert metrics.mean_absolute_logit_difference == 0.0
assert metrics.max_absolute_logit_difference == 0.0
Expand All @@ -49,13 +54,22 @@ def test_full_logit_metrics_match_direct_reference_computation():
reference_flat = reference.reshape(-1, 3).float()
candidate_flat = candidate.reshape(-1, 3).float()
reference_log_probs = F.log_softmax(reference_flat, dim=-1)
expected_kl = (reference_log_probs.exp() * (reference_log_probs - F.log_softmax(candidate_flat, dim=-1))).sum(-1)
candidate_log_probs = F.log_softmax(candidate_flat, dim=-1)
expected_kl = (reference_log_probs.exp() * (reference_log_probs - candidate_log_probs)).sum(-1)
mixture_log_probs = torch.logaddexp(reference_log_probs, candidate_log_probs) - math.log(2.0)
expected_jsd = 0.5 * (
(reference_log_probs.exp() * (reference_log_probs - mixture_log_probs)).sum(-1)
+ (candidate_log_probs.exp() * (candidate_log_probs - mixture_log_probs)).sum(-1)
)

metrics = _compute_parity_metrics(reference, candidate, chunk_tokens=1)

assert metrics.mean_kl == pytest.approx(expected_kl.mean().item(), rel=1e-6, abs=1e-8)
assert metrics.p95_kl == pytest.approx(torch.quantile(expected_kl, 0.95).item(), rel=1e-6, abs=1e-8)
assert metrics.max_kl == pytest.approx(expected_kl.max().item(), rel=1e-6, abs=1e-8)
assert metrics.mean_jsd == pytest.approx(expected_jsd.mean().item(), rel=1e-6, abs=1e-8)
assert metrics.p95_jsd == pytest.approx(torch.quantile(expected_jsd, 0.95).item(), rel=1e-6, abs=1e-8)
assert metrics.max_jsd == pytest.approx(expected_jsd.max().item(), rel=1e-6, abs=1e-8)
assert metrics.cosine_similarity == pytest.approx(
F.cosine_similarity(reference.flatten(), candidate.flatten(), dim=0).item(), rel=1e-6
)
Expand All @@ -74,6 +88,23 @@ def test_p95_is_stable_against_a_single_token_outlier_while_max_remains_diagnost
assert metrics.mean_kl > 0.0
assert metrics.p95_kl == pytest.approx(0.0, abs=1e-8)
assert metrics.max_kl > 1.0
assert metrics.mean_jsd > 0.0
assert metrics.p95_jsd == pytest.approx(0.0, abs=1e-8)
assert metrics.max_jsd > 0.0


def test_jsd_is_symmetric_and_bounded_while_kl_remains_directional():
reference = torch.tensor([[[math.log(0.9), math.log(0.1)]]])
candidate = torch.tensor([[[math.log(0.5), math.log(0.5)]]])

forward = _compute_parity_metrics(reference, candidate)
reverse = _compute_parity_metrics(candidate, reference)

assert forward.mean_kl != pytest.approx(reverse.mean_kl)
assert forward.mean_jsd == pytest.approx(reverse.mean_jsd, rel=1e-6, abs=1e-8)
assert forward.p95_jsd == pytest.approx(reverse.p95_jsd, rel=1e-6, abs=1e-8)
assert forward.max_jsd == pytest.approx(reverse.max_jsd, rel=1e-6, abs=1e-8)
assert 0.0 <= forward.max_jsd <= math.log(2.0)


def test_metric_results_do_not_depend_on_chunk_size():
Expand Down Expand Up @@ -219,6 +250,7 @@ def test_structured_threshold_overrides_accept_partial_gates_for_every_compariso
[
({"unknown": {"mean_kl": 0.01}}, "Unknown parity_threshold_overrides comparisons"),
({"source_load": {"max_kl": 0.01}}, "Unknown parity_threshold_overrides.source_load metrics"),
({"source_load": {"mean_jsd": 0.01}}, "Unknown parity_threshold_overrides.source_load metrics"),
({"hf_reload": {"mean_kl": "0.01"}}, "hf_reload.mean_kl must be numeric"),
({"cross_tp": {"cosine_similarity": 2.0}}, "cosine_similarity threshold override"),
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1407,7 +1407,7 @@ def test_compare_logits_persists_machine_readable_metrics(tmp_path):

assert failure is None
payload = json.loads((tmp_path / "parity_metrics/phase_2_automodel_model_reload.json").read_text())
assert payload["schema_version"] == 1
assert payload["schema_version"] == 2
assert payload["parity_document_sha256"] == _PARITY_DOCUMENT_SHA256
assert payload["threshold_mode"] == "profile"
assert payload["passed"] is True
Expand All @@ -1417,6 +1417,9 @@ def test_compare_logits_persists_machine_readable_metrics(tmp_path):
assert payload["candidate_logits"] == {"dtype": "torch.float32", "shape": [1, 2, 2]}
assert payload["metrics"]["token_count"] == 2
assert payload["metrics"]["mean_kl"] == pytest.approx(0.0, abs=1e-8)
assert payload["metrics"]["mean_jsd"] == pytest.approx(0.0, abs=5e-8)
assert payload["metrics"]["p95_jsd"] == pytest.approx(0.0, abs=5e-8)
assert payload["metrics"]["max_jsd"] == pytest.approx(0.0, abs=5e-8)


def test_compare_logits_marks_skipped_gate_as_informational(tmp_path):
Expand Down
5 changes: 4 additions & 1 deletion tests/unit_tests/ci_tests/test_config_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -553,7 +553,10 @@ def test_vlm_checkpoint_robustness_recipes_resolve(tmp_path, recipe_path):
if "/mistral4/" in recipe_path:
assert robustness["hf_source_post_load_dequantize"] is True
assert "parity_tolerance_profile" not in robustness
assert robustness["parity_tolerance_profile_overrides"] == {"hf_reload": "relaxed"}
assert robustness["parity_tolerance_profile_overrides"] == {
"automodel_reload": "relaxed",
"hf_reload": "relaxed",
}
for key in (
"kl_threshold",
"source_load_kl_threshold",
Expand Down
Loading