{"record":{"id":"6f5f3f54fce6a78b","repo":"sgl-project/sglang","slug":"kimi-image-placeholders-must-map-one-to-one-to-ima-6f5f3f","errorCode":null,"errorMessage":"Kimi image placeholders must map one-to-one to image data: expected {expected_image_count}, loaded {len(base_output.images)}","messagePattern":"Kimi image placeholders must map one-to-one to image data: expected (.+?), loaded (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"python/sglang/srt/multimodal/processors/kimi_k25.py","lineNumber":583,"sourceCode":"            base_output = await self.fast_load_mm_data(\n                prompt=input_text,\n                image_data=image_data,\n                multimodal_tokens=self.mm_tokens,\n                # fast_load_mm_data, unlike load_mm_data, does not derive\n                # input_ids from the prompt; without this the wrapper falls back\n                # to re-tokenizing the expanded string.\n                input_ids=input_text,\n            )\n        else:\n            base_output = await self.load_mm_data(\n                prompt=input_text,\n                image_data=image_data,\n                multimodal_tokens=self.mm_tokens,\n            )\n            # Only the text-scanning loader can come back with a different\n            # count; fast_load_mm_data fills one slot per image_data entry.\n            if len(base_output.images) != expected_image_count:\n                raise ValueError(\n                    \"Kimi image placeholders must map one-to-one to image data: \"\n                    f\"expected {expected_image_count}, loaded {len(base_output.images)}\"\n                )\n\n        mm_items, input_ids, _ = await self.process_and_combine_mm_data_async(\n            base_output,\n            self.mm_tokens,\n            sglang_original_input_ids=base_output.input_ids,\n        )\n\n        # K2.5/K2.7 encoder-DP assigns an image to exactly one TP rank. Keep\n        # its GPU transport proxy lazy until that assignment is known, avoiding a full\n        # image copy to every rank. The scheduler only honors this marker once\n        # the processor has already set the item's hash and pad value.\n        if self.keep_mm_features_on_device and self.server_args.mm_enable_dp_encoder:\n            for item in mm_items:\n                item.model_specific_data[DEFER_CUDA_IPC_FEATURE_RECONSTRUCTION_KEY] = (\n                    True","sourceCodeStart":565,"sourceCodeEnd":601,"githubUrl":"https://github.com/sgl-project/sglang/blob/0132848349585cfe6aae51c4941cbae872505f8a/python/sglang/srt/multimodal/processors/kimi_k25.py#L565-L601","documentation":"Post-load consistency check in Kimi K2.5's process_mm_data_async: after fast_load_mm_data / the text-scanning loader returns, base_output.images must still have exactly expected_image_count entries. The comment notes only the text-scanning loader can deviate; if it does, the request is rejected.","triggerScenarios":"A custom or text-scanning mm data loader returns a different number of images than were passed in image_data while placeholders matched, triggering the len(base_output.images) != expected_image_count check.","commonSituations":"Custom loader subclasses overriding fast_load_mm_data and filtering/dropping images (e.g. skipping unreadable files), or duplicate/failed URLs collapsing during loading.","solutions":["If you override the loader, return exactly one image per image_data entry; filter the request before loading instead","Drop corrupt images from both image_data and the prompt placeholders together so counts stay aligned","Check loader logs for silently swallowed load failures"],"exampleFix":"# before (custom loader drops unreadable images)\nclass MyLoader:\n    async def fast_load_mm_data(self, prompt, image_data, multimodal_tokens):\n        return BaseOutput(images=[i for i in loaded if i.ok()])  # count can shrink\n\n# after\nclass MyLoader:\n    async def fast_load_mm_data(self, prompt, image_data, multimodal_tokens):\n        assert len(images) == len(image_data)\n        return BaseOutput(images=images)  # fail loudly, never shrink","handlingStrategy":"validation","validationCode":"out = await proc.fast_load_mm_data(prompt=input_text, image_data=image_data, multimodal_tokens=proc.mm_tokens)\nassert len(out.images) == len(image_data)","typeGuard":null,"tryCatchPattern":"try:\n    mm_items, input_ids, _ = await proc.process_and_combine_mm_data_async(base_output, ...)\nexcept ValueError as e:\n    if \"loaded\" in str(e):\n        raise HttpClientError(\"image load failed; some images could not be fetched\") from e\n    raise","preventionTips":["If overriding loaders, return one image per input entry — filter before, not inside","Fail loudly on unreadable images instead of silently dropping","Monitor loader drop counts in metrics"],"tags":["kimi","k25","multimodal","loader","count-mismatch"],"backgroundTag":"multimodal-placeholder-mismatch","analyzedSha":"0132848349585cfe6aae51c4941cbae872505f8a","analyzedAt":"2026-08-28T05:10:05.995Z","schemaVersion":2},"datasetVersion":"2026-08-28T06:17:29.519Z"}