File size: 29,186 Bytes
0048827
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
"""Opt-in serving optimisations for the Image Hopper release server (cost audit, step 1).

Every option is off by default, so the published path is unchanged unless the server is started
with ``--serving-options``.  Options are either

* **exact**: the served function is bit-identical by construction (same operations in the same
  order, only Python overhead, copies or host transfers removed); or
* **tolerance-bound**: numerically close but not bit-identical.  The cost audit registers the
  shipping rule before measuring: argmax identical on every audit item and max |dp| <= 1e-3.

Options (``OPTIONS`` holds the one-line descriptions):

``lean_lora``      exact.  PEFT's LoRA wrappers are replaced by plain modules that call the
                   original base layer, plus the screen_geometry LoRA term computed with PEFT's
                   exact operations (input cast to the adapter dtype, B(A(x)) * scaling, add,
                   cast back) only while that route is active.  Route change flips one flag.
``lora_bf16``      tolerance.  As lean_lora, LoRA factors held in the base dtype (bf16).
``merged_views``   tolerance.  The screen_geometry route reads a second, merged bf16 copy of each
                   targeted weight (W + s*B@A rounded once); the default route reads W.
``uint8_pixels``   exact.  The processor resizes and patchifies as before but returns uint8
                   patches; they are uploaded from pinned memory and normalised on the device with
                   the processor's own fused mean/std arithmetic (elementwise, so identical).
``gpu_preprocess`` tolerance.  uint8_pixels plus the resize itself on the device.
``device_readout`` exact.  Option rows gathered with a cached device index tensor; one host copy.
``cuda_graphs``    tolerance.  The language model (and the last-position vocabulary row) is
                   replayed from CUDA graphs recorded at padded lengths, one set per route.
                   Right padding after the decision position cannot change a causal model's
                   output there; kernels chosen for the padded shape can round differently.
``sdpa_flash``     tolerance.  Scaled-dot-product attention restricted to the flash kernel.
``compile_text``   tolerance.  ``torch.compile(dynamic=True)`` on the language model.

One parameterised option changes outputs by design (it is neither exact nor tolerance-bound):

``image_token_cap_default=N`` output-changing.  As below, but only for questions whose
                   router-rules/v1 text route is ``default``; ``screen_geometry`` images stay
                   native.  The two caps are alternatives.
``image_token_cap=N`` output-changing.  The native route's processor max pixels drop to
                   ``N * 32 * 32`` so every image keeps at most N merged image tokens
                   (``vision.native_image_tokens``); N in [256, 16384].  Weights, router and
                   calibration are unchanged.  Off (no cap) unless given.
"""

from __future__ import annotations

import contextlib
import dataclasses
import time
from typing import Any, Callable, Iterable, Mapping, Sequence

OPTIONS: Mapping[str, str] = {
    "lean_lora": "exact: plain base layers + route-switched LoRA term with PEFT's operations",
    "lora_bf16": "tolerance: route-switched LoRA term with bf16 factors",
    "merged_views": "tolerance: screen_geometry route reads merged bf16 weight copies",
    "uint8_pixels": "exact: uint8 patches, pinned upload, device normalisation",
    "gpu_preprocess": "tolerance: uint8_pixels plus device resize",
    "device_readout": "exact: cached device index for option rows, one host copy",
    "cuda_graphs": "tolerance: language model replayed from CUDA graphs at padded lengths",
    "sdpa_flash": "tolerance: SDPA restricted to the flash kernel",
    "compile_text": "tolerance: torch.compile(dynamic=True) on the language model",
}
EXACT_OPTIONS = frozenset({"lean_lora", "uint8_pixels", "device_readout"})
IMAGE_TOKEN_CAP = "image_token_cap"
IMAGE_TOKEN_CAP_DEFAULT = "image_token_cap_default"
PARAMETERISED_OPTIONS: Mapping[str, str] = {
    IMAGE_TOKEN_CAP: "output-changing: native-route processor max pixels = N x 32 x 32 (N tokens)",
    IMAGE_TOKEN_CAP_DEFAULT: "output-changing: as image_token_cap, only for questions the text "
                             "rule routes to default (screen_geometry stays native)",
}
ROUTE_MODES = ("lean_lora", "lora_bf16", "merged_views")
# Padded lengths for the recorded graphs: 64-token steps to 1,024, then 128, 256 and 512.
GRAPH_LENGTHS = tuple(list(range(64, 1025, 64)) + list(range(1152, 2049, 128))
                      + list(range(2304, 4097, 256)) + list(range(4608, 8193, 512)))


