hiyouga/LlamaFactory · error · ValueError

{kind} placeholder count ({seen}) != number of {kind} blocks

Error message

{kind} placeholder count ({seen}) != number of {kind} blocks ({count}); media must be provided via image_url/video_url content blocks.

What it means

After rendering a multimodal conversation, the renderer counts placeholder tokens (e.g. the image/video/audio expansion token from the processor) in the final text and compares them to the number of media content blocks. A mismatch means media was referenced in text without matching image_url/video_url/audio_url blocks, or vice versa, which would desync token ids from pixel/feature tensors downstream.

Source

Thrown at src/llamafactory/v1/core/rendering/format.py:167

def _check_placeholder_counts(
    processor: "Processor", full_text: str, n_images: int, n_videos: int, n_audios: int = 0
) -> None:
    """Guard: every media placeholder in the rendered text must originate from a media block."""
    tokenizer = get_tokenizer(processor)
    for attr, count, kind in (
        ("image_token_id", n_images, "image"),
        ("video_token_id", n_videos, "video"),
        ("audio_token_id", n_audios, "audio"),
    ):
        tid = getattr(processor, attr, None)
        if tid is None:
            tid = getattr(tokenizer, attr, None)
        if tid is None:
            continue
        placeholder = tokenizer.convert_ids_to_tokens(tid)
        seen = full_text.count(placeholder)
        if seen != count:
            raise ValueError(
                f"{kind} placeholder count ({seen}) != number of {kind} blocks ({count}); "
                "media must be provided via image_url/video_url content blocks."
            )

View on GitHub (pinned to f28afaf635)

Solutions

  1. Remove hand-written placeholder tokens from the text; supply media exclusively through {'type': 'image_url'|'video_url'|'audio_url', 'value': ...} content blocks
  2. Ensure every media block you pass actually reaches the renderer (no earlier filtering of empty/None values)
  3. If you intentionally inject placeholders, make the counts match exactly — one placeholder run per media block

Example fix

# before
text = "Describe <|vision_start|><|image_pad|><|vision_end|>"
content = [{"type": "text", "value": text}, {"type": "image_url", "value": img}]

# after
content = [{"type": "text", "value": "Describe this image."}, {"type": "image_url", "value": img}]
Defensive patterns

Strategy: validation

Validate before calling

def media_blocks_match_text(blocks: list[dict], text: str, placeholder: str) -> bool:
    n_media = sum(1 for b in blocks if b["type"] in ("image_url", "video_url", "audio_url"))
    return text.count(placeholder) == n_media

Prevention

When it happens

Trigger: Sample text contains literal placeholder tokens (e.g. '<|vision_start|><|image_pad|><|vision_end|>') pasted as plain text while media is supplied via blocks (count too high), or media blocks are present but the chat template did not emit the placeholder (count too low, e.g. a template that skips images in earlier turns).

Common situations: Multimodal SFT datasets that pre-expand image markers into the text column; using a chat template that renders history differently from the final turn; missing 'value' keys so a media block was dropped earlier.

Related errors


AI-assisted analysis of hiyouga/LlamaFactory@f28afaf635 (2026-08-14). Data as JSON: /api/errors/aa1ad26f304b3f4f. Report an issue: GitHub.