Abdoulaye coumbassa openhands commited on
Commit
c095142
·
1 Parent(s): a82e5d8

fix(vision): unpack processor output by keyword in native fusion

Browse files

Transformers image processors return BatchFeature (a UserDict, not a dict
subclass), so the isinstance(..., dict) check failed and the processor output
was passed positionally into the encoder's input_ids parameter. Against a real
encoder this raised ValueError: You have to specify input_ids.

Test against collections.abc.Mapping instead, and add a regression test using a
UserDict-based processor that fails on the previous code.

Also add AGENTS.md recording verified repo facts, sizing constraints, licensing
corrections and the small-model testing approach.

Co-authored-by: openhands <openhands@all-hands.dev>

AGENTS.md ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CORTEX AI — verified facts and constraints
2
+
3
+ This file records what was *measured* in this workspace, so later sessions do not re-derive it
4
+ or repeat claims that turn out to be wrong.
5
+
6
+ ## Repository state
7
+
8
+ - `Frankenstein-Labs/Cortex-ai` HEAD = `a82e5d802d597d9429a4ecd1d4d0160e4f4eb4ad`
9
+ ("feat(cortex-ai): add native vision fusion prototype"), branch `main`, no open PRs, no tags.
10
+ - The repo is a **redistribution** of `deepseek-ai/DeepSeek-V4-Pro-0813`.
11
+ - All 66 `model-*.safetensors` shards plus `model.safetensors.index.json` have SHA-256
12
+ **identical to upstream** (67/67 LFS objects match). Verified via the HF tree API.
13
+ - There is no `git-lfs` binary in this image. Fetch weights as pointer files only:
14
+ set `GIT_LFS_SKIP_SMUDGE=1` and neutralise the smudge filter, otherwise a clone pulls ~893 GB
15
+ into a 25 GB disk.
16
+
17
+ ## Model facts (from `config.json`)
18
+
19
+ | Field | Value |
20
+ |---|---|
21
+ | architecture | `DeepseekV4ForCausalLM` (`model_type: deepseek_v4`) |
22
+ | hidden_size | 7168 |
23
+ | layers | 61 (+3 MTP) |
24
+ | vocab | 129280 |
25
+ | max positions | 1048576 |
26
+ | routed experts | 384, 6 per token |
27
+ | expert storage | `fp4`; weights FP8 `e4m3` |
28
+ | declared dtype | bfloat16 |
29
+ | tie_word_embeddings | false |
30
+
31
+ `tie_word_embeddings: false` means there is no `lm_head`-sharing trick for grafting new
32
+ modalities; any new vocabulary or modality embedding needs explicit, trained parameters.
33
+
34
+ ## Sizing reality
35
+
36
+ - Stored payload ≈ 892.76 GB; ~1.65 T params. Minimum inference memory at 1 byte/param ≈ 1.65 TB.
37
+ - This development sandbox: **4 CPU cores, ~15 GiB RAM, 25 GB disk, no GPU** (`nvidia-smi`
38
+ absent, no `/dev/nvidia*`).
39
+ - Consequence: the 1.65 T checkpoint **cannot be loaded, run, or fine-tuned here**. Any plan
40
+ requiring an end-to-end forward pass of the real checkpoint is not executable in this
41
+ environment. Validate fusion paths against small stand-ins instead.
42
+
43
+ ## Cost warning for "keep the weights untouched" plans
44
+
45
+ A vision projector must map into a 7168-dim space, but pretrained open projectors map to their
46
+ own models' hidden sizes (e.g. 4096). A randomly-initialised projector produces **noise** into the
47
+ core, so "never train anything" and "native vision that works" are mutually exclusive. Expected
48
+ behaviour must be stated honestly as an untrained prototype, or the projector must be trained.
49
+ Training a projector alone (frozen encoder + frozen core) is the cheap option; full multimodal
50
+ pretraining at 1.65 T is not affordable outside a cluster.
51
+
52
+ ## Licensing corrections (checked against the Hub, not from memory)
53
+
54
+ Claims that appear in circulating roadmap documents are wrong in several places:
55
+
56
+ | Model | Claimed | Actual (verified) |
57
+ |---|---|---|
58
+ | Qwen2.5-VL | "non-commercial" | Apache-2.0 (7B and 72B both Apache) |
59
+ | LLaVA | "MIT" | `llama2` license (7B hub repo) |
60
+ | SDXL | "non-commercial RAIL" | OpenRAIL++-M — permits commercial use with use-restrictions |
61
+ | Coqui TTS | "MIT" | Coqui Public Model License — non-commercial |
62
+ | Kimi K2.5 | "Modified MIT, 5B + MoonViT" | Modified MIT, but **1.03 T params**, `image-text-to-text` |
63
+ | HunyuanVideo | "community, non-commercial" | Tencent Hunyuan Community; **explicitly does not apply in EU/UK/South Korea** |
64
+ | Wan2.2 | Apache-2.0 | confirmed Apache-2.0, claims no rights over outputs |
65
+
66
+ Note the Qwen 2.5 vs 3 split: `Qwen2.5-VL-*` is Apache, but Qwen3-VL and some Qwen3 text models
67
+ carry the `qwen` license. Check each repo individually; never generalise from a family name.
68
+
69
+ ## Known bug found and fixed
70
+
71
+ `cortex_ai/vision/native.py::encode_image` branched on `isinstance(model_input, dict)`.
72
+ Transformers image processors return `BatchFeature`, which derives from `UserDict`, **not** from
73
+ `dict`. The check therefore failed and the processor output was passed positionally into
74
+ `SiglipModel.forward(input_ids=...)`, raising
75
+ `ValueError: You have to specify input_ids`.
76
+
77
+ Fixed by testing `isinstance(model_input, Mapping)`. Regression test:
78
+ `tests/test_native_fusion.py::test_processor_output_is_unpacked_by_keyword_not_positionally`
79
+ (fails on the old code, passes on the new).
80
+
81
+ ## Verified fusion path
82
+
83
+ `NativeVisionFusion` works against a **real** encoder (`google/siglip-base-patch16-224` vision
84
+ tower): 196 image tokens + 8 text tokens -> 204-length fused sequence, logits shaped
85
+ `(1, 204, 1000)`, and embeddings change with image content (red-vs-blue delta 0.46). It does
86
+ **not** yet work with the real 1.65 T core.
87
+
88
+ ## Test commands
89
+
90
+ ```bash
91
+ PYTHONPATH=/workspace/project python3 -m pytest tests/ -q # 6 tests
92
+ cd encoding && PYTHONPATH=$PWD python3 -m pytest test_encoding_dsv4.py -q # 4 tests
93
+ ```
94
+
95
+ Requires `torch` (CPU wheel), `torchvision`, `pillow`, `transformers`.
cortex_ai/vision/native.py CHANGED
@@ -6,6 +6,7 @@ existing 1.65T DeepSeek checkpoint is not modified, and a core is only
6
  accepted when its runtime exposes the required inputs_embeds contract.