class ServingOptionError(ValueError):
    """Unknown or conflicting serving options."""


def _parameter(item: str) -> tuple[str, int] | None:
    name, sep, raw = item.partition("=")
    if not sep:
        return None
    name, raw = name.strip(), raw.strip()
    if name not in PARAMETERISED_OPTIONS:
        raise ServingOptionError(f"unknown serving option {name!r}; parameterised options: "
                                 f"{sorted(PARAMETERISED_OPTIONS)}")
    if not raw.isdigit():
        raise ServingOptionError(f"{name} needs a positive integer, got {raw!r}")
    from open_decisions.image_jev import vision

    try:
        return name, vision.validate_image_token_cap(int(raw))
    except ValueError as error:
        raise ServingOptionError(str(error)) from None


def parse_options(value: str | Iterable[str] | None) -> tuple[str, ...]:
    """Validate options; return them in the canonical (``OPTIONS``) order.

    A parameterised option (``image_token_cap=N``) follows the named options, normalised to
    ``name=N``; giving it twice is refused.
    """
    if value is None:
        return ()
    items = [item.strip() for item in value.split(",")] if isinstance(value, str) else list(value)
    items = [item for item in items if item]
    parameters: dict[str, int] = {}
    named = []
    for item in items:
        parsed = _parameter(item)
        if parsed is None:
            named.append(item)
            continue
        name, number = parsed
        if name in parameters and parameters[name] != number:
            raise ServingOptionError(f"{name} given twice ({parameters[name]} and {number})")
        parameters[name] = number
    if len(parameters) > 1:
        raise ServingOptionError("image_token_cap and image_token_cap_default are alternatives")
    items = named
    unknown = sorted(set(items) - set(OPTIONS))
    if unknown:
        raise ServingOptionError(f"unknown serving options {unknown}; known: {sorted(OPTIONS)}"
                                 f" and {sorted(f'{k}=N' for k in PARAMETERISED_OPTIONS)}")
    chosen = set(items)
    modes = [name for name in ROUTE_MODES if name in chosen]
    if "lora_bf16" in chosen and "merged_views" in chosen:
        raise ServingOptionError("lora_bf16 and merged_views are alternative route modes")
    if "compile_text" in chosen and "cuda_graphs" in chosen:
        raise ServingOptionError("compile_text and cuda_graphs are alternatives")
    if "gpu_preprocess" in chosen:
        chosen.add("uint8_pixels")
    if modes and "lean_lora" not in chosen:
        chosen.add("lean_lora")  # lora_bf16/merged_views use the same route modules
    return (tuple(name for name in OPTIONS if name in chosen)
            + tuple(f"{name}={parameters[name]}" for name in PARAMETERISED_OPTIONS
                    if name in parameters))


def image_token_cap(options: Iterable[str], name: str = IMAGE_TOKEN_CAP) -> int | None:
    """The ``image_token_cap=N`` value among parsed options, or None (no cap: the default)."""
    for item in options:
        parsed = _parameter(item)
        if parsed is not None and parsed[0] == name:
            return parsed[1]
    return None


def image_token_cap_default(options: Iterable[str]) -> int | None:
    """The route-conditioned ``image_token_cap_default=N`` value, or None."""
    return image_token_cap(options, IMAGE_TOKEN_CAP_DEFAULT)


