Skip to content

Commit 03fcd09

Browse files
tarekziadeclaude
andcommitted
[contracts proof] Probe: fixture contracts on the PR CI runner and image
Phase 1 of the model integration contracts proof: runs the eight fixture contracts on aws-m8i-l-cache in huggingface/transformers-torch-light, on Transformers 8625785 (the baselines' build), with the image's packages and then with the fleet lock's pins. Triggered only by pushes to this branch. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 90ef040 commit 03fcd09

49 files changed

Lines changed: 34196 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
name: Contracts probe
2+
3+
# Phase 1 of the model integration contracts proof
4+
# (huggingface/model-integration-contracts, docs/transformers-ci-proof.md):
5+
# do the cpu-x86_64 fixture baselines hold on the PR CI runner group and image?
6+
# Runs the eight fixture contracts on Transformers 8625785e (the build the
7+
# baselines were recorded on) twice: with the image's packages as a PR job
8+
# would have them, then with the fleet lock's pinned packages. Non-blocking,
9+
# push to this branch only; nothing else in PR CI changes.
10+
11+
on:
12+
push:
13+
branches:
14+
- contracts-proof
15+
16+
permissions:
17+
contents: read
18+
19+
jobs:
20+
probe:
21+
runs-on:
22+
group: aws-m8i-l-cache
23+
timeout-minutes: 60
24+
container:
25+
image: huggingface/transformers-torch-light
26+
options: "--shm-size=16gb"
27+
env:
28+
TRANSFORMERS_SHA: 8625785e7e3e138a328ec2e678dc198e356e7c6c
29+
DOC_BUILDER_SHA: c69e96c087165ba49b1788e0c3464654dd60253c
30+
# Not the shared /mnt/cache: PR jobs mount it read-only, and fixtures and contracts are downloaded.
31+
HF_HOME: /tmp/hf_home
32+
HF_HUB_DISABLE_PROGRESS_BARS: "1"
33+
PYTHONUNBUFFERED: "1"
34+
OUT: /tmp/out
35+
LOCK: utils/contracts/transformers/tests/contracts/fleet.lock
36+
defaults:
37+
run:
38+
shell: bash
39+
steps:
40+
- uses: actions/checkout@v4
41+
42+
- name: Host
43+
run: |
44+
mkdir -p "$OUT"
45+
{
46+
echo "cpu: $(lscpu | sed -n 's/^Model name: *//p')"
47+
echo "cores: $(nproc)"
48+
echo "flags: $(grep -o -w -E 'avx2|avx512f|avx512_bf16|amx_bf16|amx_tile' /proc/cpuinfo | sort -u | tr '\n' ' ')"
49+
echo "python: $(python --version 2>&1) at $(command -v python)"
50+
echo "uv: $(uv --version 2>&1)"
51+
free -g | sed -n 2p
52+
} | tee "$OUT/host.txt"
53+
uv pip freeze > "$OUT/image-freeze.txt" 2>/dev/null || python -m pip freeze > "$OUT/image-freeze.txt"
54+
55+
- name: Install Transformers and runner deps (image packages)
56+
run: |
57+
start=$SECONDS
58+
git init --quiet /tmp/transformers
59+
git -C /tmp/transformers fetch --quiet --depth 1 https://github.com/huggingface/transformers "$TRANSFORMERS_SHA"
60+
git -C /tmp/transformers checkout --quiet FETCH_HEAD
61+
uv pip install /tmp/transformers
62+
uv pip install jsonschema "git+https://github.com/huggingface/doc-builder@$DOC_BUILDER_SHA"
63+
echo "install_as_is_seconds: $((SECONDS - start))" | tee -a "$OUT/host.txt"
64+
uv pip freeze > "$OUT/as-is-freeze.txt"
65+
66+
- name: Run fixture contracts (image packages)
67+
continue-on-error: true
68+
run: |
69+
for variant in default eager; do
70+
start=$SECONDS
71+
python utils/contracts/run.py --framework transformers --fixtures-only --require-all --variant "$variant" \
72+
--output "$OUT/as-is-$variant.jsonl" > /dev/null 2> "$OUT/as-is-$variant.log" || true
73+
echo "as-is $variant: $(tail -1 "$OUT/as-is-$variant.log") wall $((SECONDS - start)) s" | tee -a "$OUT/host.txt"
74+
done
75+
76+
- name: Install the fleet lock's pinned packages
77+
run: |
78+
start=$SECONDS
79+
pins() { python -c 'import json, sys
80+
env = json.load(open(sys.argv[1]))["environment"]
81+
torch = {"torch", "torchvision", "torchaudio"}
82+
print(" ".join(f"{n}=={v}" for n, v in env.items() if n != "python" and (n in torch) == (sys.argv[2] == "torch")))' "$LOCK" "$1"; }
83+
uv pip install $(pins torch) --index-url https://download.pytorch.org/whl/cpu
84+
uv pip install $(pins other)
85+
echo "install_pinned_seconds: $((SECONDS - start))" | tee -a "$OUT/host.txt"
86+
uv pip freeze > "$OUT/pinned-freeze.txt"
87+
88+
- name: Run fixture contracts (pinned packages)
89+
continue-on-error: true
90+
run: |
91+
for variant in default eager; do
92+
start=$SECONDS
93+
python utils/contracts/run.py --framework transformers --fixtures-only --require-all --variant "$variant" \
94+
--output "$OUT/pinned-$variant.jsonl" > /dev/null 2> "$OUT/pinned-$variant.log" || true
95+
echo "pinned $variant: $(tail -1 "$OUT/pinned-$variant.log") wall $((SECONDS - start)) s" | tee -a "$OUT/host.txt"
96+
done
97+
98+
- name: Summary
99+
if: always()
100+
run: |
101+
{
102+
echo "### Contracts probe on aws-m8i-l-cache"
103+
echo
104+
echo '```'
105+
cat "$OUT/host.txt"
106+
echo '```'
107+
echo
108+
python - <<'EOF'
109+
import json, os, pathlib
110+
print("| Mode | Variant | Target | Platform | Status | First failure |")
111+
print("| --- | --- | --- | --- | --- | --- |")
112+
for mode in ("as-is", "pinned"):
113+
for path in sorted(pathlib.Path(os.environ["OUT"]).glob(f"{mode}-*.jsonl")):
114+
for line in path.read_text().splitlines():
115+
r = json.loads(line)
116+
first = (r.get("failures") or [""])[0].replace("|", "/").replace("\n", " ")[:160]
117+
print(f"| {mode} | {r['variant']} | {r['target']} | {r.get('platform', '')} | {r['status']} | {first} |")
118+
EOF
119+
} >> "$GITHUB_STEP_SUMMARY"
120+
121+
- uses: actions/upload-artifact@v4
122+
if: always()
123+
with:
124+
name: contracts-probe
125+
path: /tmp/out/

‎utils/contracts/README.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Model integration contracts (proof)
2+
3+
Vendored from [huggingface/model-integration-contracts](https://github.com/huggingface/model-integration-contracts) (`example/`, commit b303626) for the Transformers CI proof (phase 1: does the fixture baseline hold on the PR CI runner and image?). Do not edit here; change the source repository and re-vendor.
4+
5+
- `run.py`, `select_contracts.py`, `patches.py`, `schemas/`: the contract runner.
6+
- `transformers/tests/contracts/fleet.lock`: pinned contracts, fixtures, checkpoints, and environment; the contracts and fixtures are downloaded from the Hub (`hf-internal-testing/*`) by revision and checked against their hashes.
7+
- `transformers/tests/contracts/expectations/<contract>/{cpu-x86_64,cpu-x86_64-bf16}/`: reviewed baselines.
8+
9+
Run by `.github/workflows/contracts-probe.yml` on pushes to `contracts-proof`.

‎utils/contracts/patches.py‎

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
"""Local workarounds for known Transformers bugs, applied by the runner before contracts run.
2+
3+
A framework's fleet lock lists the patches it needs under `local_patches`, with
4+
the reason. The runner applies them in its own process and in every isolated
5+
contract process, and records their names in each result's environment, so a
6+
baseline recorded with a workaround says so. Remove a patch, and re-record the
7+
affected baselines, once the fix is in the pinned Transformers build.
8+
"""
9+
10+
import importlib
11+
import inspect
12+
13+
# Audio models whose `masked_spec_embed` (SpecAugment's mask vector) has no
14+
# `_init_weights` branch. Checkpoints that lack the tensor get uninitialized
15+
# memory on load; HuBERT, SEW-D, SpeechT5, and Wav2Vec2-BERT already handle it.
16+
MASKED_SPEC_EMBED_MODELS = {
17+
"wav2vec2": "Wav2Vec2PreTrainedModel",
18+
"wav2vec2_conformer": "Wav2Vec2ConformerPreTrainedModel",
19+
"data2vec": "Data2VecAudioPreTrainedModel",
20+
"wavlm": "WavLMPreTrainedModel",
21+
"unispeech": "UniSpeechPreTrainedModel",
22+
"unispeech_sat": "UniSpeechSatPreTrainedModel",
23+
"sew": "SEWPreTrainedModel",
24+
}
25+
26+
27+
def masked_spec_embed_init():
28+
"""Initialize masked_spec_embed like HuBERT does: init.uniform_ in _init_weights."""
29+
import torch
30+
from transformers import initialization as init
31+
32+
patched = []
33+
for model_type, class_name in MASKED_SPEC_EMBED_MODELS.items():
34+
module_name = f"transformers.models.{model_type}.modeling_{'data2vec_audio' if model_type == 'data2vec' else model_type}"
35+
try:
36+
cls = getattr(importlib.import_module(module_name), class_name)
37+
except (ImportError, AttributeError):
38+
continue
39+
original = cls._init_weights
40+
if getattr(original, "_mic_patched", False):
41+
continue
42+
if "masked_spec_embed" in inspect.getsource(original):
43+
continue # fixed upstream (#49386): a second init.uniform_ would draw different values
44+
45+
def _init_weights(self, module, original=original):
46+
original(self, module)
47+
if isinstance(getattr(module, "masked_spec_embed", None), torch.nn.Parameter):
48+
init.uniform_(module.masked_spec_embed)
49+
50+
_init_weights._mic_patched = True
51+
cls._init_weights = _init_weights
52+
patched.append(class_name)
53+
return patched
54+
55+
56+
def whisper_decode_tensor_batch():
57+
"""Decode a 2-D tensor with timestamps per sequence, as decode already does for a list of lists (#49403).
58+
59+
WhisperTokenizer.decode only treats its input as a batch when it is a list of
60+
lists, so batch_decode(model.generate(...), decode_with_timestamps=True) merges
61+
the rows into one string (one token per row) or raises (longer rows).
62+
"""
63+
try:
64+
from transformers.models.whisper.tokenization_whisper import WhisperTokenizer
65+
except ImportError:
66+
return []
67+
original = WhisperTokenizer.decode
68+
if getattr(original, "_mic_patched", False):
69+
return []
70+
71+
def decode(self, token_ids, *args, original=original, **kwargs):
72+
if kwargs.get("decode_with_timestamps") and getattr(token_ids, "ndim", 0) == 2:
73+
token_ids = token_ids.tolist()
74+
return original(self, token_ids, *args, **kwargs)
75+
76+
decode._mic_patched = True
77+
WhisperTokenizer.decode = decode
78+
return ["WhisperTokenizer"]
79+
80+
81+
def config_field_generation_params():
82+
"""Do not count a config's own declared fields as user-set generation parameters (#49456, from #48282).
83+
84+
generate() raises when model.config holds generation parameters. It used to
85+
skip every field of the config class; since #48282 it skips only fields
86+
whose default is not None, so WhisperConfig.suppress_tokens (default None,
87+
set in the OpenAI checkpoints' config.json) makes every generate() raise.
88+
Restore the skip for every declared field.
89+
"""
90+
import dataclasses
91+
92+
from transformers import PreTrainedConfig
93+
94+
original = PreTrainedConfig._get_generation_parameters
95+
if getattr(original, "_mic_patched", False) or not hasattr(PreTrainedConfig, "default_config_fields"):
96+
return [] # already patched, or a build before the regression
97+
98+
def _get_generation_parameters(self, original=original):
99+
params = original(self)
100+
if dataclasses.is_dataclass(self):
101+
declared = {f.name for f in dataclasses.fields(self)}
102+
params = {k: v for k, v in params.items() if k not in declared}
103+
return params
104+
105+
_get_generation_parameters._mic_patched = True
106+
PreTrainedConfig._get_generation_parameters = _get_generation_parameters
107+
return ["PreTrainedConfig"]
108+
109+
PATCHES = {
110+
"masked-spec-embed-init": masked_spec_embed_init,
111+
"whisper-decode-tensor-batch": whisper_decode_tensor_batch,
112+
"config-field-generation-params": config_field_generation_params,
113+
}
114+
115+
116+
def apply(names):
117+
"""Apply the named patches; return {name: what was patched}."""
118+
unknown = sorted(set(names) - set(PATCHES))
119+
if unknown:
120+
raise SystemExit(f"unknown local patches: {unknown}")
121+
return {name: PATCHES[name]() for name in names}

0 commit comments

Comments
 (0)