Upload folder using huggingface_hub
Browse files- .gitattributes +2 -0
- README.md +45 -7
- app.py +253 -0
- examples/cat_tabby.jpg +3 -0
- examples/dog.jpg +3 -0
- examples/landscape.jpg +0 -0
- examples/rubber_ducks.jpg +0 -0
- hrm_penguin/__init__.py +7 -0
- hrm_penguin/configuration.py +116 -0
- hrm_penguin/generation.py +62 -0
- hrm_penguin/inference.py +37 -0
- hrm_penguin/model.py +180 -0
- hrm_penguin/prompting.py +15 -0
- hrm_penguin/recurrent/__init__.py +11 -0
- hrm_penguin/recurrent/hrm_multimodal.py +408 -0
- hrm_penguin/recurrent/hybrid_rope.py +78 -0
- hrm_penguin/recurrent/positions.py +76 -0
- hrm_penguin/recurrent/visual_anchor.py +28 -0
- hrm_penguin/recurrent/visual_gate.py +53 -0
- hrm_penguin/vision/__init__.py +17 -0
- hrm_penguin/vision/encoder.py +140 -0
- hrm_penguin/vision/preprocessing.py +241 -0
- hrm_penguin/vision/projector.py +42 -0
- metadata.py +82 -0
- modeling_loopvl.py +169 -0
- penguin_encoder/LICENSE.txt +289 -0
- penguin_encoder/__init__.py +1 -0
- penguin_encoder/configuration_penguinvl_encoder.py +33 -0
- penguin_encoder/image_processing_penguinvl.py +548 -0
- penguin_encoder/modeling_penguinvl_encoder.py +522 -0
- requirements.txt +5 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
examples/cat_tabby.jpg filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
examples/dog.jpg filter=lfs diff=lfs merge=lfs -text
|
README.md
CHANGED
|
@@ -1,13 +1,51 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.30.0
|
| 8 |
-
python_version: '3.12'
|
| 9 |
app_file: app.py
|
| 10 |
-
|
|
|
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: LoopVL
|
| 3 |
+
emoji: 🔁
|
| 4 |
+
colorFrom: purple
|
| 5 |
+
colorTo: yellow
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.30.0
|
|
|
|
| 8 |
app_file: app.py
|
| 9 |
+
short_description: Recurrent VLM with a controllable compute budget
|
| 10 |
+
python_version: "3.12"
|
| 11 |
+
startup_duration_timeout: 30m
|
| 12 |
---
|
| 13 |
|
| 14 |
+
# LoopVL: Recurrent Vision-Language Model
|
| 15 |
+
|
| 16 |
+
**LoopVL** ([TierFlow/LoopVL](https://huggingface.co/TierFlow/LoopVL)) is a ~1.6B
|
| 17 |
+
vision-language model that reuses its 16-layer transformer stack in nested loops
|
| 18 |
+
instead of stacking 128 layers. The distinctive control this demo exposes is the
|
| 19 |
+
**recurrent inference budget**: the H (high) and L (low) cycle counts decide how
|
| 20 |
+
many times the stack is applied per forward pass — unrolled depth
|
| 21 |
+
`16 × H × (L+1)` effective layers. The schedule can be changed at runtime without
|
| 22 |
+
reloading weights.
|
| 23 |
+
|
| 24 |
+
Ask any question about an image, pick a schedule, and compare answers across
|
| 25 |
+
compute budgets.
|
| 26 |
+
|
| 27 |
+
- **Model**: [TierFlow/LoopVL](https://huggingface.co/TierFlow/LoopVL) (BF16
|
| 28 |
+
vision/language cores + FP32 projector, visual gate and anchor)
|
| 29 |
+
- **Code**: [Tier-Flow/LoopVL](https://github.com/Tier-Flow/LoopVL) — the
|
| 30 |
+
inference runtime (`hrm_penguin`, `penguin_encoder`, `modeling_loopvl.py`,
|
| 31 |
+
`metadata.py`) is vendored here from that Apache-2.0 repository; the Penguin
|
| 32 |
+
vision encoder keeps its Tencent `LICENSE.txt` alongside.
|
| 33 |
+
- **Examples**: images from
|
| 34 |
+
[linoyts/repo-to-space-example-inputs](https://huggingface.co/datasets/linoyts/repo-to-space-example-inputs)
|
| 35 |
+
(free to use, CC0-style).
|
| 36 |
+
|
| 37 |
+
## How it works
|
| 38 |
+
|
| 39 |
+
1. The image is resized under a dynamic-resolution policy (256–2048 visual
|
| 40 |
+
tokens) and patched by the PenguinVL vision encoder.
|
| 41 |
+
2. Visual tokens are injected into the text sequence; the HRM-Text recurrent
|
| 42 |
+
LM loops its stack `H × (L+1)` times per forward pass, re-injecting an
|
| 43 |
+
anchored version of the visual tokens after each high cycle, gated by a
|
| 44 |
+
learned saturation-aware gate.
|
| 45 |
+
3. Decoding is greedy, recomputing the full sequence per token (no KV cache).
|
| 46 |
+
|
| 47 |
+
## Reproducing the reference
|
| 48 |
+
|
| 49 |
+
The `answer` endpoint follows the authors' `runtime/infer.py` one-to-one:
|
| 50 |
+
same prompt template (`condition\nImage:\n` + placeholders + `\nInstruction:\n…`),
|
| 51 |
+
same greedy decode, same `decode(skip_special_tokens=True).strip()` postprocess.
|
app.py
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""LoopVL demo: a recurrent vision-language model with a user-controlled visual compute budget.
|
| 2 |
+
|
| 3 |
+
Loads the public TierFlow/LoopVL checkpoint (~1.6B parameters) with the authors'
|
| 4 |
+
single-file loader, then answers questions about an image. The distinctive control
|
| 5 |
+
is the recurrent schedule: the model reuses its 16-layer transformer stack, and the
|
| 6 |
+
H (high) and L (low) cycle counts decide how many times the stack is applied per
|
| 7 |
+
forward pass — the "inference budget" the paper sweeps across.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import os
|
| 11 |
+
|
| 12 |
+
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
|
| 13 |
+
|
| 14 |
+
import spaces # MUST come before any torch / CUDA-touching import
|
| 15 |
+
|
| 16 |
+
import time
|
| 17 |
+
from PIL import Image, ImageFile
|
| 18 |
+
|
| 19 |
+
import gradio as gr
|
| 20 |
+
import torch
|
| 21 |
+
|
| 22 |
+
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
| 23 |
+
|
| 24 |
+
MODEL_ID = "TierFlow/LoopVL"
|
| 25 |
+
DEVICE = "cuda"
|
| 26 |
+
|
| 27 |
+
# Vendored from https://github.com/Tier-Flow/LoopVL (Apache-2.0; Penguin encoder
|
| 28 |
+
# keeps its Tencent LICENSE.txt alongside).
|
| 29 |
+
from modeling_loopvl import LoopVLForConditionalGeneration, load_tokenizer_and_processor
|
| 30 |
+
from hrm_penguin.generation import greedy_generate
|
| 31 |
+
from hrm_penguin.inference import build_inference_batch
|
| 32 |
+
from hrm_penguin.recurrent.visual_anchor import VisualAnchor
|
| 33 |
+
|
| 34 |
+
TRIGGER = "\nExplain your reasoning, then give the final answer."
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def _download_weights() -> str:
|
| 38 |
+
"""Fetch config + tokenizer + single safetensors checkpoint into a local dir."""
|
| 39 |
+
from huggingface_hub import snapshot_download
|
| 40 |
+
|
| 41 |
+
return snapshot_download(
|
| 42 |
+
MODEL_ID,
|
| 43 |
+
allow_patterns=[
|
| 44 |
+
"config.json",
|
| 45 |
+
"generation_config.json",
|
| 46 |
+
"preprocessor_config.json",
|
| 47 |
+
"tokenizer.json",
|
| 48 |
+
"tokenizer_config.json",
|
| 49 |
+
"model.safetensors",
|
| 50 |
+
],
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
# --- Load everything at module scope, eagerly on "cuda" (string device). ------
|
| 55 |
+
_model_dir = _download_weights()
|
| 56 |
+
model = LoopVLForConditionalGeneration.from_pretrained(_model_dir, device=DEVICE)
|
| 57 |
+
tokenizer, processor = load_tokenizer_and_processor(_model_dir, model.config)
|
| 58 |
+
|
| 59 |
+
# Base visual-anchor coefficients (H_cycles=2 in the shipped checkpoint). Any
|
| 60 |
+
# schedule change rebuilds VisualAnchor, re-copying these values (clamped).
|
| 61 |
+
# Kept on CPU: schedule changes run inside the ZeroGPU worker, where a plain
|
| 62 |
+
# CPU tensor is trivially inherited across forks and copied cross-device.
|
| 63 |
+
recurrent = model.recurrent_model
|
| 64 |
+
BASE_ANCHOR_VALUES = recurrent.visual_anchor.raw_alpha.detach().to("cpu").clone()
|
| 65 |
+
ANCHOR_MAX_SCALE = recurrent.visual_anchor.max_scale
|
| 66 |
+
LAST_SCHEDULE = {"h": int(recurrent.config.H_cycles), "l": int(recurrent.config.L_cycles)}
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def _apply_schedule(h_cycles: int, l_cycles: int) -> None:
|
| 70 |
+
"""Set the recurrent inference schedule (the authors' configure() pattern).
|
| 71 |
+
|
| 72 |
+
The forward pass reads config.H_cycles / config.L_cycles each call, and the
|
| 73 |
+
visual anchor holds one learned coefficient per high cycle, so changing H
|
| 74 |
+
rebuilds it and re-copies the base coefficients (clamped, as in the paper).
|
| 75 |
+
"""
|
| 76 |
+
h_cycles, l_cycles = int(h_cycles), int(l_cycles)
|
| 77 |
+
if h_cycles < 1 or l_cycles < 1:
|
| 78 |
+
raise ValueError("H and L cycle counts must be positive")
|
| 79 |
+
if LAST_SCHEDULE["h"] == h_cycles and LAST_SCHEDULE["l"] == l_cycles:
|
| 80 |
+
return # already configured; avoids rebuilding the anchor needlessly
|
| 81 |
+
recurrent.config.H_cycles = h_cycles
|
| 82 |
+
recurrent.config.L_cycles = l_cycles
|
| 83 |
+
old = recurrent.visual_anchor
|
| 84 |
+
new = VisualAnchor(h_cycles, max_scale=ANCHOR_MAX_SCALE).to(
|
| 85 |
+
device=old.raw_alpha.device, dtype=old.raw_alpha.dtype
|
| 86 |
+
)
|
| 87 |
+
with torch.no_grad():
|
| 88 |
+
for i in range(h_cycles):
|
| 89 |
+
new.raw_alpha[i].copy_(
|
| 90 |
+
BASE_ANCHOR_VALUES[min(i, len(BASE_ANCHOR_VALUES) - 1)]
|
| 91 |
+
)
|
| 92 |
+
recurrent.visual_anchor = new
|
| 93 |
+
LAST_SCHEDULE["h"], LAST_SCHEDULE["l"] = h_cycles, l_cycles
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
# Preset schedules from the paper's Table 4 sweep: (H, L) -> unrolled depth
|
| 97 |
+
# 16 * H * (L + 1). H2L3 (the trained schedule) is the default.
|
| 98 |
+
SCHEDULES = [
|
| 99 |
+
("H1L1 (32 layers, fastest)", 1, 1),
|
| 100 |
+
("H1L3 (64 layers)", 1, 3),
|
| 101 |
+
("H2L1 (64 layers)", 2, 1),
|
| 102 |
+
("H2L3 (128 layers, default)", 2, 3),
|
| 103 |
+
("H2L5 (192 layers)", 2, 5),
|
| 104 |
+
("H4L3 (256 layers)", 4, 3),
|
| 105 |
+
]
|
| 106 |
+
_SCHEDULE_LOOKUP = {name: (h, l) for name, h, l in SCHEDULES}
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def _estimate_duration(*args, **kwargs) -> int:
|
| 110 |
+
"""Callable GPU duration: base time grows with unrolled depth and token budget."""
|
| 111 |
+
schedule = kwargs.get("schedule") or (args[2] if len(args) > 2 else None)
|
| 112 |
+
budget = kwargs.get("max_tokens") or (args[3] if len(args) > 3 else 32)
|
| 113 |
+
try:
|
| 114 |
+
h, l = _SCHEDULE_LOOKUP.get(str(schedule), (2, 3))
|
| 115 |
+
except Exception:
|
| 116 |
+
h, l = 2, 3
|
| 117 |
+
depth = 16 * h * (l + 1)
|
| 118 |
+
return min(300, 20 + int(depth * 0.09) + int(budget * 0.35))
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
@spaces.GPU(duration=_estimate_duration)
|
| 122 |
+
def answer(
|
| 123 |
+
image: Image.Image,
|
| 124 |
+
prompt: str,
|
| 125 |
+
schedule: str = "H2L3 (128 layers, default)",
|
| 126 |
+
max_tokens: int = 32,
|
| 127 |
+
use_trigger: bool = False,
|
| 128 |
+
progress=gr.Progress(track_tqdm=True),
|
| 129 |
+
) -> str:
|
| 130 |
+
"""Answer a question about an image using LoopVL, a recurrent vision-language model.
|
| 131 |
+
|
| 132 |
+
Args:
|
| 133 |
+
image: the input image to reason about.
|
| 134 |
+
prompt: the question or instruction about the image.
|
| 135 |
+
schedule: recurrent compute schedule (H and L cycle counts). Higher means
|
| 136 |
+
more effective transformer layers per forward pass.
|
| 137 |
+
max_tokens: maximum number of tokens to generate.
|
| 138 |
+
use_trigger: append the authors' reasoning trigger to the instruction.
|
| 139 |
+
progress: Gradio progress bar.
|
| 140 |
+
|
| 141 |
+
Returns:
|
| 142 |
+
The model's answer text.
|
| 143 |
+
"""
|
| 144 |
+
if image is None:
|
| 145 |
+
return "Please provide an image."
|
| 146 |
+
prompt = (prompt or "").strip()
|
| 147 |
+
if not prompt:
|
| 148 |
+
return "Please provide a question or instruction about the image."
|
| 149 |
+
|
| 150 |
+
h, l = _SCHEDULE_LOOKUP.get(schedule, (2, 3))
|
| 151 |
+
_apply_schedule(h, l)
|
| 152 |
+
|
| 153 |
+
instruction = prompt + (TRIGGER if use_trigger else "")
|
| 154 |
+
pil_image = image if isinstance(image, Image.Image) else Image.fromarray(image)
|
| 155 |
+
batch = build_inference_batch(
|
| 156 |
+
tokenizer,
|
| 157 |
+
processor,
|
| 158 |
+
pil_image.convert("RGB"),
|
| 159 |
+
instruction,
|
| 160 |
+
model.config,
|
| 161 |
+
torch.device(DEVICE),
|
| 162 |
+
)
|
| 163 |
+
max_tokens = max(1, min(int(max_tokens), 96))
|
| 164 |
+
start = time.perf_counter()
|
| 165 |
+
with torch.inference_mode():
|
| 166 |
+
generated = greedy_generate(
|
| 167 |
+
model,
|
| 168 |
+
batch,
|
| 169 |
+
max_new_tokens=max_tokens,
|
| 170 |
+
eos_token_id=tokenizer.eos_token_id,
|
| 171 |
+
)
|
| 172 |
+
elapsed = time.perf_counter() - start
|
| 173 |
+
prediction = tokenizer.decode(generated[0], skip_special_tokens=True).strip()
|
| 174 |
+
depth = 16 * h * (l + 1)
|
| 175 |
+
label = f"Answer (schedule {schedule.split(' ')[0]} → {depth} effective layers, {elapsed:.1f}s):"
|
| 176 |
+
return f"{label}\n{prediction}" if prediction else f"{label}\n(no tokens generated)"
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
CSS = """
|
| 180 |
+
#col-container { max-width: 1100px; margin: 0 auto; }
|
| 181 |
+
.dark .gradio-container { color: var(--body-text-color); }
|
| 182 |
+
"""
|
| 183 |
+
|
| 184 |
+
with gr.Blocks(theme=gr.themes.Citrus(), css=CSS) as demo:
|
| 185 |
+
with gr.Column(elem_id="col-container"):
|
| 186 |
+
gr.Markdown(
|
| 187 |
+
"""
|
| 188 |
+
# LoopVL: Recurrent Vision-Language Model
|
| 189 |
+
|
| 190 |
+
**LoopVL** (~1.6B params) reuses its 16-layer transformer stack in loops
|
| 191 |
+
instead of stacking 128 layers. You control the compute budget: pick an
|
| 192 |
+
**H/L schedule** to decide how many times the stack runs per forward pass,
|
| 193 |
+
then ask anything about the image.
|
| 194 |
+
|
| 195 |
+
[Model](https://huggingface.co/TierFlow/LoopVL) ·
|
| 196 |
+
[Code](https://github.com/Tier-Flow/LoopVL)
|
| 197 |
+
"""
|
| 198 |
+
)
|
| 199 |
+
with gr.Row():
|
| 200 |
+
with gr.Column(scale=1):
|
| 201 |
+
image_in = gr.Image(
|
| 202 |
+
type="pil", label="Image", sources=["upload", "clipboard"]
|
| 203 |
+
)
|
| 204 |
+
prompt_in = gr.Textbox(
|
| 205 |
+
label="Question / instruction",
|
| 206 |
+
placeholder="What is in the image?",
|
| 207 |
+
lines=2,
|
| 208 |
+
)
|
| 209 |
+
schedule_in = gr.Dropdown(
|
| 210 |
+
choices=[name for name, _, _ in SCHEDULES],
|
| 211 |
+
value="H2L3 (128 layers, default)",
|
| 212 |
+
label="Recurrent schedule (compute budget)",
|
| 213 |
+
info="Unrolled depth = 16 × H × (L+1) effective layers per forward pass",
|
| 214 |
+
)
|
| 215 |
+
run_btn = gr.Button("Answer", variant="primary")
|
| 216 |
+
with gr.Accordion("Advanced", open=False):
|
| 217 |
+
max_tokens_in = gr.Slider(
|
| 218 |
+
minimum=8, maximum=96, step=8, value=32,
|
| 219 |
+
label="Max new tokens",
|
| 220 |
+
)
|
| 221 |
+
trigger_in = gr.Checkbox(
|
| 222 |
+
value=False,
|
| 223 |
+
label="Append reasoning trigger",
|
| 224 |
+
info='Adds "Explain your reasoning, then give the final answer." '
|
| 225 |
+
"(the authors' math-reasoning prompt)",
|
| 226 |
+
)
|
| 227 |
+
with gr.Column(scale=1):
|
| 228 |
+
answer_out = gr.Textbox(label="Answer", lines=8, show_copy_button=True)
|
| 229 |
+
|
| 230 |
+
gr.Examples(
|
| 231 |
+
examples=[
|
| 232 |
+
["examples/cat_tabby.jpg", "What animal is this, and what is it doing?"],
|
| 233 |
+
["examples/rubber_ducks.jpg", "How many objects are in this image? Describe them."],
|
| 234 |
+
["examples/landscape.jpg", "Describe the scene and the weather."],
|
| 235 |
+
["examples/dog.jpg", "What breed of dog does this look like?"],
|
| 236 |
+
],
|
| 237 |
+
inputs=[image_in, prompt_in],
|
| 238 |
+
outputs=answer_out,
|
| 239 |
+
fn=answer,
|
| 240 |
+
cache_examples=True,
|
| 241 |
+
cache_mode="lazy",
|
| 242 |
+
label="Examples (click to load, then press Answer)",
|
| 243 |
+
)
|
| 244 |
+
|
| 245 |
+
run_btn.click(
|
| 246 |
+
answer,
|
| 247 |
+
inputs=[image_in, prompt_in, schedule_in, max_tokens_in, trigger_in],
|
| 248 |
+
outputs=answer_out,
|
| 249 |
+
api_name="generate",
|
| 250 |
+
concurrency_limit=1,
|
| 251 |
+
)
|
| 252 |
+
|
| 253 |
+
demo.launch(mcp_server=True)
|
examples/cat_tabby.jpg
ADDED
|
Git LFS Details
|
examples/dog.jpg
ADDED
|
Git LFS Details
|
examples/landscape.jpg
ADDED
|
examples/rubber_ducks.jpg
ADDED
|
hrm_penguin/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Penguin-Encoder x HRM-Text recurrent VLM."""
|
| 2 |
+
|
| 3 |
+
from .configuration import HrmPenguinConfig
|
| 4 |
+
|
| 5 |
+
__all__ = ["HrmPenguinConfig"]
|
| 6 |
+
__version__ = "0.1.0"
|
| 7 |
+
|
hrm_penguin/configuration.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from dataclasses import asdict, dataclass, field, fields
|
| 4 |
+
from typing import Any
|
| 5 |
+
|
| 6 |
+
@dataclass
|
| 7 |
+
class VisionConfig:
|
| 8 |
+
model_path: str = "runtime/models/Penguin-Encoder"
|
| 9 |
+
hidden_size: int = 1024
|
| 10 |
+
patch_size: int = 14
|
| 11 |
+
input_size: int = 336
|
| 12 |
+
grid_height: int = 24
|
| 13 |
+
grid_width: int = 24
|
| 14 |
+
visual_tokens: int = 576
|
| 15 |
+
dynamic_resolution: bool = False
|
| 16 |
+
min_visual_tokens: int = 256
|
| 17 |
+
max_visual_tokens: int = 1024
|
| 18 |
+
attn_implementation: str = "sdpa"
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
@dataclass
|
| 22 |
+
class LanguageConfig:
|
| 23 |
+
model_path: str = "runtime/models/HRM-Text-1B"
|
| 24 |
+
hidden_size: int = 1536
|
| 25 |
+
num_attention_heads: int = 12
|
| 26 |
+
head_dim: int = 128
|
| 27 |
+
H_cycles: int = 2
|
| 28 |
+
L_cycles: int = 3
|
| 29 |
+
rope_theta: float = 10000.0
|
| 30 |
+
rope_parameters: dict[str, Any] = field(default_factory=dict)
|
| 31 |
+
max_position_embeddings: int = 4096
|
| 32 |
+
attn_implementation: str = "sdpa"
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
@dataclass
|
| 36 |
+
class RecurrentVisualConfig:
|
| 37 |
+
rope_mode: str = "hybrid_2d"
|
| 38 |
+
use_visual_anchor: bool = True
|
| 39 |
+
use_visual_gate: bool = True
|
| 40 |
+
gate_dim: int = 256
|
| 41 |
+
anchor_raw_init: float = -2.0
|
| 42 |
+
anchor_max_scale: float = 0.5
|
| 43 |
+
gate_saturation_metric_lower: float = 0.05
|
| 44 |
+
gate_saturation_metric_upper: float = 1.95
|
| 45 |
+
|
| 46 |
+
def validate(self) -> None:
|
| 47 |
+
if self.rope_mode not in {"original_1d", "hybrid_2d"}:
|
| 48 |
+
raise ValueError(f"Unsupported rope_mode: {self.rope_mode}")
|
| 49 |
+
if not 0 <= self.gate_saturation_metric_lower < 1:
|
| 50 |
+
raise ValueError("Invalid lower Gate saturation metric bound")
|
| 51 |
+
if not 1 < self.gate_saturation_metric_upper <= 2:
|
| 52 |
+
raise ValueError("Invalid upper Gate saturation metric bound")
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
@dataclass
|
| 56 |
+
class InferenceConfig:
|
| 57 |
+
max_seq_len: int = 1024
|
| 58 |
+
use_cache: bool = False
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
@dataclass
|
| 62 |
+
class HrmPenguinConfig:
|
| 63 |
+
vision: VisionConfig = field(default_factory=VisionConfig)
|
| 64 |
+
language: LanguageConfig = field(default_factory=LanguageConfig)
|
| 65 |
+
recurrent_visual: RecurrentVisualConfig = field(default_factory=RecurrentVisualConfig)
|
| 66 |
+
inference: InferenceConfig = field(default_factory=InferenceConfig)
|
| 67 |
+
|
| 68 |
+
def validate(self) -> None:
|
| 69 |
+
self.recurrent_visual.validate()
|
| 70 |
+
if self.vision.input_size % self.vision.patch_size:
|
| 71 |
+
raise ValueError("input_size must be divisible by patch_size")
|
| 72 |
+
expected = self.vision.grid_height * self.vision.grid_width
|
| 73 |
+
if expected != self.vision.visual_tokens:
|
| 74 |
+
raise ValueError("visual token count does not match grid")
|
| 75 |
+
if self.vision.min_visual_tokens < 1:
|
| 76 |
+
raise ValueError("min_visual_tokens must be positive")
|
| 77 |
+
if self.vision.max_visual_tokens < self.vision.min_visual_tokens:
|
| 78 |
+
raise ValueError("max_visual_tokens must be >= min_visual_tokens")
|
| 79 |
+
if (
|
| 80 |
+
self.vision.dynamic_resolution
|
| 81 |
+
and self.inference.max_seq_len <= self.vision.max_visual_tokens + 2
|
| 82 |
+
):
|
| 83 |
+
raise ValueError(
|
| 84 |
+
"Dynamic resolution max_seq_len must leave room for instruction and response"
|
| 85 |
+
)
|
| 86 |
+
if self.inference.max_seq_len > self.language.max_position_embeddings:
|
| 87 |
+
raise ValueError(
|
| 88 |
+
"max_seq_len exceeds the HRM language model position limit"
|
| 89 |
+
)
|
| 90 |
+
if self.language.head_dim % 2:
|
| 91 |
+
raise ValueError("head_dim must be even for hybrid RoPE")
|
| 92 |
+
|
| 93 |
+
@classmethod
|
| 94 |
+
def from_dict(cls, raw: dict[str, Any]) -> "HrmPenguinConfig":
|
| 95 |
+
"""Read the model architecture and inference settings from embedded metadata."""
|
| 96 |
+
def component(kind, name, defaults=None):
|
| 97 |
+
values = dict(defaults or {})
|
| 98 |
+
values.update(raw.get(name, {}))
|
| 99 |
+
allowed = {item.name for item in fields(kind)}
|
| 100 |
+
return kind(**{key: value for key, value in values.items() if key in allowed})
|
| 101 |
+
|
| 102 |
+
language = component(LanguageConfig, "language")
|
| 103 |
+
cfg = cls(
|
| 104 |
+
vision=component(VisionConfig, "vision"),
|
| 105 |
+
language=language,
|
| 106 |
+
recurrent_visual=component(RecurrentVisualConfig, "recurrent_visual"),
|
| 107 |
+
inference=component(
|
| 108 |
+
InferenceConfig, "inference",
|
| 109 |
+
{"max_seq_len": language.max_position_embeddings},
|
| 110 |
+
),
|
| 111 |
+
)
|
| 112 |
+
cfg.validate()
|
| 113 |
+
return cfg
|
| 114 |
+
|
| 115 |
+
def to_dict(self) -> dict[str, Any]:
|
| 116 |
+
return asdict(self)
|
hrm_penguin/generation.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
|
| 5 |
+
from .recurrent.positions import build_compact_positions
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
@torch.no_grad()
|
| 9 |
+
def greedy_generate(
|
| 10 |
+
model,
|
| 11 |
+
batch: dict[str, torch.Tensor],
|
| 12 |
+
max_new_tokens: int = 64,
|
| 13 |
+
eos_token_id: int | None = None,
|
| 14 |
+
) -> torch.LongTensor:
|
| 15 |
+
"""Correctness-first generation that recomputes the recurrent sequence each step."""
|
| 16 |
+
if batch["input_ids"].shape[0] != 1:
|
| 17 |
+
raise ValueError("greedy_generate currently supports batch_size=1")
|
| 18 |
+
model.eval()
|
| 19 |
+
input_ids = batch["input_ids"]
|
| 20 |
+
header_length = int(batch["header_length"])
|
| 21 |
+
instruction_length = int(batch["instruction_length"])
|
| 22 |
+
visual_token_count = int(batch["visual_token_counts"][0])
|
| 23 |
+
grid_height = int(batch["grid_heights"][0])
|
| 24 |
+
grid_width = int(batch["grid_widths"][0])
|
| 25 |
+
visual_embeddings = model.encode_images(
|
| 26 |
+
batch["pixel_values"], batch["grid_sizes"], batch["merge_sizes"]
|
| 27 |
+
)
|
| 28 |
+
generated: list[torch.Tensor] = []
|
| 29 |
+
for _ in range(max_new_tokens):
|
| 30 |
+
response_length = input_ids.shape[1] - header_length - visual_token_count - instruction_length
|
| 31 |
+
mask_positions, rope_positions, visual_mask, instruction_mask = build_compact_positions(
|
| 32 |
+
torch.tensor([header_length], device=input_ids.device),
|
| 33 |
+
torch.tensor([instruction_length], device=input_ids.device),
|
| 34 |
+
torch.tensor([response_length], device=input_ids.device),
|
| 35 |
+
visual_token_counts=torch.tensor([visual_token_count], device=input_ids.device),
|
| 36 |
+
grid_heights=torch.tensor([grid_height], device=input_ids.device),
|
| 37 |
+
grid_widths=torch.tensor([grid_width], device=input_ids.device),
|
| 38 |
+
grid_h=model.config.vision.grid_height,
|
| 39 |
+
grid_w=model.config.vision.grid_width,
|
| 40 |
+
rope_mode=model.config.recurrent_visual.rope_mode,
|
| 41 |
+
)
|
| 42 |
+
text_embeddings = model.language_model.get_input_embeddings()(input_ids)
|
| 43 |
+
inputs_embeds = model._replace_visual_tokens(text_embeddings, visual_embeddings, visual_mask)
|
| 44 |
+
prefix_length = header_length + visual_token_count + instruction_length
|
| 45 |
+
token_type_ids = torch.zeros_like(input_ids)
|
| 46 |
+
token_type_ids[:, :prefix_length] = 1
|
| 47 |
+
outputs = model.recurrent_model(
|
| 48 |
+
inputs_embeds=inputs_embeds,
|
| 49 |
+
attention_mask=torch.ones_like(input_ids),
|
| 50 |
+
token_type_ids=token_type_ids,
|
| 51 |
+
position_ids=mask_positions,
|
| 52 |
+
rope_position_ids=rope_positions,
|
| 53 |
+
visual_mask=visual_mask,
|
| 54 |
+
instruction_mask=instruction_mask,
|
| 55 |
+
use_cache=False,
|
| 56 |
+
)
|
| 57 |
+
next_token = model.language_model.lm_head(outputs.last_hidden_state[:, -1]).argmax(dim=-1)
|
| 58 |
+
generated.append(next_token)
|
| 59 |
+
input_ids = torch.cat((input_ids, next_token[:, None]), dim=1)
|
| 60 |
+
if eos_token_id is not None and bool((next_token == eos_token_id).all()):
|
| 61 |
+
break
|
| 62 |
+
return torch.stack(generated, dim=1) if generated else input_ids[:, :0]
|
hrm_penguin/inference.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
from .prompting import PromptParts, tokenize_without_special_tokens
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def build_inference_batch(tokenizer, image_processor, image, instruction, config, device):
|
| 8 |
+
prompt = PromptParts()
|
| 9 |
+
header = tokenize_without_special_tokens(tokenizer, prompt.header)
|
| 10 |
+
instruction_ids = tokenize_without_special_tokens(
|
| 11 |
+
tokenizer, prompt.instruction_prefix + instruction + prompt.instruction_suffix
|
| 12 |
+
)
|
| 13 |
+
placeholder = tokenizer.pad_token_id if tokenizer.pad_token_id is not None else tokenizer.eos_token_id
|
| 14 |
+
processed = image_processor(image, return_tensors="pt")
|
| 15 |
+
grid = processed["grid_sizes"][0].long()
|
| 16 |
+
merge = int(processed["merge_sizes"][0])
|
| 17 |
+
if int(grid[0]) != 1 or int(grid[1]) % merge or int(grid[2]) % merge:
|
| 18 |
+
raise ValueError(f"Unsupported inference grid: {grid.tolist()}, merge={merge}")
|
| 19 |
+
grid_height = int(grid[1]) // merge
|
| 20 |
+
grid_width = int(grid[2]) // merge
|
| 21 |
+
visual_tokens = grid_height * grid_width
|
| 22 |
+
input_ids = torch.tensor(
|
| 23 |
+
[header + [placeholder] * visual_tokens + instruction_ids],
|
| 24 |
+
dtype=torch.long,
|
| 25 |
+
device=device,
|
| 26 |
+
)
|
| 27 |
+
return {
|
| 28 |
+
"input_ids": input_ids,
|
| 29 |
+
"header_length": len(header),
|
| 30 |
+
"instruction_length": len(instruction_ids),
|
| 31 |
+
"visual_token_counts": torch.tensor([visual_tokens], device=device),
|
| 32 |
+
"grid_heights": torch.tensor([grid_height], device=device),
|
| 33 |
+
"grid_widths": torch.tensor([grid_width], device=device),
|
| 34 |
+
"pixel_values": processed["pixel_values"].to(device),
|
| 35 |
+
"grid_sizes": processed["grid_sizes"].to(device),
|
| 36 |
+
"merge_sizes": processed["merge_sizes"].to(device),
|
| 37 |
+
}
|
hrm_penguin/model.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from dataclasses import dataclass
|
| 4 |
+
|
| 5 |
+
import torch
|
| 6 |
+
import torch.nn.functional as F
|
| 7 |
+
from torch import nn
|
| 8 |
+
|
| 9 |
+
from .configuration import HrmPenguinConfig
|
| 10 |
+
from .recurrent.hrm_multimodal import MultimodalHrmTextModel
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
@dataclass
|
| 14 |
+
class HrmPenguinOutput:
|
| 15 |
+
logits: torch.Tensor | None
|
| 16 |
+
past_key_values: object | None = None
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class HrmPenguinForConditionalGeneration(nn.Module):
|
| 20 |
+
def __init__(self, vision_encoder, language_model, projector, config: HrmPenguinConfig):
|
| 21 |
+
super().__init__()
|
| 22 |
+
self.vision_encoder = vision_encoder
|
| 23 |
+
self.language_model = language_model
|
| 24 |
+
self.projector = projector
|
| 25 |
+
self.config = config
|
| 26 |
+
self.last_effective_depth_metrics: dict[str, torch.Tensor] = {}
|
| 27 |
+
|
| 28 |
+
@property
|
| 29 |
+
def recurrent_model(self) -> MultimodalHrmTextModel:
|
| 30 |
+
return self.language_model.model
|
| 31 |
+
|
| 32 |
+
@staticmethod
|
| 33 |
+
def _replace_visual_tokens(
|
| 34 |
+
text_embeddings: torch.Tensor,
|
| 35 |
+
visual_embeddings: torch.Tensor,
|
| 36 |
+
visual_mask: torch.Tensor,
|
| 37 |
+
image_batch_indices: torch.LongTensor | None = None,
|
| 38 |
+
) -> torch.Tensor:
|
| 39 |
+
counts = visual_mask.sum(dim=1)
|
| 40 |
+
visual_rows = (counts > 0).nonzero(as_tuple=False).flatten()
|
| 41 |
+
if image_batch_indices is None:
|
| 42 |
+
image_batch_indices = visual_rows
|
| 43 |
+
image_batch_indices = image_batch_indices.to(device=visual_mask.device, dtype=torch.long)
|
| 44 |
+
if not torch.equal(visual_rows, image_batch_indices):
|
| 45 |
+
raise ValueError(
|
| 46 |
+
f"visual rows {visual_rows.tolist()} do not match image batch indices "
|
| 47 |
+
f"{image_batch_indices.tolist()}"
|
| 48 |
+
)
|
| 49 |
+
if visual_embeddings.ndim == 2:
|
| 50 |
+
if int(counts[visual_rows].sum()) != visual_embeddings.shape[0]:
|
| 51 |
+
raise ValueError(
|
| 52 |
+
f"visual_mask counts {counts.tolist()} do not match packed visual "
|
| 53 |
+
f"embeddings {tuple(visual_embeddings.shape)}"
|
| 54 |
+
)
|
| 55 |
+
elif visual_embeddings.ndim == 3:
|
| 56 |
+
if visual_rows.numel() != visual_embeddings.shape[0] or not torch.all(
|
| 57 |
+
counts[visual_rows] == visual_embeddings.shape[1]
|
| 58 |
+
):
|
| 59 |
+
raise ValueError(
|
| 60 |
+
f"visual_mask counts {counts.tolist()} do not match visual embeddings "
|
| 61 |
+
f"{tuple(visual_embeddings.shape)}"
|
| 62 |
+
)
|
| 63 |
+
else:
|
| 64 |
+
raise ValueError(
|
| 65 |
+
"visual_embeddings must be packed [sum(N_i), D] or dense [B, N, D]"
|
| 66 |
+
)
|
| 67 |
+
expanded_mask = visual_mask.unsqueeze(-1).expand_as(text_embeddings)
|
| 68 |
+
replacement = torch.zeros_like(text_embeddings).masked_scatter(
|
| 69 |
+
expanded_mask, visual_embeddings.to(text_embeddings.dtype).reshape(-1)
|
| 70 |
+
)
|
| 71 |
+
return torch.where(expanded_mask, replacement, text_embeddings)
|
| 72 |
+
|
| 73 |
+
def encode_images(self, pixel_values, grid_sizes, merge_sizes) -> torch.Tensor:
|
| 74 |
+
vision_parameter = next(self.vision_encoder.parameters())
|
| 75 |
+
pixel_values = pixel_values.to(device=vision_parameter.device, dtype=vision_parameter.dtype)
|
| 76 |
+
grid_sizes = grid_sizes.to(vision_parameter.device)
|
| 77 |
+
merge_sizes = merge_sizes.to(vision_parameter.device)
|
| 78 |
+
with torch.no_grad():
|
| 79 |
+
features = self.vision_encoder(pixel_values, grid_sizes, merge_sizes)
|
| 80 |
+
return self.projector(features)
|
| 81 |
+
|
| 82 |
+
def forward(
|
| 83 |
+
self,
|
| 84 |
+
input_ids: torch.LongTensor,
|
| 85 |
+
attention_mask: torch.Tensor,
|
| 86 |
+
token_type_ids: torch.LongTensor,
|
| 87 |
+
position_ids: torch.LongTensor,
|
| 88 |
+
rope_position_ids: torch.LongTensor,
|
| 89 |
+
visual_mask: torch.BoolTensor,
|
| 90 |
+
instruction_mask: torch.BoolTensor,
|
| 91 |
+
image_batch_indices: torch.LongTensor | None = None,
|
| 92 |
+
visual_token_counts: torch.LongTensor | None = None,
|
| 93 |
+
grid_heights: torch.LongTensor | None = None,
|
| 94 |
+
grid_widths: torch.LongTensor | None = None,
|
| 95 |
+
pixel_values: torch.Tensor | None = None,
|
| 96 |
+
grid_sizes: torch.Tensor | None = None,
|
| 97 |
+
merge_sizes: torch.Tensor | None = None,
|
| 98 |
+
vision_features: torch.Tensor | None = None,
|
| 99 |
+
collect_visual_diagnostics: bool = False,
|
| 100 |
+
collect_depth_diagnostics: bool = False,
|
| 101 |
+
) -> HrmPenguinOutput:
|
| 102 |
+
mask_counts = visual_mask.sum(dim=1)
|
| 103 |
+
if visual_token_counts is not None and not torch.equal(
|
| 104 |
+
mask_counts, visual_token_counts.to(mask_counts.device)
|
| 105 |
+
):
|
| 106 |
+
raise ValueError("visual_mask does not match visual_token_counts")
|
| 107 |
+
if grid_heights is not None and grid_widths is not None:
|
| 108 |
+
expected_counts = grid_heights.to(mask_counts.device) * grid_widths.to(mask_counts.device)
|
| 109 |
+
if not torch.equal(mask_counts, expected_counts):
|
| 110 |
+
raise ValueError("visual token counts do not match dynamic grid areas")
|
| 111 |
+
text_embeddings = self.language_model.get_input_embeddings()(input_ids)
|
| 112 |
+
if bool(visual_mask.any()):
|
| 113 |
+
if vision_features is None:
|
| 114 |
+
if pixel_values is None or grid_sizes is None or merge_sizes is None:
|
| 115 |
+
raise ValueError(
|
| 116 |
+
"pixel_values/grid_sizes/merge_sizes or vision_features are required"
|
| 117 |
+
)
|
| 118 |
+
vision_features = self.encode_images(pixel_values, grid_sizes, merge_sizes)
|
| 119 |
+
# encode_images returns projected values for the regular path.
|
| 120 |
+
visual_embeddings = vision_features
|
| 121 |
+
else:
|
| 122 |
+
visual_embeddings = self.projector(vision_features)
|
| 123 |
+
inputs_embeds = self._replace_visual_tokens(
|
| 124 |
+
text_embeddings,
|
| 125 |
+
visual_embeddings,
|
| 126 |
+
visual_mask,
|
| 127 |
+
image_batch_indices=image_batch_indices,
|
| 128 |
+
)
|
| 129 |
+
else:
|
| 130 |
+
inputs_embeds = text_embeddings
|
| 131 |
+
outputs = self.recurrent_model(
|
| 132 |
+
inputs_embeds=inputs_embeds,
|
| 133 |
+
attention_mask=attention_mask,
|
| 134 |
+
token_type_ids=token_type_ids,
|
| 135 |
+
position_ids=position_ids,
|
| 136 |
+
rope_position_ids=rope_position_ids,
|
| 137 |
+
visual_mask=visual_mask,
|
| 138 |
+
instruction_mask=instruction_mask,
|
| 139 |
+
use_cache=False,
|
| 140 |
+
collect_visual_diagnostics=collect_visual_diagnostics,
|
| 141 |
+
collect_depth_diagnostics=collect_depth_diagnostics,
|
| 142 |
+
)
|
| 143 |
+
logits = None
|
| 144 |
+
self.last_effective_depth_metrics = {}
|
| 145 |
+
diagnostics = self.recurrent_model.last_visual_diagnostics
|
| 146 |
+
if (
|
| 147 |
+
collect_depth_diagnostics
|
| 148 |
+
and diagnostics is not None
|
| 149 |
+
and diagnostics.logit_lens_response_hidden
|
| 150 |
+
and diagnostics.logit_lens_response_hidden[-1].numel()
|
| 151 |
+
):
|
| 152 |
+
with torch.no_grad():
|
| 153 |
+
final_hidden = diagnostics.logit_lens_response_hidden[-1]
|
| 154 |
+
final_log_probs = F.log_softmax(
|
| 155 |
+
self.language_model.lm_head(final_hidden).float(), dim=-1
|
| 156 |
+
)
|
| 157 |
+
final_probs = final_log_probs.exp()
|
| 158 |
+
for name, hidden in zip(
|
| 159 |
+
diagnostics.logit_lens_names,
|
| 160 |
+
diagnostics.logit_lens_response_hidden,
|
| 161 |
+
strict=True,
|
| 162 |
+
):
|
| 163 |
+
if hidden.shape != final_hidden.shape:
|
| 164 |
+
continue
|
| 165 |
+
intermediate_log_probs = F.log_softmax(
|
| 166 |
+
self.language_model.lm_head(hidden).float(), dim=-1
|
| 167 |
+
)
|
| 168 |
+
self.last_effective_depth_metrics[f"{name}_kl_to_final"] = (
|
| 169 |
+
F.kl_div(
|
| 170 |
+
intermediate_log_probs,
|
| 171 |
+
final_probs,
|
| 172 |
+
reduction="batchmean",
|
| 173 |
+
log_target=False,
|
| 174 |
+
)
|
| 175 |
+
)
|
| 176 |
+
logits = self.language_model.lm_head(outputs.last_hidden_state)
|
| 177 |
+
return HrmPenguinOutput(
|
| 178 |
+
logits=logits,
|
| 179 |
+
past_key_values=outputs.past_key_values,
|
| 180 |
+
)
|
hrm_penguin/prompting.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from dataclasses import dataclass
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
@dataclass(frozen=True)
|
| 7 |
+
class PromptParts:
|
| 8 |
+
header: str = "<|im_start|>condition\nImage:\n"
|
| 9 |
+
text_header: str = "<|im_start|>condition\n"
|
| 10 |
+
instruction_prefix: str = "\nInstruction:\n"
|
| 11 |
+
instruction_suffix: str = "<|im_end|>\n"
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def tokenize_without_special_tokens(tokenizer, text: str) -> list[int]:
|
| 15 |
+
return tokenizer(text, add_special_tokens=False)["input_ids"]
|
hrm_penguin/recurrent/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .hybrid_rope import HybridHrmRotaryEmbedding, apply_rotary_pos_emb
|
| 2 |
+
from .visual_anchor import VisualAnchor
|
| 3 |
+
from .visual_gate import LoopAwareVisualGate
|
| 4 |
+
|
| 5 |
+
__all__ = [
|
| 6 |
+
"HybridHrmRotaryEmbedding",
|
| 7 |
+
"LoopAwareVisualGate",
|
| 8 |
+
"VisualAnchor",
|
| 9 |
+
"apply_rotary_pos_emb",
|
| 10 |
+
]
|
| 11 |
+
|
hrm_penguin/recurrent/hrm_multimodal.py
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from dataclasses import dataclass
|
| 4 |
+
|
| 5 |
+
import torch
|
| 6 |
+
import torch.nn.functional as F
|
| 7 |
+
|
| 8 |
+
from transformers.cache_utils import Cache, DynamicCache
|
| 9 |
+
from transformers.masking_utils import create_causal_mask
|
| 10 |
+
from transformers.modeling_outputs import BaseModelOutputWithPast
|
| 11 |
+
from transformers.models.hrm_text.modeling_hrm_text import HrmTextModel
|
| 12 |
+
|
| 13 |
+
from .hybrid_rope import HybridHrmRotaryEmbedding
|
| 14 |
+
from .visual_anchor import VisualAnchor
|
| 15 |
+
from .visual_gate import LoopAwareVisualGate, masked_mean
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
@dataclass
|
| 19 |
+
class RecurrentVisualDiagnostics:
|
| 20 |
+
gate_mean: list[torch.Tensor]
|
| 21 |
+
gate_std: list[torch.Tensor]
|
| 22 |
+
gate_min: list[torch.Tensor]
|
| 23 |
+
gate_max: list[torch.Tensor]
|
| 24 |
+
gate_entropy: list[torch.Tensor]
|
| 25 |
+
gate_saturation_rate: list[torch.Tensor]
|
| 26 |
+
alpha: list[torch.Tensor]
|
| 27 |
+
reinjection_norm: list[torch.Tensor]
|
| 28 |
+
current_visual_norm: list[torch.Tensor]
|
| 29 |
+
reinjection_ratio: list[torch.Tensor]
|
| 30 |
+
gate_maps: list[list[torch.Tensor]]
|
| 31 |
+
transition_names: list[str]
|
| 32 |
+
transition_delta_rms: list[torch.Tensor]
|
| 33 |
+
transition_relative_delta: list[torch.Tensor]
|
| 34 |
+
transition_cosine: list[torch.Tensor]
|
| 35 |
+
transition_visual_delta_rms: list[torch.Tensor]
|
| 36 |
+
transition_visual_cosine: list[torch.Tensor]
|
| 37 |
+
transition_response_delta_rms: list[torch.Tensor]
|
| 38 |
+
transition_response_cosine: list[torch.Tensor]
|
| 39 |
+
logit_lens_names: list[str]
|
| 40 |
+
logit_lens_response_hidden: list[torch.Tensor]
|
| 41 |
+
|
| 42 |
+
@classmethod
|
| 43 |
+
def empty(cls) -> "RecurrentVisualDiagnostics":
|
| 44 |
+
return cls(
|
| 45 |
+
[], [], [], [], [], [], [], [], [], [], [],
|
| 46 |
+
[], [], [], [], [], [], [], [], [], [],
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
@staticmethod
|
| 50 |
+
def _state_change(
|
| 51 |
+
before: torch.Tensor,
|
| 52 |
+
after: torch.Tensor,
|
| 53 |
+
mask: torch.BoolTensor,
|
| 54 |
+
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 55 |
+
before_selected = before.detach()[mask].float()
|
| 56 |
+
after_selected = after.detach()[mask].float()
|
| 57 |
+
if not before_selected.numel():
|
| 58 |
+
nan = after.new_tensor(float("nan"), dtype=torch.float32)
|
| 59 |
+
return nan, nan, nan
|
| 60 |
+
delta = after_selected - before_selected
|
| 61 |
+
delta_rms = delta.square().mean().sqrt()
|
| 62 |
+
relative_delta = delta.norm() / before_selected.norm().clamp_min(1e-12)
|
| 63 |
+
cosine = F.cosine_similarity(before_selected, after_selected, dim=-1).mean()
|
| 64 |
+
return delta_rms, relative_delta, cosine
|
| 65 |
+
|
| 66 |
+
def record_transition(
|
| 67 |
+
self,
|
| 68 |
+
name: str,
|
| 69 |
+
before: torch.Tensor,
|
| 70 |
+
after: torch.Tensor,
|
| 71 |
+
valid_mask: torch.BoolTensor,
|
| 72 |
+
visual_mask: torch.BoolTensor | None,
|
| 73 |
+
response_mask: torch.BoolTensor,
|
| 74 |
+
*,
|
| 75 |
+
logit_lens_token_limit: int = 4,
|
| 76 |
+
) -> None:
|
| 77 |
+
delta_rms, relative_delta, cosine = self._state_change(before, after, valid_mask)
|
| 78 |
+
self.transition_names.append(name)
|
| 79 |
+
self.transition_delta_rms.append(delta_rms)
|
| 80 |
+
self.transition_relative_delta.append(relative_delta)
|
| 81 |
+
self.transition_cosine.append(cosine)
|
| 82 |
+
|
| 83 |
+
if visual_mask is not None and bool(visual_mask.any()):
|
| 84 |
+
visual_delta, _, visual_cosine = self._state_change(before, after, visual_mask)
|
| 85 |
+
else:
|
| 86 |
+
visual_delta = after.new_tensor(float("nan"), dtype=torch.float32)
|
| 87 |
+
visual_cosine = after.new_tensor(float("nan"), dtype=torch.float32)
|
| 88 |
+
self.transition_visual_delta_rms.append(visual_delta)
|
| 89 |
+
self.transition_visual_cosine.append(visual_cosine)
|
| 90 |
+
|
| 91 |
+
response_delta, _, response_cosine = self._state_change(
|
| 92 |
+
before, after, response_mask
|
| 93 |
+
)
|
| 94 |
+
self.transition_response_delta_rms.append(response_delta)
|
| 95 |
+
self.transition_response_cosine.append(response_cosine)
|
| 96 |
+
|
| 97 |
+
response_hidden = after.detach()[response_mask]
|
| 98 |
+
if response_hidden.shape[0] > logit_lens_token_limit:
|
| 99 |
+
positions = torch.linspace(
|
| 100 |
+
0,
|
| 101 |
+
response_hidden.shape[0] - 1,
|
| 102 |
+
steps=logit_lens_token_limit,
|
| 103 |
+
device=response_hidden.device,
|
| 104 |
+
).round().long()
|
| 105 |
+
response_hidden = response_hidden.index_select(0, positions)
|
| 106 |
+
self.logit_lens_names.append(name)
|
| 107 |
+
self.logit_lens_response_hidden.append(response_hidden)
|
| 108 |
+
|
| 109 |
+
def record(
|
| 110 |
+
self,
|
| 111 |
+
gate: torch.Tensor,
|
| 112 |
+
alpha: torch.Tensor,
|
| 113 |
+
reinjection: torch.Tensor,
|
| 114 |
+
current_visual: torch.Tensor,
|
| 115 |
+
valid_mask: torch.BoolTensor | None = None,
|
| 116 |
+
saturation_lower: float = 0.05,
|
| 117 |
+
saturation_upper: float = 1.95,
|
| 118 |
+
) -> None:
|
| 119 |
+
detached = gate.detach().float()
|
| 120 |
+
if valid_mask is None:
|
| 121 |
+
valid_mask = torch.ones_like(detached, dtype=torch.bool)
|
| 122 |
+
selected = detached[valid_mask]
|
| 123 |
+
if not selected.numel():
|
| 124 |
+
return
|
| 125 |
+
probability = (selected / 2.0).clamp(1e-6, 1.0 - 1e-6)
|
| 126 |
+
entropy = -(probability * probability.log() + (1 - probability) * (1 - probability).log())
|
| 127 |
+
reinjection_norm = reinjection.detach().float()[valid_mask].norm()
|
| 128 |
+
current_visual_norm = current_visual.detach().float()[valid_mask].norm()
|
| 129 |
+
ratio = reinjection_norm / current_visual_norm.clamp_min(1e-12)
|
| 130 |
+
self.gate_mean.append(selected.mean())
|
| 131 |
+
self.gate_std.append(selected.std())
|
| 132 |
+
self.gate_min.append(selected.min())
|
| 133 |
+
self.gate_max.append(selected.max())
|
| 134 |
+
self.gate_entropy.append(entropy.mean())
|
| 135 |
+
self.gate_saturation_rate.append(
|
| 136 |
+
((selected < saturation_lower) | (selected > saturation_upper)).float().mean()
|
| 137 |
+
)
|
| 138 |
+
self.alpha.append(alpha.detach().float())
|
| 139 |
+
self.reinjection_norm.append(reinjection_norm)
|
| 140 |
+
self.current_visual_norm.append(current_visual_norm)
|
| 141 |
+
self.reinjection_ratio.append(ratio)
|
| 142 |
+
self.gate_maps.append(
|
| 143 |
+
[detached[row, valid_mask[row]].cpu() for row in range(detached.shape[0])]
|
| 144 |
+
)
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
class MultimodalHrmTextModel(HrmTextModel):
|
| 148 |
+
"""Minimal HRM recurrent patch for spatial RoPE and gated visual re-grounding."""
|
| 149 |
+
|
| 150 |
+
def __init__(
|
| 151 |
+
self,
|
| 152 |
+
config,
|
| 153 |
+
rope_mode: str = "hybrid_2d",
|
| 154 |
+
use_visual_anchor: bool = True,
|
| 155 |
+
use_visual_gate: bool = True,
|
| 156 |
+
gate_dim: int = 256,
|
| 157 |
+
anchor_raw_init: float = -2.0,
|
| 158 |
+
anchor_max_scale: float = 0.5,
|
| 159 |
+
gate_saturation_metric_lower: float = 0.05,
|
| 160 |
+
gate_saturation_metric_upper: float = 1.95,
|
| 161 |
+
):
|
| 162 |
+
super().__init__(config)
|
| 163 |
+
if rope_mode not in {"original_1d", "hybrid_2d"}:
|
| 164 |
+
raise ValueError(f"Unsupported rope_mode: {rope_mode}")
|
| 165 |
+
self.rope_mode = rope_mode
|
| 166 |
+
self.use_visual_anchor = use_visual_anchor
|
| 167 |
+
self.use_visual_gate = use_visual_gate
|
| 168 |
+
self.gate_saturation_metric_lower = float(gate_saturation_metric_lower)
|
| 169 |
+
self.gate_saturation_metric_upper = float(gate_saturation_metric_upper)
|
| 170 |
+
rope_parameters = getattr(config, "rope_parameters", None) or {}
|
| 171 |
+
rope_theta = getattr(config, "rope_theta", None) or rope_parameters.get("rope_theta", 10000.0)
|
| 172 |
+
self.hybrid_rotary_emb = HybridHrmRotaryEmbedding(
|
| 173 |
+
head_dim=config.head_dim,
|
| 174 |
+
theta=rope_theta,
|
| 175 |
+
config=config,
|
| 176 |
+
)
|
| 177 |
+
self.visual_gate = LoopAwareVisualGate(config.hidden_size, gate_dim)
|
| 178 |
+
self.visual_anchor = VisualAnchor(
|
| 179 |
+
config.H_cycles,
|
| 180 |
+
raw_init=anchor_raw_init,
|
| 181 |
+
max_scale=anchor_max_scale,
|
| 182 |
+
)
|
| 183 |
+
self.last_visual_diagnostics: RecurrentVisualDiagnostics | None = None
|
| 184 |
+
|
| 185 |
+
@classmethod
|
| 186 |
+
def from_text_model(cls, text_model: HrmTextModel, **kwargs) -> "MultimodalHrmTextModel":
|
| 187 |
+
model = cls(text_model.config, **kwargs)
|
| 188 |
+
incompatible = model.load_state_dict(text_model.state_dict(), strict=False)
|
| 189 |
+
allowed_missing = {
|
| 190 |
+
"visual_gate.visual_proj.weight",
|
| 191 |
+
"visual_gate.query_proj.weight",
|
| 192 |
+
"visual_gate.out_proj.weight",
|
| 193 |
+
"visual_gate.out_proj.bias",
|
| 194 |
+
"visual_anchor.raw_alpha",
|
| 195 |
+
}
|
| 196 |
+
unexpected_missing = set(incompatible.missing_keys).difference(allowed_missing)
|
| 197 |
+
if unexpected_missing or incompatible.unexpected_keys:
|
| 198 |
+
raise RuntimeError(
|
| 199 |
+
f"HRM conversion mismatch: missing={sorted(unexpected_missing)}, "
|
| 200 |
+
f"unexpected={incompatible.unexpected_keys}"
|
| 201 |
+
)
|
| 202 |
+
return model
|
| 203 |
+
|
| 204 |
+
@staticmethod
|
| 205 |
+
def _extract_tokens(
|
| 206 |
+
states: torch.Tensor, mask: torch.Tensor
|
| 207 |
+
) -> tuple[torch.Tensor, torch.BoolTensor]:
|
| 208 |
+
counts = mask.sum(dim=1)
|
| 209 |
+
max_count = int(counts.max()) if counts.numel() else 0
|
| 210 |
+
tokens = states.new_zeros((states.shape[0], max_count, states.shape[-1]))
|
| 211 |
+
valid = torch.zeros((states.shape[0], max_count), dtype=torch.bool, device=states.device)
|
| 212 |
+
for row, count in enumerate(counts.tolist()):
|
| 213 |
+
if count:
|
| 214 |
+
tokens[row, :count] = states[row, mask[row]]
|
| 215 |
+
valid[row, :count] = True
|
| 216 |
+
return tokens, valid
|
| 217 |
+
|
| 218 |
+
@staticmethod
|
| 219 |
+
def _scatter_tokens(
|
| 220 |
+
tokens: torch.Tensor,
|
| 221 |
+
valid: torch.BoolTensor,
|
| 222 |
+
mask: torch.Tensor,
|
| 223 |
+
reference: torch.Tensor,
|
| 224 |
+
) -> torch.Tensor:
|
| 225 |
+
output = torch.zeros_like(reference)
|
| 226 |
+
for row in range(reference.shape[0]):
|
| 227 |
+
output[row, mask[row]] = tokens[row, valid[row]]
|
| 228 |
+
return output
|
| 229 |
+
|
| 230 |
+
def _run_stack(self, stack, hidden_states: torch.Tensor, **kwargs) -> torch.Tensor:
|
| 231 |
+
return stack(hidden_states, **kwargs)
|
| 232 |
+
|
| 233 |
+
def forward(
|
| 234 |
+
self,
|
| 235 |
+
input_ids: torch.LongTensor | None = None,
|
| 236 |
+
attention_mask: torch.Tensor | None = None,
|
| 237 |
+
position_ids: torch.LongTensor | None = None,
|
| 238 |
+
past_key_values: Cache | None = None,
|
| 239 |
+
token_type_ids: torch.LongTensor | None = None,
|
| 240 |
+
inputs_embeds: torch.FloatTensor | None = None,
|
| 241 |
+
use_cache: bool | None = None,
|
| 242 |
+
rope_position_ids: torch.LongTensor | None = None,
|
| 243 |
+
visual_mask: torch.BoolTensor | None = None,
|
| 244 |
+
instruction_mask: torch.BoolTensor | None = None,
|
| 245 |
+
collect_visual_diagnostics: bool = False,
|
| 246 |
+
collect_depth_diagnostics: bool = False,
|
| 247 |
+
**kwargs,
|
| 248 |
+
) -> BaseModelOutputWithPast:
|
| 249 |
+
if (input_ids is None) ^ (inputs_embeds is not None):
|
| 250 |
+
raise ValueError("You must specify exactly one of input_ids or inputs_embeds")
|
| 251 |
+
if inputs_embeds is None:
|
| 252 |
+
inputs_embeds = self.embed_tokens(input_ids)
|
| 253 |
+
inputs_embeds = inputs_embeds * self.embedding_scale
|
| 254 |
+
|
| 255 |
+
if use_cache and past_key_values is None:
|
| 256 |
+
past_key_values = DynamicCache(config=self.config)
|
| 257 |
+
if position_ids is None:
|
| 258 |
+
past_seen_tokens = past_key_values.get_seq_length() if past_key_values is not None else 0
|
| 259 |
+
position_ids = torch.arange(inputs_embeds.shape[1], device=inputs_embeds.device) + past_seen_tokens
|
| 260 |
+
position_ids = position_ids.unsqueeze(0)
|
| 261 |
+
|
| 262 |
+
mask_kwargs = {
|
| 263 |
+
"config": self.config,
|
| 264 |
+
"inputs_embeds": inputs_embeds,
|
| 265 |
+
"attention_mask": attention_mask,
|
| 266 |
+
"past_key_values": past_key_values,
|
| 267 |
+
"position_ids": position_ids,
|
| 268 |
+
}
|
| 269 |
+
is_first_iteration = past_key_values is None or not past_key_values.is_initialized
|
| 270 |
+
if token_type_ids is not None and is_first_iteration and self.config.prefix_lm:
|
| 271 |
+
mask_kwargs["block_sequence_ids"] = torch.where(token_type_ids == 1, 0, -1)
|
| 272 |
+
causal_attention_mask = create_causal_mask(**mask_kwargs)
|
| 273 |
+
|
| 274 |
+
if self.rope_mode == "hybrid_2d" and rope_position_ids is not None:
|
| 275 |
+
position_embeddings = self.hybrid_rotary_emb(inputs_embeds, rope_position_ids)
|
| 276 |
+
else:
|
| 277 |
+
position_embeddings = self.rotary_emb(inputs_embeds, position_ids)
|
| 278 |
+
|
| 279 |
+
hidden_states_high_cycle = inputs_embeds
|
| 280 |
+
hidden_states_low_cycle = (
|
| 281 |
+
self.z_L_init.to(dtype=hidden_states_high_cycle.dtype, device=hidden_states_high_cycle.device)
|
| 282 |
+
.expand_as(hidden_states_high_cycle)
|
| 283 |
+
.contiguous()
|
| 284 |
+
)
|
| 285 |
+
multimodal = visual_mask is not None and bool(visual_mask.any())
|
| 286 |
+
if multimodal:
|
| 287 |
+
if instruction_mask is None:
|
| 288 |
+
raise ValueError("instruction_mask is required when visual_mask is provided")
|
| 289 |
+
multimodal_rows = visual_mask.any(dim=1)
|
| 290 |
+
multimodal_indices = multimodal_rows.nonzero(as_tuple=False).flatten()
|
| 291 |
+
visual_anchor_tokens, visual_token_valid = self._extract_tokens(
|
| 292 |
+
inputs_embeds[multimodal_rows], visual_mask[multimodal_rows]
|
| 293 |
+
)
|
| 294 |
+
visual_anchor_tokens = visual_anchor_tokens.clone()
|
| 295 |
+
else:
|
| 296 |
+
multimodal_rows = None
|
| 297 |
+
multimodal_indices = None
|
| 298 |
+
visual_anchor_tokens = None
|
| 299 |
+
visual_token_valid = None
|
| 300 |
+
|
| 301 |
+
diagnostics = (
|
| 302 |
+
RecurrentVisualDiagnostics.empty()
|
| 303 |
+
if collect_visual_diagnostics or collect_depth_diagnostics
|
| 304 |
+
else None
|
| 305 |
+
)
|
| 306 |
+
valid_token_mask = (
|
| 307 |
+
attention_mask.bool()
|
| 308 |
+
if attention_mask is not None
|
| 309 |
+
else torch.ones(inputs_embeds.shape[:2], dtype=torch.bool, device=inputs_embeds.device)
|
| 310 |
+
)
|
| 311 |
+
response_mask = valid_token_mask & (
|
| 312 |
+
token_type_ids == 0 if token_type_ids is not None else valid_token_mask
|
| 313 |
+
)
|
| 314 |
+
num_layers_per_stack = self.config.num_layers_per_stack
|
| 315 |
+
for high_cycle_idx in range(self.config.H_cycles):
|
| 316 |
+
if multimodal and self.use_visual_anchor:
|
| 317 |
+
assert multimodal_rows is not None and multimodal_indices is not None
|
| 318 |
+
current_visual, current_visual_valid = self._extract_tokens(
|
| 319 |
+
hidden_states_high_cycle[multimodal_rows], visual_mask[multimodal_rows]
|
| 320 |
+
)
|
| 321 |
+
assert visual_token_valid is not None
|
| 322 |
+
if not torch.equal(current_visual_valid, visual_token_valid):
|
| 323 |
+
raise RuntimeError("Dynamic visual-token layout changed inside HRM recurrence")
|
| 324 |
+
instruction_state = masked_mean(
|
| 325 |
+
hidden_states_high_cycle[multimodal_rows],
|
| 326 |
+
instruction_mask[multimodal_rows],
|
| 327 |
+
)
|
| 328 |
+
if self.use_visual_gate:
|
| 329 |
+
gates = self.visual_gate(current_visual, instruction_state)
|
| 330 |
+
else:
|
| 331 |
+
gates = torch.ones(current_visual.shape[:2], dtype=current_visual.dtype, device=current_visual.device)
|
| 332 |
+
gates = gates.masked_fill(~visual_token_valid, 0.0)
|
| 333 |
+
reinjection_tokens = self.visual_anchor(visual_anchor_tokens, gates, high_cycle_idx)
|
| 334 |
+
reinjection_rows = self._scatter_tokens(
|
| 335 |
+
reinjection_tokens,
|
| 336 |
+
visual_token_valid,
|
| 337 |
+
visual_mask[multimodal_rows],
|
| 338 |
+
hidden_states_high_cycle[multimodal_rows],
|
| 339 |
+
)
|
| 340 |
+
reinjection_full = torch.zeros_like(hidden_states_high_cycle).index_copy(
|
| 341 |
+
0, multimodal_indices, reinjection_rows
|
| 342 |
+
)
|
| 343 |
+
hidden_states_high_cycle = hidden_states_high_cycle + reinjection_full
|
| 344 |
+
if diagnostics is not None:
|
| 345 |
+
diagnostics.record(
|
| 346 |
+
gates,
|
| 347 |
+
self.visual_anchor.alpha(high_cycle_idx),
|
| 348 |
+
reinjection_tokens,
|
| 349 |
+
current_visual,
|
| 350 |
+
visual_token_valid,
|
| 351 |
+
self.gate_saturation_metric_lower,
|
| 352 |
+
self.gate_saturation_metric_upper,
|
| 353 |
+
)
|
| 354 |
+
|
| 355 |
+
for low_cycle_idx in range(self.config.L_cycles):
|
| 356 |
+
cycle_offset = (
|
| 357 |
+
high_cycle_idx * (self.config.L_cycles + 1) + low_cycle_idx
|
| 358 |
+
) * num_layers_per_stack
|
| 359 |
+
previous_low = hidden_states_low_cycle
|
| 360 |
+
hidden_states_low_cycle = self._run_stack(
|
| 361 |
+
self.L_module,
|
| 362 |
+
hidden_states_low_cycle.to(hidden_states_high_cycle.device) + hidden_states_high_cycle,
|
| 363 |
+
attention_mask=causal_attention_mask,
|
| 364 |
+
past_key_values=past_key_values,
|
| 365 |
+
position_embeddings=position_embeddings,
|
| 366 |
+
position_ids=position_ids,
|
| 367 |
+
cycle_offset=cycle_offset,
|
| 368 |
+
**kwargs,
|
| 369 |
+
)
|
| 370 |
+
if diagnostics is not None and collect_depth_diagnostics:
|
| 371 |
+
diagnostics.record_transition(
|
| 372 |
+
f"h{high_cycle_idx}_l{low_cycle_idx}",
|
| 373 |
+
previous_low,
|
| 374 |
+
hidden_states_low_cycle,
|
| 375 |
+
valid_token_mask,
|
| 376 |
+
visual_mask,
|
| 377 |
+
response_mask,
|
| 378 |
+
)
|
| 379 |
+
|
| 380 |
+
cycle_offset = (
|
| 381 |
+
high_cycle_idx * (self.config.L_cycles + 1) + self.config.L_cycles
|
| 382 |
+
) * num_layers_per_stack
|
| 383 |
+
previous_high = hidden_states_high_cycle
|
| 384 |
+
hidden_states_high_cycle = self._run_stack(
|
| 385 |
+
self.H_module,
|
| 386 |
+
hidden_states_high_cycle + hidden_states_low_cycle.to(hidden_states_high_cycle.device),
|
| 387 |
+
attention_mask=causal_attention_mask,
|
| 388 |
+
past_key_values=past_key_values,
|
| 389 |
+
position_embeddings=position_embeddings,
|
| 390 |
+
position_ids=position_ids,
|
| 391 |
+
cycle_offset=cycle_offset,
|
| 392 |
+
**kwargs,
|
| 393 |
+
)
|
| 394 |
+
if diagnostics is not None and collect_depth_diagnostics:
|
| 395 |
+
diagnostics.record_transition(
|
| 396 |
+
f"h{high_cycle_idx}_h",
|
| 397 |
+
previous_high,
|
| 398 |
+
hidden_states_high_cycle,
|
| 399 |
+
valid_token_mask,
|
| 400 |
+
visual_mask,
|
| 401 |
+
response_mask,
|
| 402 |
+
)
|
| 403 |
+
|
| 404 |
+
self.last_visual_diagnostics = diagnostics
|
| 405 |
+
return BaseModelOutputWithPast(
|
| 406 |
+
last_hidden_state=hidden_states_high_cycle,
|
| 407 |
+
past_key_values=past_key_values,
|
| 408 |
+
)
|
hrm_penguin/recurrent/hybrid_rope.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
from torch import nn
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def rotate_half(x: torch.Tensor) -> torch.Tensor:
|
| 8 |
+
first, second = x.chunk(2, dim=-1)
|
| 9 |
+
return torch.cat((-second, first), dim=-1)
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def apply_rotary_pos_emb(
|
| 13 |
+
query: torch.Tensor,
|
| 14 |
+
key: torch.Tensor,
|
| 15 |
+
cos: torch.Tensor,
|
| 16 |
+
sin: torch.Tensor,
|
| 17 |
+
unsqueeze_dim: int = 1,
|
| 18 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 19 |
+
cos = cos.unsqueeze(unsqueeze_dim)
|
| 20 |
+
sin = sin.unsqueeze(unsqueeze_dim)
|
| 21 |
+
return (
|
| 22 |
+
query * cos + rotate_half(query) * sin,
|
| 23 |
+
key * cos + rotate_half(key) * sin,
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class HybridHrmRotaryEmbedding(nn.Module):
|
| 28 |
+
"""2D visual RoPE that exactly reduces to HRM 1D RoPE for positions (p, p)."""
|
| 29 |
+
|
| 30 |
+
def __init__(
|
| 31 |
+
self,
|
| 32 |
+
head_dim: int = 128,
|
| 33 |
+
theta: float = 10000.0,
|
| 34 |
+
config=None,
|
| 35 |
+
):
|
| 36 |
+
super().__init__()
|
| 37 |
+
if head_dim % 2:
|
| 38 |
+
raise ValueError("head_dim must be even")
|
| 39 |
+
attention_scaling = 1.0
|
| 40 |
+
rope_parameters = getattr(config, "rope_parameters", None) or {}
|
| 41 |
+
rope_type = rope_parameters.get("rope_type", "default")
|
| 42 |
+
if config is not None and rope_type != "default":
|
| 43 |
+
from transformers.modeling_rope_utils import ROPE_INIT_FUNCTIONS
|
| 44 |
+
|
| 45 |
+
if rope_type not in ROPE_INIT_FUNCTIONS:
|
| 46 |
+
raise ValueError(f"Unsupported hybrid RoPE type: {rope_type}")
|
| 47 |
+
inv_freq, attention_scaling = ROPE_INIT_FUNCTIONS[rope_type](
|
| 48 |
+
config,
|
| 49 |
+
device=None,
|
| 50 |
+
)
|
| 51 |
+
inv_freq = inv_freq.float()
|
| 52 |
+
else:
|
| 53 |
+
inv_freq = 1.0 / (
|
| 54 |
+
theta ** (torch.arange(0, head_dim, 2, dtype=torch.float32) / head_dim)
|
| 55 |
+
)
|
| 56 |
+
self.head_dim = head_dim
|
| 57 |
+
self.rope_type = rope_type
|
| 58 |
+
self.attention_scaling = float(attention_scaling)
|
| 59 |
+
self.register_buffer("inv_freq", inv_freq, persistent=False)
|
| 60 |
+
|
| 61 |
+
def forward(
|
| 62 |
+
self,
|
| 63 |
+
x: torch.Tensor,
|
| 64 |
+
rope_position_ids: torch.LongTensor,
|
| 65 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 66 |
+
if rope_position_ids.ndim == 2:
|
| 67 |
+
rope_position_ids = rope_position_ids.unsqueeze(0).expand(2, -1, -1)
|
| 68 |
+
if rope_position_ids.ndim != 3 or rope_position_ids.shape[0] != 2:
|
| 69 |
+
raise ValueError("rope_position_ids must have shape [2, B, S]")
|
| 70 |
+
positions = rope_position_ids.float()
|
| 71 |
+
freqs = positions.unsqueeze(-1) * self.inv_freq.float().view(1, 1, 1, -1)
|
| 72 |
+
# Standard HRM cos is [freqs, freqs]. Selecting axis0's first half
|
| 73 |
+
# and axis1's second half preserves exact 1D behavior for (p, p).
|
| 74 |
+
angles = torch.cat((freqs[0], freqs[1]), dim=-1)
|
| 75 |
+
if angles.shape[-1] != self.head_dim:
|
| 76 |
+
raise RuntimeError(f"RoPE produced {angles.shape[-1]} dims, expected {self.head_dim}")
|
| 77 |
+
scale = self.attention_scaling
|
| 78 |
+
return (angles.cos() * scale).to(x.dtype), (angles.sin() * scale).to(x.dtype)
|
hrm_penguin/recurrent/positions.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def build_compact_positions(
|
| 7 |
+
header_lengths: torch.LongTensor,
|
| 8 |
+
instruction_lengths: torch.LongTensor,
|
| 9 |
+
response_lengths: torch.LongTensor,
|
| 10 |
+
visual_token_counts: torch.LongTensor | None = None,
|
| 11 |
+
grid_heights: torch.LongTensor | None = None,
|
| 12 |
+
grid_widths: torch.LongTensor | None = None,
|
| 13 |
+
grid_h: int = 24,
|
| 14 |
+
grid_w: int = 24,
|
| 15 |
+
rope_mode: str = "hybrid_2d",
|
| 16 |
+
) -> tuple[torch.LongTensor, torch.LongTensor, torch.BoolTensor, torch.BoolTensor]:
|
| 17 |
+
"""Build true sequence positions and independent compact spatial RoPE positions."""
|
| 18 |
+
batch = int(header_lengths.numel())
|
| 19 |
+
visual_tokens = grid_h * grid_w
|
| 20 |
+
if visual_token_counts is None:
|
| 21 |
+
visual_token_counts = torch.full_like(header_lengths, visual_tokens)
|
| 22 |
+
if grid_heights is None:
|
| 23 |
+
grid_heights = torch.where(
|
| 24 |
+
visual_token_counts > 0,
|
| 25 |
+
torch.full_like(visual_token_counts, grid_h),
|
| 26 |
+
torch.zeros_like(visual_token_counts),
|
| 27 |
+
)
|
| 28 |
+
if grid_widths is None:
|
| 29 |
+
grid_widths = torch.where(
|
| 30 |
+
visual_token_counts > 0,
|
| 31 |
+
torch.full_like(visual_token_counts, grid_w),
|
| 32 |
+
torch.zeros_like(visual_token_counts),
|
| 33 |
+
)
|
| 34 |
+
if not (
|
| 35 |
+
grid_heights.shape == visual_token_counts.shape
|
| 36 |
+
and grid_widths.shape == visual_token_counts.shape
|
| 37 |
+
):
|
| 38 |
+
raise ValueError("Per-sample grid tensors must match visual_token_counts")
|
| 39 |
+
expected_counts = grid_heights * grid_widths
|
| 40 |
+
if bool((expected_counts != visual_token_counts).any()):
|
| 41 |
+
raise ValueError(
|
| 42 |
+
"visual token counts must exactly equal per-sample grid area: "
|
| 43 |
+
f"counts={visual_token_counts.tolist()}, "
|
| 44 |
+
f"heights={grid_heights.tolist()}, widths={grid_widths.tolist()}"
|
| 45 |
+
)
|
| 46 |
+
total_lengths = header_lengths + visual_token_counts + instruction_lengths + response_lengths
|
| 47 |
+
max_length = int(total_lengths.max().item())
|
| 48 |
+
device = header_lengths.device
|
| 49 |
+
mask_positions = torch.arange(max_length, device=device).expand(batch, -1).clone()
|
| 50 |
+
rope_positions = mask_positions.unsqueeze(0).expand(2, -1, -1).clone()
|
| 51 |
+
visual_mask = torch.zeros((batch, max_length), dtype=torch.bool, device=device)
|
| 52 |
+
instruction_mask = torch.zeros_like(visual_mask)
|
| 53 |
+
|
| 54 |
+
for index in range(batch):
|
| 55 |
+
header = int(header_lengths[index])
|
| 56 |
+
instruction = int(instruction_lengths[index])
|
| 57 |
+
response = int(response_lengths[index])
|
| 58 |
+
row_visual_tokens = int(visual_token_counts[index])
|
| 59 |
+
row_grid_h = int(grid_heights[index])
|
| 60 |
+
row_grid_w = int(grid_widths[index])
|
| 61 |
+
visual_start = header
|
| 62 |
+
visual_end = visual_start + row_visual_tokens
|
| 63 |
+
instruction_start = visual_end
|
| 64 |
+
instruction_end = instruction_start + instruction
|
| 65 |
+
response_end = instruction_end + response
|
| 66 |
+
visual_mask[index, visual_start:visual_end] = True
|
| 67 |
+
instruction_mask[index, instruction_start:instruction_end] = True
|
| 68 |
+
if rope_mode == "hybrid_2d" and row_visual_tokens:
|
| 69 |
+
rows = torch.arange(row_grid_h, device=device).repeat_interleave(row_grid_w) + header
|
| 70 |
+
cols = torch.arange(row_grid_w, device=device).repeat(row_grid_h) + header
|
| 71 |
+
rope_positions[0, index, visual_start:visual_end] = rows
|
| 72 |
+
rope_positions[1, index, visual_start:visual_end] = cols
|
| 73 |
+
text_start = header + max(row_grid_h, row_grid_w)
|
| 74 |
+
tail = torch.arange(response_end - instruction_start, device=device) + text_start
|
| 75 |
+
rope_positions[:, index, instruction_start:response_end] = tail.unsqueeze(0)
|
| 76 |
+
return mask_positions, rope_positions, visual_mask, instruction_mask
|
hrm_penguin/recurrent/visual_anchor.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
from torch import nn
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class VisualAnchor(nn.Module):
|
| 8 |
+
def __init__(
|
| 9 |
+
self,
|
| 10 |
+
H_cycles: int = 2,
|
| 11 |
+
raw_init: float = -2.0,
|
| 12 |
+
max_scale: float = 0.5,
|
| 13 |
+
):
|
| 14 |
+
super().__init__()
|
| 15 |
+
self.raw_alpha = nn.Parameter(torch.full((H_cycles,), float(raw_init)))
|
| 16 |
+
self.max_scale = float(max_scale)
|
| 17 |
+
|
| 18 |
+
def alpha(self, cycle: int) -> torch.Tensor:
|
| 19 |
+
return self.max_scale * torch.sigmoid(self.raw_alpha[cycle])
|
| 20 |
+
|
| 21 |
+
def forward(
|
| 22 |
+
self,
|
| 23 |
+
visual_anchor: torch.Tensor,
|
| 24 |
+
gates: torch.Tensor,
|
| 25 |
+
cycle: int,
|
| 26 |
+
) -> torch.Tensor:
|
| 27 |
+
alpha = self.alpha(cycle).to(dtype=visual_anchor.dtype)
|
| 28 |
+
return alpha * gates.unsqueeze(-1) * visual_anchor
|
hrm_penguin/recurrent/visual_gate.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
import torch.nn.functional as F
|
| 5 |
+
from torch import nn
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class LoopAwareVisualGate(nn.Module):
|
| 9 |
+
def __init__(self, hidden_size: int = 1536, gate_dim: int = 256):
|
| 10 |
+
super().__init__()
|
| 11 |
+
self.visual_proj = nn.Linear(hidden_size, gate_dim, bias=False)
|
| 12 |
+
self.query_proj = nn.Linear(hidden_size, gate_dim, bias=False)
|
| 13 |
+
self.out_proj = nn.Linear(gate_dim, 1, bias=True)
|
| 14 |
+
self.reset_output_head()
|
| 15 |
+
|
| 16 |
+
def reset_output_head(self) -> None:
|
| 17 |
+
"""Make the gate neutral while retaining its learned feature projections."""
|
| 18 |
+
nn.init.zeros_(self.out_proj.weight)
|
| 19 |
+
nn.init.zeros_(self.out_proj.bias)
|
| 20 |
+
|
| 21 |
+
def reset_parameters(self) -> None:
|
| 22 |
+
"""Initialize a neutral gate before checkpoint loading."""
|
| 23 |
+
self.visual_proj.reset_parameters()
|
| 24 |
+
self.query_proj.reset_parameters()
|
| 25 |
+
self.reset_output_head()
|
| 26 |
+
|
| 27 |
+
def forward(
|
| 28 |
+
self,
|
| 29 |
+
visual_states: torch.Tensor,
|
| 30 |
+
instruction_state: torch.Tensor,
|
| 31 |
+
) -> torch.Tensor:
|
| 32 |
+
output_dtype = visual_states.dtype
|
| 33 |
+
parameter_dtype = self.visual_proj.weight.dtype
|
| 34 |
+
# Keep these linears and sigmoid in fp32 when the surrounding model
|
| 35 |
+
# uses bf16 autocast, preserving the checkpoint's gate precision.
|
| 36 |
+
with torch.autocast(device_type=visual_states.device.type, enabled=False):
|
| 37 |
+
visual = self.visual_proj(visual_states.to(parameter_dtype))
|
| 38 |
+
query = self.query_proj(instruction_state.to(parameter_dtype)).unsqueeze(1)
|
| 39 |
+
score = self.out_proj(F.silu(visual + query)).squeeze(-1)
|
| 40 |
+
# Preserve the evaluated checkpoint's floating-point operation
|
| 41 |
+
# order, including the bounded auxiliary gate calculation.
|
| 42 |
+
bounded_score = score.clamp(-8.0, 8.0)
|
| 43 |
+
surrogate_score = score + (bounded_score - score).detach()
|
| 44 |
+
raw_gate = 2.0 * torch.sigmoid(score)
|
| 45 |
+
surrogate_gate = 2.0 * torch.sigmoid(surrogate_score)
|
| 46 |
+
gate = raw_gate.detach() + surrogate_gate - surrogate_gate.detach()
|
| 47 |
+
return gate.to(output_dtype)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def masked_mean(states: torch.Tensor, mask: torch.Tensor) -> torch.Tensor:
|
| 51 |
+
weights = mask.to(states.dtype).unsqueeze(-1)
|
| 52 |
+
denominator = weights.sum(dim=1).clamp_min(1.0)
|
| 53 |
+
return (states * weights).sum(dim=1) / denominator
|
hrm_penguin/vision/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .encoder import PenguinVisionEncoder
|
| 2 |
+
from .preprocessing import (
|
| 3 |
+
PenguinDynamicResolutionProcessor,
|
| 4 |
+
PenguinLetterboxProcessor,
|
| 5 |
+
build_penguin_image_processor,
|
| 6 |
+
letterbox_image,
|
| 7 |
+
)
|
| 8 |
+
from .projector import VisionProjector
|
| 9 |
+
|
| 10 |
+
__all__ = [
|
| 11 |
+
"PenguinVisionEncoder",
|
| 12 |
+
"PenguinLetterboxProcessor",
|
| 13 |
+
"PenguinDynamicResolutionProcessor",
|
| 14 |
+
"build_penguin_image_processor",
|
| 15 |
+
"VisionProjector",
|
| 16 |
+
"letterbox_image",
|
| 17 |
+
]
|
hrm_penguin/vision/encoder.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from types import MethodType
|
| 4 |
+
|
| 5 |
+
import torch
|
| 6 |
+
import torch.nn.functional as F
|
| 7 |
+
from torch import nn
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def _rotate_half(value: torch.Tensor) -> torch.Tensor:
|
| 11 |
+
first, second = value.chunk(2, dim=-1)
|
| 12 |
+
return torch.cat((-second, first), dim=-1)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def _apply_penguin_rotary(query, key, cos, sin):
|
| 16 |
+
section = [cos.shape[-1] // 2, cos.shape[-1] // 2]
|
| 17 |
+
cos = torch.cat([axis[index % 2] for index, axis in enumerate(cos.split(section, dim=-1))], dim=-1)
|
| 18 |
+
sin = torch.cat([axis[index % 2] for index, axis in enumerate(sin.split(section, dim=-1))], dim=-1)
|
| 19 |
+
cos = cos.unsqueeze(1)
|
| 20 |
+
sin = sin.unsqueeze(1)
|
| 21 |
+
return query * cos + _rotate_half(query) * sin, key * cos + _rotate_half(key) * sin
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def _penguin_sdpa_attention_forward(
|
| 25 |
+
module,
|
| 26 |
+
hidden_states: torch.Tensor,
|
| 27 |
+
position_embeddings,
|
| 28 |
+
attention_mask=None,
|
| 29 |
+
past_key_value=None,
|
| 30 |
+
cache_position=None,
|
| 31 |
+
cu_seqlens=None,
|
| 32 |
+
**kwargs,
|
| 33 |
+
):
|
| 34 |
+
"""SDPA equivalent of Penguin's packed, non-causal FlashAttention path."""
|
| 35 |
+
input_shape = hidden_states.shape[:-1]
|
| 36 |
+
hidden_shape = (*input_shape, -1, module.head_dim)
|
| 37 |
+
query = module.q_norm(module.q_proj(hidden_states).view(hidden_shape)).transpose(1, 2)
|
| 38 |
+
key = module.k_norm(module.k_proj(hidden_states).view(hidden_shape)).transpose(1, 2)
|
| 39 |
+
value = module.v_proj(hidden_states).view(hidden_shape).transpose(1, 2)
|
| 40 |
+
cos, sin = position_embeddings
|
| 41 |
+
query, key = _apply_penguin_rotary(query, key, cos, sin)
|
| 42 |
+
if past_key_value is not None:
|
| 43 |
+
cache_kwargs = {"sin": sin, "cos": cos, "cache_position": cache_position}
|
| 44 |
+
key, value = past_key_value.update(key, value, module.layer_idx, cache_kwargs)
|
| 45 |
+
|
| 46 |
+
if cu_seqlens is None:
|
| 47 |
+
attended = F.scaled_dot_product_attention(
|
| 48 |
+
query,
|
| 49 |
+
key,
|
| 50 |
+
value,
|
| 51 |
+
attn_mask=attention_mask,
|
| 52 |
+
dropout_p=0.0,
|
| 53 |
+
is_causal=False,
|
| 54 |
+
enable_gqa=query.shape[1] != key.shape[1],
|
| 55 |
+
)
|
| 56 |
+
else:
|
| 57 |
+
# Penguin packs image sequences along S inside a singleton batch. Splitting
|
| 58 |
+
# here prevents patches from different images attending to one another.
|
| 59 |
+
boundaries = cu_seqlens.detach().cpu().tolist()
|
| 60 |
+
chunks = []
|
| 61 |
+
for start, end in zip(boundaries[:-1], boundaries[1:]):
|
| 62 |
+
chunks.append(
|
| 63 |
+
F.scaled_dot_product_attention(
|
| 64 |
+
query[:, :, start:end],
|
| 65 |
+
key[:, :, start:end],
|
| 66 |
+
value[:, :, start:end],
|
| 67 |
+
dropout_p=0.0,
|
| 68 |
+
is_causal=False,
|
| 69 |
+
enable_gqa=query.shape[1] != key.shape[1],
|
| 70 |
+
)
|
| 71 |
+
)
|
| 72 |
+
attended = torch.cat(chunks, dim=2)
|
| 73 |
+
attended = attended.transpose(1, 2).reshape(*input_shape, -1).contiguous()
|
| 74 |
+
return module.o_proj(attended), None
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def install_penguin_sdpa(model: nn.Module) -> int:
|
| 78 |
+
patched = 0
|
| 79 |
+
for module in model.modules():
|
| 80 |
+
if module.__class__.__name__ == "PenguinVLAttention":
|
| 81 |
+
module.forward = MethodType(_penguin_sdpa_attention_forward, module)
|
| 82 |
+
patched += 1
|
| 83 |
+
if patched == 0:
|
| 84 |
+
raise RuntimeError("No PenguinVLAttention modules found for SDPA patch")
|
| 85 |
+
return patched
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def install_penguin_transformers_compatibility() -> None:
|
| 89 |
+
"""Bridge Penguin remote code written for Transformers 4.51 to 5.x RoPE APIs."""
|
| 90 |
+
from transformers.modeling_rope_utils import ROPE_INIT_FUNCTIONS
|
| 91 |
+
|
| 92 |
+
if "default" not in ROPE_INIT_FUNCTIONS:
|
| 93 |
+
ROPE_INIT_FUNCTIONS["default"] = compute_default_rope_parameters
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def compute_default_rope_parameters(config, device=None, **kwargs):
|
| 97 |
+
parameters = getattr(config, "rope_parameters", None) or getattr(config, "rope_scaling", None) or {}
|
| 98 |
+
theta = float(parameters.get("rope_theta", getattr(config, "rope_theta", 10000.0)))
|
| 99 |
+
dimension = int(getattr(config, "head_dim", config.hidden_size // config.num_attention_heads))
|
| 100 |
+
inverse_frequency = 1.0 / (
|
| 101 |
+
theta ** (torch.arange(0, dimension, 2, dtype=torch.float32, device=device) / dimension)
|
| 102 |
+
)
|
| 103 |
+
return inverse_frequency, 1.0
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
class PenguinVisionEncoder(nn.Module):
|
| 107 |
+
"""Thin checked wrapper around the official Penguin encoder."""
|
| 108 |
+
|
| 109 |
+
def __init__(self, model: nn.Module, hidden_size: int = 1024):
|
| 110 |
+
super().__init__()
|
| 111 |
+
self.model = model
|
| 112 |
+
self.hidden_size = hidden_size
|
| 113 |
+
|
| 114 |
+
def forward(self, pixel_values, grid_sizes, merge_sizes) -> torch.Tensor:
|
| 115 |
+
features = self.model(
|
| 116 |
+
pixel_values=pixel_values,
|
| 117 |
+
grid_sizes=grid_sizes,
|
| 118 |
+
merge_sizes=merge_sizes,
|
| 119 |
+
)
|
| 120 |
+
if hasattr(features, "last_hidden_state"):
|
| 121 |
+
features = features.last_hidden_state
|
| 122 |
+
if features.ndim != 2 or features.shape[-1] != self.hidden_size:
|
| 123 |
+
raise RuntimeError(
|
| 124 |
+
f"Expected packed Penguin output [sum(N_i), {self.hidden_size}], "
|
| 125 |
+
f"got {tuple(features.shape)}"
|
| 126 |
+
)
|
| 127 |
+
if grid_sizes.ndim != 2 or grid_sizes.shape[1] != 3:
|
| 128 |
+
raise RuntimeError(f"Invalid grid_sizes shape: {tuple(grid_sizes.shape)}")
|
| 129 |
+
if merge_sizes.ndim != 1 or merge_sizes.shape[0] != grid_sizes.shape[0]:
|
| 130 |
+
raise RuntimeError(f"Invalid merge_sizes shape: {tuple(merge_sizes.shape)}")
|
| 131 |
+
spatial = grid_sizes[:, 1:] // merge_sizes[:, None]
|
| 132 |
+
if bool((spatial * merge_sizes[:, None] != grid_sizes[:, 1:]).any()):
|
| 133 |
+
raise RuntimeError("Grid dimensions must be divisible by merge_size")
|
| 134 |
+
counts = grid_sizes[:, 0] * spatial[:, 0] * spatial[:, 1]
|
| 135 |
+
if features.shape[0] != int(counts.sum()):
|
| 136 |
+
raise RuntimeError(
|
| 137 |
+
"Packed Penguin features do not match dynamic grids: "
|
| 138 |
+
f"features={features.shape[0]}, counts={counts.tolist()}"
|
| 139 |
+
)
|
| 140 |
+
return features
|
hrm_penguin/vision/preprocessing.py
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import math
|
| 4 |
+
from collections.abc import Sequence
|
| 5 |
+
|
| 6 |
+
from PIL import Image, ImageOps
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def letterbox_image(
|
| 10 |
+
image: Image.Image,
|
| 11 |
+
size: int = 336,
|
| 12 |
+
fill: tuple[int, int, int] = (128, 128, 128),
|
| 13 |
+
) -> Image.Image:
|
| 14 |
+
"""EXIF-correct, convert to RGB, preserve aspect ratio, and center pad."""
|
| 15 |
+
image = ImageOps.exif_transpose(image).convert("RGB")
|
| 16 |
+
width, height = image.size
|
| 17 |
+
if width < 1 or height < 1:
|
| 18 |
+
raise ValueError(f"Invalid image size: {image.size}")
|
| 19 |
+
scale = min(size / width, size / height)
|
| 20 |
+
new_width = max(1, round(width * scale))
|
| 21 |
+
new_height = max(1, round(height * scale))
|
| 22 |
+
image = image.resize((new_width, new_height), Image.Resampling.BICUBIC)
|
| 23 |
+
canvas = Image.new("RGB", (size, size), fill)
|
| 24 |
+
canvas.paste(image, ((size - new_width) // 2, (size - new_height) // 2))
|
| 25 |
+
return canvas
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class PenguinLetterboxProcessor:
|
| 29 |
+
"""Letterbox first, then delegate rescale/normalize/patchify to Penguin."""
|
| 30 |
+
|
| 31 |
+
def __init__(self, processor, image_size: int = 336):
|
| 32 |
+
self.processor = processor
|
| 33 |
+
self.image_size = image_size
|
| 34 |
+
|
| 35 |
+
@property
|
| 36 |
+
def neutral_fill(self) -> tuple[int, int, int]:
|
| 37 |
+
mean = getattr(self.processor, "image_mean", (0.5, 0.5, 0.5))
|
| 38 |
+
if isinstance(mean, (float, int)):
|
| 39 |
+
mean = (mean, mean, mean)
|
| 40 |
+
return tuple(max(0, min(255, round(float(value) * 255))) for value in mean)
|
| 41 |
+
|
| 42 |
+
def __call__(self, images: Image.Image | Sequence[Image.Image], return_tensors: str = "pt"):
|
| 43 |
+
if isinstance(images, Image.Image):
|
| 44 |
+
images = [images]
|
| 45 |
+
boxed = [letterbox_image(image, self.image_size, self.neutral_fill) for image in images]
|
| 46 |
+
result = self.processor(
|
| 47 |
+
images=boxed,
|
| 48 |
+
do_resize=False,
|
| 49 |
+
merge_size=1,
|
| 50 |
+
return_tensors=return_tensors,
|
| 51 |
+
)
|
| 52 |
+
required = {"pixel_values", "grid_sizes", "merge_sizes"}
|
| 53 |
+
missing = required.difference(result.keys())
|
| 54 |
+
if missing:
|
| 55 |
+
raise RuntimeError(f"Penguin processor missing fields: {sorted(missing)}")
|
| 56 |
+
return result
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
class PenguinDynamicResolutionProcessor:
|
| 60 |
+
"""Use Penguin's native aspect-ratio preserving resize without image padding.
|
| 61 |
+
|
| 62 |
+
Penguin already packs differently sized images into one flat patch tensor.
|
| 63 |
+
This wrapper fixes the supported token range, applies EXIF orientation, and
|
| 64 |
+
validates that every emitted token corresponds to a real resized image patch.
|
| 65 |
+
"""
|
| 66 |
+
|
| 67 |
+
def __init__(
|
| 68 |
+
self,
|
| 69 |
+
processor,
|
| 70 |
+
min_visual_tokens: int = 256,
|
| 71 |
+
max_visual_tokens: int = 1024,
|
| 72 |
+
):
|
| 73 |
+
if min_visual_tokens < 1 or max_visual_tokens < min_visual_tokens:
|
| 74 |
+
raise ValueError("Invalid dynamic visual-token range")
|
| 75 |
+
self.processor = processor
|
| 76 |
+
self.min_visual_tokens = int(min_visual_tokens)
|
| 77 |
+
self.max_visual_tokens = int(max_visual_tokens)
|
| 78 |
+
# The remote Penguin processor reads these attributes while selecting
|
| 79 |
+
# aligned target sizes. merge_size=1 keeps one encoder token per patch.
|
| 80 |
+
self.processor.min_tokens = self.min_visual_tokens
|
| 81 |
+
self.processor.max_tokens = self.max_visual_tokens
|
| 82 |
+
|
| 83 |
+
def _fit_single(
|
| 84 |
+
self,
|
| 85 |
+
image: Image.Image,
|
| 86 |
+
return_tensors: str,
|
| 87 |
+
min_visual_tokens: int | None = None,
|
| 88 |
+
max_visual_tokens: int | None = None,
|
| 89 |
+
):
|
| 90 |
+
"""Fit one real image to the configured patch-token contract.
|
| 91 |
+
|
| 92 |
+
Penguin's helper internally multiplies ``min_tokens`` by 1.5 and
|
| 93 |
+
``max_tokens`` by 0.95 before choosing a grid. Compensate for those
|
| 94 |
+
implementation factors, then tighten and retry if aspect-ratio rounding
|
| 95 |
+
crosses the public limits. No canvas or synthetic patch is introduced.
|
| 96 |
+
"""
|
| 97 |
+
|
| 98 |
+
minimum = int(min_visual_tokens or self.min_visual_tokens)
|
| 99 |
+
maximum = int(max_visual_tokens or self.max_visual_tokens)
|
| 100 |
+
if minimum < 1 or maximum < minimum or maximum > self.max_visual_tokens:
|
| 101 |
+
raise ValueError(f"Invalid per-image token range [{minimum}, {maximum}]")
|
| 102 |
+
original_min = self.processor.min_tokens
|
| 103 |
+
original_max = self.processor.max_tokens
|
| 104 |
+
# Floor is intentional: using ceil(256 / 1.5) makes Penguin's own
|
| 105 |
+
# ``ceil`` alignment jump a square 16x16 grid to 17x17.
|
| 106 |
+
delegate_min = max(1, math.floor(minimum / 1.5))
|
| 107 |
+
delegate_max = max(delegate_min, math.ceil(maximum / 0.95))
|
| 108 |
+
last_count = None
|
| 109 |
+
try:
|
| 110 |
+
for _ in range(12):
|
| 111 |
+
self.processor.min_tokens = delegate_min
|
| 112 |
+
self.processor.max_tokens = delegate_max
|
| 113 |
+
result = self.processor(
|
| 114 |
+
images=[image],
|
| 115 |
+
do_resize=True,
|
| 116 |
+
merge_size=1,
|
| 117 |
+
return_tensors=return_tensors,
|
| 118 |
+
)
|
| 119 |
+
grids = result["grid_sizes"]
|
| 120 |
+
count = int(grids.prod(dim=1)[0])
|
| 121 |
+
last_count = count
|
| 122 |
+
if minimum <= count <= maximum:
|
| 123 |
+
return result
|
| 124 |
+
if count > maximum:
|
| 125 |
+
delegate_max = max(
|
| 126 |
+
delegate_min,
|
| 127 |
+
math.floor(
|
| 128 |
+
delegate_max
|
| 129 |
+
* maximum
|
| 130 |
+
/ max(count, 1)
|
| 131 |
+
* 0.98
|
| 132 |
+
),
|
| 133 |
+
)
|
| 134 |
+
else:
|
| 135 |
+
delegate_min = min(
|
| 136 |
+
delegate_max,
|
| 137 |
+
math.ceil(
|
| 138 |
+
delegate_min
|
| 139 |
+
* minimum
|
| 140 |
+
/ max(count, 1)
|
| 141 |
+
* 1.02
|
| 142 |
+
),
|
| 143 |
+
)
|
| 144 |
+
raise RuntimeError(
|
| 145 |
+
"Penguin token-budget tightening did not converge: "
|
| 146 |
+
f"last_count={last_count}, range="
|
| 147 |
+
f"[{minimum}, {maximum}]"
|
| 148 |
+
)
|
| 149 |
+
finally:
|
| 150 |
+
self.processor.min_tokens = original_min
|
| 151 |
+
self.processor.max_tokens = original_max
|
| 152 |
+
|
| 153 |
+
def __call__(
|
| 154 |
+
self,
|
| 155 |
+
images: Image.Image | Sequence[Image.Image],
|
| 156 |
+
return_tensors: str = "pt",
|
| 157 |
+
max_visual_tokens: int | Sequence[int] | None = None,
|
| 158 |
+
):
|
| 159 |
+
if isinstance(images, Image.Image):
|
| 160 |
+
images = [images]
|
| 161 |
+
images = [ImageOps.exif_transpose(image).convert("RGB") for image in images]
|
| 162 |
+
if not images:
|
| 163 |
+
raise ValueError("At least one image is required")
|
| 164 |
+
if any(image.width < 1 or image.height < 1 for image in images):
|
| 165 |
+
raise ValueError("Images must have positive dimensions")
|
| 166 |
+
# Penguin's video-oriented processor shares max_tokens across every
|
| 167 |
+
# item passed in one call. LoopVL defines the budget per image, so
|
| 168 |
+
# select each grid independently and concatenate the already-packed
|
| 169 |
+
# patch tensors afterwards.
|
| 170 |
+
if max_visual_tokens is None:
|
| 171 |
+
caps = [self.max_visual_tokens] * len(images)
|
| 172 |
+
elif isinstance(max_visual_tokens, int):
|
| 173 |
+
caps = [int(max_visual_tokens)] * len(images)
|
| 174 |
+
else:
|
| 175 |
+
caps = [int(value) for value in max_visual_tokens]
|
| 176 |
+
if len(caps) != len(images):
|
| 177 |
+
raise ValueError("One visual-token cap is required per image")
|
| 178 |
+
per_image = [
|
| 179 |
+
self._fit_single(
|
| 180 |
+
image,
|
| 181 |
+
return_tensors,
|
| 182 |
+
min_visual_tokens=self.min_visual_tokens,
|
| 183 |
+
max_visual_tokens=cap,
|
| 184 |
+
)
|
| 185 |
+
for image, cap in zip(images, caps, strict=True)
|
| 186 |
+
]
|
| 187 |
+
first = per_image[0]
|
| 188 |
+
pixel_values = first["pixel_values"]
|
| 189 |
+
grid_sizes = first["grid_sizes"]
|
| 190 |
+
merge_sizes = first["merge_sizes"]
|
| 191 |
+
if len(per_image) > 1:
|
| 192 |
+
import torch
|
| 193 |
+
|
| 194 |
+
pixel_values = torch.cat([item["pixel_values"] for item in per_image], dim=0)
|
| 195 |
+
grid_sizes = torch.cat([item["grid_sizes"] for item in per_image], dim=0)
|
| 196 |
+
merge_sizes = torch.cat([item["merge_sizes"] for item in per_image], dim=0)
|
| 197 |
+
result = {
|
| 198 |
+
"pixel_values": pixel_values,
|
| 199 |
+
"grid_sizes": grid_sizes,
|
| 200 |
+
"merge_sizes": merge_sizes,
|
| 201 |
+
}
|
| 202 |
+
required = {"pixel_values", "grid_sizes", "merge_sizes"}
|
| 203 |
+
missing = required.difference(result.keys())
|
| 204 |
+
if missing:
|
| 205 |
+
raise RuntimeError(f"Penguin processor missing fields: {sorted(missing)}")
|
| 206 |
+
|
| 207 |
+
grids = result["grid_sizes"]
|
| 208 |
+
merges = result["merge_sizes"]
|
| 209 |
+
if grids.ndim != 2 or grids.shape[1] != 3 or grids.shape[0] != len(images):
|
| 210 |
+
raise RuntimeError(f"Unexpected Penguin grid_sizes shape: {tuple(grids.shape)}")
|
| 211 |
+
if merges.ndim != 1 or merges.shape[0] != len(images) or bool((merges != 1).any()):
|
| 212 |
+
raise RuntimeError("Dynamic LoopVL input requires merge_size=1 for every image")
|
| 213 |
+
if bool((grids[:, 0] != 1).any()):
|
| 214 |
+
raise RuntimeError("LoopVL supports one image frame per sample")
|
| 215 |
+
|
| 216 |
+
counts = grids.prod(dim=1)
|
| 217 |
+
import torch
|
| 218 |
+
|
| 219 |
+
cap_tensor = torch.tensor(caps, device=counts.device, dtype=counts.dtype)
|
| 220 |
+
if bool(((counts < self.min_visual_tokens) | (counts > cap_tensor)).any()):
|
| 221 |
+
raise RuntimeError(
|
| 222 |
+
"Penguin emitted visual token counts outside per-image budgets: "
|
| 223 |
+
f"counts={counts.tolist()}, caps={caps}"
|
| 224 |
+
)
|
| 225 |
+
pixel_values = result["pixel_values"]
|
| 226 |
+
if pixel_values.ndim != 2 or pixel_values.shape[0] != int(counts.sum()):
|
| 227 |
+
raise RuntimeError(
|
| 228 |
+
"Packed pixel patches do not exactly match the real dynamic grids: "
|
| 229 |
+
f"patches={tuple(pixel_values.shape)}, grids={grids.tolist()}"
|
| 230 |
+
)
|
| 231 |
+
return result
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
def build_penguin_image_processor(processor, vision_config):
|
| 235 |
+
if getattr(vision_config, "dynamic_resolution", False):
|
| 236 |
+
return PenguinDynamicResolutionProcessor(
|
| 237 |
+
processor,
|
| 238 |
+
min_visual_tokens=vision_config.min_visual_tokens,
|
| 239 |
+
max_visual_tokens=vision_config.max_visual_tokens,
|
| 240 |
+
)
|
| 241 |
+
return PenguinLetterboxProcessor(processor, vision_config.input_size)
|
hrm_penguin/vision/projector.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
import torch.nn.functional as F
|
| 5 |
+
from torch import nn
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class VisionProjector(nn.Module):
|
| 9 |
+
def __init__(
|
| 10 |
+
self,
|
| 11 |
+
input_size: int = 1024,
|
| 12 |
+
hidden_size: int = 1536,
|
| 13 |
+
target_raw_rms: float = 1.0,
|
| 14 |
+
eps: float = 1e-6,
|
| 15 |
+
):
|
| 16 |
+
super().__init__()
|
| 17 |
+
self.fc1 = nn.Linear(input_size, hidden_size, bias=True)
|
| 18 |
+
self.act = nn.GELU()
|
| 19 |
+
self.fc2 = nn.Linear(hidden_size, hidden_size, bias=True)
|
| 20 |
+
self.eps = eps
|
| 21 |
+
self.register_buffer(
|
| 22 |
+
"visual_embedding_scale",
|
| 23 |
+
torch.tensor(float(target_raw_rms), dtype=torch.float32),
|
| 24 |
+
)
|
| 25 |
+
self.last_rms_diagnostics: dict[str, torch.Tensor] | None = None
|
| 26 |
+
|
| 27 |
+
def forward(self, features: torch.Tensor) -> torch.Tensor:
|
| 28 |
+
features = features.to(dtype=self.fc1.weight.dtype)
|
| 29 |
+
projected = self.fc2(self.act(self.fc1(features)))
|
| 30 |
+
scaled = F.rms_norm(
|
| 31 |
+
projected,
|
| 32 |
+
normalized_shape=(projected.shape[-1],),
|
| 33 |
+
weight=None,
|
| 34 |
+
eps=self.eps,
|
| 35 |
+
)
|
| 36 |
+
scaled = scaled * self.visual_embedding_scale.to(projected.dtype)
|
| 37 |
+
self.last_rms_diagnostics = {
|
| 38 |
+
"vision_encoder_rms": features.detach().float().square().mean().sqrt(),
|
| 39 |
+
"projector_rms": projected.detach().float().square().mean().sqrt(),
|
| 40 |
+
"visual_scaled_rms": scaled.detach().float().square().mean().sqrt(),
|
| 41 |
+
}
|
| 42 |
+
return scaled
|
metadata.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Standard-library-only metadata reader for LoopVL weight snapshots.
|
| 2 |
+
|
| 3 |
+
No code is imported from the snapshot. Version 2 stores all data files at its
|
| 4 |
+
root; version 1 retains its original nested image-processor JSON for backwards
|
| 5 |
+
compatibility. Both versions use the same code distributed in this runtime.
|
| 6 |
+
"""
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
import json
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
COMPONENT_DTYPES = {
|
| 14 |
+
"vision": "bfloat16",
|
| 15 |
+
"language": "bfloat16",
|
| 16 |
+
"projector": "float32",
|
| 17 |
+
"visual_gate": "float32",
|
| 18 |
+
"visual_anchor": "float32",
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
FLAT_DATA_FILES = (
|
| 22 |
+
"model.safetensors",
|
| 23 |
+
"config.json",
|
| 24 |
+
"generation_config.json",
|
| 25 |
+
"preprocessor_config.json",
|
| 26 |
+
"tokenizer.json",
|
| 27 |
+
"tokenizer_config.json",
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
PROCESSOR_FIELDS = (
|
| 31 |
+
"do_convert_rgb", "do_normalize", "do_rescale", "do_resize",
|
| 32 |
+
"image_mean", "image_std", "max_tokens", "min_tokens", "patch_size",
|
| 33 |
+
"resample", "rescale_factor",
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def read_checkpoint_metadata(model_dir: str | Path) -> dict:
|
| 38 |
+
"""Read and validate format metadata without torch, transformers or imports."""
|
| 39 |
+
root = Path(model_dir).expanduser().resolve()
|
| 40 |
+
raw = json.loads((root / "config.json").read_text(encoding="utf-8"))
|
| 41 |
+
if raw.get("model_type") != "loopvl" or raw.get("format_version") not in (1, 2):
|
| 42 |
+
raise ValueError("Expected LoopVL single-file checkpoint format_version 1 or 2")
|
| 43 |
+
if raw.get("component_dtypes") != COMPONENT_DTYPES:
|
| 44 |
+
raise ValueError("This loader requires the original BF16 cores / FP32 adapters")
|
| 45 |
+
for field in ("loopvl_config", "text_config", "vision_config"):
|
| 46 |
+
if not isinstance(raw.get(field), dict):
|
| 47 |
+
raise ValueError(f"Checkpoint lacks embedded {field}")
|
| 48 |
+
if raw["text_config"].get("model_type") != "hrm_text":
|
| 49 |
+
raise ValueError("Expected the embedded hrm_text language configuration")
|
| 50 |
+
if raw["vision_config"].get("model_type") != "penguinvl_vision_encoder":
|
| 51 |
+
raise ValueError("Expected the embedded penguinvl_vision_encoder configuration")
|
| 52 |
+
return raw
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def read_processor_config(model_dir: str | Path, raw: dict | None = None) -> dict:
|
| 56 |
+
"""Return the original processor parameters, never a remote-code reference.
|
| 57 |
+
|
| 58 |
+
The old root preprocessor_config.json was only a bundle description and is
|
| 59 |
+
deliberately not used for format 1. Omitting the true image mean/std would
|
| 60 |
+
silently select CLIP defaults and change pixel values.
|
| 61 |
+
"""
|
| 62 |
+
root = Path(model_dir).expanduser().resolve()
|
| 63 |
+
if raw is None:
|
| 64 |
+
raw = read_checkpoint_metadata(root)
|
| 65 |
+
version = raw.get("format_version")
|
| 66 |
+
if version == 1:
|
| 67 |
+
path = root / "penguin_encoder" / "preprocessor_config.json"
|
| 68 |
+
elif version == 2:
|
| 69 |
+
path = root / "preprocessor_config.json"
|
| 70 |
+
else:
|
| 71 |
+
raise ValueError("Unsupported processor metadata format")
|
| 72 |
+
values = json.loads(path.read_text(encoding="utf-8"))
|
| 73 |
+
missing = sorted(set(PROCESSOR_FIELDS) - set(values))
|
| 74 |
+
if missing:
|
| 75 |
+
raise ValueError(f"Missing actual Penguin image-processor parameters: {missing}")
|
| 76 |
+
kind = values.get("image_processor_type", "PenguinVLImageProcessor")
|
| 77 |
+
if kind != "PenguinVLImageProcessor":
|
| 78 |
+
raise ValueError(f"Unexpected image_processor_type: {kind!r}")
|
| 79 |
+
# Loading uses the pinned GitHub class directly. Old auto_map metadata is
|
| 80 |
+
# not executable and must never redirect imports into a checkpoint folder.
|
| 81 |
+
values.pop("auto_map", None)
|
| 82 |
+
return values
|
modeling_loopvl.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Load LoopVL's unchanged architecture from a data-only weight snapshot.
|
| 2 |
+
|
| 3 |
+
This is a small explicit loader, not a claim of native Transformers AutoModel
|
| 4 |
+
support. All model parameters and persistent buffers live in model.safetensors.
|
| 5 |
+
The original hrm_penguin implementation and Penguin classes live in this GitHub
|
| 6 |
+
runtime, not in the weight repository. Loading neither imports checkpoint code
|
| 7 |
+
nor needs the old split checkpoint or the network.
|
| 8 |
+
"""
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
|
| 13 |
+
import torch
|
| 14 |
+
from safetensors.torch import load_file
|
| 15 |
+
from transformers import AutoConfig, AutoTokenizer
|
| 16 |
+
from transformers import HrmTextForCausalLM
|
| 17 |
+
|
| 18 |
+
from metadata import COMPONENT_DTYPES, read_checkpoint_metadata, read_processor_config
|
| 19 |
+
from penguin_encoder.configuration_penguinvl_encoder import PenguinVLVisionEncoderConfig
|
| 20 |
+
from penguin_encoder.image_processing_penguinvl import PenguinVLImageProcessor
|
| 21 |
+
from penguin_encoder.modeling_penguinvl_encoder import (
|
| 22 |
+
PenguinVLVisionEncoderModel,
|
| 23 |
+
VisualRotaryEmbedding,
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
from hrm_penguin.configuration import HrmPenguinConfig
|
| 27 |
+
from hrm_penguin.model import HrmPenguinForConditionalGeneration
|
| 28 |
+
from hrm_penguin.recurrent.hrm_multimodal import MultimodalHrmTextModel
|
| 29 |
+
from hrm_penguin.vision.encoder import (
|
| 30 |
+
PenguinVisionEncoder,
|
| 31 |
+
compute_default_rope_parameters,
|
| 32 |
+
install_penguin_sdpa,
|
| 33 |
+
install_penguin_transformers_compatibility,
|
| 34 |
+
)
|
| 35 |
+
from hrm_penguin.vision.preprocessing import build_penguin_image_processor
|
| 36 |
+
from hrm_penguin.vision.projector import VisionProjector
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def read_config(model_dir: str | Path) -> tuple[dict, HrmPenguinConfig]:
|
| 40 |
+
"""Read embedded model configs without importing code from model_dir."""
|
| 41 |
+
root = Path(model_dir).expanduser().resolve()
|
| 42 |
+
raw = read_checkpoint_metadata(root)
|
| 43 |
+
source = raw["loopvl_config"]
|
| 44 |
+
config = HrmPenguinConfig.from_dict(source)
|
| 45 |
+
# Metadata paths only: both towers are constructed from embedded configs.
|
| 46 |
+
# No tower-specific directory or Python file is required in a v2 snapshot.
|
| 47 |
+
config.vision.model_path = str(root)
|
| 48 |
+
config.language.model_path = str(root)
|
| 49 |
+
config.validate()
|
| 50 |
+
return raw, config
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def load_tokenizer_and_processor(model_dir: str | Path, config=None):
|
| 54 |
+
"""Use the original tokenizer, image preprocessor and dynamic-resolution policy."""
|
| 55 |
+
root = Path(model_dir).expanduser().resolve()
|
| 56 |
+
raw = read_checkpoint_metadata(root)
|
| 57 |
+
if config is None:
|
| 58 |
+
_, config = read_config(root)
|
| 59 |
+
tokenizer = AutoTokenizer.from_pretrained(str(root), local_files_only=True)
|
| 60 |
+
official = PenguinVLImageProcessor.from_dict(read_processor_config(root, raw))
|
| 61 |
+
processor = build_penguin_image_processor(official, config.vision)
|
| 62 |
+
return tokenizer, processor
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
# Short public synonym used by the bundled preprocessor metadata.
|
| 66 |
+
load_components = load_tokenizer_and_processor
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
class LoopVLForConditionalGeneration(HrmPenguinForConditionalGeneration):
|
| 70 |
+
"""The existing LoopVL computation graph with a single-weight-file loader.
|
| 71 |
+
|
| 72 |
+
Usage::
|
| 73 |
+
|
| 74 |
+
model = LoopVLForConditionalGeneration.from_pretrained("./model", device="cuda:0")
|
| 75 |
+
|
| 76 |
+
No dtype/quantization override is offered: the checkpoint intentionally mixes
|
| 77 |
+
BF16 language/vision parameters with FP32 projector, visual gate and anchor.
|
| 78 |
+
Moving the model to a device is safe; calling model.half()/bfloat16() would
|
| 79 |
+
change those FP32 adapters and is not a lossless operation.
|
| 80 |
+
"""
|
| 81 |
+
|
| 82 |
+
@classmethod
|
| 83 |
+
def from_pretrained(cls, model_dir: str | Path, *, device="cpu"):
|
| 84 |
+
root = Path(model_dir).expanduser().resolve()
|
| 85 |
+
raw, config = read_config(root)
|
| 86 |
+
if torch.get_default_dtype() != torch.float32:
|
| 87 |
+
raise ValueError("Load with torch's default dtype set to torch.float32")
|
| 88 |
+
|
| 89 |
+
# Build under HF's BF16 construction context, not model.to(BF16).
|
| 90 |
+
# Penguin explicitly creates FP32 nonpersistent rotary buffers; converting
|
| 91 |
+
# the entire vision module afterward would round those frequencies.
|
| 92 |
+
install_penguin_transformers_compatibility()
|
| 93 |
+
# Patch the exact static class instantiated below, rather than a class
|
| 94 |
+
# copied from a weight repository into transformers' remote-code cache.
|
| 95 |
+
if not hasattr(VisualRotaryEmbedding, "compute_default_rope_parameters"):
|
| 96 |
+
VisualRotaryEmbedding.compute_default_rope_parameters = staticmethod(
|
| 97 |
+
compute_default_rope_parameters
|
| 98 |
+
)
|
| 99 |
+
vision_values = dict(raw["vision_config"])
|
| 100 |
+
vision_values.pop("auto_map", None)
|
| 101 |
+
vision_config = PenguinVLVisionEncoderConfig.from_dict(vision_values)
|
| 102 |
+
# AutoModel.from_config previously dispatched to this same _from_config
|
| 103 |
+
# method. Keep its dtype construction context and attention setup.
|
| 104 |
+
vision_model = PenguinVLVisionEncoderModel._from_config(
|
| 105 |
+
vision_config,
|
| 106 |
+
dtype=torch.bfloat16,
|
| 107 |
+
attn_implementation=config.vision.attn_implementation,
|
| 108 |
+
)
|
| 109 |
+
if config.vision.attn_implementation == "sdpa":
|
| 110 |
+
install_penguin_sdpa(vision_model)
|
| 111 |
+
vision = PenguinVisionEncoder(vision_model, hidden_size=config.vision.hidden_size)
|
| 112 |
+
|
| 113 |
+
text_values = dict(raw["text_config"])
|
| 114 |
+
model_type = text_values.pop("model_type")
|
| 115 |
+
if model_type != "hrm_text":
|
| 116 |
+
raise ValueError(f"Unexpected language model_type: {model_type!r}")
|
| 117 |
+
text_config = AutoConfig.for_model(model_type, **text_values)
|
| 118 |
+
text_config.max_position_embeddings = config.language.max_position_embeddings
|
| 119 |
+
text_config.rope_theta = config.language.rope_theta
|
| 120 |
+
if config.language.rope_parameters:
|
| 121 |
+
text_config.rope_parameters = dict(config.language.rope_parameters)
|
| 122 |
+
text_config.use_cache = bool(config.inference.use_cache)
|
| 123 |
+
language = HrmTextForCausalLM._from_config(
|
| 124 |
+
text_config,
|
| 125 |
+
dtype=torch.bfloat16,
|
| 126 |
+
attn_implementation=config.language.attn_implementation,
|
| 127 |
+
)
|
| 128 |
+
|
| 129 |
+
# Reproduce the historical construction order exactly. The multimodal
|
| 130 |
+
# model is first constructed in FP32 and then converted to BF16, including
|
| 131 |
+
# its nonpersistent hybrid RoPE buffers. Only adapters are restored to FP32.
|
| 132 |
+
recurrent = config.recurrent_visual
|
| 133 |
+
language.model = MultimodalHrmTextModel.from_text_model(
|
| 134 |
+
language.model,
|
| 135 |
+
rope_mode=recurrent.rope_mode,
|
| 136 |
+
use_visual_anchor=recurrent.use_visual_anchor,
|
| 137 |
+
use_visual_gate=recurrent.use_visual_gate,
|
| 138 |
+
gate_dim=recurrent.gate_dim,
|
| 139 |
+
anchor_raw_init=recurrent.anchor_raw_init,
|
| 140 |
+
anchor_max_scale=recurrent.anchor_max_scale,
|
| 141 |
+
gate_saturation_metric_lower=recurrent.gate_saturation_metric_lower,
|
| 142 |
+
gate_saturation_metric_upper=recurrent.gate_saturation_metric_upper,
|
| 143 |
+
).to(dtype=torch.bfloat16)
|
| 144 |
+
language.model.visual_gate.float()
|
| 145 |
+
language.model.visual_anchor.float()
|
| 146 |
+
projector = VisionProjector(
|
| 147 |
+
config.vision.hidden_size, config.language.hidden_size
|
| 148 |
+
).float()
|
| 149 |
+
# visual_embedding_scale is a persistent checkpoint buffer, so loading
|
| 150 |
+
# its stored value is restored alongside the model parameters.
|
| 151 |
+
model = cls(vision, language, projector, config)
|
| 152 |
+
|
| 153 |
+
weights = load_file(str(root / "model.safetensors"), device="cpu")
|
| 154 |
+
expected = model.state_dict()
|
| 155 |
+
missing = sorted(set(expected) - set(weights))
|
| 156 |
+
unexpected = sorted(set(weights) - set(expected))
|
| 157 |
+
if missing or unexpected:
|
| 158 |
+
raise RuntimeError(f"Checkpoint key mismatch: missing={missing}, unexpected={unexpected}")
|
| 159 |
+
for name, tensor in weights.items():
|
| 160 |
+
target = expected[name]
|
| 161 |
+
if tensor.shape != target.shape or tensor.dtype != target.dtype:
|
| 162 |
+
raise RuntimeError(
|
| 163 |
+
f"Refusing implicit conversion for {name}: checkpoint "
|
| 164 |
+
f"{tuple(tensor.shape)} {tensor.dtype}, model {tuple(target.shape)} {target.dtype}"
|
| 165 |
+
)
|
| 166 |
+
model.load_state_dict(weights, strict=True)
|
| 167 |
+
del expected, weights
|
| 168 |
+
model.checkpoint_directory = str(root)
|
| 169 |
+
return model.to(device=torch.device(device)).eval().requires_grad_(False)
|
penguin_encoder/LICENSE.txt
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Tencent is pleased to support the open source community by making Penguin-VL 高效视觉语言理解模型 available.
|
| 2 |
+
|
| 3 |
+
Copyright (C) 2026 Tencent. All rights reserved.
|
| 4 |
+
|
| 5 |
+
The open-source software and/or models included in this distribution may have been modified by Tencent (“Tencent Modifications”). All Tencent Modifications are Copyright (C) Tencent.
|
| 6 |
+
|
| 7 |
+
Penguin-VL 高效视觉语言理解模型 is licensed under the License Terms of Penguin-VL 高效视觉语言理解模型, except for the third-party components listed below, which remain licensed under their respective original terms. Penguin-VL 高效视觉语言理解模型 does not impose any additional restrictions beyond those specified in the original licenses of these third-party components. Users are required to comply with all applicable terms and conditions of the original licenses and to ensure that the use of these third-party components conforms to all relevant laws and regulations.
|
| 8 |
+
|
| 9 |
+
For the avoidance of doubt, Penguin-VL 高效视觉语言理解模型 refers solely to training code, inference code, parameters, and weights made publicly available by Tencent in accordance with the License Terms of Penguin-VL 高效视觉语言理解模型.
|
| 10 |
+
|
| 11 |
+
Terms of the License Terms of Penguin-VL 高效视觉语言理解模型:
|
| 12 |
+
--------------------------------------------------------------------
|
| 13 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
| 14 |
+
|
| 15 |
+
0. Additional Territorial Limitation ** Penguin-VL 高效视觉语言理解模型 IS NOT INTENDED FOR USE WITHIN THE EUROPEAN UNION.** IN THE EVENT OF ANY CONFLICT, THIS CLAUSE SHALL PREVAIL.
|
| 16 |
+
|
| 17 |
+
1. Definitions.
|
| 18 |
+
|
| 19 |
+
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
|
| 20 |
+
|
| 21 |
+
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
|
| 22 |
+
|
| 23 |
+
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%!)(MISSING) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
|
| 24 |
+
|
| 25 |
+
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
|
| 26 |
+
|
| 27 |
+
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
|
| 28 |
+
|
| 29 |
+
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
|
| 30 |
+
|
| 31 |
+
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
|
| 32 |
+
|
| 33 |
+
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
|
| 34 |
+
|
| 35 |
+
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
|
| 36 |
+
|
| 37 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
|
| 38 |
+
|
| 39 |
+
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
|
| 40 |
+
|
| 41 |
+
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
|
| 42 |
+
|
| 43 |
+
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
|
| 44 |
+
|
| 45 |
+
You must give any other recipients of the Work or Derivative Works a copy of this License; and
|
| 46 |
+
|
| 47 |
+
You must cause any modified files to carry prominent notices stating that You changed the files; and
|
| 48 |
+
|
| 49 |
+
You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
|
| 50 |
+
|
| 51 |
+
If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
|
| 52 |
+
|
| 53 |
+
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
|
| 54 |
+
|
| 55 |
+
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
|
| 56 |
+
|
| 57 |
+
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
|
| 58 |
+
|
| 59 |
+
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
|
| 60 |
+
|
| 61 |
+
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
|
| 62 |
+
|
| 63 |
+
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
END OF TERMS AND CONDITIONS
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
Dependencies and Licenses:
|
| 71 |
+
|
| 72 |
+
This open-source project, Penguin-VL 高效视觉语言理解模型, builds upon the following open-source models and/or software components, each of which remains licensed under its original license. Certain models or software may include modifications made by Tencent (“Tencent Modifications”), which are Copyright (C) Tencent.
|
| 73 |
+
|
| 74 |
+
In case you believe there have been errors in the attribution below, you may submit the concerns to us for review and correction.
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
Open Source Model Licensed under the Apache License 2.0:
|
| 78 |
+
--------------------------------------------------------------------
|
| 79 |
+
1. Qwen3
|
| 80 |
+
Copyright 2024 Alibaba Cloud
|
| 81 |
+
Please note this model has been modified by Tencent in this distribution.
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
Terms of the Apache License 2.0:
|
| 86 |
+
--------------------------------------------------------------------
|
| 87 |
+
Apache License
|
| 88 |
+
Version 2.0, January 2004
|
| 89 |
+
http://www.apache.org/licenses/
|
| 90 |
+
|
| 91 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
| 92 |
+
|
| 93 |
+
1. Definitions.
|
| 94 |
+
|
| 95 |
+
"License" shall mean the terms and conditions for use, reproduction,
|
| 96 |
+
and distribution as defined by Sections 1 through 9 of this document.
|
| 97 |
+
|
| 98 |
+
"Licensor" shall mean the copyright owner or entity authorized by
|
| 99 |
+
the copyright owner that is granting the License.
|
| 100 |
+
|
| 101 |
+
"Legal Entity" shall mean the union of the acting entity and all
|
| 102 |
+
other entities that control, are controlled by, or are under common
|
| 103 |
+
control with that entity. For the purposes of this definition,
|
| 104 |
+
"control" means (i) the power, direct or indirect, to cause the
|
| 105 |
+
direction or management of such entity, whether by contract or
|
| 106 |
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
| 107 |
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
| 108 |
+
|
| 109 |
+
"You" (or "Your") shall mean an individual or Legal Entity
|
| 110 |
+
exercising permissions granted by this License.
|
| 111 |
+
|
| 112 |
+
"Source" form shall mean the preferred form for making modifications,
|
| 113 |
+
including but not limited to software source code, documentation
|
| 114 |
+
source, and configuration files.
|
| 115 |
+
|
| 116 |
+
"Object" form shall mean any form resulting from mechanical
|
| 117 |
+
transformation or translation of a Source form, including but
|
| 118 |
+
not limited to compiled object code, generated documentation,
|
| 119 |
+
and conversions to other media types.
|
| 120 |
+
|
| 121 |
+
"Work" shall mean the work of authorship, whether in Source or
|
| 122 |
+
Object form, made available under the License, as indicated by a
|
| 123 |
+
copyright notice that is included in or attached to the work
|
| 124 |
+
(an example is provided in the Appendix below).
|
| 125 |
+
|
| 126 |
+
"Derivative Works" shall mean any work, whether in Source or Object
|
| 127 |
+
form, that is based on (or derived from) the Work and for which the
|
| 128 |
+
editorial revisions, annotations, elaborations, or other modifications
|
| 129 |
+
represent, as a whole, an original work of authorship. For the purposes
|
| 130 |
+
of this License, Derivative Works shall not include works that remain
|
| 131 |
+
separable from, or merely link (or bind by name) to the interfaces of,
|
| 132 |
+
the Work and Derivative Works thereof.
|
| 133 |
+
|
| 134 |
+
"Contribution" shall mean any work of authorship, including
|
| 135 |
+
the original version of the Work and any modifications or additions
|
| 136 |
+
to that Work or Derivative Works thereof, that is intentionally
|
| 137 |
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
| 138 |
+
or by an individual or Legal Entity authorized to submit on behalf of
|
| 139 |
+
the copyright owner. For the purposes of this definition, "submitted"
|
| 140 |
+
means any form of electronic, verbal, or written communication sent
|
| 141 |
+
to the Licensor or its representatives, including but not limited to
|
| 142 |
+
communication on electronic mailing lists, source code control systems,
|
| 143 |
+
and issue tracking systems that are managed by, or on behalf of, the
|
| 144 |
+
Licensor for the purpose of discussing and improving the Work, but
|
| 145 |
+
excluding communication that is conspicuously marked or otherwise
|
| 146 |
+
designated in writing by the copyright owner as "Not a Contribution."
|
| 147 |
+
|
| 148 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
| 149 |
+
on behalf of whom a Contribution has been received by Licensor and
|
| 150 |
+
subsequently incorporated within the Work.
|
| 151 |
+
|
| 152 |
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
| 153 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 154 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 155 |
+
copyright license to reproduce, prepare Derivative Works of,
|
| 156 |
+
publicly display, publicly perform, sublicense, and distribute the
|
| 157 |
+
Work and such Derivative Works in Source or Object form.
|
| 158 |
+
|
| 159 |
+
3. Grant of Patent License. Subject to the terms and conditions of
|
| 160 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 161 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 162 |
+
(except as stated in this section) patent license to make, have made,
|
| 163 |
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
| 164 |
+
where such license applies only to those patent claims licensable
|
| 165 |
+
by such Contributor that are necessarily infringed by their
|
| 166 |
+
Contribution(s) alone or by combination of their Contribution(s)
|
| 167 |
+
with the Work to which such Contribution(s) was submitted. If You
|
| 168 |
+
institute patent litigation against any entity (including a
|
| 169 |
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
| 170 |
+
or a Contribution incorporated within the Work constitutes direct
|
| 171 |
+
or contributory patent infringement, then any patent licenses
|
| 172 |
+
granted to You under this License for that Work shall terminate
|
| 173 |
+
as of the date such litigation is filed.
|
| 174 |
+
|
| 175 |
+
4. Redistribution. You may reproduce and distribute copies of the
|
| 176 |
+
Work or Derivative Works thereof in any medium, with or without
|
| 177 |
+
modifications, and in Source or Object form, provided that You
|
| 178 |
+
meet the following conditions:
|
| 179 |
+
|
| 180 |
+
(a) You must give any other recipients of the Work or
|
| 181 |
+
Derivative Works a copy of this License; and
|
| 182 |
+
|
| 183 |
+
(b) You must cause any modified files to carry prominent notices
|
| 184 |
+
stating that You changed the files; and
|
| 185 |
+
|
| 186 |
+
(c) You must retain, in the Source form of any Derivative Works
|
| 187 |
+
that You distribute, all copyright, patent, trademark, and
|
| 188 |
+
attribution notices from the Source form of the Work,
|
| 189 |
+
excluding those notices that do not pertain to any part of
|
| 190 |
+
the Derivative Works; and
|
| 191 |
+
|
| 192 |
+
(d) If the Work includes a "NOTICE" text file as part of its
|
| 193 |
+
distribution, then any Derivative Works that You distribute must
|
| 194 |
+
include a readable copy of the attribution notices contained
|
| 195 |
+
within such NOTICE file, excluding those notices that do not
|
| 196 |
+
pertain to any part of the Derivative Works, in at least one
|
| 197 |
+
of the following places: within a NOTICE text file distributed
|
| 198 |
+
as part of the Derivative Works; within the Source form or
|
| 199 |
+
documentation, if provided along with the Derivative Works; or,
|
| 200 |
+
within a display generated by the Derivative Works, if and
|
| 201 |
+
wherever such third-party notices normally appear. The contents
|
| 202 |
+
of the NOTICE file are for informational purposes only and
|
| 203 |
+
do not modify the License. You may add Your own attribution
|
| 204 |
+
notices within Derivative Works that You distribute, alongside
|
| 205 |
+
or as an addendum to the NOTICE text from the Work, provided
|
| 206 |
+
that such additional attribution notices cannot be construed
|
| 207 |
+
as modifying the License.
|
| 208 |
+
|
| 209 |
+
You may add Your own copyright statement to Your modifications and
|
| 210 |
+
may provide additional or different license terms and conditions
|
| 211 |
+
for use, reproduction, or distribution of Your modifications, or
|
| 212 |
+
for any such Derivative Works as a whole, provided Your use,
|
| 213 |
+
reproduction, and distribution of the Work otherwise complies with
|
| 214 |
+
the conditions stated in this License.
|
| 215 |
+
|
| 216 |
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
| 217 |
+
any Contribution intentionally submitted for inclusion in the Work
|
| 218 |
+
by You to the Licensor shall be under the terms and conditions of
|
| 219 |
+
this License, without any additional terms or conditions.
|
| 220 |
+
Notwithstanding the above, nothing herein shall supersede or modify
|
| 221 |
+
the terms of any separate license agreement you may have executed
|
| 222 |
+
with Licensor regarding such Contributions.
|
| 223 |
+
|
| 224 |
+
6. Trademarks. This License does not grant permission to use the trade
|
| 225 |
+
names, trademarks, service marks, or product names of the Licensor,
|
| 226 |
+
except as required for reasonable and customary use in describing the
|
| 227 |
+
origin of the Work and reproducing the content of the NOTICE file.
|
| 228 |
+
|
| 229 |
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
| 230 |
+
agreed to in writing, Licensor provides the Work (and each
|
| 231 |
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
| 232 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
| 233 |
+
implied, including, without limitation, any warranties or conditions
|
| 234 |
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
| 235 |
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
| 236 |
+
appropriateness of using or redistributing the Work and assume any
|
| 237 |
+
risks associated with Your exercise of permissions under this License.
|
| 238 |
+
|
| 239 |
+
8. Limitation of Liability. In no event and under no legal theory,
|
| 240 |
+
whether in tort (including negligence), contract, or otherwise,
|
| 241 |
+
unless required by applicable law (such as deliberate and grossly
|
| 242 |
+
negligent acts) or agreed to in writing, shall any Contributor be
|
| 243 |
+
liable to You for damages, including any direct, indirect, special,
|
| 244 |
+
incidental, or consequential damages of any character arising as a
|
| 245 |
+
result of this License or out of the use or inability to use the
|
| 246 |
+
Work (including but not limited to damages for loss of goodwill,
|
| 247 |
+
work stoppage, computer failure or malfunction, or any and all
|
| 248 |
+
other commercial damages or losses), even if such Contributor
|
| 249 |
+
has been advised of the possibility of such damages.
|
| 250 |
+
|
| 251 |
+
9. Accepting Warranty or Additional Liability. While redistributing
|
| 252 |
+
the Work or Derivative Works thereof, You may choose to offer,
|
| 253 |
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
| 254 |
+
or other liability obligations and/or rights consistent with this
|
| 255 |
+
License. However, in accepting such obligations, You may act only
|
| 256 |
+
on Your own behalf and on Your sole responsibility, not on behalf
|
| 257 |
+
of any other Contributor, and only if You agree to indemnify,
|
| 258 |
+
defend, and hold each Contributor harmless for any liability
|
| 259 |
+
incurred by, or claims asserted against, such Contributor by reason
|
| 260 |
+
of your accepting any such warranty or additional liability.
|
| 261 |
+
|
| 262 |
+
END OF TERMS AND CONDITIONS
|
| 263 |
+
|
| 264 |
+
APPENDIX: How to apply the Apache License to your work.
|
| 265 |
+
|
| 266 |
+
To apply the Apache License to your work, attach the following
|
| 267 |
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
| 268 |
+
replaced with your own identifying information. (Don't include
|
| 269 |
+
the brackets!) The text should be enclosed in the appropriate
|
| 270 |
+
comment syntax for the file format. We also recommend that a
|
| 271 |
+
file or class name and description of purpose be included on the
|
| 272 |
+
same "printed page" as the copyright notice for easier
|
| 273 |
+
identification within third-party archives.
|
| 274 |
+
|
| 275 |
+
Copyright [yyyy] [name of copyright owner]
|
| 276 |
+
|
| 277 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
| 278 |
+
you may not use this file except in compliance with the License.
|
| 279 |
+
You may obtain a copy of the License at
|
| 280 |
+
|
| 281 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
| 282 |
+
|
| 283 |
+
Unless required by applicable law or agreed to in writing, software
|
| 284 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
| 285 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 286 |
+
See the License for the specific language governing permissions and
|
| 287 |
+
limitations under the License.
|
| 288 |
+
|
| 289 |
+
|
penguin_encoder/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Pinned Penguin vision implementation, distributed as code in GitHub only."""
|
penguin_encoder/configuration_penguinvl_encoder.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""PenguinVL vision encoder model configuration."""
|
| 2 |
+
|
| 3 |
+
from transformers import Qwen3Config
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class PenguinVLVisionEncoderConfig(Qwen3Config):
|
| 7 |
+
|
| 8 |
+
model_type = "penguinvl_vision_encoder"
|
| 9 |
+
|
| 10 |
+
def __init__(
|
| 11 |
+
self,
|
| 12 |
+
hidden_size=1536,
|
| 13 |
+
intermediate_size=8960,
|
| 14 |
+
num_hidden_layers=12,
|
| 15 |
+
num_attention_heads=12,
|
| 16 |
+
num_channels=3,
|
| 17 |
+
patch_size=14,
|
| 18 |
+
layer_norm_eps=1e-6,
|
| 19 |
+
attention_dropout=0.0,
|
| 20 |
+
num_key_value_heads=2,
|
| 21 |
+
**kwargs,
|
| 22 |
+
):
|
| 23 |
+
super().__init__(**kwargs)
|
| 24 |
+
|
| 25 |
+
self.hidden_size = hidden_size
|
| 26 |
+
self.intermediate_size = intermediate_size
|
| 27 |
+
self.num_hidden_layers = num_hidden_layers
|
| 28 |
+
self.num_attention_heads = num_attention_heads
|
| 29 |
+
self.num_channels = num_channels
|
| 30 |
+
self.patch_size = patch_size
|
| 31 |
+
self.attention_dropout = attention_dropout
|
| 32 |
+
self.num_key_value_heads = num_key_value_heads
|
| 33 |
+
self.layer_norm_eps = layer_norm_eps
|
penguin_encoder/image_processing_penguinvl.py
ADDED
|
@@ -0,0 +1,548 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Adopted from https://github.com/huggingface/transformers/blob/main/src/transformers/models/qwen2_vl/image_processing_qwen2_vl.py.
|
| 2 |
+
# Below is the original copyright:
|
| 3 |
+
# Copyright 2024 The Qwen team, Alibaba Group and the HuggingFace Inc. team. All rights reserved.
|
| 4 |
+
#
|
| 5 |
+
# This code is based on EleutherAI's GPT-NeoX library and the GPT-NeoX
|
| 6 |
+
# and OPT implementations in this library. It has been modified from its
|
| 7 |
+
# original forms to accommodate minor architectural differences compared
|
| 8 |
+
# to GPT-NeoX and OPT used by the Meta AI team that trained the model.
|
| 9 |
+
#
|
| 10 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 11 |
+
# you may not use this file except in compliance with the License.
|
| 12 |
+
# You may obtain a copy of the License at
|
| 13 |
+
#
|
| 14 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 15 |
+
#
|
| 16 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 17 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 18 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 19 |
+
# See the License for the specific language governing permissions and
|
| 20 |
+
# limitations under the License.
|
| 21 |
+
"""Image processor class for PenguinVL."""
|
| 22 |
+
|
| 23 |
+
import math
|
| 24 |
+
from typing import Dict, List, Optional, Union
|
| 25 |
+
|
| 26 |
+
import numpy as np
|
| 27 |
+
|
| 28 |
+
import torch
|
| 29 |
+
from transformers.image_processing_utils import BaseImageProcessor, BatchFeature
|
| 30 |
+
from transformers.image_utils import ImageInput
|
| 31 |
+
from transformers.image_transforms import (
|
| 32 |
+
convert_to_rgb,
|
| 33 |
+
resize,
|
| 34 |
+
to_channel_dimension_format,
|
| 35 |
+
)
|
| 36 |
+
from transformers.image_utils import (
|
| 37 |
+
OPENAI_CLIP_MEAN,
|
| 38 |
+
OPENAI_CLIP_STD,
|
| 39 |
+
ChannelDimension,
|
| 40 |
+
ImageInput,
|
| 41 |
+
PILImageResampling,
|
| 42 |
+
get_image_size,
|
| 43 |
+
infer_channel_dimension_format,
|
| 44 |
+
is_scaled_image,
|
| 45 |
+
is_valid_image,
|
| 46 |
+
make_list_of_images,
|
| 47 |
+
to_numpy_array,
|
| 48 |
+
)
|
| 49 |
+
try:
|
| 50 |
+
from transformers.image_utils import VideoInput
|
| 51 |
+
except:
|
| 52 |
+
from transformers.video_utils import VideoInput
|
| 53 |
+
from transformers.utils import TensorType, is_vision_available, logging
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
logger = logging.get_logger(__name__)
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
if is_vision_available():
|
| 60 |
+
from PIL import Image
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def is_valid_video(video) -> bool:
|
| 64 |
+
if isinstance(video, (list, tuple)):
|
| 65 |
+
return all(is_valid_image(frame) for frame in video)
|
| 66 |
+
elif isinstance(video, np.ndarray):
|
| 67 |
+
return video.ndim == 4
|
| 68 |
+
elif isinstance(video, torch.Tensor):
|
| 69 |
+
return video.ndim == 4
|
| 70 |
+
return False
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def make_batched_images(images) -> List[List[ImageInput]]:
|
| 74 |
+
"""
|
| 75 |
+
Normalize visual inputs to ``List[List[ImageInput]]`` – a list of *clips*,
|
| 76 |
+
where each clip is a list of frames.
|
| 77 |
+
|
| 78 |
+
Supported input formats::
|
| 79 |
+
|
| 80 |
+
Nested clips : [[image], [f1, f2, ...], ...] → returned as-is
|
| 81 |
+
Flat frames : [f1, f2, ...] → [[f1, f2, ...]]
|
| 82 |
+
Single image : image → [[image]]
|
| 83 |
+
|
| 84 |
+
Returns:
|
| 85 |
+
List of clips, where each clip is a list of valid images / frames.
|
| 86 |
+
"""
|
| 87 |
+
if isinstance(images, (list, tuple)) and len(images) > 0:
|
| 88 |
+
if isinstance(images[0], (list, tuple)):
|
| 89 |
+
return [list(clip) for clip in images]
|
| 90 |
+
if all(is_valid_image(f) for f in images):
|
| 91 |
+
return [list(images)]
|
| 92 |
+
if is_valid_image(images):
|
| 93 |
+
return [[images]]
|
| 94 |
+
raise ValueError(f"Could not make batched images from {images}")
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def simple_batched_resize(
|
| 98 |
+
images,
|
| 99 |
+
factor: int = 28,
|
| 100 |
+
min_tokens: int = 4 * 4,
|
| 101 |
+
max_tokens: int = 16384,
|
| 102 |
+
input_data_format: str = None,
|
| 103 |
+
frame_types=None
|
| 104 |
+
):
|
| 105 |
+
"""
|
| 106 |
+
Compute per-frame target (h, w) for a video frame list under a token budget (key/intermediate may differ).
|
| 107 |
+
|
| 108 |
+
Uses the Temporal Redundancy-Aware (TRA) token compression strategy: key and intermediate frames
|
| 109 |
+
can have different target areas (e.g. 1:16 ratio when compressing) to stay within max_tokens.
|
| 110 |
+
|
| 111 |
+
Args:
|
| 112 |
+
images: List of video frames (each PIL Image or ndarray).
|
| 113 |
+
factor: Alignment granularity (height and width are multiples of factor), default 28.
|
| 114 |
+
min_tokens: Minimum tokens per frame (used to derive min_pixels), default 16.
|
| 115 |
+
max_tokens: Token cap for total pixel budget, default 16384.
|
| 116 |
+
input_data_format: Channel format when not PIL, e.g. "channels_first".
|
| 117 |
+
frame_types: Per-frame type list, 0=key, 1=intermediate; None means all key.
|
| 118 |
+
|
| 119 |
+
Returns:
|
| 120 |
+
image_sizes: List of (h, w) per frame, one-to-one with images.
|
| 121 |
+
"""
|
| 122 |
+
min_pixels = min_tokens * factor * factor * 1.5
|
| 123 |
+
max_pixels = max_tokens * factor * factor * 0.95
|
| 124 |
+
|
| 125 |
+
# --- Base info ---
|
| 126 |
+
first_image = images[0]
|
| 127 |
+
if isinstance(first_image, Image.Image):
|
| 128 |
+
width, height = first_image.size
|
| 129 |
+
else:
|
| 130 |
+
height, width = get_image_size(first_image, channel_dim=input_data_format)
|
| 131 |
+
|
| 132 |
+
aspect_ratio = height / width
|
| 133 |
+
raw_area = height * width
|
| 134 |
+
|
| 135 |
+
num_frames = len(images)
|
| 136 |
+
if frame_types is not None:
|
| 137 |
+
ft_list = frame_types.tolist() if hasattr(frame_types, 'tolist') else frame_types
|
| 138 |
+
num_intermediate = ft_list.count(1)
|
| 139 |
+
num_key = ft_list.count(0)
|
| 140 |
+
else:
|
| 141 |
+
num_key = num_frames
|
| 142 |
+
num_intermediate = 0
|
| 143 |
+
ft_list = [0] * num_frames
|
| 144 |
+
|
| 145 |
+
def get_dims_from_area(target_area, ar, fac):
|
| 146 |
+
"""Compute aligned (h, w) from target area and aspect ratio; area = w²·ar => w = sqrt(area/ar)."""
|
| 147 |
+
w_new = math.sqrt(target_area / ar)
|
| 148 |
+
h_new = w_new * ar
|
| 149 |
+
|
| 150 |
+
h_bar = round(h_new / fac) * fac
|
| 151 |
+
w_bar = round(w_new / fac) * fac
|
| 152 |
+
h_bar = max(h_bar, fac)
|
| 153 |
+
w_bar = max(w_bar, fac)
|
| 154 |
+
|
| 155 |
+
return h_bar, w_bar
|
| 156 |
+
|
| 157 |
+
# --- Stage 1: No-downscale check ---
|
| 158 |
+
# If total pixels within budget, keep original size for both key and intermediate frames.
|
| 159 |
+
total_raw_pixels = num_frames * raw_area
|
| 160 |
+
target_key_area = raw_area
|
| 161 |
+
target_intermediate_area = raw_area
|
| 162 |
+
|
| 163 |
+
if total_raw_pixels > max_pixels:
|
| 164 |
+
# --- Stage 2: Sync compression ---
|
| 165 |
+
# Over budget: compress with 1:16 area ratio, intermediate_area = key_area / 16.
|
| 166 |
+
# Constraint: N_key·A_key + N_intermediate·(A_key/16) = max_pixels => A_key = max_pixels / (N_key + N_intermediate/16).
|
| 167 |
+
effective_count = num_key + (num_intermediate / 16.0)
|
| 168 |
+
calc_key_area = max_pixels / effective_count
|
| 169 |
+
calc_intermediate_area = calc_key_area / 16.0
|
| 170 |
+
|
| 171 |
+
# --- Stage 3: Intermediate-frame floor ---
|
| 172 |
+
# If computed intermediate area is below min_pixels, pin intermediate to min_pixels and give remaining budget to key.
|
| 173 |
+
if calc_intermediate_area >= min_pixels:
|
| 174 |
+
target_key_area = calc_key_area
|
| 175 |
+
target_intermediate_area = calc_intermediate_area
|
| 176 |
+
else:
|
| 177 |
+
target_intermediate_area = min_pixels
|
| 178 |
+
pixels_taken_by_intermediate = num_intermediate * min_pixels
|
| 179 |
+
remaining_for_key = max_pixels - pixels_taken_by_intermediate
|
| 180 |
+
target_key_area = remaining_for_key / num_key
|
| 181 |
+
|
| 182 |
+
# --- Stage 4: Key-frame hard floor ---
|
| 183 |
+
if target_key_area < min_pixels:
|
| 184 |
+
target_key_area = min_pixels
|
| 185 |
+
|
| 186 |
+
# --- Area to aligned dimensions ---
|
| 187 |
+
k_h, k_w = get_dims_from_area(target_key_area, aspect_ratio, factor)
|
| 188 |
+
if num_intermediate > 0:
|
| 189 |
+
i_h, i_w = get_dims_from_area(target_intermediate_area, aspect_ratio, factor)
|
| 190 |
+
else:
|
| 191 |
+
i_h, i_w = 0, 0
|
| 192 |
+
|
| 193 |
+
def ensure_min_hw(h, w, min_p, raw_ar):
|
| 194 |
+
"""If area still below min_pixels after alignment (rounding), recompute from min area and align upward."""
|
| 195 |
+
if h * w < min_p:
|
| 196 |
+
w = math.sqrt(min_p / raw_ar)
|
| 197 |
+
h = w * raw_ar
|
| 198 |
+
h = math.ceil(h / factor) * factor
|
| 199 |
+
w = math.ceil(w / factor) * factor
|
| 200 |
+
return h, w
|
| 201 |
+
|
| 202 |
+
k_h, k_w = ensure_min_hw(k_h, k_w, min_pixels, aspect_ratio)
|
| 203 |
+
if num_intermediate > 0:
|
| 204 |
+
i_h, i_w = ensure_min_hw(i_h, i_w, min_pixels, aspect_ratio)
|
| 205 |
+
|
| 206 |
+
image_sizes = [
|
| 207 |
+
(i_h, i_w) if ft_list[i] == 1 else (k_h, k_w)
|
| 208 |
+
for i in range(num_frames)
|
| 209 |
+
]
|
| 210 |
+
return image_sizes
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
class PenguinVLImageProcessor(BaseImageProcessor):
|
| 214 |
+
r"""
|
| 215 |
+
Constructs a PenguinVL image processor that dynamically resizes images based on the original images.
|
| 216 |
+
|
| 217 |
+
Args:
|
| 218 |
+
do_resize (`bool`, *optional*, defaults to `True`):
|
| 219 |
+
Whether to resize the image's (height, width) dimensions.
|
| 220 |
+
resample (`PILImageResampling`, *optional*, defaults to `Resampling.BICUBIC`):
|
| 221 |
+
Resampling filter to use when resizing the image.
|
| 222 |
+
do_rescale (`bool`, *optional*, defaults to `True`):
|
| 223 |
+
Whether to rescale the image by the specified scale `rescale_factor`.
|
| 224 |
+
rescale_factor (`int` or `float`, *optional*, defaults to `1/255`):
|
| 225 |
+
Scale factor to use if rescaling the image.
|
| 226 |
+
do_normalize (`bool`, *optional*, defaults to `True`):
|
| 227 |
+
Whether to normalize the image.
|
| 228 |
+
image_mean (`float` or `List[float]`, *optional*, defaults to `[0.48145466, 0.4578275, 0.40821073]`):
|
| 229 |
+
Mean to use if normalizing the image. This is a float or list of floats for each channel in the image.
|
| 230 |
+
image_std (`float` or `List[float]`, *optional*, defaults to `[0.26862954, 0.26130258, 0.27577711]`):
|
| 231 |
+
Standard deviation to use if normalizing the image. This is a float or list of floats for each channel in the image.
|
| 232 |
+
do_convert_rgb (`bool`, *optional*, defaults to `True`):
|
| 233 |
+
Whether to convert the image to RGB.
|
| 234 |
+
min_pixels (`int`, *optional*, defaults to `56 * 56`):
|
| 235 |
+
The min pixels of the image to resize the image.
|
| 236 |
+
max_pixels (`int`, *optional*, defaults to `28 * 28 * 1280`):
|
| 237 |
+
The max pixels of the image to resize the image.
|
| 238 |
+
patch_size (`int`, *optional*, defaults to 14):
|
| 239 |
+
The spacial patch size of the vision encoder.
|
| 240 |
+
"""
|
| 241 |
+
|
| 242 |
+
model_input_names = ["pixel_values", "grid_sizes", "merge_sizes"]
|
| 243 |
+
|
| 244 |
+
def __init__(
|
| 245 |
+
self,
|
| 246 |
+
do_resize: bool = True,
|
| 247 |
+
resample: PILImageResampling = PILImageResampling.BICUBIC,
|
| 248 |
+
do_rescale: bool = True,
|
| 249 |
+
rescale_factor: Union[int, float] = 1 / 255,
|
| 250 |
+
do_normalize: bool = True,
|
| 251 |
+
image_mean: Optional[Union[float, List[float]]] = None,
|
| 252 |
+
image_std: Optional[Union[float, List[float]]] = None,
|
| 253 |
+
do_convert_rgb: bool = True,
|
| 254 |
+
min_tokens: int = 4 * 4,
|
| 255 |
+
max_tokens: int = 16384,
|
| 256 |
+
patch_size: int = 14,
|
| 257 |
+
**kwargs,
|
| 258 |
+
) -> None:
|
| 259 |
+
super().__init__(**kwargs)
|
| 260 |
+
self.do_resize = do_resize
|
| 261 |
+
self.resample = resample
|
| 262 |
+
self.do_rescale = do_rescale
|
| 263 |
+
self.rescale_factor = rescale_factor
|
| 264 |
+
self.do_normalize = do_normalize
|
| 265 |
+
self.image_mean = image_mean if image_mean is not None else OPENAI_CLIP_MEAN
|
| 266 |
+
self.image_std = image_std if image_std is not None else OPENAI_CLIP_STD
|
| 267 |
+
self.min_tokens = min_tokens
|
| 268 |
+
self.max_tokens = max_tokens
|
| 269 |
+
self.patch_size = patch_size
|
| 270 |
+
self.do_convert_rgb = do_convert_rgb
|
| 271 |
+
|
| 272 |
+
def _allocate_token_budget(self, clips, clip_merge_sizes, input_data_format):
|
| 273 |
+
"""Distribute self.max_tokens across clips proportionally to their raw token counts."""
|
| 274 |
+
clip_raw_tokens = []
|
| 275 |
+
for clip, ms in zip(clips, clip_merge_sizes):
|
| 276 |
+
first_frame = clip[0]
|
| 277 |
+
if isinstance(first_frame, Image.Image):
|
| 278 |
+
w, h = first_frame.size
|
| 279 |
+
else:
|
| 280 |
+
h, w = get_image_size(first_frame, channel_dim=input_data_format)
|
| 281 |
+
factor = self.patch_size * ms
|
| 282 |
+
clip_raw_tokens.append(len(clip) * h * w / (factor * factor))
|
| 283 |
+
|
| 284 |
+
total_raw_tokens = sum(clip_raw_tokens)
|
| 285 |
+
if total_raw_tokens <= self.max_tokens:
|
| 286 |
+
return [self.max_tokens] * len(clips)
|
| 287 |
+
|
| 288 |
+
return [
|
| 289 |
+
max(self.min_tokens * len(clip), raw * self.max_tokens / total_raw_tokens)
|
| 290 |
+
for clip, raw in zip(clips, clip_raw_tokens)
|
| 291 |
+
]
|
| 292 |
+
|
| 293 |
+
def _preprocess(
|
| 294 |
+
self,
|
| 295 |
+
images: Union[ImageInput, VideoInput],
|
| 296 |
+
target_size: List[int],
|
| 297 |
+
merge_size: int = 1,
|
| 298 |
+
do_resize: bool = None,
|
| 299 |
+
resample: PILImageResampling = None,
|
| 300 |
+
do_rescale: bool = None,
|
| 301 |
+
rescale_factor: float = None,
|
| 302 |
+
do_normalize: bool = None,
|
| 303 |
+
image_mean: Optional[Union[float, List[float]]] = None,
|
| 304 |
+
image_std: Optional[Union[float, List[float]]] = None,
|
| 305 |
+
do_convert_rgb: bool = None,
|
| 306 |
+
data_format: Optional[ChannelDimension] = ChannelDimension.FIRST,
|
| 307 |
+
input_data_format: Optional[Union[str, ChannelDimension]] = None,
|
| 308 |
+
):
|
| 309 |
+
"""
|
| 310 |
+
Preprocess an image or batch of images. Copy of the `preprocess` method from `CLIPImageProcessor`.
|
| 311 |
+
|
| 312 |
+
Args:
|
| 313 |
+
images (`ImageInput`):
|
| 314 |
+
Image or batch of images to preprocess. Expects pixel values ranging from 0 to 255. If pixel values range from 0 to 1, set `do_rescale=False`.
|
| 315 |
+
target_size (`List[int]`):
|
| 316 |
+
The target size to resize the image to. Should be a list of two integers: [target_height, target_width].
|
| 317 |
+
merge_size (`int`, *optional*, defaults to `1`):
|
| 318 |
+
The merge size after the vision encoder.
|
| 319 |
+
do_resize (`bool`, *optional*, defaults to `self.do_resize`):
|
| 320 |
+
Whether to resize the image.
|
| 321 |
+
resample (`PILImageResampling`, *optional*, defaults to `self.resample`):
|
| 322 |
+
Resampling filter to use if resizing the image. This can be one of the `PILImageResampling` enums.
|
| 323 |
+
do_rescale (`bool`, *optional*, defaults to `self.do_rescale`):
|
| 324 |
+
Whether to rescale the image.
|
| 325 |
+
rescale_factor (`float`, *optional*, defaults to `self.rescale_factor`):
|
| 326 |
+
Scale factor to use if rescaling the image.
|
| 327 |
+
do_normalize (`bool`, *optional*, defaults to `self.do_normalize`):
|
| 328 |
+
Whether to normalize the image.
|
| 329 |
+
image_mean (`float` or `List[float]`, *optional*, defaults to `self.image_mean`):
|
| 330 |
+
Mean to use if normalizing the image. Can be a float or a list of floats corresponding to the number of channels in the image.
|
| 331 |
+
image_std (`float` or `List[float]`, *optional*, defaults to `self.image_std`):
|
| 332 |
+
Standard deviation to use if normalizing the image. Can be a float or a list of floats corresponding to the number of channels in the image.
|
| 333 |
+
do_convert_rgb (`bool`, *optional*, defaults to `self.do_convert_rgb`):
|
| 334 |
+
Whether to convert the image to RGB.
|
| 335 |
+
data_format (`ChannelDimension`, *optional*, defaults to `ChannelDimension.FIRST`):
|
| 336 |
+
The channel dimension format for the output image. Can be one of:
|
| 337 |
+
- `"channels_first"` or `ChannelDimension.FIRST`: image in (num_channels, height, width) format.
|
| 338 |
+
- `"channels_last"` or `ChannelDimension.LAST`: image in (height, width, num_channels) format.
|
| 339 |
+
- Unset: Use the channel dimension format of the input image.
|
| 340 |
+
input_data_format (`ChannelDimension` or `str`, *optional*):
|
| 341 |
+
The channel dimension format for the input image. Can be one of:
|
| 342 |
+
- `"channels_first"` or `ChannelDimension.FIRST`: image in (num_channels, height, width) format.
|
| 343 |
+
- `"channels_last"` or `ChannelDimension.LAST`: image in (height, width, num_channels) format.
|
| 344 |
+
- `"none"` or `ChannelDimension.NONE`: image in (height, width) format. - `"none"` or `ChannelDimension.NONE`: image in (height, width) format.
|
| 345 |
+
"""
|
| 346 |
+
images = make_list_of_images(images)
|
| 347 |
+
|
| 348 |
+
if do_convert_rgb:
|
| 349 |
+
images = [convert_to_rgb(image) for image in images]
|
| 350 |
+
|
| 351 |
+
# All transformations expect numpy arrays.
|
| 352 |
+
images = [to_numpy_array(image) for image in images]
|
| 353 |
+
|
| 354 |
+
if is_scaled_image(images[0]) and do_rescale:
|
| 355 |
+
logger.warning_once(
|
| 356 |
+
"It looks like you are trying to rescale already rescaled images. If the input"
|
| 357 |
+
" images have pixel values between 0 and 1, set `do_rescale=False` to avoid rescaling them again."
|
| 358 |
+
)
|
| 359 |
+
if input_data_format is None:
|
| 360 |
+
# We assume that all images have the same channel dimension format.
|
| 361 |
+
input_data_format = infer_channel_dimension_format(images[0])
|
| 362 |
+
|
| 363 |
+
height, width = get_image_size(images[0], channel_dim=input_data_format)
|
| 364 |
+
resized_height, resized_width = height, width
|
| 365 |
+
processed_images = []
|
| 366 |
+
for image in images:
|
| 367 |
+
if do_resize:
|
| 368 |
+
resized_height, resized_width = target_size
|
| 369 |
+
image = resize(
|
| 370 |
+
image, size=(resized_height, resized_width), resample=resample, input_data_format=input_data_format
|
| 371 |
+
)
|
| 372 |
+
|
| 373 |
+
if do_rescale:
|
| 374 |
+
image = self.rescale(image, scale=rescale_factor, input_data_format=input_data_format)
|
| 375 |
+
|
| 376 |
+
if do_normalize:
|
| 377 |
+
image = self.normalize(
|
| 378 |
+
image=image, mean=image_mean, std=image_std, input_data_format=input_data_format
|
| 379 |
+
)
|
| 380 |
+
|
| 381 |
+
image = to_channel_dimension_format(image, data_format, input_channel_dim=input_data_format)
|
| 382 |
+
processed_images.append(image)
|
| 383 |
+
|
| 384 |
+
patches = np.array(processed_images)
|
| 385 |
+
if data_format == ChannelDimension.LAST:
|
| 386 |
+
patches = patches.transpose(0, 3, 1, 2)
|
| 387 |
+
t = patches.shape[0]
|
| 388 |
+
channel = patches.shape[1]
|
| 389 |
+
grid_h, grid_w = resized_height // self.patch_size, resized_width // self.patch_size
|
| 390 |
+
patches = patches.reshape(
|
| 391 |
+
t,
|
| 392 |
+
channel,
|
| 393 |
+
grid_h // merge_size,
|
| 394 |
+
merge_size,
|
| 395 |
+
self.patch_size,
|
| 396 |
+
grid_w // merge_size,
|
| 397 |
+
merge_size,
|
| 398 |
+
self.patch_size,
|
| 399 |
+
)
|
| 400 |
+
patches = patches.transpose(0, 2, 5, 3, 6, 1, 4, 7)
|
| 401 |
+
flatten_patches = patches.reshape(
|
| 402 |
+
t * grid_h * grid_w, channel * self.patch_size * self.patch_size
|
| 403 |
+
)
|
| 404 |
+
|
| 405 |
+
return flatten_patches, (t, grid_h, grid_w)
|
| 406 |
+
|
| 407 |
+
def preprocess(
|
| 408 |
+
self,
|
| 409 |
+
images: ImageInput,
|
| 410 |
+
do_resize: bool = None,
|
| 411 |
+
resample: PILImageResampling = None,
|
| 412 |
+
do_rescale: bool = None,
|
| 413 |
+
rescale_factor: float = None,
|
| 414 |
+
do_normalize: bool = None,
|
| 415 |
+
image_mean: Optional[Union[float, List[float]]] = None,
|
| 416 |
+
image_std: Optional[Union[float, List[float]]] = None,
|
| 417 |
+
do_convert_rgb: bool = None,
|
| 418 |
+
merge_size: Optional[Union[int, List[int]]] = None,
|
| 419 |
+
frame_types: Optional[Union[int, List[int]]] = None,
|
| 420 |
+
return_tensors: Optional[Union[str, TensorType]] = None,
|
| 421 |
+
data_format: Optional[ChannelDimension] = ChannelDimension.FIRST,
|
| 422 |
+
input_data_format: Optional[Union[str, ChannelDimension]] = None,
|
| 423 |
+
):
|
| 424 |
+
"""
|
| 425 |
+
Args:
|
| 426 |
+
images (`ImageInput`):
|
| 427 |
+
Image to preprocess. Expects a single or batch of images with pixel values ranging from 0 to 255. If
|
| 428 |
+
passing in images with pixel values between 0 and 1, set `do_rescale=False`.
|
| 429 |
+
do_resize (`bool`, *optional*, defaults to `self.do_resize`):
|
| 430 |
+
Whether to resize the image.
|
| 431 |
+
resample (`int`, *optional*, defaults to `self.resample`):
|
| 432 |
+
Resampling filter to use if resizing the image. This can be one of the enum `PILImageResampling`. Only
|
| 433 |
+
has an effect if `do_resize` is set to `True`.
|
| 434 |
+
do_rescale (`bool`, *optional*, defaults to `self.do_rescale`):
|
| 435 |
+
Whether to rescale the image.
|
| 436 |
+
rescale_factor (`float`, *optional*, defaults to `self.rescale_factor`):
|
| 437 |
+
Rescale factor to rescale the image by if `do_rescale` is set to `True`.
|
| 438 |
+
do_normalize (`bool`, *optional*, defaults to `self.do_normalize`):
|
| 439 |
+
Whether to normalize the image.
|
| 440 |
+
image_mean (`float` or `List[float]`, *optional*, defaults to `self.image_mean`):
|
| 441 |
+
Image mean to use for normalization. Only has an effect if `do_normalize` is set to `True`.
|
| 442 |
+
image_std (`float` or `List[float]`, *optional*, defaults to `self.image_std`):
|
| 443 |
+
Image standard deviation to use for normalization. Only has an effect if `do_normalize` is set to
|
| 444 |
+
`True`.
|
| 445 |
+
do_convert_rgb (`bool`, *optional*, defaults to `self.do_convert_rgb`):
|
| 446 |
+
Whether to convert the image to RGB.
|
| 447 |
+
return_tensors (`str` or `TensorType`, *optional*):
|
| 448 |
+
The type of tensors to return. Can be one of:
|
| 449 |
+
- Unset: Return a list of `np.ndarray`.
|
| 450 |
+
- `TensorType.TENSORFLOW` or `'tf'`: Return a batch of type `tf.Tensor`.
|
| 451 |
+
- `TensorType.PYTORCH` or `'pt'`: Return a batch of type `torch.Tensor`.
|
| 452 |
+
- `TensorType.NUMPY` or `'np'`: Return a batch of type `np.ndarray`.
|
| 453 |
+
- `TensorType.JAX` or `'jax'`: Return a batch of type `jax.numpy.ndarray`.
|
| 454 |
+
data_format (`ChannelDimension` or `str`, *optional*, defaults to `ChannelDimension.FIRST`):
|
| 455 |
+
The channel dimension format for the output image. Can be one of:
|
| 456 |
+
- `"channels_first"` or `ChannelDimension.FIRST`: image in (num_channels, height, width) format.
|
| 457 |
+
- `"channels_last"` or `ChannelDimension.LAST`: image in (height, width, num_channels) format.
|
| 458 |
+
- Unset: Use the channel dimension format of the input image.
|
| 459 |
+
input_data_format (`ChannelDimension` or `str`, *optional*):
|
| 460 |
+
The channel dimension format for the input image. If unset, the channel dimension format is inferred
|
| 461 |
+
from the input image. Can be one of:
|
| 462 |
+
- `"channels_first"` or `ChannelDimension.FIRST`: image in (num_channels, height, width) format.
|
| 463 |
+
- `"channels_last"` or `ChannelDimension.LAST`: image in (height, width, num_channels) format.
|
| 464 |
+
- `"none"` or `ChannelDimension.NONE`: image in (height, width) format.
|
| 465 |
+
|
| 466 |
+
"""
|
| 467 |
+
do_resize = do_resize if do_resize is not None else self.do_resize
|
| 468 |
+
resample = resample if resample is not None else self.resample
|
| 469 |
+
do_rescale = do_rescale if do_rescale is not None else self.do_rescale
|
| 470 |
+
rescale_factor = rescale_factor if rescale_factor is not None else self.rescale_factor
|
| 471 |
+
do_normalize = do_normalize if do_normalize is not None else self.do_normalize
|
| 472 |
+
image_mean = image_mean if image_mean is not None else self.image_mean
|
| 473 |
+
image_std = image_std if image_std is not None else self.image_std
|
| 474 |
+
merge_size = merge_size if merge_size is not None else self.merge_size
|
| 475 |
+
do_convert_rgb = do_convert_rgb if do_convert_rgb is not None else self.do_convert_rgb
|
| 476 |
+
|
| 477 |
+
clips = make_batched_images(images)
|
| 478 |
+
num_clips = len(clips)
|
| 479 |
+
|
| 480 |
+
if isinstance(merge_size, (list, tuple)):
|
| 481 |
+
assert len(merge_size) == num_clips, (
|
| 482 |
+
f"merge_size length ({len(merge_size)}) must match number of clips ({num_clips})"
|
| 483 |
+
)
|
| 484 |
+
clip_merge_sizes = list(merge_size)
|
| 485 |
+
else:
|
| 486 |
+
clip_merge_sizes = [merge_size] * num_clips
|
| 487 |
+
|
| 488 |
+
if frame_types is None:
|
| 489 |
+
clip_frame_types = [None] * num_clips
|
| 490 |
+
elif isinstance(frame_types, (list, tuple)) and len(frame_types) > 0:
|
| 491 |
+
if isinstance(frame_types[0], (list, tuple)) or frame_types[0] is None:
|
| 492 |
+
assert len(frame_types) == num_clips, (
|
| 493 |
+
f"frame_types length ({len(frame_types)}) must match number of clips ({num_clips})"
|
| 494 |
+
)
|
| 495 |
+
clip_frame_types = list(frame_types)
|
| 496 |
+
else:
|
| 497 |
+
assert num_clips == 1, "Flat frame_types is only supported for a single clip"
|
| 498 |
+
clip_frame_types = [frame_types]
|
| 499 |
+
else:
|
| 500 |
+
clip_frame_types = [None] * num_clips
|
| 501 |
+
|
| 502 |
+
pixel_values, grid_sizes, per_frame_merge_sizes = [], [], []
|
| 503 |
+
|
| 504 |
+
clip_max_tokens_list = self._allocate_token_budget(
|
| 505 |
+
clips, clip_merge_sizes, input_data_format,
|
| 506 |
+
)
|
| 507 |
+
|
| 508 |
+
for clip, ms, ft, clip_max_tokens in zip(clips, clip_merge_sizes, clip_frame_types, clip_max_tokens_list):
|
| 509 |
+
target_sizes = simple_batched_resize(
|
| 510 |
+
clip,
|
| 511 |
+
factor=self.patch_size * ms,
|
| 512 |
+
min_tokens=self.min_tokens,
|
| 513 |
+
max_tokens=clip_max_tokens,
|
| 514 |
+
input_data_format=input_data_format,
|
| 515 |
+
frame_types=ft,
|
| 516 |
+
)
|
| 517 |
+
|
| 518 |
+
for frame, target_size in zip(clip, target_sizes):
|
| 519 |
+
patches, grid_size = self._preprocess(
|
| 520 |
+
frame,
|
| 521 |
+
target_size=target_size,
|
| 522 |
+
merge_size=ms,
|
| 523 |
+
do_resize=do_resize,
|
| 524 |
+
resample=resample,
|
| 525 |
+
do_rescale=do_rescale,
|
| 526 |
+
rescale_factor=rescale_factor,
|
| 527 |
+
do_normalize=do_normalize,
|
| 528 |
+
image_mean=image_mean,
|
| 529 |
+
image_std=image_std,
|
| 530 |
+
data_format=data_format,
|
| 531 |
+
do_convert_rgb=do_convert_rgb,
|
| 532 |
+
input_data_format=input_data_format,
|
| 533 |
+
)
|
| 534 |
+
pixel_values.append(patches)
|
| 535 |
+
grid_sizes.append(grid_size)
|
| 536 |
+
per_frame_merge_sizes.append(ms)
|
| 537 |
+
|
| 538 |
+
pixel_values = np.concatenate(pixel_values, axis=0)
|
| 539 |
+
grid_sizes = np.array(grid_sizes)
|
| 540 |
+
merge_sizes = np.array(per_frame_merge_sizes)
|
| 541 |
+
|
| 542 |
+
data = {
|
| 543 |
+
"pixel_values": pixel_values,
|
| 544 |
+
"grid_sizes": grid_sizes,
|
| 545 |
+
"merge_sizes": merge_sizes,
|
| 546 |
+
}
|
| 547 |
+
|
| 548 |
+
return BatchFeature(data=data, tensor_type=return_tensors)
|
penguin_encoder/modeling_penguinvl_encoder.py
ADDED
|
@@ -0,0 +1,522 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from torch import nn
|
| 2 |
+
import torch
|
| 3 |
+
import math
|
| 4 |
+
import warnings
|
| 5 |
+
from .configuration_penguinvl_encoder import PenguinVLVisionEncoderConfig
|
| 6 |
+
from transformers.modeling_utils import PreTrainedModel
|
| 7 |
+
from transformers.models.qwen3.modeling_qwen3 import Qwen3Model, Qwen3Attention, rotate_half, Qwen3DecoderLayer
|
| 8 |
+
from typing import List, Optional, Tuple, Union
|
| 9 |
+
from transformers.modeling_outputs import BaseModelOutputWithPast
|
| 10 |
+
from transformers.processing_utils import Unpack
|
| 11 |
+
from transformers.modeling_flash_attention_utils import FlashAttentionKwargs
|
| 12 |
+
from transformers.cache_utils import Cache, DynamicCache
|
| 13 |
+
from transformers.utils import logging, is_flash_attn_greater_or_equal_2_10, is_flash_attn_2_available
|
| 14 |
+
from transformers.modeling_rope_utils import ROPE_INIT_FUNCTIONS
|
| 15 |
+
from torch.nn.init import _calculate_fan_in_and_fan_out
|
| 16 |
+
import torch.nn.functional as F
|
| 17 |
+
if is_flash_attn_2_available():
|
| 18 |
+
from transformers.modeling_flash_attention_utils import _flash_attention_forward
|
| 19 |
+
from flash_attn import flash_attn_varlen_func
|
| 20 |
+
|
| 21 |
+
logger = logging.get_logger(__name__)
|
| 22 |
+
|
| 23 |
+
class PenguinVLVisionEncoderEmbeddings(nn.Module):
|
| 24 |
+
|
| 25 |
+
def __init__(self, config: PenguinVLVisionEncoderConfig):
|
| 26 |
+
super().__init__()
|
| 27 |
+
self.config = config
|
| 28 |
+
self.embed_dim = config.hidden_size
|
| 29 |
+
self.patch_size = config.patch_size
|
| 30 |
+
|
| 31 |
+
self.patch_embedding = nn.Conv2d(
|
| 32 |
+
in_channels=config.num_channels,
|
| 33 |
+
out_channels=self.embed_dim,
|
| 34 |
+
kernel_size=self.patch_size,
|
| 35 |
+
stride=self.patch_size,
|
| 36 |
+
padding="valid",
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
|
| 40 |
+
hidden_states = hidden_states.view(
|
| 41 |
+
-1, self.config.num_channels, self.patch_size, self.patch_size
|
| 42 |
+
)
|
| 43 |
+
patch_embeds = self.patch_embedding(hidden_states)
|
| 44 |
+
embeddings = patch_embeds.view(-1, self.embed_dim)
|
| 45 |
+
|
| 46 |
+
return embeddings
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
# Adapted from Qwen2VLRotaryEmbedding in transformers/models/qwen2/modeling_qwen2.py
|
| 50 |
+
class VisualRotaryEmbedding(nn.Module):
|
| 51 |
+
def __init__(
|
| 52 |
+
self,
|
| 53 |
+
dim=None,
|
| 54 |
+
max_position_embeddings=2048,
|
| 55 |
+
base=10000,
|
| 56 |
+
device=None,
|
| 57 |
+
scaling_factor=1.0,
|
| 58 |
+
rope_type="default",
|
| 59 |
+
config = None,
|
| 60 |
+
):
|
| 61 |
+
super().__init__()
|
| 62 |
+
# TODO (joao): remove the `if` below, only used for BC
|
| 63 |
+
self.rope_kwargs = {}
|
| 64 |
+
if config is None:
|
| 65 |
+
logger.warning_once(
|
| 66 |
+
"`Qwen2VLRotaryEmbedding` can now be fully parameterized by passing the model config through the "
|
| 67 |
+
"`config` argument. All other arguments will be removed in v4.46"
|
| 68 |
+
)
|
| 69 |
+
self.rope_kwargs = {
|
| 70 |
+
"rope_type": rope_type,
|
| 71 |
+
"factor": scaling_factor,
|
| 72 |
+
"dim": dim,
|
| 73 |
+
"base": base,
|
| 74 |
+
"max_position_embeddings": max_position_embeddings,
|
| 75 |
+
}
|
| 76 |
+
self.rope_type = rope_type
|
| 77 |
+
self.max_seq_len_cached = max_position_embeddings
|
| 78 |
+
self.original_max_seq_len = max_position_embeddings
|
| 79 |
+
else:
|
| 80 |
+
# BC: "rope_type" was originally "type"
|
| 81 |
+
if config.rope_scaling is not None:
|
| 82 |
+
self.rope_type = config.rope_scaling.get("rope_type", config.rope_scaling.get("type"))
|
| 83 |
+
else:
|
| 84 |
+
self.rope_type = "default"
|
| 85 |
+
self.max_seq_len_cached = config.max_position_embeddings
|
| 86 |
+
self.original_max_seq_len = config.max_position_embeddings
|
| 87 |
+
|
| 88 |
+
self.config = config
|
| 89 |
+
self.rope_init_fn = ROPE_INIT_FUNCTIONS[self.rope_type]
|
| 90 |
+
|
| 91 |
+
inv_freq, self.attention_scaling = self.rope_init_fn(self.config, device, **self.rope_kwargs)
|
| 92 |
+
self.register_buffer("inv_freq", inv_freq, persistent=False)
|
| 93 |
+
self.original_inv_freq = self.inv_freq
|
| 94 |
+
|
| 95 |
+
def _dynamic_frequency_update(self, position_ids, device):
|
| 96 |
+
"""
|
| 97 |
+
dynamic RoPE layers should recompute `inv_freq` in the following situations:
|
| 98 |
+
1 - growing beyond the cached sequence length (allow scaling)
|
| 99 |
+
2 - the current sequence length is in the original scale (avoid losing precision with small sequences)
|
| 100 |
+
"""
|
| 101 |
+
seq_len = torch.max(position_ids) + 1
|
| 102 |
+
if seq_len > self.max_seq_len_cached: # growth
|
| 103 |
+
inv_freq, self.attention_scaling = self.rope_init_fn(
|
| 104 |
+
self.config, device, seq_len=seq_len, **self.rope_kwargs
|
| 105 |
+
)
|
| 106 |
+
self.register_buffer("inv_freq", inv_freq, persistent=False) # TODO joao: may break with compilation
|
| 107 |
+
self.max_seq_len_cached = seq_len
|
| 108 |
+
|
| 109 |
+
if seq_len < self.original_max_seq_len and self.max_seq_len_cached > self.original_max_seq_len: # reset
|
| 110 |
+
self.register_buffer("inv_freq", self.original_inv_freq, persistent=False)
|
| 111 |
+
self.max_seq_len_cached = self.original_max_seq_len
|
| 112 |
+
|
| 113 |
+
@torch.no_grad()
|
| 114 |
+
def forward(self, x, position_ids):
|
| 115 |
+
if "dynamic" in self.rope_type:
|
| 116 |
+
self._dynamic_frequency_update(position_ids, device=x.device)
|
| 117 |
+
|
| 118 |
+
inv_freq_expanded = self.inv_freq[None, None, :, None].float().expand(2, position_ids.shape[1], -1, 1)
|
| 119 |
+
position_ids_expanded = position_ids[:, :, None, :].float() # shape (2, bs, 1, positions)
|
| 120 |
+
# Force float32 (see https://github.com/huggingface/transformers/pull/29285)
|
| 121 |
+
device_type = x.device.type
|
| 122 |
+
device_type = device_type if isinstance(device_type, str) and device_type != "mps" else "cpu"
|
| 123 |
+
with torch.autocast(device_type=device_type, enabled=False):
|
| 124 |
+
freqs = (inv_freq_expanded.float() @ position_ids_expanded.float()).transpose(2, 3)
|
| 125 |
+
emb = torch.cat((freqs, freqs), dim=-1)
|
| 126 |
+
cos = emb.cos()
|
| 127 |
+
sin = emb.sin()
|
| 128 |
+
|
| 129 |
+
# Advanced RoPE types (e.g. yarn) apply a post-processing scaling factor, equivalent to scaling attention
|
| 130 |
+
cos = cos * self.attention_scaling
|
| 131 |
+
sin = sin * self.attention_scaling
|
| 132 |
+
|
| 133 |
+
return cos.to(dtype=x.dtype), sin.to(dtype=x.dtype)
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def apply_multimodal_rotary_pos_emb(q, k, cos, sin, unsqueeze_dim=1):
|
| 137 |
+
rope_section = [cos.shape[-1] // 2, cos.shape[-1] // 2]
|
| 138 |
+
cos = torch.cat([m[i % 2] for i, m in enumerate(cos.split(rope_section, dim=-1))], dim=-1).unsqueeze(unsqueeze_dim)
|
| 139 |
+
sin = torch.cat([m[i % 2] for i, m in enumerate(sin.split(rope_section, dim=-1))], dim=-1).unsqueeze(unsqueeze_dim)
|
| 140 |
+
|
| 141 |
+
q_embed = (q * cos) + (rotate_half(q) * sin)
|
| 142 |
+
k_embed = (k * cos) + (rotate_half(k) * sin)
|
| 143 |
+
return q_embed, k_embed
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
class PenguinVLAttention(Qwen3Attention):
|
| 147 |
+
"""Multi-headed attention from 'Attention Is All You Need' paper"""
|
| 148 |
+
|
| 149 |
+
# Copied from transformers.models.llama.modeling_llama.LlamaFlashAttention2.__init__
|
| 150 |
+
def __init__(self, *args, **kwargs):
|
| 151 |
+
super().__init__(*args, **kwargs)
|
| 152 |
+
self.is_causal = False
|
| 153 |
+
|
| 154 |
+
def forward(
|
| 155 |
+
self,
|
| 156 |
+
hidden_states: torch.Tensor,
|
| 157 |
+
position_embeddings: Tuple[torch.Tensor, torch.Tensor],
|
| 158 |
+
attention_mask: Optional[torch.Tensor],
|
| 159 |
+
past_key_value: Optional[Cache] = None,
|
| 160 |
+
cache_position: Optional[torch.LongTensor] = None,
|
| 161 |
+
cu_seqlens: Optional[torch.Tensor] = None,
|
| 162 |
+
**kwargs: Unpack[FlashAttentionKwargs],
|
| 163 |
+
) -> Tuple[torch.Tensor, Optional[torch.Tensor], Optional[Tuple[torch.Tensor]]]:
|
| 164 |
+
input_shape = hidden_states.shape[:-1]
|
| 165 |
+
hidden_shape = (*input_shape, -1, self.head_dim)
|
| 166 |
+
|
| 167 |
+
query_states = self.q_norm(self.q_proj(hidden_states).view(hidden_shape)).transpose(1, 2)
|
| 168 |
+
key_states = self.k_norm(self.k_proj(hidden_states).view(hidden_shape)).transpose(1, 2)
|
| 169 |
+
value_states = self.v_proj(hidden_states).view(hidden_shape).transpose(1, 2)
|
| 170 |
+
|
| 171 |
+
cos, sin = position_embeddings
|
| 172 |
+
query_states, key_states = apply_multimodal_rotary_pos_emb(query_states, key_states, cos, sin)
|
| 173 |
+
|
| 174 |
+
if past_key_value is not None:
|
| 175 |
+
# sin and cos are specific to RoPE models; cache_position needed for the static cache
|
| 176 |
+
cache_kwargs = {"sin": sin, "cos": cos, "cache_position": cache_position}
|
| 177 |
+
key_states, value_states = past_key_value.update(key_states, value_states, self.layer_idx, cache_kwargs)
|
| 178 |
+
|
| 179 |
+
# This is before the transpose
|
| 180 |
+
seq_len = query_states.shape[2]
|
| 181 |
+
|
| 182 |
+
# Restore the attention dtype if normalization promoted hidden states to FP32.
|
| 183 |
+
target_dtype = None
|
| 184 |
+
if query_states.dtype == torch.float32:
|
| 185 |
+
if torch.is_autocast_enabled():
|
| 186 |
+
target_dtype = torch.get_autocast_gpu_dtype()
|
| 187 |
+
# Handle the case where the model is quantized
|
| 188 |
+
elif hasattr(self.config, "_pre_quantization_dtype"):
|
| 189 |
+
target_dtype = self.config._pre_quantization_dtype
|
| 190 |
+
else:
|
| 191 |
+
target_dtype = next(layer for layer in self.modules() if isinstance(layer, torch.nn.Linear)).weight.dtype
|
| 192 |
+
|
| 193 |
+
# FA2 always relies on the value set in the module, so remove it if present in kwargs to avoid passing it twice
|
| 194 |
+
kwargs.pop("is_causal", None)
|
| 195 |
+
|
| 196 |
+
# Reashape to the expected shape for Flash Attention
|
| 197 |
+
query_states = query_states.transpose(1, 2).squeeze(0)
|
| 198 |
+
key_states = key_states.transpose(1, 2).squeeze(0)
|
| 199 |
+
value_states = value_states.transpose(1, 2).squeeze(0)
|
| 200 |
+
|
| 201 |
+
max_seqlen = (cu_seqlens[1:] - cu_seqlens[:-1]).max().item()
|
| 202 |
+
attn_output = flash_attn_varlen_func(
|
| 203 |
+
query_states,
|
| 204 |
+
key_states,
|
| 205 |
+
value_states,
|
| 206 |
+
cu_seqlens_q=cu_seqlens,
|
| 207 |
+
cu_seqlens_k=cu_seqlens,
|
| 208 |
+
max_seqlen_q=max_seqlen,
|
| 209 |
+
max_seqlen_k=max_seqlen,
|
| 210 |
+
dropout_p=0.0 if not self.training else self.attention_dropout,
|
| 211 |
+
causal=self.is_causal
|
| 212 |
+
)
|
| 213 |
+
|
| 214 |
+
attn_output = attn_output.reshape(*input_shape, -1).contiguous()
|
| 215 |
+
attn_output = self.o_proj(attn_output)
|
| 216 |
+
return attn_output, None
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
class PenguinVLDecoderLayer(Qwen3DecoderLayer):
|
| 220 |
+
def __init__(self, config: PenguinVLVisionEncoderConfig, layer_idx: int):
|
| 221 |
+
super(PenguinVLDecoderLayer, self).__init__(config, layer_idx)
|
| 222 |
+
self.self_attn = PenguinVLAttention(config, layer_idx)
|
| 223 |
+
|
| 224 |
+
def forward(
|
| 225 |
+
self,
|
| 226 |
+
hidden_states: torch.Tensor,
|
| 227 |
+
attention_mask: Optional[torch.Tensor] = None,
|
| 228 |
+
position_ids: Optional[torch.LongTensor] = None,
|
| 229 |
+
past_key_value: Optional[Cache] = None,
|
| 230 |
+
output_attentions: Optional[bool] = False,
|
| 231 |
+
use_cache: Optional[bool] = False,
|
| 232 |
+
cache_position: Optional[torch.LongTensor] = None,
|
| 233 |
+
position_embeddings: Optional[Tuple[torch.Tensor, torch.Tensor]] = None, # necessary, but kept here for BC
|
| 234 |
+
cu_seqlens: Optional[torch.Tensor] = None,
|
| 235 |
+
**kwargs: Unpack[FlashAttentionKwargs],
|
| 236 |
+
) -> Tuple[torch.FloatTensor, Optional[Tuple[torch.FloatTensor, torch.FloatTensor]]]:
|
| 237 |
+
residual = hidden_states
|
| 238 |
+
|
| 239 |
+
hidden_states = self.input_layernorm(hidden_states)
|
| 240 |
+
|
| 241 |
+
# Self Attention
|
| 242 |
+
hidden_states, self_attn_weights = self.self_attn(
|
| 243 |
+
hidden_states=hidden_states,
|
| 244 |
+
attention_mask=attention_mask,
|
| 245 |
+
position_ids=position_ids,
|
| 246 |
+
past_key_value=past_key_value,
|
| 247 |
+
output_attentions=output_attentions,
|
| 248 |
+
use_cache=use_cache,
|
| 249 |
+
cache_position=cache_position,
|
| 250 |
+
position_embeddings=position_embeddings,
|
| 251 |
+
cu_seqlens=cu_seqlens,
|
| 252 |
+
**kwargs,
|
| 253 |
+
)
|
| 254 |
+
hidden_states = residual + hidden_states
|
| 255 |
+
|
| 256 |
+
# Fully Connected
|
| 257 |
+
residual = hidden_states
|
| 258 |
+
hidden_states = self.post_attention_layernorm(hidden_states)
|
| 259 |
+
hidden_states = self.mlp(hidden_states)
|
| 260 |
+
hidden_states = residual + hidden_states
|
| 261 |
+
|
| 262 |
+
outputs = (hidden_states,)
|
| 263 |
+
if output_attentions:
|
| 264 |
+
outputs += (self_attn_weights,)
|
| 265 |
+
|
| 266 |
+
return outputs
|
| 267 |
+
|
| 268 |
+
|
| 269 |
+
class PenguinVLVisionEncoderFromQwen3Model(Qwen3Model):
|
| 270 |
+
def __init__(self, config: PenguinVLVisionEncoderConfig):
|
| 271 |
+
super().__init__(config)
|
| 272 |
+
self.layers = nn.ModuleList(
|
| 273 |
+
[PenguinVLDecoderLayer(config, layer_idx) for layer_idx in range(config.num_hidden_layers)]
|
| 274 |
+
)
|
| 275 |
+
self.rotary_emb = VisualRotaryEmbedding(config=config)
|
| 276 |
+
del self.embed_tokens
|
| 277 |
+
|
| 278 |
+
@staticmethod
|
| 279 |
+
def _prepare_4d_causal_attention_mask_with_cache_position(
|
| 280 |
+
attention_mask: torch.Tensor,
|
| 281 |
+
sequence_length: int,
|
| 282 |
+
target_length: int,
|
| 283 |
+
dtype: torch.dtype,
|
| 284 |
+
device: torch.device,
|
| 285 |
+
cache_position: torch.Tensor,
|
| 286 |
+
batch_size: int,
|
| 287 |
+
config: PenguinVLVisionEncoderConfig,
|
| 288 |
+
past_key_values: Cache,
|
| 289 |
+
):
|
| 290 |
+
"""
|
| 291 |
+
Override the original causal mask method to create full attention mask instead.
|
| 292 |
+
Creates a full attention 4D mask of shape `(batch_size, 1, query_length, key_value_length)`
|
| 293 |
+
from a 2D mask of shape `(batch_size, key_value_length)`.
|
| 294 |
+
|
| 295 |
+
For vision encoding, we want full attention between all patches, not causal attention.
|
| 296 |
+
"""
|
| 297 |
+
if attention_mask is not None and attention_mask.dim() == 4:
|
| 298 |
+
# In this case we assume that the mask comes already in inverted form and requires no inversion or slicing.
|
| 299 |
+
full_attention_mask = attention_mask
|
| 300 |
+
else:
|
| 301 |
+
# Create full attention mask (all zeros, meaning attend to all positions)
|
| 302 |
+
# We only mask based on the provided attention_mask for padding
|
| 303 |
+
if attention_mask is not None:
|
| 304 |
+
# Use the provided attention_mask to handle padding
|
| 305 |
+
min_dtype = torch.finfo(dtype).min
|
| 306 |
+
full_attention_mask = torch.zeros(
|
| 307 |
+
(sequence_length, target_length), dtype=dtype, device=device
|
| 308 |
+
)
|
| 309 |
+
# Expand to 4D
|
| 310 |
+
full_attention_mask = full_attention_mask[None, None, :, :].expand(batch_size, 1, -1, -1)
|
| 311 |
+
|
| 312 |
+
# Apply padding mask if provided
|
| 313 |
+
full_attention_mask = full_attention_mask.clone() # copy to contiguous memory for in-place edit
|
| 314 |
+
if attention_mask.shape[-1] > target_length:
|
| 315 |
+
attention_mask = attention_mask[:, :target_length]
|
| 316 |
+
mask_length = attention_mask.shape[-1]
|
| 317 |
+
padding_mask = attention_mask[:, None, None, :] == 0
|
| 318 |
+
full_attention_mask[:, :, :, :mask_length] = full_attention_mask[:, :, :, :mask_length].masked_fill(
|
| 319 |
+
padding_mask, min_dtype
|
| 320 |
+
)
|
| 321 |
+
else:
|
| 322 |
+
# No attention mask provided, create all-zeros mask (full attention)
|
| 323 |
+
full_attention_mask = torch.zeros(
|
| 324 |
+
(batch_size, 1, sequence_length, target_length), dtype=dtype, device=device
|
| 325 |
+
)
|
| 326 |
+
return full_attention_mask
|
| 327 |
+
|
| 328 |
+
def get_rope_index(self, grid_sizes, merge_sizes, position_ids):
|
| 329 |
+
position_ids = position_ids.contiguous()
|
| 330 |
+
batch_size = grid_sizes.shape[0]
|
| 331 |
+
|
| 332 |
+
# Vision Part: Generate 2D position indices for vision tokens
|
| 333 |
+
vision_pos_ids = []
|
| 334 |
+
for (t, h, w), merge_size in zip(grid_sizes, merge_sizes):
|
| 335 |
+
# Generate height position indices
|
| 336 |
+
hpos_ids = torch.arange(h).unsqueeze(1).expand(-1, w).to(position_ids.device)
|
| 337 |
+
hpos_ids = hpos_ids.reshape(
|
| 338 |
+
h // merge_size,
|
| 339 |
+
merge_size,
|
| 340 |
+
w // merge_size,
|
| 341 |
+
merge_size,
|
| 342 |
+
)
|
| 343 |
+
hpos_ids = hpos_ids.permute(0, 2, 1, 3)
|
| 344 |
+
hpos_ids = hpos_ids.flatten()
|
| 345 |
+
|
| 346 |
+
# Generate width position indices
|
| 347 |
+
wpos_ids = torch.arange(w).unsqueeze(0).expand(h, -1).to(position_ids.device)
|
| 348 |
+
wpos_ids = wpos_ids.reshape(
|
| 349 |
+
h // merge_size,
|
| 350 |
+
merge_size,
|
| 351 |
+
w // merge_size,
|
| 352 |
+
merge_size,
|
| 353 |
+
)
|
| 354 |
+
wpos_ids = wpos_ids.permute(0, 2, 1, 3)
|
| 355 |
+
wpos_ids = wpos_ids.flatten()
|
| 356 |
+
|
| 357 |
+
# Stack height and width to create 2D positions
|
| 358 |
+
vision_pos_ids.append(torch.stack([hpos_ids, wpos_ids], dim=-1).repeat(t, 1))
|
| 359 |
+
|
| 360 |
+
num_start_idx = 0
|
| 361 |
+
for batch_idx in range(batch_size):
|
| 362 |
+
pos_len = vision_pos_ids[batch_idx].shape[0]
|
| 363 |
+
position_ids[:, 0, num_start_idx: num_start_idx+pos_len] = vision_pos_ids[batch_idx].permute(1, 0)
|
| 364 |
+
num_start_idx += pos_len
|
| 365 |
+
|
| 366 |
+
return position_ids
|
| 367 |
+
|
| 368 |
+
|
| 369 |
+
def forward(
|
| 370 |
+
self,
|
| 371 |
+
input_ids: Optional[torch.LongTensor] = None,
|
| 372 |
+
attention_mask: Optional[torch.Tensor] = None,
|
| 373 |
+
position_ids: Optional[torch.LongTensor] = None,
|
| 374 |
+
past_key_values: Optional[Cache] = None,
|
| 375 |
+
inputs_embeds: Optional[torch.FloatTensor] = None,
|
| 376 |
+
use_cache: Optional[bool] = None,
|
| 377 |
+
output_attentions: Optional[bool] = None,
|
| 378 |
+
output_hidden_states: Optional[bool] = None,
|
| 379 |
+
cache_position: Optional[torch.LongTensor] = None,
|
| 380 |
+
grid_sizes: Optional[torch.Tensor] = None,
|
| 381 |
+
merge_sizes: Optional[torch.Tensor] = None,
|
| 382 |
+
**flash_attn_kwargs: Unpack[FlashAttentionKwargs],
|
| 383 |
+
) -> BaseModelOutputWithPast:
|
| 384 |
+
output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions
|
| 385 |
+
output_hidden_states = (
|
| 386 |
+
output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states
|
| 387 |
+
)
|
| 388 |
+
use_cache = use_cache if use_cache is not None else self.config.use_cache
|
| 389 |
+
|
| 390 |
+
if (input_ids is None) ^ (inputs_embeds is not None):
|
| 391 |
+
raise ValueError("You must specify exactly one of input_ids or inputs_embeds")
|
| 392 |
+
|
| 393 |
+
# TODO (joao): remove this exception in v4.56 -- it exists for users that try to pass a legacy cache
|
| 394 |
+
if not isinstance(past_key_values, (type(None), Cache)):
|
| 395 |
+
raise ValueError("The `past_key_values` should be either a `Cache` object or `None`.")
|
| 396 |
+
|
| 397 |
+
if inputs_embeds is None:
|
| 398 |
+
inputs_embeds = self.embed_tokens(input_ids)
|
| 399 |
+
|
| 400 |
+
if use_cache and past_key_values is None:
|
| 401 |
+
past_key_values = DynamicCache()
|
| 402 |
+
|
| 403 |
+
if cache_position is None:
|
| 404 |
+
past_seen_tokens = past_key_values.get_seq_length() if past_key_values is not None else 0
|
| 405 |
+
cache_position = torch.arange(
|
| 406 |
+
past_seen_tokens, past_seen_tokens + inputs_embeds.shape[1], device=inputs_embeds.device
|
| 407 |
+
)
|
| 408 |
+
|
| 409 |
+
# the hard coded `2` is for temporal, height and width.
|
| 410 |
+
if position_ids is None:
|
| 411 |
+
position_ids = cache_position.view(1, 1, -1).expand(2, inputs_embeds.shape[0], -1)
|
| 412 |
+
elif position_ids.dim() == 2:
|
| 413 |
+
position_ids = position_ids[None, ...].expand(2, position_ids.shape[0], -1)
|
| 414 |
+
position_ids = self.get_rope_index(grid_sizes, merge_sizes, position_ids)
|
| 415 |
+
|
| 416 |
+
causal_mask = None
|
| 417 |
+
|
| 418 |
+
hidden_states = inputs_embeds
|
| 419 |
+
|
| 420 |
+
# create position embeddings to be shared across the decoder layers
|
| 421 |
+
position_embeddings = self.rotary_emb(hidden_states, position_ids)
|
| 422 |
+
|
| 423 |
+
# decoder layers
|
| 424 |
+
all_hidden_states = () if output_hidden_states else None
|
| 425 |
+
all_self_attns = () if output_attentions else None
|
| 426 |
+
|
| 427 |
+
# Calculate cumulative sequence lengths for the grid sizes
|
| 428 |
+
cu_seqlens = torch.repeat_interleave(grid_sizes[:, 1] * grid_sizes[:, 2], grid_sizes[:, 0]).cumsum(dim=0, dtype=torch.int32)
|
| 429 |
+
cu_seqlens = F.pad(cu_seqlens, (1, 0), value=0)
|
| 430 |
+
|
| 431 |
+
for decoder_layer in self.layers[: self.config.num_hidden_layers]:
|
| 432 |
+
if output_hidden_states:
|
| 433 |
+
all_hidden_states += (hidden_states,)
|
| 434 |
+
|
| 435 |
+
layer_outputs = decoder_layer(
|
| 436 |
+
hidden_states,
|
| 437 |
+
attention_mask=causal_mask,
|
| 438 |
+
position_ids=position_ids,
|
| 439 |
+
past_key_value=past_key_values,
|
| 440 |
+
output_attentions=output_attentions,
|
| 441 |
+
use_cache=use_cache,
|
| 442 |
+
cache_position=cache_position,
|
| 443 |
+
position_embeddings=position_embeddings,
|
| 444 |
+
cu_seqlens=cu_seqlens,
|
| 445 |
+
**flash_attn_kwargs,
|
| 446 |
+
)
|
| 447 |
+
|
| 448 |
+
hidden_states = layer_outputs[0]
|
| 449 |
+
|
| 450 |
+
if output_attentions:
|
| 451 |
+
all_self_attns += (layer_outputs[1],)
|
| 452 |
+
|
| 453 |
+
hidden_states = self.norm(hidden_states)
|
| 454 |
+
|
| 455 |
+
# add hidden states from the last decoder layer
|
| 456 |
+
if output_hidden_states:
|
| 457 |
+
all_hidden_states += (hidden_states,)
|
| 458 |
+
|
| 459 |
+
return BaseModelOutputWithPast(
|
| 460 |
+
last_hidden_state=hidden_states,
|
| 461 |
+
past_key_values=past_key_values if use_cache else None,
|
| 462 |
+
hidden_states=all_hidden_states,
|
| 463 |
+
attentions=all_self_attns,
|
| 464 |
+
)
|
| 465 |
+
|
| 466 |
+
|
| 467 |
+
class PenguinVLVisionEncoderModel(PreTrainedModel):
|
| 468 |
+
|
| 469 |
+
config_class = PenguinVLVisionEncoderConfig
|
| 470 |
+
base_model_prefix = "penguinvl_vision_encoder"
|
| 471 |
+
main_input_name = "pixel_values"
|
| 472 |
+
_no_split_modules = [
|
| 473 |
+
"PenguinVLVisionEncoderEmbeddings",
|
| 474 |
+
]
|
| 475 |
+
_supports_flash_attn_2 = True
|
| 476 |
+
_supports_sdpa = True
|
| 477 |
+
|
| 478 |
+
def __init__(self, config: PenguinVLVisionEncoderConfig):
|
| 479 |
+
super().__init__(config=config)
|
| 480 |
+
self.embeddings = PenguinVLVisionEncoderEmbeddings(config)
|
| 481 |
+
self.encoder = PenguinVLVisionEncoderFromQwen3Model(config)
|
| 482 |
+
|
| 483 |
+
self.post_init()
|
| 484 |
+
|
| 485 |
+
|
| 486 |
+
def forward(self, pixel_values, grid_sizes, merge_sizes=None) -> torch.Tensor:
|
| 487 |
+
hidden_states = self.embeddings(pixel_values)
|
| 488 |
+
encoder_output = self.encoder(
|
| 489 |
+
inputs_embeds=hidden_states[None, ...],
|
| 490 |
+
grid_sizes=grid_sizes,
|
| 491 |
+
merge_sizes=merge_sizes,
|
| 492 |
+
output_hidden_states=True,
|
| 493 |
+
)
|
| 494 |
+
hidden_states = encoder_output.hidden_states
|
| 495 |
+
hidden_states = hidden_states[-1].squeeze(0)
|
| 496 |
+
|
| 497 |
+
hidden_states_chunks = hidden_states.split(grid_sizes.prod(dim=1).tolist(), dim=0)
|
| 498 |
+
outputs = []
|
| 499 |
+
|
| 500 |
+
for hidden_states, grid_size, merge_size in zip(hidden_states_chunks, grid_sizes, merge_sizes):
|
| 501 |
+
# NOTE: previous implementation, which supports downsampling with any factor
|
| 502 |
+
c = hidden_states.shape[-1]
|
| 503 |
+
hidden_states = hidden_states.view(
|
| 504 |
+
grid_size[0], grid_size[1] // merge_size, grid_size[2] // merge_size, merge_size, merge_size, c
|
| 505 |
+
).permute(0, 1, 3, 2, 4, 5)
|
| 506 |
+
hidden_states = hidden_states.reshape(
|
| 507 |
+
grid_size[0], grid_size[1], grid_size[2], c
|
| 508 |
+
).permute(0, 3, 1, 2)
|
| 509 |
+
hidden_states = torch.nn.functional.interpolate(
|
| 510 |
+
hidden_states,
|
| 511 |
+
size=(grid_size[1] // merge_size, grid_size[2] // merge_size),
|
| 512 |
+
mode='bilinear'
|
| 513 |
+
)
|
| 514 |
+
hidden_states = hidden_states.permute(0, 2, 3, 1).view(-1, c)
|
| 515 |
+
|
| 516 |
+
# NOTE: simplified implementation, which only supports downsampling with integer factor
|
| 517 |
+
# NOTE: this implementation is mathematically equivalent to the previous one when merge_size is 1 or 2 but may cause slightly different results
|
| 518 |
+
# hidden_states = hidden_states.view(-1, merge_size * merge_size, hidden_states.size(-1))
|
| 519 |
+
# hidden_states = hidden_states.mean(dim=1)
|
| 520 |
+
|
| 521 |
+
outputs.append(hidden_states)
|
| 522 |
+
return torch.cat(outputs, dim=0)
|
requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
transformers==5.15.1
|
| 2 |
+
safetensors==0.8.0
|
| 3 |
+
pillow==12.2.0
|
| 4 |
+
pyyaml==6.0.3
|
| 5 |
+
accelerate==1.15.0
|