def is_exact(options: Iterable[str]) -> bool:
    """True only for options that keep the served function bit-identical (no cap)."""
    return set(options) <= EXACT_OPTIONS


def route_mode(options: Iterable[str]) -> str | None:
    chosen = set(options)
    if "merged_views" in chosen:
        return "merged"
    if "lora_bf16" in chosen:
        return "bf16"
    if "lean_lora" in chosen:
        return "exact"
    return None


# ---------------------------------------------------------------------------------- route modules
class RouteState:
    """One shared flag: True while the screen_geometry route is served."""

    __slots__ = ("on",)

    def __init__(self, on: bool = True):
        self.on = bool(on)


def _route_linear_class():
    import torch
    from torch import nn
    from torch.nn import functional as F

    class RouteLinear(nn.Module):
        """A LoRA-targeted linear layer with the adapter switched by ``RouteState``.

        ``exact`` replicates PEFT's vanilla LoRA forward operation by operation:
        ``result = base(x); x = x.to(A.dtype); result = result + B(A(x)) * scaling;
        result.to(result_dtype)``.  Off-route it calls the original base layer only.
        """

        def __init__(self, base, lora_a, lora_b, scaling: float, state: RouteState, mode: str):
            super().__init__()
            if mode not in ("exact", "bf16", "merged"):
                raise ValueError("unknown route mode")
            self.base = base
            self.state = state
            self.mode = mode
            self.scaling = float(scaling)
            weight = base.weight
            if mode == "merged":
                delta = (lora_b.detach().float() @ lora_a.detach().float()) * self.scaling
                merged = (weight.detach().float() + delta.to(weight.device)).to(weight.dtype)
                self.register_buffer("merged_weight", merged, persistent=False)
                self.lora_a = self.lora_b = None
            else:
                dtype = weight.dtype if mode == "bf16" else lora_a.dtype
                self.register_buffer("lora_a", lora_a.detach().to(dtype), persistent=False)
                self.register_buffer("lora_b", lora_b.detach().to(dtype), persistent=False)

        @property
        def weight(self):  # modules that inspect .weight keep working
            return self.base.weight

        def forward(self, x):
            if not self.state.on:
                return self.base(x)
            if self.mode == "merged":
                return F.linear(x, self.merged_weight, self.base.bias)
            result = self.base(x)
            result_dtype = result.dtype
            if x.dtype != self.lora_a.dtype:
                x = x.to(dtype=self.lora_a.dtype)
            result = result + F.linear(F.linear(x, self.lora_a), self.lora_b) * self.scaling
            return result.to(result_dtype)

    return RouteLinear, torch


def _lora_layers(root) -> list[tuple[str, Any]]:
    return [(name, module) for name, module in root.named_modules()
            if hasattr(module, "base_layer") and hasattr(module, "lora_A")
            and hasattr(module, "lora_B") and hasattr(module, "scaling")]


