Repository navigation
feat: Detect refusal-style VLM image generations in bench_mlxlm.py #2134
Description
Follow-up to #1683 (the generation-content check its Scope section puts out of scope). Root cause of #1683 is fixed in PR #2131.
Problem / Background
In #1683, granite-vision-3.2-2b-4bit answered descriptive image prompts with refusals such as "I'm sorry, but I can't provide a description of the image as I can't see it." and "I'm sorry, but I can't see any images. I can only provide text-based responses." while prompt_tokens stayed a healthy 1545 (1485 image tokens inserted). The image tokens were present but mispositioned: <image> was declared only in tokenizer_config.json, the tokenizer split it, and the image block was spliced after the first token (fixed in PR #2131). No length-based gate can see this class, and docs/benchmark_results/model_tests_m1ultra.md:242 already records the gap.
Current Behavior
scripts/bench_mlxlm.py:489(insidebench_one, :441) returnsFAIL:image_not_appliedonly whenprompt_tokens <= text_only_prompt_tokens, that is, when the image never expanded the prompt.- The VLM child (
SUBPROCESS_VLM_CODE, :302) keeps only the lastGenerationResult(last = r, :381-384), so the generated text is never captured, and theRESULTdict (:393-399) carries counts and rates only. - EOS is suppressed (:346-370), so output past the first answer can be filler; the refusal appears at the start.
VLM_PROMPT(:28) is the only image prompt: "What is in this image?".
Proposed Solution
- In
SUBPROCESS_VLM_CODE, accumulater.textacross the measuredstream_generateloop into one string and add it to theRESULTdict asgenerated_text. The warmup loop is unchanged. - Add a module-level, side-effect-free function
vlm_refusal_reason(text: str) -> str | Nonetoscripts/bench_mlxlm.py. It lowercases the text, normalizes the typographic apostrophe’to', and returns the first matching phrase from a module-level tupleVLM_REFUSAL_PHRASES = ("can't see", "cannot see", "unable to view", "don't have the ability to view", "can't provide a description"), orNone. It matches anywhere in the text. - In
bench_one, forvlmruns only and only after theimage_not_appliedcheck passed, callvlm_refusal_reason(data.get("generated_text") or ""); on a match return("FAIL:image_ignored", None)and print the matched phrase to stderr so the sweep log shows why. Text runs are never checked. - Document
FAIL:image_ignoredin the comment block next to theimage_not_appliedcheck, and updatedocs/benchmark_results/model_tests_m1ultra.md:242to say the sweep now flags this class.
Rejected: a second descriptive prompt alongside VLM_PROMPT. It doubles VLM sweep time and changes the CSV row shape, and the phrase check on the existing prompt already catches the #1683 output. Do not add it.
Scope
In scope: scripts/bench_mlxlm.py, a new tests/test_bench_mlxlm_refusal.py, the docs line above.
Out of scope: the mlxcel-side sweep (scripts/bench_decode.sh driving src/bin/bench_decode.rs). The #1683 refusal came from mlxcel, but bench_decode.rs never prints generated text, so the same check there needs a Rust change and a separate issue, which should reuse VLM_REFUSAL_PHRASES rather than copy the list.
Implementation Notes
- Reuse: keep the classifier next to the existing
image_not_appliedlogic; no new module. - Edge cases: missing or empty
generated_text(zero tokens) means no match, so the status staysOK; matching is case-insensitive. - Constraints: the test imports
bench_mlxlmwithscripts/onsys.path(it already inserts its own directory forvlm_detect); mlx imports live only inside the child code strings, so the import works without mlx installed. Keep it that way.
Acceptance Criteria
-
vlm_refusal_reasonreturns a phrase for both fix(granite_vision): descriptive prompts draw a refusal, colour questions work #1683 strings verbatim, and for a variant using’. - It returns
Nonefor a normal description, for example "The image shows a solid orange square on a white background." - A VLM run whose output matches is recorded as
FAIL:image_ignoredin the CSV row and on stderr, distinct fromFAIL:image_not_applied. -
tests/test_bench_mlxlm_refusal.py(stdlibunittest, same style astests/test_bench_decode_oom_classifier.py) covers the cases above.
Verification
python3 -m unittest tests/test_bench_mlxlm_refusal.py
python3 scripts/bench_mlxlm.py --helpManual: one bench_mlxlm.py VLM run on a working checkpoint still records OK.
Activity
[-]test: Detect refusal-style generations on VLM image prompts in bench[/-][+]feat: Detect refusal-style VLM image generations in bench_mlxlm.py[/+]fix(tokenizer): register config-only added tokens like transformers (#…