* Use synthetic CC0 assets instead of third-party licensed media in tests * work file * Fix `_format_py_obj` crashing on values it has no branch for `_format_py_obj` assigns `output` inside a `str`/`int`/`float`/`list`/`tuple`/`dict` chain with no trailing `else`, so any other type raises `UnboundLocalError: local variable 'output' referenced before assignment`. A `torch.Size` hits this: it is a `tuple` subclass, but the branch tests `__class__.__name__`, which is `"Size"`. Any test whose captured assertion argument is a shape then loses its report — the CI value capture dies inside the reporting helper instead of printing the actual value, which is exactly what the failing test needs. Fall back to `repr`, which for `torch.Size` gives the copy-pastable `torch.Size([1, 736, 736])`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Refresh integration expectations recorded against the old third-party assets Values taken from the A10 run-slow run on this branch (https://github.com/huggingface/transformers/actions/runs/34103194667), read out of each job's `captured_info.txt`. Single-GPU and multi-GPU agreed everywhere they overlapped. Refreshed: aya_vision, chameleon, chinese_clip, cohere2_vision, cosmos3_omni, diffusion_gemma (the two minified `generate_with_image_batched*` tests), fast_vlm, higgs_audio_v2, idefics2, instructblip. For instructblip both assertions are updated: the run stopped at the token-id comparison, so the decoded-text expectations come from running the new ids back through each checkpoint's tokenizer. Where every key of an `Expectations` dict was recorded against the old asset, the dict collapses to a single `(None, None)`: only cuda 8.x could be re-measured, and a stale `("xpu", 3)` / `("cuda", 7)` value is worse than falling back to the one value we verified. chmv2 keeps its TODO — the new asset changes the output resolution (448 -> 736), and the depth slice cannot be re-recorded until a run gets past the shape assertion. Left untouched, per the run's own `git bisect` results: edgetam (timm `FeatureListNet.forward()` signature), the four diffusion_gemma tests that fail identically on the base commit, and cohere2_vision's OOM / unchanged-fixture drift / local-path-checkpoint failures. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Fix two more `captured_info` corruptions: backslashes and integer-tensor dtype Both silently produced a report that cannot be read back, which is the whole point of the capture: * `_quote_string` escapes quotes but not backslashes, so a value containing one emits a literal that means something else -- and a value *ending* in one swallows the closing quote, leaving an unterminated literal. mllama's decoded output ends in `\`, so its captured value was unusable. * `_format_tensor` strips `tensor(` and `)` but a non-default dtype is repr'd as a trailing kwarg, e.g. `tensor([83, 362], dtype=torch.int16)`. Stripping only the wrapper leaves `dtype=torch.int16` stranded *inside* the literal. Every integer tensor hit this (the pp_ocrv5/v6 detection boxes). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Refresh the remaining expectations from the full-coverage GPU run Round 1 only exercised 16 models -- `get_pr_run_slow_jobs.py` caps its suggestion at `MAX_NUM_JOBS_TO_SUGGEST = 16` and sorts alphabetically, so everything from `internvl` on was never run. This is the second pass, over all 58 model dirs plus `pipelines` and `cli` (https://github.com/huggingface/transformers/actions/runs/34150746428). Of the 173 failures there, the run's own `git bisect` attributes 87 to this branch; 41 fail identically on the base commit (edgetam's timm signature mismatch, four diffusion_gemma tests, cohere2_vision's local-path checkpoints) and 15 are flaky. 83 of the 87 had a captured value; all of those are refreshed here. Two notes on how the values were placed, since both went wrong on the first attempt: * Targets are resolved by enclosing test function *and* the assertion's line. A file usually holds several `EXPECTED_*` blocks and a test that checks several outputs rebinds the same name once per output, so "first match in the file" and "last assignment in the function" both write into the wrong test. This also fixes a bad round-1 edit that landed the multi-speaker value in higgs_audio_v2's `test_single_speaker_smart_voice`. * Which captured side is the expectation is decided structurally, not from the argument name: `assertEqual(EXPECTED, computed)` puts the stale expectation in the slot named `first`. Three tests are written that way. Where every key of an `Expectations` dict was recorded against the old asset the dict collapses to a single `(None, None)`; only cuda 8.x could be re-measured, and a stale xpu/rocm value is worse than the one value we verified. Still marked TODO, awaiting the next run: cosmos3_omni's batched test (OOM), diffusion_gemma's image-token count and qwen3_asr's timestamps (assertions the capture does not instrument), qwen2_5_vl's second batch element and pp_ocrv6_medium_det's boxes (never reached), and instructblip plus prompt_depth_anything, whose assets were just regenerated: * `lavis_confusing_pictures.jpg` was flat geometric shapes standing in for the LAVIS "man ironing on a taxi" photo, and instructblip-flan-t5 answered "What is unusual about this image?" by repeating one sentence until `max_length`. * `promptda_arkit_depth.png` was saved as RGB, but a prompt-depth map is a single-channel metric image -- PromptDepthAnything failed outright with "expected input[1, 3, 27, 36] to have 1 channels". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * qwen3_asr: use real Mandarin speech instead of a synthetic sine tone `audio/mandarin_asr.wav` in the synthetic-assets repo is a 520 Hz sine tone, so Qwen3-ASR transcribed it as nothing: the expected tokens for the second batch element collapsed to `[11528, 2240, 151704, 151645, ...]` -- language tag, then immediate EOS. The test stayed deterministic but stopped exercising transcription at all. Point both references at `mandarin_voxcpm_zh.wav`, mirrored into `hf-internal-testing/dummy-audio-samples` from `adarshxs/voxcpm2-native-generated-audio-user-ref` (`ref_zh.wav`): 4s of apache-2.0 Mandarin TTS, transcript "你好世界,人工智能正在改变我们的生活方式。" per the source's `metadata.json`. Same source repo and licence as the already-GREEN `zs_short.wav`/`zs_medium.wav` there, and recorded in that repo's `provenance.jsonl` with the same fields. Both fixture tests need re-recording against the new clip, so the batched one is marked again; `test_fixture_timestamps_batched` already was. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Repoint the qwen2_audio/qwen3_asr docs at the real Mandarin clip too The previous commit only fixed the test and the CI prefetch list, but five doc snippets still pointed at the sine tone. These are copy-paste examples, so a reader running them would get an empty transcription and reasonably conclude the model was broken. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Collapse two pixel-identical duplicate assets The migration replaced third-party URLs one-for-one, so several assets are the same picture under a name that records which test it came from. Two pairs are *exactly* identical, so pointing the tests at a single copy cannot change a tensor: images/gemma_golden_gate.png == images/dreamstime_golden_gate_flowers.jpg images/invoice_docquery_b.png == images/invoice_docquery_a.png (byte-identical) Both duplicates are deleted from the assets repo and their entries in the manifest now resolve to the survivor, so the old-URL mapping stays complete. Their prefetch entries go too, since the survivors are already listed. Four further pairs are the same content re-encoded rather than identical (mistral3_view / mistral3_statue_of_liberty / mistral3_golden_gate against their originals, and paddle_ocr_demo2 against paddle_general_ocr_001). Collapsing those would take the count from 40 to 36, but they differ by up to 48/255 per channel, so each one risks another expectation re-record and is better done as one deliberate batch than mixed into this refresh. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Collapse the four re-encoded duplicate assets These are the same picture as an existing asset, re-encoded rather than byte-identical, so unlike the previous pair they do shift the input tensor slightly: mistral3_view.jpg -> llava_view.jpg (max 8/255, 3.1% of pixels) mistral3_statue_of_liberty.jpg -> statue_of_liberty.jpg (max 9/255, 2.5%) mistral3_golden_gate.jpg -> dreamstime_golden_gate_flowers.jpg (max 27/255, 9.8%) paddle_ocr_demo2.jpg -> paddle_general_ocr_001.png (max 48/255, 100%) The six tests that consume them are marked for re-recording: two in mistral3, and all four in `PaddleOCRVLIntegrationTest`, whose URL lives in `setUp`. `paddle_ocr_demo2.jpg` also appeared in a paddleocr_vl docstring, so the change went into `modular_paddleocr_vl.py` and `modeling_paddleocr_vl.py` was regenerated from it. With this the assets repo is 36 files, every one referenced and every reference resolving. Two duplicate groups are deliberately kept: `azure_slide_1..4` are 0.998-1.000 correlated but differ in ~2.5% of pixels, and phi4_multimodal's multi-image test wants four distinct slides -- those want regenerating with more variety, not deduplicating. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Refresh expectations from the round 3 GPU run 27 of the 33 round-3 failures that `git bisect` attributed to this branch, across 19 files. Values come from each job's captured_info.txt, not from re-deriving them. Stale `Expectations` dicts collapse to a single `(None, None)` key: every hardware key was recorded against an asset this branch replaces, and only cuda 8.x can be re-measured on this CI, so a stale ("xpu", 3) is worse than the one value we verified. Three pixel_values expectations (qwen2_vl, qwen2_5_omni, qwen3_omni_moe) failed on a bare `assert torch.allclose(...)`, which the capture helper does not instrument. That assertion is CPU-only image-processor preprocessing, so those values are computed off-CI instead; each test's own expected_input_ids reproduces exactly, qwen2_5_omni's slice agrees with qwen2_vl's independently computed one, and each recomputes within the test's own atol=3e-3. Two pre-existing inconsistencies fixed on the way through: - instructblip::test_inference_vicuna_7b had a refreshed token-id expectation next to a stale text expectation, so the two contradicted each other. The ids decode to the new text exactly. - pp_formulanet's expectation held a literal backspace byte, from `\backslash` written in a non-raw string. qwen2_5_vl::test_small_model_integration_test_batch_different_resolutions needed element [1], not element [0] as its TODO claimed: the loop reaches i=1, so [0] already passes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Make the last two blocked expectations reportable, and drop a stray file Three of the open TODOs were not waiting on an asset or a measurement -- they were written as assertion forms the capture helper cannot instrument, so every round reports "AssertionError: False is not true" and no value, and the next round would have reported exactly the same thing: - prompt_depth_anything's two tests used `assertTrue(torch.allclose(a, b, atol=1e-3))`. Now `torch.testing.assert_close(a, b, atol=1e-3, rtol=1e-5)` -- rtol spelled out because that is torch.allclose's default, so the tolerance is unchanged. - diffusion_gemma's image-token check used `assertTrue(a == b)`, which hides both operands. Now `assertEqual`, which also reports them. Round 3 incidentally disproves what these TODOs claimed: prompt_depth_anything asserts `predicted_depth.shape == (1, 756, 1008)` *before* the value check, and that passed, so the single-channel depth map was already in place. The stale asset was never the problem there. Also removes src/transformers/utils/hub.py.save, a 40K editor backup committed by accident in7a367e7. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Refresh the second DQA expectation layer from round 4 The four DocumentQuestionAnswering tests each hold three or four sequential expectation blocks against the same invoice. A test aborts at its first failing assertion, so a round's `captured_info.txt` only ever reports the *first* stale block per test -- round 3 refreshed layer 1, and round 4 (`34213965306`) reached and reported layer 2. Bisect confirms all four as ours; the other 24 pipelines rows in that run are pre-existing and untouched. Round 4 also settles the question left open when these values were first recorded. `INVOICE_URL` moved `invoice_docquery_b.png` -> `_a.png` in1660b7f, and `_b` is now deleted from the Hub, so the two could not be compared. But both were ours: `make_document_assets()` in `.ai/generated_assets/assemble_transformers_synthetic_assets.py` is one deterministic path whose only per-file variable is `title`, and both entries pass "INVOICE". Re-running it reproduces `_a` and `_b` with the same sha256, and that output is byte-identical to the live `_a`. The layer-1 values measured against `_b` were therefore valid, which is why every one of them passed this round. Two blocks still hold the old values and are marked; both need measuring rather than inferring, and `pipelines` is in the next round anyway: - `test_large_model_pt` line 238 -- the batched form - `test_large_model_pt_layoutlm` line 354 -- the `word_boxes` path, image=None `test_large_model_pt_donut` needed nothing: it already asserted `{"answer": "syn-2026-001"}` and passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Two more `_format_py_obj` bugs that corrupt the value it reports Same family as the three already fixed in this helper: each one silently destroys the value the capture report exists to provide. - A 1-element tuple was rendered without its trailing comma, so `(5,)` came out as `(5)` -- which parses back as the int 5, not a tuple. Pasting that into an expectation gives `assertEqual(some.shape, 5)`, an assertion that can never hold. Found because round 4 reported `pp_ocrv6_medium_det`'s `assertEqual(results[0]["scores"].shape, (5,))` expectation as `(5)`. - An empty list, tuple or dict raised `IndexError` from `element_types[0]` in `use_one_line_repr`, taking the whole report down. Plausible input: a detection test that finds nothing -- and detection counts in this branch do change (pp_ocrv6 went 5 -> 4). Both round-trip through `ast.literal_eval` now, including nested cases such as `((1,),)`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Refresh 13 expectations from round 4 Round 4 (`34213965306`) bisect: 20 rows ours, 31 pre-existing, 2 ambiguous, 1 flaky. This lands the 13 ours-rows that produced a usable capture, across 12 files. Every one of round 3's refreshed tests advanced to a later assertion in this run and none failed where it had been refreshed, which confirms two things that had been taken on trust: - the three `expected_pixel_slice` values derived off-CI from the image processor (qwen2_vl 542 -> 550, qwen2_5_omni 653 -> 669, qwen3_omni_moe 773 -> 789 all cleared the pixel assertion), and - the invoice byte-identity argument behind the pipelines values. `prompt_depth_anything` reported real numbers for the first time, which is what converting its `assertTrue(torch.allclose(...))` was for. So did `diffusion_gemma`'s image-token count, now 260; its TODO is removed. Deliberately not refreshed, on the bisect verdict: - `mllama::test_11b_model_integration_multi_image_generate` is pre-existing. It pins the *wrong* answer on purpose -- the comment above it records that the quantized model says "dock" where it should say "stop sign". Round 4 returns the correct stop-sign answer, so refreshing the string would delete a tracked bug and leave the comment contradicting the assertion. Whether the synthetic asset has stopped exercising that bug is a separate question. - `mllama::test_11b_model_integration_batched_generate` is ambiguous, and its new value is padded-batch gibberish ("This image showst's a photo of a person named ...") -- the same objection as `chameleon::test_model_7b_batched`. - `instructblip::test_inference_flant5_xl` is ambiguous. - `diffusion_gemma`'s three `test_minified_*` rows and `test_diffusion_gemma_chat_template` are pre-existing; the model is red on `main`. - `qwen2_vl::test_small_model_integration_test_batch` is flaky (CUDA OOM). - `qwen3_asr::test_fixture_batch_matches` needs a human: both captured sides resolve to computed values, so which one is the expectation is not decidable structurally. `pp_ocrv6_medium_det` gets its scores *shape* only (5 -> 4). The scores tensor still holds five values and the labels shape still asserts `(5,)`, so its TODO stays: like the DQA tests, it peels one assertion per round. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Clear 6 resolved TODOs, and refresh paddleocr_vl's processor expectations Round 4 shows six `# TODO(synthetic-assets)` markers no longer describe anything outstanding. Three guard tests that now simply pass -- cosmos3_omni's batched integration test (it used to OOM), mistral3's `test_mistral3_integration_batched_generate`, and paddleocr_vl's `test_small_model_integration_test_batch`. Three were refreshed from round 4's capture in the preceding commit: mistral3's multi-image test and both prompt_depth_anything tests. paddleocr_vl's remaining failure was `assert 211 == 1389`, a bare assert on `len(inputs.input_ids[0])`. Everything it checks before the generation is processor-only work, so it is reproducible off the CI, and two independent checks say the local run matches this CI: the length comes out 1389, exactly what the run reported, and the `expected_input_ids` first-ten already in the file reproduce unchanged. So the token count and the pixel slice are refreshed, and the three bare asserts become `assertEqual` / `torch.testing.assert_close` so any later drift is reported with a value instead of a bare "False is not true". The same edits apply to `test_small_model_integration_test_flashatt2`, which feeds the same processor the same messages and so asserts the same numbers; it did not run this round for want of flash-attn hardware, and would otherwise have failed identically. `test_small_model_integration_test_batch_flashatt2` loses its marker for the same reason its non-flash twin did. `EXPECTED_DECODED_TEXT` is a generation, not preprocessing, so it still needs a GPU round; both markers now say that rather than blaming the asset. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Two more capture-helper bugs, and instrument assertSequenceEqual `use_one_line_repr` compared `type(obj) is not type(obj[0])`, but on a dict `obj[0]` is a *key* lookup rather than positional, so any dict expectation holding a container value raised `KeyError: 0` and took down the whole `captured_info.txt` report, not just that one value. Use the dict's single value as the element to compare. Latent so far: the DQA expectations are lists of flat dicts, and `word_boxes` is an input, never an expectation. `assertSequenceEqual` was not patched, so it reported nothing at all. That is the assertion `pp_doclayout_v2`/`v3` use for `expected_labels`, whose detection counts change 14 -> 8 and 13 -> 8, so those values were unreachable by any number of GPU rounds. Both verified end to end by running pytest with `patch_testing_methods_to_collect_info()` active and reading the report. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Refresh 7 expectations from round 5 Round 5 (34244551302) brought 7 dirs back green, including all three `expected_pixel_slice` values that were derived off-CI in round 3 -- those now clear the generation assertions past them too. Refreshed: - pp_ocrv6_medium_det: scores 5 -> 4 values, and the `labels` shape (5,) -> (4,). `boxes (4,4,2)` and `scores (4,)` already passed, so three assertions in the test agree on 4 detections. - pp_doclayout_v2 / v3: scores 14 -> 8 and 13 -> 8. - omdet_turbo: only the third of three batched scores moved, matching the fact that exactly one of its three images was repointed. - paddleocr_vl: the OCR text on all four integration tests. Its `_batch` sibling, refreshed in an earlier round, already held the same text. - pipelines DQA x2: `us-001` -> `SYN-2026-001`, which is what the asset generator's deterministic `make_document_assets()` draws. - qwen3_asr `expected_results_batched.json`: the Mandarin row. This fixture had been self-contradictory since round 2, which refreshed its `token_ids` to the sine-tone result (`language None`, empty) but left `transcriptions` at the original clip's sentence. Both halves now decode to each other, and the text matches the transcript recorded in the audio's provenance.jsonl. The two paddleocr_vl `flashatt2` values are mirrored from the eager tests rather than measured: `@require_flash_attn` keeps them out of run_models_gpu, so no round can reach them. Same model, image, prompt, generation args and decode path; the pairs held identical expectations before the asset swap. Noted in the code as wanting a local flash-attn run. `pp_doclayout_v2`/`v3` gain markers for their `expected_labels` next layer, which had none and was unreportable until assertSequenceEqual was instrumented. `qwen3_asr::test_fixture_timestamps_batched`'s marker now records the real blocker: its stale value is an *input* to the forced aligner, so it must be corrected before timestamps can be measured, and `assertAlmostEqual` is not instrumented either. Round 5's bisect matrix died in `actions/download-artifact` with a 403 before bisecting anything, so these rows were filtered on per-test causal evidence instead: the test reads an asset this branch repointed, the failure is an expectation mismatch, and the captured value cross-checks against an already-passing assertion or the asset's known content. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Refresh 3 expectations from round 6 Round 6 (34262517871) validated every round-5 refresh. Four went green (pp_ocrv6_medium_det, paddleocr_vl, both pipelines DQA rows, and the qwen3_asr JSON fixture); the other three advanced past the assertion they were refreshed at, which is the progress signal rather than "still failing": - omdet_turbo: scores held, now `expected_slice_boxes`. Only the third of three rows changed, the same one-of-three-images signature as its scores. - pp_doclayout_v2 / v3: scores held, now `expected_labels` -- reported for the first time, since assertSequenceEqual had never been instrumented. Both give [6, 22, 22, 22, 21, 22, 22, 22]: 8 entries matching the 8 detections, and identical between v2 and v3, as expected for one asset through one model family. Bisect was healthy again this round (round 5's 403 was transient) and confirms the substitute filter used there made no wrong calls: every row it admitted has gone green or advanced, and every row it excluded is still pre-existing. Markers retargeted onto `expected_slice_boxes` in v2/v3, the one remaining stale block in each -- its four rows are the first four of the old detection set. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Never let the capture helper replace a test's own failure Instrumenting `assertSequenceEqual` in the previous commit was unsafe: it is not a leaf. `unittest`'s `assertListEqual` delegates to it as `self.assertSequenceEqual(list1, list2, msg, seq_type=list)`, so a failing `assertListEqual` enters the patched `assertSequenceEqual` with four positional args while the parsed call-site expressions still describe the outer call's two. `positional_args[arg_pos]` then raised `IndexError`, and since the patch wraps the assertion, that replaced the test's real failure and took down the whole `captured_info.txt` report. `omdet_turbo::test_inference_object_detection_head_batched` produced no report at all because of it. Guarding the `IndexError` alone would be worse than the crash: the indices stay off by one, so `seq1` would be labelled with `seq2`'s expression and the report would attribute values to the wrong names, silently. So reconcile the counts and report nothing when the call-site expressions cannot be matched to this call, and skip writing a record with no values. Direct `assertSequenceEqual` calls still capture correctly; delegated ones are skipped, and the outer `assertListEqual` -- already patched -- reports the same values with the right expressions. Note for reproducing: this needs `CI=true`. Without it the inner patched method swallows the `AssertionError` and defers it to `tearDown`, so the outer patched method never sees it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Refresh 3 expectations from round 7 Round 7 (34268218309) advanced all three rows again, each past the assertion refreshed in round 6: - pp_doclayout_v2 / v3: `expected_slice_boxes`, the last stale block in each. Scores and labels from rounds 5-6 both held. - omdet_turbo: `expected_text_labels`, third of three entries, 6 -> 7 labels. Its value came from the unittest diff in `test_outputs.txt` rather than a capture, because the report died on the IndexError fixed in the previous commit. The diff pins both sides and its context lines prove entries 1-2 are unchanged, and the count is independently corroborated: the `assertListEqual([len(result["scores"]) ...], [4, 4, 7])` above it passed, so image 3 really has 7 detections against a stale expectation listing 6. Markers are down to 2, and neither is a refresh job: qwen3_asr's batched timestamps need their aligner input corrected first, and instructblip is waiting on the lavis_confusing_pictures.jpg re-record. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Refresh pp_doclayout_v3's polygon points from round 8 Round 8 (34320893899) brought omdet_turbo and pp_doclayout_v2 green on both devices -- every expectation in those two files is now refreshed. pp_doclayout_v3 advanced once more, into `expected_slice_polygon_points`, a block no earlier round had reached because it only exists in v3. The new value needs no further round to trust: `expected_slice_boxes[1]`, refreshed last round and now passing, is [115.5250, 223.8100, 479.4319, 261.9810], and the polygon is exactly that box's four corners as integers, within the test's own atol=2. It is also a shape change, (5,2) -> (4,2), so the old 5-point polygon was not merely reordered. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Refresh test_load_img_url for the synthetic invoice's size `tests/utils/test_image_utils.py` imports `INVOICE_URL` from the document QA pipeline tests, and this branch repointed that constant from the impira/docquery invoice to the synthetic `invoice_docquery_a.png`. The synthetic asset is 900x1200, so `np.array(img).shape` is (1200, 900, 3) where the test still asserted the original's (1061, 750, 3). Measured directly off the Hub; this is pure `load_image`, so it needs no GPU run. Nine slow-CI rounds could not have caught this: `run-slow` job names are directories, so a round only ever covers tests/models/* plus pipelines and cli. Tests under tests/utils surface in PR CI's tests_non_model shards instead. A sweep for other instances is clean. Of the 44 cross-test imports in the repo, this is the only one pulling from a file this PR changed; the rest import test_modeling_common, test_tokenization_common or test_audio_utils, none of which this branch touches. The four non-model test files referencing the assets directly (cli/test_serve.py and three pipelines files) are already in the rounds. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Refresh 10 expectations the AMD round measured Round 10 (34352147816) was the first full-coverage AMD round, and its bisect blames this branch for 11 rows. They split two ways. Five are `@require_flash_attn` tests no NVIDIA round can reach, so their values were never refreshed for the synthetic images at all — MI300 is the only CI that runs them. In each case ROCm returns exactly what the eager sibling already expects, so a single value serves both devices rather than a new hardware key: qwen2_vl batch_flashatt2, batch_wo_image_flashatt2 qwen2_5_vl batch_flashatt2, batch_wo_image_flashatt2 (element 0) qwen2_5_omni batch_flashatt2 (collapses cuda 8.6 / rocm 9.4, both stale) The other five are genuine cuda/ROCm divergence that the new images exposed: collapsing those dicts to `(None, None)` left MI300 reading an A10-recorded value that used to agree and no longer does. They get a `("rocm", (9, 4))` key. fast_vlm test_small_model_integration_test (plain str -> Expectations) internvl batched_generate_multi_image, interleaved_images_videos mistral3 mistral3_integration_batched_generate_multi_image qwen3_vl test_small_model_integration_test AMD's reusable workflow lives in `hf-workflows` and does not run `utils/get_test_reports.py`, so there is no `captured_info.txt` for any of this — every value here came out of the traceback instead. Each was verified by feeding the reconstruction back through `unittest.util._common_shorten_repr` and requiring it to reproduce the report's abbreviation byte for byte, character counts included. That check is what confirms the two qwen2_vl rows, whose diffs the report truncated at 1408 and 881 characters. The 16 remaining AMD rows fail on `main` too and are left alone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Drop llava_next's comparison against the original image's dumps `test_small_model_integration_test` verified the processor output against `nielsr/test-image::llava_1_6_{input_ids,pixel_values}.pt`, dumps recorded from the original third-party radar image. Synthetic pixels can never reproduce them, so the test aborted there and its generation expectation — still the old "radar chart / MMM-Vet" text — was never reached by any round. Re-recording the dumps from this very processor would only compare it against itself, so the check is dropped rather than repointed. The generation assertion below it still exercises the whole pipeline; it is marked stale for the next round to measure. Round 3 read this row as pre-existing and round 10 reads it as ours because `main` has since added `atol=0.1` to the pixel comparison, which is what made it start passing there. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Align qwen3_asr's batched timestamps against the right transcript Entry 1's `text` is an *input* to the forced aligner, not an expectation, and it still held the original third-party clip's transcript — so the test aligned 甚至出现交易几乎停滞的情况。 (13 characters) against audio that says something else entirely, which is where the 0.0 != 0.4 came from. Re-recording timestamps against a wrong transcript would have pinned meaningless numbers, so the transcript is corrected first: it now matches what the ASR test already pins in `expected_results_batched.json`. The entry count moves 13 -> 19 with it. `assertAlmostEqual` is uninstrumented, so no CI round can ever report these values; `Qwen3-ForcedAligner-0.6B-hf` is small enough to measure off-CI, which is the path taken here (CPU, bfloat16, same batched call the test makes). Two checks stand behind the numbers: - entry 0, untouched, reproduces the existing fixture **byte for byte** under the same local run — so the measurement agrees with the CI hardware wherever the two can be compared; - the new alignment spans 0.00s to 3.92s, against a clip that is 4.00s long and whose speech energy runs 0.00s to 3.90s. Closes the last row round 10 bisected to this branch on both CUDA and AMD. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Refresh instructblip's flan-t5-xl text, the last ambiguous row This row was filed ambiguous in rounds 3, 4 and 10, and round 10's capture settles it: on this branch the token-ids assertion already **passes** and only the text one fails — the classic half-refreshed pair, one side re-recorded after the `lavis_confusing_pictures.jpg` regeneration and the other left describing the flat-geometric-shapes stand-in that preceded it. The "DIFFERENT error message" bisect verdict is what that looks like from the outside: `main` still fails on the ids, this branch fails one assertion later. Read that status as a layer already peeled, not as an undecidable row. Cross-checked the way this plan recommends for id/text pairs — decoding one into the other. `AutoTokenizer.from_pretrained("Salesforce/instructblip-flan-t5-xl")` over the 110 ids the test already expects reproduces the captured `generated_text` exactly, so the two sides now agree by construction. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Address the review: drop dead helpers, unify glass_breaking, kill the collision Three review threads, all in `fetch_hub_objects_for_ci.py`. **`get_synthetic_asset_url` / `get_synthetic_asset_path` (ydshieh).** They have no callers anywhere in the tree — the tests spell the URL out. Removed rather than documented. **"should we use one of these breaking glasses then in all tests?" (zucchini).** Yes, and it turned out to cost nothing. There were two different `glass_breaking.mp3`: the synthetic CC0 one this PR added, and `dummy-audio-samples/glass_breaking.mp3`, whose provenance record reads `verdict: REVIEW, license: unknown` ("no licence declared; an undeclared licence is not a permission") — exactly the kind of file this PR exists to remove. Its only consumer left was `pe_audio`, whose two tests are `@unittest.skip`ped and assert nothing, so repointing them needs no re-recording and no round. The whole repo now uses one glass_breaking. **"doesn't that mean `000000039769` will be overshadowed?" (zucchini).** It would, and the allowlist entry documented the trap instead of removing it. The collision is between `coco_panoptic/000000039769.png` and `val2017/000000039769.png`, both **pre-existing on `main`** and both unused: nothing resolves either through `url_to_local_path`, and every consumer of that basename opens the in-repo `tests/fixtures/tests_samples/COCO/000000039769.png`. The widely used cats image is the `.jpg`, which never collided. So the panoptic entry is dropped rather than renamed on the Hub. With both causes gone, `KNOWN_BASENAME_COLLISIONS` goes too and `_check_basename_collisions` now guards the full 111-URL list unconditionally — which is the point of having it. It passes with no exemptions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Round 11: llava_next measured, two more ROCm layers, and a reportable aligner test Round 11 (`34449486455` CUDA / `34449486637` AMD, on `28fce388`, 10 dirs) took CUDA from 11 ours-rows to 2 and AMD from 27 to 5 — of the AMD five, `fast_vlm`'s batch row and `instructblip` fail on `main` too. - `llava_next` — first measurement this value has ever had; the input checks used to abort the test before generation. It matches the wording three sibling tests in the same file already expect for this image. - `internvl::interleaved_images_videos` and `mistral3::batched_generate_multi_image` — the next ROCm layer, exactly where the previous commit predicted: refreshing one `Expectations` block only reveals the next, since a test stops at its first failing assertion. Both values verified against the report's abbreviation. Worth noting for the haiku: ROCm returns `calm lake` here but an entirely different haiku in `batched_generate_multi_image`, so the two are not transferable — the test's own comment about batching altering the output is right, and mirroring between them would have been wrong. `qwen3_asr::test_fixture_timestamps_batched` is not a hardware split: **CUDA and AMD report the identical `0.24`**, and it is the off-CI measurement that disagrees by two frames on one boundary. So the fixture wants one CI recording, not a tolerance and not device keys. Getting it needs the assertion to be reportable. Element-wise `assertAlmostEqual` is uninstrumented, so a round yields exactly one of the 38 numbers and names neither the sample nor the word; `zip` also truncated a short prediction silently, which is what the separate length check was there to catch. Comparing each sample as one list subsumes both and lets a single round capture everything. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Pin the aligner timestamps to the values CI measures Round 12 confirmed llava_next, internvl and mistral3 green, and left exactly one ours-row on each device: this fixture. The previous commit made the assertion reportable, and that paid off immediately — one round captured all 19 timestamps instead of the single number `assertAlmostEqual` was willing to print. 13 of the 19 disagreed with the off-CI measurement, every one of them by a single frame or two (0.04-0.08s); the words, the ordering and the 0.00-3.92s span are identical, so it is boundary placement drifting between CPU and GPU, not a different alignment. Sample 0 needed no change: it passed on CI, so no capture was produced for it, which is its own confirmation. The fixture now holds GPU-measured values and so no longer reproduces on CPU. That is the right trade for a `@slow` integration test — CUDA and ROCm report identical numbers, so one recording covers both devices, exactly as it did on `main` before this branch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Regenerate the azure slides, which were four copies of one slide `azure_slide_1..4` came out of one code path whose only per-file variable was the header title, so they were 0.994-0.997 correlated while phi4_multimodal's multi-image test shows them to the model as four distinct slides and asks it to "Summarize the deck of slides." The deck had one slide in it, four times. Each is now a different layout of the same subject — a title card, a bulleted service list, a three-stage diagram with arrows, and a bar chart with axes. Pairwise correlation drops to 0.04-0.70, the remainder being the shared banner and background. Still 1280x720, still drawn with PIL, still CC0. Uploaded as `2414d981` in the dataset repo, with the reasoning recorded in `metadata/asset_manifest.json` (`7fab93ea`) alongside the other regenerations. Checked the rest of the set rather than only the pair already flagged: correlating all 31 images pairwise, the azure slides were the *only* group above 0.90. The four document assets that share the same generator body sit at 0.77-0.86 — close, but the model reads different text off each, and their expectations are settled, so they are left alone. The generator's other instance of the same flaw is fixed too: it still emitted `invoice_docquery_b.png` with `_a`'s title, which is how those two became byte-identical before being collapsed. The earlier description-driven generator shows the intent was a distinct receipt page; the assembler's single code path is what lost it. Nothing references `_b`, and the manifest already maps both impira/docquery revisions to `_a`, so the generator now matches what ships. phi4_multimodal's expectation is marked stale for the next round to measure. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Round 14: the regenerated slides measured, and one more ROCm key The full-coverage round (61 dirs, `34465008568` / `34465008470`, on `2c12499b`) leaves **1 ours-row on CUDA and 3 on AMD**, and two of those are the same test. - `phi4_multimodal::test_multi_image_vision_text_generation` — the value the regenerated slides were always going to need. CUDA and AMD return it identically, so it stays a plain string. It also confirms the regeneration did what it was for: the model now answers "It outlines a four-part..." — picked up from the title card's subtitle, which exists only on the new slide 1. It is reading four slides, not one slide four times. - `qwen3_omni_moe::test_small_model_integration_test_multiturn` — a cuda/ROCm split (ROCm carries the answer on into ", and the dog is a Labrador Retriever."), so it becomes an `Expectations` with a `("rocm", (9, 4))` key. **`pp_doclayout_v2::test_inference_equivalence_for_static_and_dynamic_anchors_0_float32` is left alone even though bisect blames this branch.** It builds its inputs from `prepare_config_and_inputs_for_common()` — random weights, random inputs, no asset of any kind — and the branch does not touch it. Its max diff wanders across runs (0.00125, 0.00134, 0.00186, 0.00203) against an `atol=1e-4`, so it sits 12-20x over tolerance on MI300 whichever commit runs it; rounds 10 and 13 saw it fail on `main` too. Which side a given round blames is a coin flip. A bisect verdict is evidence, not a conclusion: check the failing test actually reads something this branch changed before refreshing anything. The other 14 AMD rows fail on `main` too, and `mllama`'s CUDA row is the documented padded-batch won't-fix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
13 KiB
Generation with LLMs
LLMs (Large Language Models) sind die Schlüsselkomponente bei der Texterstellung. Kurz gesagt, bestehen sie aus großen, vortrainierten Transformationsmodellen, die darauf trainiert sind, das nächste Wort (oder genauer gesagt Token) aus einem Eingabetext vorherzusagen. Da sie jeweils ein Token vorhersagen, müssen Sie etwas Aufwändigeres tun, um neue Sätze zu generieren, als nur das Modell aufzurufen - Sie müssen eine autoregressive Generierung durchführen.
Die autoregressive Generierung ist ein Verfahren zur Inferenzzeit, bei dem ein Modell mit seinen eigenen generierten Ausgaben iterativ aufgerufen wird, wenn einige anfängliche Eingaben vorliegen. In 🤗 Transformers wird dies von der Methode [~generation.GenerationMixin.generate] übernommen, die allen Modellen mit generativen Fähigkeiten zur Verfügung steht.
Dieses Tutorial zeigt Ihnen, wie Sie:
- Text mit einem LLM generieren
- Vermeiden Sie häufige Fallstricke
- Nächste Schritte, damit Sie das Beste aus Ihrem LLM herausholen können
Bevor Sie beginnen, stellen Sie sicher, dass Sie alle erforderlichen Bibliotheken installiert haben:
pip install transformers bitsandbytes>=0.39.0 -q
Text generieren
Ein Sprachmodell, das für causal language modeling trainiert wurde, nimmt eine Folge von Text-Token als Eingabe und gibt die Wahrscheinlichkeitsverteilung für das nächste Token zurück.
Ein wichtiger Aspekt der autoregressiven Generierung mit LLMs ist die Auswahl des nächsten Tokens aus dieser Wahrscheinlichkeitsverteilung. In diesem Schritt ist alles möglich, solange Sie am Ende ein Token für die nächste Iteration haben. Das heißt, es kann so einfach sein wie die Auswahl des wahrscheinlichsten Tokens aus der Wahrscheinlichkeitsverteilung oder so komplex wie die Anwendung von einem Dutzend Transformationen vor der Stichprobenziehung aus der resultierenden Verteilung.
Der oben dargestellte Prozess wird iterativ wiederholt, bis eine bestimmte Abbruchbedingung erreicht ist. Im Idealfall wird die Abbruchbedingung vom Modell vorgegeben, das lernen sollte, wann es ein Ende-der-Sequenz-Token (EOS) ausgeben muss. Ist dies nicht der Fall, stoppt die Generierung, wenn eine vordefinierte Maximallänge erreicht ist.
Damit sich Ihr Modell so verhält, wie Sie es für Ihre Aufgabe erwarten, müssen Sie den Schritt der Token-Auswahl und die Abbruchbedingung richtig einstellen. Aus diesem Grund haben wir zu jedem Modell eine [~generation.GenerationConfig]-Datei, die eine gute generative Standardparametrisierung enthält und zusammen mit Ihrem Modell geladen wird.
Lassen Sie uns über Code sprechen!
Wenn Sie an der grundlegenden Verwendung von LLMs interessiert sind, ist unsere High-Level-Schnittstelle Pipeline ein guter Ausgangspunkt. LLMs erfordern jedoch oft fortgeschrittene Funktionen wie Quantisierung und Feinsteuerung des Token-Auswahlschritts, was am besten über [~generation.GenerationMixin.generate] erfolgt. Die autoregressive Generierung mit LLMs ist ebenfalls ressourcenintensiv und sollte für einen angemessenen Durchsatz auf einer GPU ausgeführt werden.
Zunächst müssen Sie das Modell laden.
>>> from transformers import AutoModelForCausalLM, BitsAndBytesConfig
>>> model = AutoModelForCausalLM.from_pretrained(
... "openlm-research/open_llama_7b", device_map="auto", quantization_config=BitsAndBytesConfig(load_in_4bit=True)
... )
Sie werden zwei Flags in dem Aufruf from_pretrained bemerken:
device_mapstellt sicher, dass das Modell auf Ihre GPU(s) übertragen wirdload_in_4bitwendet dynamische 4-Bit-Quantisierung an, um die Ressourcenanforderungen massiv zu reduzieren
Es gibt noch andere Möglichkeiten, ein Modell zu initialisieren, aber dies ist eine gute Grundlage, um mit einem LLM zu beginnen.
Als nächstes müssen Sie Ihre Texteingabe mit einem tokenizer vorverarbeiten.
>>> from transformers import AutoTokenizer
>>> tokenizer = AutoTokenizer.from_pretrained("openlm-research/open_llama_7b")
>>> model_inputs = tokenizer(["A list of colors: red, blue"], return_tensors="pt").to("cuda")
Die Variable model_inputs enthält die tokenisierte Texteingabe sowie die Aufmerksamkeitsmaske. Obwohl [~generation.GenerationMixin.generate] sein Bestes tut, um die Aufmerksamkeitsmaske abzuleiten, wenn sie nicht übergeben wird, empfehlen wir, sie für optimale Ergebnisse wann immer möglich zu übergeben.
Rufen Sie schließlich die Methode [~generation.GenerationMixin.generate] auf, um die generierten Token zurückzugeben, die vor dem Drucken in Text umgewandelt werden sollten.
>>> generated_ids = model.generate(**model_inputs)
>>> tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
'A list of colors: red, blue, green, yellow, black, white, and brown'
Und das war's! Mit ein paar Zeilen Code können Sie sich die Macht eines LLM zunutze machen.
Häufige Fallstricke
Es gibt viele Generierungsstrategien, und manchmal sind die Standardwerte für Ihren Anwendungsfall vielleicht nicht geeignet. Wenn Ihre Ausgaben nicht mit dem übereinstimmen, was Sie erwarten, haben wir eine Liste der häufigsten Fallstricke erstellt und wie Sie diese vermeiden können.
>>> from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
>>> tokenizer = AutoTokenizer.from_pretrained("openlm-research/open_llama_7b")
>>> tokenizer.pad_token = tokenizer.eos_token # Llama has no pad token by default
>>> model = AutoModelForCausalLM.from_pretrained(
... "openlm-research/open_llama_7b", device_map="auto", quantization_config=BitsAndBytesConfig(load_in_4bit=True)
... )
Generierte Ausgabe ist zu kurz/lang
Wenn in der Datei [~generation.GenerationConfig] nichts angegeben ist, gibt generate standardmäßig bis zu 20 Token zurück. Wir empfehlen dringend, max_new_tokens in Ihrem generate-Aufruf manuell zu setzen, um die maximale Anzahl neuer Token zu kontrollieren, die zurückgegeben werden können. Beachten Sie, dass LLMs (genauer gesagt, decoder-only models) auch die Eingabeaufforderung als Teil der Ausgabe zurückgeben.
>>> model_inputs = tokenizer(["A sequence of numbers: 1, 2"], return_tensors="pt").to("cuda")
>>> # By default, the output will contain up to 20 tokens
>>> generated_ids = model.generate(**model_inputs)
>>> tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
'A sequence of numbers: 1, 2, 3, 4, 5'
>>> # Setting `max_new_tokens` allows you to control the maximum length
>>> generated_ids = model.generate(**model_inputs, max_new_tokens=50)
>>> tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
'A sequence of numbers: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,'
Falscher Generierungsmodus
Standardmäßig und sofern nicht in der Datei [~generation.GenerationConfig] angegeben, wählt generate bei jeder Iteration das wahrscheinlichste Token aus (gierige Dekodierung). Je nach Aufgabe kann dies unerwünscht sein; kreative Aufgaben wie Chatbots oder das Schreiben eines Aufsatzes profitieren vom Sampling. Andererseits profitieren Aufgaben, bei denen es auf die Eingabe ankommt, wie z.B. Audiotranskription oder Übersetzung, von der gierigen Dekodierung. Aktivieren Sie das Sampling mit do_sample=True. Mehr zu diesem Thema erfahren Sie in diesem Blogbeitrag.
>>> # Set seed or reproducibility -- you don't need this unless you want full reproducibility
>>> from transformers import set_seed
>>> set_seed(0)
>>> model_inputs = tokenizer(["I am a cat."], return_tensors="pt").to("cuda")
>>> # LLM + greedy decoding = repetitive, boring output
>>> generated_ids = model.generate(**model_inputs)
>>> tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
'I am a cat. I am a cat. I am a cat. I am a cat'
>>> # With sampling, the output becomes more creative!
>>> generated_ids = model.generate(**model_inputs, do_sample=True)
>>> tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
'I am a cat.\nI just need to be. I am always.\nEvery time'
Falsche Auffüllseite
LLMs sind decoder-only-Architekturen, d.h. sie iterieren weiter über Ihre Eingabeaufforderung. Wenn Ihre Eingaben nicht die gleiche Länge haben, müssen sie aufgefüllt werden. Da LLMs nicht darauf trainiert sind, mit aufgefüllten Token fortzufahren, muss Ihre Eingabe links aufgefüllt werden. Vergessen Sie auch nicht, die Aufmerksamkeitsmaske an generate zu übergeben!
>>> # The tokenizer initialized above has right-padding active by default: the 1st sequence,
>>> # which is shorter, has padding on the right side. Generation fails.
>>> model_inputs = tokenizer(
... ["1, 2, 3", "A, B, C, D, E"], padding=True, return_tensors="pt"
... ).to("cuda")
>>> generated_ids = model.generate(**model_inputs)
>>> tokenizer.batch_decode(generated_ids[0], skip_special_tokens=True)[0]
''
>>> # With left-padding, it works as expected!
>>> tokenizer = AutoTokenizer.from_pretrained("openlm-research/open_llama_7b", padding_side="left")
>>> tokenizer.pad_token = tokenizer.eos_token # Llama has no pad token by default
>>> model_inputs = tokenizer(
... ["1, 2, 3", "A, B, C, D, E"], padding=True, return_tensors="pt"
... ).to("cuda")
>>> generated_ids = model.generate(**model_inputs)
>>> tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
'1, 2, 3, 4, 5, 6,'
Weitere Ressourcen
Während der Prozess der autoregressiven Generierung relativ einfach ist, kann die optimale Nutzung Ihres LLM ein schwieriges Unterfangen sein, da es viele bewegliche Teile gibt. Für Ihre nächsten Schritte, die Ihnen helfen, tiefer in die LLM-Nutzung und das Verständnis einzutauchen:
Fortgeschrittene Nutzung generieren
- Leitfaden zur Steuerung verschiedener Generierungsmethoden, zur Einrichtung der Generierungskonfigurationsdatei und zum Streaming der Ausgabe;
- API-Referenz zu [
~generation.GenerationConfig], [~generation.GenerationMixin.generate] und generate-bezogene Klassen.
LLM-Ranglisten
- Open LLM Leaderboard, das sich auf die Qualität der Open-Source-Modelle konzentriert;
- Open LLM-Perf Leaderboard, das sich auf den LLM-Durchsatz konzentriert.
Latenz und Durchsatz
- Leitfaden zur dynamischen Quantisierung, der Ihnen zeigt, wie Sie Ihren Speicherbedarf drastisch reduzieren können.
Verwandte Bibliotheken
- text-generation-inference, ein produktionsreifer Server für LLMs;
optimum, eine Erweiterung von 🤗 Transformers, die für bestimmte Hardware-Geräte optimiert.