File size: 19,403 Bytes
c33608b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
"""Real multimodal patchification and packing into one Dendro token space."""

from __future__ import annotations

from dataclasses import dataclass
from typing import Any

import torch
from torch.nn import functional as F

from ._source_bound import SourceBoundModule
from .configuration_dendro_omni import DendroOmniConfig
from .source import DendroSourceLayer
from .spatial import (
    MODALITY_AUDIO,
    MODALITY_IMAGE,
    MODALITY_POSITION_OFFSETS,
    MODALITY_SENSOR,
    MODALITY_TEXT,
    MODALITY_VIDEO,
    DendroSpatialEncoder,
)


@dataclass(slots=True)
class ModalitySegment:
    name: str
    modality_id: int
    start: int
    end: int
    shape: tuple[int, ...]

    @property
    def length(self) -> int:
        return self.end - self.start


@dataclass(slots=True)
class DendroModalityLayout:
    modality_ids: torch.Tensor
    sequence_positions: torch.Tensor
    logical_positions: torch.Tensor
    coordinates: torch.Tensor
    is_prefix: torch.Tensor
    attention_mask: torch.Tensor
    segments: tuple[ModalitySegment, ...]
    text_start: int
    text_length: int

    def to_dict(self) -> dict[str, Any]:
        return {
            "segments": [
                {
                    "name": segment.name,
                    "modality_id": segment.modality_id,
                    "start": segment.start,
                    "end": segment.end,
                    "length": segment.length,
                    "shape": segment.shape,
                }
                for segment in self.segments
            ],
            "text_start": self.text_start,
            "text_length": self.text_length,
            "total_length": int(self.modality_ids.shape[-1]),
            "prefix_length": int(self.is_prefix[0].sum().item()) if self.is_prefix.numel() else 0,
        }


@dataclass(slots=True)
class DendroPackedInput:
    hidden_states: torch.Tensor
    layout: DendroModalityLayout
    aligned_labels: torch.Tensor | None = None