def install_route_modules(peft_model, *, adapter_name: str, mode: str):
    """Replace PEFT LoRA wrappers by ``RouteLinear``; return (plain model, state, record)."""
    from torch import nn

    RouteLinear, torch = _route_linear_class()
    root = peft_model.get_base_model() if hasattr(peft_model, "get_base_model") else peft_model
    state = RouteState(True)
    layers = _lora_layers(root)
    if not layers:
        raise ServingOptionError("no LoRA layers found to replace")
    extra_bytes = 0
    for name, module in layers:
        if not isinstance(module.base_layer, nn.Linear):
            raise ServingOptionError(f"{name}: LoRA on a non-linear layer is not supported")
        if adapter_name not in module.lora_A or adapter_name not in module.lora_B:
            raise ServingOptionError(f"{name}: adapter {adapter_name!r} missing")
        if adapter_name in getattr(module, "lora_variant", {}) or getattr(module, "merged", False):
            raise ServingOptionError(f"{name}: DoRA/variant or merged LoRA is not supported")
        if getattr(module, "fan_in_fan_out", False):
            raise ServingOptionError(f"{name}: fan_in_fan_out is not supported")
        dropout = module.lora_dropout[adapter_name]
        if not isinstance(dropout, nn.Identity) and getattr(dropout, "p", 0.0) != 0.0:
            raise ServingOptionError(f"{name}: LoRA dropout must be zero at serving")
        lora_a, lora_b = module.lora_A[adapter_name], module.lora_B[adapter_name]
        if getattr(lora_a, "bias", None) is not None or getattr(lora_b, "bias", None) is not None:
            raise ServingOptionError(f"{name}: LoRA bias is not supported")
        replacement = RouteLinear(module.base_layer, lora_a.weight, lora_b.weight,
                                  module.scaling[adapter_name], state, mode)
        if mode == "merged":
            extra_bytes += replacement.merged_weight.numel() * replacement.merged_weight.element_size()
        parent_name, _, child = name.rpartition(".")
        parent = root.get_submodule(parent_name) if parent_name else root
        setattr(parent, child, replacement)
    del peft_model
    if torch.cuda.is_available():
        torch.cuda.empty_cache()
    record = {"route_mode": mode, "replaced_layers": len(layers),
              "merged_view_bytes": extra_bytes}
    return root.eval(), state, record


class RouteSwitch:
    """Adapter context for ``RouteLinear`` models: one flag assignment per route change."""

    def __init__(self, state: RouteState):
        self.state = state

    def __call__(self, route):
        from open_decisions.image_jev import router

        if route not in router.ROUTES:
            raise ValueError("unknown adapter route")
        self.state.on = route == router.SCREEN_GEOMETRY
        return contextlib.nullcontext()


# ------------------------------------------------------------------------------------ processor
class PixelProcessor:
    """Processor proxy returning device-normalised pixels from uint8 patches.

    The image processor normalises before patchifying; normalisation is per channel and
    elementwise and patchify is a permutation plus temporal duplication, so normalising the
    uint8 patches with the same fused mean/std tensors gives identical float32 values.
    """

    def __init__(self, processor, device, *, gpu_resize: bool = False):
        object.__setattr__(self, "_inner", processor)
        object.__setattr__(self, "_device", device)
        object.__setattr__(self, "_gpu_resize", bool(gpu_resize))
        object.__setattr__(self, "_norm", None)

    def __getattr__(self, name):
        return getattr(self._inner, name)

    def __setattr__(self, name, value):
        setattr(self._inner, name, value)

    def _mean_std(self, device):
        import torch

        if self._norm is None or self._norm[0] != device:
            ip = self._inner.image_processor
            mean = torch.tensor(ip.image_mean, device=device) * (1.0 / ip.rescale_factor)
            std = torch.tensor(ip.image_std, device=device) * (1.0 / ip.rescale_factor)
            per = int(ip.temporal_patch_size) * int(ip.patch_size) * int(ip.patch_size)
            object.__setattr__(self, "_norm", (device, mean.to(torch.float32).repeat_interleave(per),
                                               std.to(torch.float32).repeat_interleave(per)))
        return self._norm[1], self._norm[2]

    def normalize(self, patches):
        import torch

        ip = self._inner.image_processor
        if not (getattr(ip, "do_rescale", True) and getattr(ip, "do_normalize", True)):
            raise RuntimeError("uint8_pixels expects the processor's default rescale+normalize")
        mean, std = self._mean_std(patches.device)
        return patches.to(dtype=torch.float32).sub_(mean).div_(std)

    def __call__(self, *args, **kwargs):
        import torch

        if kwargs.get("images") is None and len(args) < 2:
            return self._inner(*args, **kwargs)
        images_kwargs = dict(kwargs.pop("images_kwargs", None) or {})
        images_kwargs.update(do_rescale=False, do_normalize=False)
        if self._gpu_resize and self._device is not None:
            images_kwargs["device"] = self._device
        output = self._inner(*args, images_kwargs=images_kwargs, **kwargs)
        pixels = output["pixel_values"]
        if pixels.dtype != torch.uint8:
            raise RuntimeError(f"expected uint8 patches, got {pixels.dtype}")
        device = self._device
        if device is not None and str(device).startswith("cuda") and pixels.device.type == "cpu":
            pixels = pixels.pin_memory().to(device, non_blocking=True)
        elif device is not None and pixels.device != torch.device(device):
            pixels = pixels.to(device)
        output["pixel_values"] = self.normalize(pixels)
        return output