7
  """
8
 
 
9
  from dataclasses import dataclass
10
  from typing import Any, Callable
11
 
@@ -47,7 +48,13 @@ class NativeVisionFusion(nn.Module):
47
  if self.image_processor is not None
48
  else image
49
  )
50
- encoded = self.vision_encoder(**model_input) if isinstance(model_input, dict) else self.vision_encoder(model_input)
 
 
 
 
 
 
51
  if hasattr(encoded, "last_hidden_state"):
52
  encoded = encoded.last_hidden_state
53
  if not isinstance(encoded, Tensor) or encoded.ndim != 3:
 
6
  accepted when its runtime exposes the required inputs_embeds contract.
7
  """
8
 
9
+ from collections.abc import Mapping
10
  from dataclasses import dataclass
11
  from typing import Any, Callable
12
 
 
48
  if self.image_processor is not None
49
  else image
50
  )
51
+ # Transformers processors return BatchFeature, a UserDict rather than a
52
+ # dict subclass, so a plain isinstance(..., dict) check would pass the
53
+ # processor output positionally into the encoder's input_ids parameter.
54
+ if isinstance(model_input, Mapping):
55
+ encoded = self.vision_encoder(**model_input)
56
+ else:
57
+ encoded = self.vision_encoder(model_input)
58
  if hasattr(encoded, "last_hidden_state"):
59
  encoded = encoded.last_hidden_state
60
  if not isinstance(encoded, Tensor) or encoded.ndim != 3:
tests/test_native_fusion.py CHANGED
@@ -1,3 +1,5 @@
 
 
1
  import torch
2
  from torch import nn
3
 
@@ -31,3 +33,29 @@ def test_native_image_embedding_fusion_executes_real_tensor_path():
31
  assert result.text_token_count == 2
32
  assert core.last_inputs_embeds.shape == (1, 5, 8)
33
  assert result.outputs["attention_mask"].shape == (1, 5)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from collections import UserDict
2
+
3
  import torch
4
  from torch import nn
5
 
 
33
  assert result.text_token_count == 2
34
  assert core.last_inputs_embeds.shape == (1, 5, 8)
35
  assert result.outputs["attention_mask"].shape == (1, 5)
36
+
37
+
38
+ class BatchFeatureLikeEncoder(nn.Module):
39
+ """Encoder whose processor output is a UserDict, as Transformers processors return."""
40
+
41
+ def forward(self, pixel_values):
42
+ return torch.ones(pixel_values.shape[0], 2, 4)
43
+
44
+
45
+ class BatchFeatureLikeProcessor:
46
+ def __call__(self, image, return_tensors=None):
47
+ return UserDict({"pixel_values": torch.zeros(1, 3, 16, 16)})
48
+
49
+
50
+ def test_processor_output_is_unpacked_by_keyword_not_positionally():
51
+ core = TinyCore()
52
+ fusion = build_native_fusion(
53
+ BatchFeatureLikeEncoder(),
54
+ core,
55
+ vision_hidden_size=4,
56
+ cortex_hidden_size=8,
57
+ image_processor=BatchFeatureLikeProcessor(),
58
+ )
59
+ result = fusion(input_ids=torch.tensor([[1, 2]]), image="unused")
60
+ assert result.image_token_count == 2
61
+ assert result.text_token_count == 2