class DendroOmniInputProjector(SourceBoundModule):
    """Parameterless modality adapters backed entirely by ``DendroSourceLayer``."""

    def __init__(self, config: DendroOmniConfig, source: DendroSourceLayer) -> None:
        super().__init__(source)
        self.config = config
        self.spatial_encoder = DendroSpatialEncoder(config, source)

    @staticmethod
    def _batch_size(*values: torch.Tensor | None) -> int:
        batches = [int(value.shape[0]) for value in values if value is not None]
        if not batches:
            raise ValueError("At least one text, image, audio, video, or sensor input is required")
        if any(batch != batches[0] for batch in batches):
            raise ValueError(f"All modalities must share a batch size, got {batches}")
        return batches[0]

    @staticmethod
    def _normalize_coords(index: torch.Tensor, maximum: int) -> torch.Tensor:
        if maximum <= 1:
            return torch.zeros_like(index, dtype=torch.float32)
        return index.float() / float(maximum - 1) * 2.0 - 1.0

    def _token_features(self, input_ids: torch.Tensor, token_hidden: torch.Tensor) -> torch.Tensor:
        source = self.source
        offset = self.config.byte_offset
        byte = input_ids - offset
        atom_ids = torch.zeros_like(input_ids)
        atom_ids = torch.where((byte >= ord("0")) & (byte <= ord("9")), 1, atom_ids)
        atom_ids = torch.where(
            ((byte >= ord("A")) & (byte <= ord("Z"))) | ((byte >= ord("a")) & (byte <= ord("z"))),
            2,
            atom_ids,
        )
        atom_ids = torch.where((byte == 9) | (byte == 10) | (byte == 13) | (byte == 32), 3, atom_ids)
        atom_ids = torch.where((byte >= 128) & (byte <= 255), 4, atom_ids)
        atom_ids = torch.where(input_ids < offset, 5, atom_ids)
        atoms = source.embedding(atom_ids, "token/atoms", 6, self.config.hidden_size)

        # Atom -> bond -> molecule composition is deliberately pointwise here. Any
        # cross-token neighborhood operation belongs inside cache-aware attention;
        # otherwise a one-token decode chunk would not match full-sequence training.
        bond_input = token_hidden * torch.tanh(atoms)
        bonds = source.project(bond_input, "token/bonds", self.config.hidden_size, low_bit=False)
        molecule_input = F.silu(bonds) + 0.5 * token_hidden + 0.25 * atoms
        molecules = source.project(molecule_input, "token/molecules", self.config.hidden_size, low_bit=False)
        gate = source.gate(torch.cat([token_hidden, atoms], dim=-1), "token/compose_gate", self.config.hidden_size)
        return token_hidden + 0.15 * atoms + 0.10 * gate * bonds + 0.10 * (1.0 - gate) * molecules

    def _text(
        self,
        input_ids: torch.Tensor | None,
        inputs_embeds: torch.Tensor | None,
        attention_mask: torch.Tensor | None,
        *,
        position_start: int,
        prefix_mask: torch.Tensor | None,
    ) -> tuple[torch.Tensor, dict[str, torch.Tensor], tuple[int, ...]] | None:
        if input_ids is None and inputs_embeds is None:
            return None
        if input_ids is not None and inputs_embeds is not None:
            raise ValueError("Pass input_ids or inputs_embeds, not both")
        if inputs_embeds is None:
            assert input_ids is not None
            hidden = self.source.embedding(
                input_ids,
                "token",
                self.config.vocab_size,
                self.config.hidden_size,
            )
            hidden = self._token_features(input_ids, hidden)
        else:
            if inputs_embeds.shape[-1] != self.config.hidden_size:
                raise ValueError("inputs_embeds last dimension must equal hidden_size")
            hidden = inputs_embeds
        batch, length = hidden.shape[:2]
        device = hidden.device
        positions = torch.arange(position_start, position_start + length, device=device).expand(batch, -1)
        logical = positions + MODALITY_POSITION_OFFSETS[MODALITY_TEXT]
        coords = torch.zeros(batch, length, 4, device=device, dtype=hidden.dtype)
        # Absolute coordinates must be invariant to chunking.  Normalizing by the
        # current input length made a token receive different spatial features during
        # full-sequence training and cached one-token decoding.
        denominator = max(1, self.config.max_position_embeddings - 1)
        coords[..., 0] = (positions.to(hidden.dtype) / denominator * 2.0 - 1.0).clamp(-1.0, 1.0)
        modality = torch.full((batch, length), MODALITY_TEXT, device=device, dtype=torch.long)
        is_prefix = (
            prefix_mask.to(device=device, dtype=torch.bool)
            if prefix_mask is not None
            else torch.zeros(batch, length, device=device, dtype=torch.bool)
        )
        mask = (
            attention_mask.to(device=device, dtype=torch.bool)
            if attention_mask is not None
            else torch.ones(batch, length, device=device, dtype=torch.bool)
        )
        return hidden, {
            "modality": modality,
            "positions": positions,
            "logical": logical,
            "coords": coords,
            "prefix": is_prefix,
            "mask": mask,
        }, (length,)

    def _image(self, pixel_values: torch.Tensor | None) -> tuple[torch.Tensor, dict[str, torch.Tensor], tuple[int, ...]] | None:
        if pixel_values is None:
            return None
        if pixel_values.ndim != 4:
            raise ValueError("pixel_values must be [batch, channels, height, width]")
        batch, channels, height, width = pixel_values.shape
        if channels != self.config.image_channels:
            raise ValueError(f"Expected {self.config.image_channels} image channels, got {channels}")
        patch = self.config.image_patch_size
        pad_h, pad_w = (-height) % patch, (-width) % patch
        values = F.pad(pixel_values, (0, pad_w, 0, pad_h))
        grid_h, grid_w = values.shape[-2] // patch, values.shape[-1] // patch
        patches = F.unfold(values, kernel_size=patch, stride=patch).transpose(1, 2)
        hidden = self.source.project(patches, "modality/image_patch", self.config.hidden_size)
        length = hidden.shape[1]
        y = torch.arange(grid_h, device=hidden.device).repeat_interleave(grid_w)
        x = torch.arange(grid_w, device=hidden.device).repeat(grid_h)
        coords = torch.zeros(batch, length, 4, device=hidden.device, dtype=hidden.dtype)
        coords[..., 1] = self._normalize_coords(y, grid_h)
        coords[..., 2] = self._normalize_coords(x, grid_w)
        local = torch.arange(length, device=hidden.device).expand(batch, -1)
        return hidden, self._metadata(local, coords, MODALITY_IMAGE, prefix=True), (grid_h, grid_w)

    def _audio(self, audio_values: torch.Tensor | None) -> tuple[torch.Tensor, dict[str, torch.Tensor], tuple[int, ...]] | None:
        if audio_values is None:
            return None
        if audio_values.ndim == 3:
            audio_values = audio_values.mean(dim=1)
        if audio_values.ndim != 2:
            raise ValueError("audio_values must be [batch, samples] or [batch, channels, samples]")
        patch, stride = self.config.audio_patch_size, self.config.audio_patch_stride
        if audio_values.shape[-1] < patch:
            audio_values = F.pad(audio_values, (0, patch - audio_values.shape[-1]))
        remainder = (audio_values.shape[-1] - patch) % stride
        if remainder:
            audio_values = F.pad(audio_values, (0, stride - remainder))
        windows = audio_values.unfold(-1, patch, stride)
        hidden = self.source.project(windows, "modality/audio_patch", self.config.hidden_size)
        length = hidden.shape[1]
        local = torch.arange(length, device=hidden.device).expand(hidden.shape[0], -1)
        coords = torch.zeros(hidden.shape[0], length, 4, device=hidden.device, dtype=hidden.dtype)
        coords[..., 0] = self._normalize_coords(torch.arange(length, device=hidden.device), length)
        # Frequency-energy coordinate gives raw wave patches a useful second axis.
        spectrum = torch.fft.rfft(windows.float(), dim=-1).abs().mean(dim=-1)
        spectrum = spectrum / spectrum.amax(dim=-1, keepdim=True).clamp_min(1e-8)
        coords[..., 3] = spectrum.to(hidden.dtype) * 2.0 - 1.0
        return hidden, self._metadata(local, coords, MODALITY_AUDIO, prefix=True), (length, patch)

    def _video(self, video_values: torch.Tensor | None) -> tuple[torch.Tensor, dict[str, torch.Tensor], tuple[int, ...]] | None:
        if video_values is None:
            return None
        if video_values.ndim != 5:
            raise ValueError("video_values must be [batch, frames, channels, height, width]")
        batch, frames, channels, height, width = video_values.shape
        if channels != self.config.image_channels:
            raise ValueError(f"Expected {self.config.image_channels} video channels, got {channels}")
        tube, patch = self.config.video_tubelet_size, self.config.video_patch_size
        pad_t, pad_h, pad_w = (-frames) % tube, (-height) % patch, (-width) % patch
        # F.pad follows reverse dimension order for [B,T,C,H,W].
        values = F.pad(video_values, (0, pad_w, 0, pad_h, 0, 0, 0, pad_t))
        tg, hg, wg = values.shape[1] // tube, values.shape[3] // patch, values.shape[4] // patch
        blocks = values.reshape(batch, tg, tube, channels, hg, patch, wg, patch)
        blocks = blocks.permute(0, 1, 4, 6, 2, 3, 5, 7).reshape(batch, tg * hg * wg, -1)
        hidden = self.source.project(blocks, "modality/video_tubelet", self.config.hidden_size)
        t = torch.arange(tg, device=hidden.device).repeat_interleave(hg * wg)
        y = torch.arange(hg, device=hidden.device).repeat_interleave(wg).repeat(tg)
        x = torch.arange(wg, device=hidden.device).repeat(hg * tg)
        coords = torch.zeros(batch, hidden.shape[1], 4, device=hidden.device, dtype=hidden.dtype)
        coords[..., 0] = self._normalize_coords(t, tg)
        coords[..., 1] = self._normalize_coords(y, hg)
        coords[..., 2] = self._normalize_coords(x, wg)
        local = torch.arange(hidden.shape[1], device=hidden.device).expand(batch, -1)
        return hidden, self._metadata(local, coords, MODALITY_VIDEO, prefix=True), (tg, hg, wg)

    def _sensor(self, sensor_values: torch.Tensor | None) -> tuple[torch.Tensor, dict[str, torch.Tensor], tuple[int, ...]] | None:
        if sensor_values is None:
            return None
        if sensor_values.ndim == 2:
            sensor_values = sensor_values.unsqueeze(1)
        if sensor_values.ndim != 3:
            raise ValueError("sensor_values must be [batch, steps, features] or [batch, features]")
        target = self.config.sensor_feature_size
        if sensor_values.shape[-1] < target:
            sensor_values = F.pad(sensor_values, (0, target - sensor_values.shape[-1]))
        elif sensor_values.shape[-1] > target:
            sensor_values = sensor_values[..., :target]
        hidden = self.source.project(sensor_values, "modality/sensor", self.config.hidden_size)
        length = hidden.shape[1]
        local = torch.arange(length, device=hidden.device).expand(hidden.shape[0], -1)
        coords = torch.zeros(hidden.shape[0], length, 4, device=hidden.device, dtype=hidden.dtype)
        coords[..., 0] = self._normalize_coords(torch.arange(length, device=hidden.device), length)
        coords[..., 3] = sensor_values.float().std(dim=-1).to(hidden.dtype).clamp(max=1.0) * 2.0 - 1.0
        return hidden, self._metadata(local, coords, MODALITY_SENSOR, prefix=True), (length, target)

    @staticmethod
    def _metadata(
        local: torch.Tensor,
        coords: torch.Tensor,
        modality_id: int,
        *,
        prefix: bool,
    ) -> dict[str, torch.Tensor]:
        batch, length = local.shape
        device = local.device
        return {
            "modality": torch.full((batch, length), modality_id, device=device, dtype=torch.long),
            "positions": local,
            "logical": local + MODALITY_POSITION_OFFSETS[modality_id],
            "coords": coords,
            "prefix": torch.full((batch, length), prefix, device=device, dtype=torch.bool),
            "mask": torch.ones(batch, length, device=device, dtype=torch.bool),
        }

    def forward(
        self,
        *,
        input_ids: torch.Tensor | None = None,
        inputs_embeds: torch.Tensor | None = None,
        attention_mask: torch.Tensor | None = None,
        pixel_values: torch.Tensor | None = None,
        audio_values: torch.Tensor | None = None,
        video_values: torch.Tensor | None = None,
        sensor_values: torch.Tensor | None = None,
        prefix_mask: torch.Tensor | None = None,
        labels: torch.Tensor | None = None,
        position_start: int = 0,
    ) -> DendroPackedInput:
        self._batch_size(input_ids, inputs_embeds, pixel_values, audio_values, video_values, sensor_values)
        # Non-text modalities form a bidirectional perceptual prefix. Text remains
        # last so causal decoding can append tokens without repacking old inputs.
        parts = [
            ("image", MODALITY_IMAGE, self._image(pixel_values)),
            ("video", MODALITY_VIDEO, self._video(video_values)),
            ("audio", MODALITY_AUDIO, self._audio(audio_values)),
            ("sensor", MODALITY_SENSOR, self._sensor(sensor_values)),
            (
                "text",
                MODALITY_TEXT,
                self._text(
                    input_ids,
                    inputs_embeds,
                    attention_mask,
                    position_start=position_start,
                    prefix_mask=prefix_mask,
                ),
            ),
        ]
        hidden_parts: list[torch.Tensor] = []
        metadata: dict[str, list[torch.Tensor]] = {
            "modality": [],
            "positions": [],
            "logical": [],
            "coords": [],
            "prefix": [],
            "mask": [],
        }
        segments: list[ModalitySegment] = []
        cursor = 0
        text_start, text_length = 0, 0
        for name, modality_id, result in parts:
            if result is None:
                continue
            hidden, info, original_shape = result
            length = int(hidden.shape[1])
            # Physical sequence positions are contiguous across the packed sequence.
            physical = torch.arange(cursor + position_start, cursor + position_start + length, device=hidden.device)
            info["positions"] = physical.expand(hidden.shape[0], -1)
            # Logical modality offsets are applied to the same absolute physical
            # positions so a token keeps identical coordinates when a multimodal
            # prefix is processed in one call or reused through KV cache.
            info["logical"] = info["positions"] + MODALITY_POSITION_OFFSETS[modality_id]
            if modality_id == MODALITY_TEXT:
                denominator = max(1, self.config.max_position_embeddings - 1)
                info["coords"][..., 0] = (
                    info["positions"].to(hidden.dtype) / denominator * 2.0 - 1.0
                ).clamp(-1.0, 1.0)
            hidden_parts.append(hidden)
            for key in metadata:
                metadata[key].append(info[key])
            segments.append(ModalitySegment(name, modality_id, cursor, cursor + length, original_shape))
            if modality_id == MODALITY_TEXT:
                text_start, text_length = cursor, length
            cursor += length
        if not hidden_parts:
            raise RuntimeError("No modality generated tokens")

        hidden = torch.cat(hidden_parts, dim=1)
        combined = {key: torch.cat(values, dim=1) for key, values in metadata.items()}
        hidden = self.spatial_encoder(
            hidden,
            modality_ids=combined["modality"],
            sequence_positions=combined["positions"],
            logical_positions=combined["logical"],
            coordinates=combined["coords"],
            is_prefix=combined["prefix"],
        )
        layout = DendroModalityLayout(
            modality_ids=combined["modality"],
            sequence_positions=combined["positions"],
            logical_positions=combined["logical"],
            coordinates=combined["coords"],
            is_prefix=combined["prefix"],
            attention_mask=combined["mask"],
            segments=tuple(segments),
            text_start=text_start,
            text_length=text_length,
        )

        aligned_labels = labels
        if labels is not None:
            if labels.shape[0] != hidden.shape[0]:
                raise ValueError("labels batch size does not match inputs")
            if labels.shape[1] == text_length and text_start > 0:
                prefix_labels = torch.full(
                    (labels.shape[0], text_start),
                    -100,
                    device=labels.device,
                    dtype=labels.dtype,
                )
                aligned_labels = torch.cat([prefix_labels, labels], dim=1)
            elif labels.shape[1] != hidden.shape[1]:
                raise ValueError(
                    f"labels length {labels.shape[1]} must equal text length {text_length} "
                    f"or packed length {hidden.shape[1]}"
                )
        return DendroPackedInput(hidden_states=hidden, layout=layout, aligned_labels=aligned_labels)