# --------------------------------------------------------------------------------------- readout
class DeviceReadout:
    """``vision.option_probs`` with a cached device index (same operations after the gather)."""

    def __init__(self):
        self._index: dict[tuple, Any] = {}

    def __call__(self, logits, letter_token_ids, temperature: float) -> list[float]:
        import math

        import torch

        if not math.isfinite(temperature) or temperature <= 0:
            raise ValueError("temperature must be finite and greater than zero")
        key = (str(logits.device), tuple(letter_token_ids))
        index = self._index.get(key)
        if index is None:
            if any(not 0 <= int(i) < logits.numel() for i in letter_token_ids):
                raise ValueError("letter_token_ids must be valid vocabulary indices")
            index = torch.tensor(list(letter_token_ids), dtype=torch.long, device=logits.device)
            self._index[key] = index
        selected = logits.index_select(0, index).to(dtype=torch.float32) / temperature
        values = torch.log_softmax(selected, dim=-1).exp().detach().cpu().tolist()
        if not all(math.isfinite(value) for value in values):
            raise ValueError("option probabilities are non-finite")
        return values


# ---------------------------------------------------------------------------------------- graphs
def hf_model(model):
    return model.get_base_model() if hasattr(model, "get_base_model") else model


def _move(value, device):
    from open_decisions.image_jev.speed import move_to_device

    return move_to_device(value, device)


def embeds_positions(model, prepared, device):
    """The multimodal forward up to the language model (the model's own methods, same order)."""
    import torch

    top = hf_model(model)
    core = top.model
    ids = _move(prepared.input_ids, device)
    pixels = _move(prepared.pixel_values, device)
    grid = _move(prepared.image_grid_thw, device)
    mm = getattr(prepared, "mm_token_type_ids", None)
    mm = None if mm is None else _move(mm, device)
    embeds = core.get_input_embeddings()(ids)
    if pixels is not None:
        features = core.get_image_features(pixels, grid, return_dict=True).pooler_output
        features = torch.cat(features, dim=0).to(embeds.device, embeds.dtype)
        mask, _ = core.get_placeholder_mask(ids, inputs_embeds=embeds, image_features=features)
        embeds = embeds.masked_scatter(mask, features)
    positions = core.compute_3d_position_ids(
        input_ids=ids, inputs_embeds=embeds, image_grid_thw=grid, video_grid_thw=None,
        attention_mask=None, past_key_values=None, mm_token_type_ids=mm)
    return embeds, positions


def pad_positions(positions, size: int):
    """Right-pad (3, 1, n) position ids to ``size`` by continuing each axis from its last value."""
    import torch

    n = positions.shape[-1]
    if size < n:
        raise ValueError("padded size is shorter than the sequence")
    if size == n:
        return positions
    tail = positions[..., -1:] + torch.arange(1, size - n + 1, device=positions.device,
                                              dtype=positions.dtype)
    return torch.cat((positions, tail), dim=-1)


def eager_last_logits(model, embeds, positions):
    top = hf_model(model)
    hidden = top.model.language_model(input_ids=None, position_ids=positions, attention_mask=None,
                                      past_key_values=None, inputs_embeds=embeds,
                                      use_cache=False).last_hidden_state
    return top.lm_head(hidden[:, -1:, :])[0, 0]


