File size: 6,136 Bytes
ba3bbc7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
"""Stage-1 vision projector training for CORTEX AI.

Trains *only* ``VisionProjector`` on top of a frozen real vision encoder and a frozen small
causal LM::

    IMAGE -> real vision encoder -> VisionProjector -> PrefixFusion -> frozen causal LM -> logits

Nothing here touches the DeepSeek-V4-Pro shards: the ``model-*-of-*.safetensors`` files are only
read to hash their LFS pointers, and the checkpoint written by this script is a separate file.

Examples::

    python examples/train_vision_projector.py --config configs/vision_projector.json
    python examples/train_vision_projector.py --max-steps 20 --batch-size 2
"""

from __future__ import annotations

import argparse
import json
import sys
from dataclasses import asdict, fields
from pathlib import Path

import torch

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))

from cortex_ai.vision.train import (  # noqa: E402
    ProjectorTrainingConfig,
    build_fusion_pipeline,
    count_parameters,
    iterate_caption_batches,
    run_training,
    save_projector,
)


def load_dataset(config: ProjectorTrainingConfig) -> tuple[list, list[str]]:
    """Load real images and captions from the configured Hub dataset."""
    from datasets import load_dataset as hf_load_dataset

    ds = hf_load_dataset(config.dataset, split=config.dataset_split, streaming=True)
    images, captions = [], []
    limit = config.limit or 0
    for example in ds:
        image = example.get("jpg") or example.get("image")
        text = example.get("txt") or example.get("caption") or example.get("text")
        if image is None or not text:
            continue
        if isinstance(text, list):
            text = text[0]
        images.append(image.convert("RGB"))
        captions.append(str(text))
        if limit and len(images) >= limit:
            break
    if not images:
        raise SystemExit(f"no usable image/caption pairs found in {config.dataset}")
    return images, captions


def parse_args(argv: list[str] | None = None) -> ProjectorTrainingConfig:
    p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    p.add_argument("--config", help="JSON file of ProjectorTrainingConfig fields")
    defaults = ProjectorTrainingConfig()
    for f in fields(defaults):
        value = getattr(defaults, f.name)
        if isinstance(value, bool):
            p.add_argument(f"--{f.name.replace('_', '-')}", type=lambda s: s.lower() == "true", default=None)
        else:
            p.add_argument(
                f"--{f.name.replace('_', '-')}",
                type=type(value) if value is not None else str,
                default=None,
            )
    args = p.parse_args(argv)

    values = {}
    if args.config:
        values.update(json.loads(Path(args.config).read_text()))
    for f in fields(defaults):
        v = getattr(args, f.name.replace("-", "_"))
        if v is not None:
            values[f.name] = v
    return ProjectorTrainingConfig(**values)


def main(argv: list[str] | None = None) -> int:
    config = parse_args(argv)
    torch.manual_seed(config.seed)

    print("=" * 78)
    print("CORTEX AI - stage-1 vision projector training (experimental)")
    print("=" * 78)
    print(f"config        : {json.dumps(asdict(config), indent=2)}")

    from transformers import AutoImageProcessor, AutoModelForCausalLM, AutoTokenizer, SiglipVisionModel

    dtype = config.resolved_dtype()
    device = config.resolved_device()

    print(f"\n[1] loading real vision encoder : {config.vision_model}")
    image_processor = AutoImageProcessor.from_pretrained(config.vision_model)
    vision_encoder = SiglipVisionModel.from_pretrained(config.vision_model).to(dtype=dtype, device=device)

    print(f"[2] loading real frozen LM      : {config.language_model}")
    core = AutoModelForCausalLM.from_pretrained(config.language_model, dtype=dtype).to(device)
    tokenizer = AutoTokenizer.from_pretrained(config.language_model)
    if tokenizer.pad_token is None:
        tokenizer.pad_token = tokenizer.eos_token

    print("[3] assembling fusion pipeline (encoder + LM frozen, projector trainable)")
    fusion, vision_hidden, lm_hidden = build_fusion_pipeline(
        vision_encoder, core, config, image_processor=image_processor
    )
    projector_params = count_parameters(fusion.projector)
    print(f"    vision_hidden_size   = {vision_hidden}")
    print(f"    lm_hidden_size       = {lm_hidden}")
    print(f"    projector_type       = {config.projector_type}")
    print(f"    projector_parameters = {projector_params:,}")
    print(f"    dtype / device       = {config.dtype} / {config.device}")

    print(f"\n[4] loading real dataset        : {config.dataset} (split={config.dataset_split})")
    images, captions = load_dataset(config)
    print(f"    {len(images)} image/caption pairs")
    print(f"    sample caption: {captions[0][:70]!r}")

    batches = iterate_caption_batches(images, captions, tokenizer, image_processor, config)

    def on_step(step: int, loss: float) -> None:
        if step == 1 or step % config.log_every == 0:
            print(f"    step {step:4d}  loss={loss:.4f}")

    print(f"\n[5] training projector for up to {config.max_steps} steps (projector only)")
    report = run_training(
        fusion, batches, config, projector_params, vision_hidden, lm_hidden, on_step=on_step
    )

    path = save_projector(fusion.projector, config, report)
    report.checkpoint_path = str(path)

    print("\n" + "=" * 78)
    print("RESULT")
    print("=" * 78)
    print(report.to_json())
    print(f"\ncheckpoint written: {path}")
    print("\nPROVEN by this run:")
    print("  - real image -> real encoder -> projector -> PrefixFusion -> frozen LM executed")
    print("  - projector received gradients and its parameters changed")
    print("  - vision encoder and causal LM parameters are unchanged")
    print("\nNOT PROVEN:")
    print("  - that this projector works with the real DeepSeek-V4-Pro core")
    print("  - native multimodal understanding of the 1.65T checkpoint")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())