class DecoderGraphs:
    """CUDA graphs of language model + last-row vocabulary projection at padded lengths."""

    def __init__(self, model, *, lengths: Sequence[int], routes: Sequence[Any],
                 set_route: Callable[[Any], Any] | None, device, log=print):
        import torch

        top = hf_model(model)
        self.model = model
        lm, head = top.model.language_model, top.lm_head
        hidden = int(top.config.text_config.hidden_size)
        dtype = next(lm.parameters()).dtype
        self.graphs: dict[tuple[Any, int], tuple] = {}
        self.lengths = sorted(set(int(n) for n in lengths))
        started = time.monotonic()
        pool = None
        with torch.inference_mode():
            for route in routes:
                if set_route is not None:
                    set_route(route)
                for n in sorted(self.lengths, reverse=True):  # longest first; shorter reuse pool
                    embeds = torch.zeros(1, n, hidden, device=device, dtype=dtype)
                    positions = torch.arange(n, device=device).view(1, 1, n).expand(3, 1, n).contiguous()
                    last = torch.zeros(1, dtype=torch.long, device=device)

                    def step(embeds=embeds, positions=positions, last=last):
                        state = lm(input_ids=None, position_ids=positions, attention_mask=None,
                                   past_key_values=None, inputs_embeds=embeds,
                                   use_cache=False).last_hidden_state
                        return head(state[0].index_select(0, last))[0]

                    side = torch.cuda.Stream()
                    side.wait_stream(torch.cuda.current_stream())
                    with torch.cuda.stream(side):
                        for _ in range(3):
                            step()
                    torch.cuda.current_stream().wait_stream(side)
                    graph = torch.cuda.CUDAGraph()
                    with torch.cuda.graph(graph, pool=pool):
                        out = step()
                    pool = graph.pool()
                    self.graphs[(route, n)] = (graph, embeds, positions, last, out)
        self.capture_seconds = time.monotonic() - started
        self.routes = list(routes)
        log(f"captured {len(self.graphs)} graphs in {self.capture_seconds:.1f} s")

    def fits(self, n: int) -> bool:
        return bool(self.lengths) and n <= self.lengths[-1]

    def run(self, route, embeds, positions):
        n = embeds.shape[1]
        size = next(x for x in self.lengths if x >= n)
        graph, static_embeds, static_positions, last, out = self.graphs[(route, size)]
        static_embeds.zero_()
        static_embeds[:, :n].copy_(embeds)
        static_positions.copy_(pad_positions(positions, size))
        last.fill_(n - 1)
        graph.replay()
        return out.clone()


class GraphLogits:
    """``logits_fn`` for the Predictor: eager multimodal front, graph-replayed language model."""

    def __init__(self, graphs: DecoderGraphs | None, device, *, route_key: Callable[[Any], Any]):
        self.graphs, self.device, self.route_key = graphs, device, route_key
        self.replayed = self.eager = 0

    def __call__(self, model, prepared, *, timer, route=None):
        from open_decisions.image_jev.speed import PhaseTimer

        timer = timer or PhaseTimer()
        with timer.phase("forward"):
            embeds, positions = embeds_positions(model, prepared, self.device)
            n = embeds.shape[1]
            if self.graphs is not None and self.graphs.fits(n):
                self.replayed += 1
                return self.graphs.run(self.route_key(route), embeds, positions)
            self.eager += 1
            return eager_last_logits(model, embeds, positions)


# ------------------------------------------------------------------------------------------ apply
@dataclasses.dataclass
class Serving:
    model: Any
    processor: Any
    adapter_context: Callable | None
    inference_context: Callable[[], Any] | None
    logits_fn: Callable | None
    readout_fn: Callable | None
    record: dict
    image_token_cap: int | None = None
    image_token_cap_default: int | None = None


def _sdpa_flash_context():
    from torch.nn.attention import SDPBackend, sdpa_kernel

    return sdpa_kernel([SDPBackend.FLASH_ATTENTION])


def apply_serving_options(model, processor, options: Sequence[str], *, device, routed: bool,
                          adapter_name: str | None, adapter_context: Callable | None,
                          inference_context: Callable[[], Any] | None,
                          graph_lengths: Sequence[int] = GRAPH_LENGTHS, log=print) -> Serving:
    """Apply validated ``options`` to a loaded (PEFT or plain) model and its processor."""
    import torch

    options = parse_options(options)
    record: dict[str, Any] = {"serving_options": list(options), "exact": is_exact(options)}
    cap = image_token_cap(options)
    if cap is not None:
        from open_decisions.image_jev import vision

        record["image_token_cap"] = cap
        record["native_max_pixels"] = vision.native_max_pixels(cap)
    cap_default = image_token_cap_default(options)
    if cap_default is not None:
        from open_decisions.image_jev import vision

        record["image_token_cap_default"] = cap_default
        record["native_max_pixels_default_route"] = vision.native_max_pixels(cap_default)
    started = time.monotonic()
    mode = route_mode(options)
    if mode is not None:
        if not routed or adapter_name is None:
            raise ServingOptionError("route modes need the routed single-adapter system")
        model, state, route_record = install_route_modules(model, adapter_name=adapter_name,
                                                           mode=mode)
        adapter_context = RouteSwitch(state)
        record.update(route_record)
    if "uint8_pixels" in options:
        processor = PixelProcessor(processor, device, gpu_resize="gpu_preprocess" in options)
    readout_fn = DeviceReadout() if "device_readout" in options else None
    if "sdpa_flash" in options:
        base_context = inference_context

        @contextlib.contextmanager
        def flash_context():
            with (base_context() if base_context is not None else contextlib.nullcontext()):
                with _sdpa_flash_context():
                    yield

        inference_context = flash_context
    if "compile_text" in options:
        core = hf_model(model).model
        core.language_model = torch.compile(core.language_model, dynamic=True)
        record["compile"] = "torch.compile(language_model, dynamic=True)"
    logits_fn = None
    if "cuda_graphs" in options:
        if not str(device).startswith("cuda"):
            raise ServingOptionError("cuda_graphs needs a CUDA device")
        from open_decisions.image_jev import router

        routes = list(router.ROUTES) if routed else [None]
        set_route = adapter_context if routed else None
        context = inference_context() if inference_context is not None else contextlib.nullcontext()
        with context:
            graphs = DecoderGraphs(model, lengths=graph_lengths, routes=routes,
                                   set_route=set_route, device=device, log=log)
        logits_fn = GraphLogits(graphs, device, route_key=(lambda route: route) if routed
                                else (lambda route: None))
        record.update({"graph_count": len(graphs.graphs), "graph_lengths": list(graphs.lengths),
                       "graph_capture_seconds": round(graphs.capture_seconds, 2)})
    record["apply_seconds"] = round(time.monotonic() - started, 2)
    return Serving(model=model, processor=processor, adapter_context=adapter_context,
                   inference_context=inference_context, logits_fn=logits_fn,
                   readout_fn=readout_fn, record=record, image_token_cap=cap,
                   image_token_cap_default=cap_default)


__all__ = [
    "DecoderGraphs", "DeviceReadout", "EXACT_OPTIONS", "GRAPH_LENGTHS", "GraphLogits",
    "IMAGE_TOKEN_CAP", "IMAGE_TOKEN_CAP_DEFAULT", "OPTIONS", "PARAMETERISED_OPTIONS",
    "image_token_cap", "image_token_cap_default",
    "PixelProcessor", "RouteState", "RouteSwitch", "Serving", "ServingOptionError",
    "apply_serving_options", "install_route_modules", "is_exact", "pad_positions",
    "parse_options", "route_mode",
]