File size: 42,447 Bytes
0acbed8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
"""HF-native TimeBraid model with directly registered language and TS components."""

from __future__ import annotations

from collections.abc import Iterable, Mapping, Sequence
from dataclasses import dataclass, fields
from typing import TYPE_CHECKING, Any, Optional, Union

import torch
import torch.nn as nn
from transformers import (
    AutoConfig,
    AutoModelForCausalLM,
    PretrainedConfig,
    PreTrainedModel,
)
from transformers.cache_utils import Cache
from transformers.modeling_outputs import CausalLMOutputWithPast
from transformers.modeling_utils import get_parameter_dtype
from transformers.utils import ModelOutput

from .mot.bridge_runtime import initialize_timebraid_components
from .mot.bridge_runtime_mot import sync_gradient_checkpointing_from_decoder_layers
from .mot.config_contract import (
    MOT_HF_CONFIG_CONTRACT_FIELDS,
    collect_mot_hf_config_contract,
)
from .mot.generation_route import (
    GenerationRoute,
    require_mixed_generation_inputs,
    resolve_generation_route,
)
from .mot.kv_cache import MoTDynamicCache
from .mot.model import (
    NormalizedMoTGenerationControls,
    TimeBraidGenerateOutput,
    TimeBraidPayload,
    merge_timebraid_losses,
    normalize_mot_generation_controls,
    run_timebraid_decoder,
    run_timebraid_generate,
    validate_mot_generation_input_ids,
)
from .mot.structures import IGNORE_INDEX, TIMEBRAID_TS_PAYLOAD_FIELDS

if TYPE_CHECKING:  # pragma: no cover - import cycle only matters at runtime
    from ..processing_timebraid import TimeBraidBatchFeature

_FINITE_SCAN_CHUNK_ELEMENTS = 8 * 1024 * 1024
_NON_INFERENCE_CONFIG_FIELDS = (
    "mot_timesfm_model_name_or_path",
    "mot_ts_roi_mse_alpha",
    "mot_ts_loss_weight",
    "mot_lm_loss_weight",
    "mot_ts_understanding_loss_weight",
)
_FORBIDDEN_CONFIG_FIELDS = _NON_INFERENCE_CONFIG_FIELDS
_MOT_GENERATION_CONTROL_FIELDS = (
    "mot_target_horizons",
    "mot_forecast_head_len",
    "mot_target_total_lengths",
    "mot_target_history_span_idxs",
    "mot_return_forecast_quantiles",
)


@dataclass
class TimeBraidLossBreakdown(ModelOutput):
    """Fixed scalar loss accounting returned beside the differentiable loss."""

    loss: Optional[torch.Tensor] = None
    lm_loss: Optional[torch.Tensor] = None
    ts_aux_loss: Optional[torch.Tensor] = None
    ts_aux_point_loss: Optional[torch.Tensor] = None
    ts_aux_quantile_loss: Optional[torch.Tensor] = None
    ts_aux_global_point_loss: Optional[torch.Tensor] = None
    ts_aux_global_quantile_loss: Optional[torch.Tensor] = None
    ts_aux_global_uncapped_loss: Optional[torch.Tensor] = None
    ts_aux_global_cap_scale: Optional[torch.Tensor] = None
    ts_aux_roi_loss: Optional[torch.Tensor] = None
    ts_aux_roi_point_loss: Optional[torch.Tensor] = None
    ts_aux_roi_weighted_point_loss: Optional[torch.Tensor] = None
    ts_aux_roi_uncapped_loss: Optional[torch.Tensor] = None
    ts_aux_roi_weighted_uncapped_loss: Optional[torch.Tensor] = None
    ts_aux_roi_cap_scale: Optional[torch.Tensor] = None
    ts_aux_roi_supervised_points: Optional[torch.Tensor] = None
    ts_aux_roi_supervised_targets: Optional[torch.Tensor] = None
    ts_aux_roi_alpha: Optional[torch.Tensor] = None
    ts_aux_understanding_loss: Optional[torch.Tensor] = None
    ts_aux_understanding_weighted_loss: Optional[torch.Tensor] = None
    ts_aux_understanding_time_loss: Optional[torch.Tensor] = None
    ts_aux_understanding_fft_loss: Optional[torch.Tensor] = None
    ts_aux_understanding_patches: Optional[torch.Tensor] = None
    ts_aux_understanding_points: Optional[torch.Tensor] = None
    ts_aux_understanding_weight: Optional[torch.Tensor] = None


@dataclass
class TimeBraidOutput(CausalLMOutputWithPast):
    """Stable causal-LM output for every dictionary-returning TimeBraid forward."""

    token_accuracy: Optional[torch.Tensor] = None
    predicted_tokens: Optional[torch.Tensor] = None
    loss_breakdown: Optional[TimeBraidLossBreakdown] = None


def _present_forbidden_config_fields(config: object) -> list[str]:
    if isinstance(config, Mapping):
        return sorted(field for field in _FORBIDDEN_CONFIG_FIELDS if field in config)
    return sorted(field for field in _FORBIDDEN_CONFIG_FIELDS if hasattr(config, field))


def _validate_finite_named_tensors(
    named_tensors: Iterable[tuple[str, torch.Tensor]],
    *,
    owner: str,
) -> None:
    for name, tensor in named_tensors:
        if not tensor.is_floating_point() and not tensor.is_complex():
            continue
        flat = tensor.detach().contiguous().view(-1)
        for start in range(0, int(flat.numel()), _FINITE_SCAN_CHUNK_ELEMENTS):
            chunk = flat[start : start + _FINITE_SCAN_CHUNK_ELEMENTS]
            finite_mask = torch.isfinite(chunk)
            if bool(torch.all(finite_mask)):
                continue
            chunk_index = int(
                torch.nonzero(torch.logical_not(finite_mask), as_tuple=False)[0]
                .detach()
                .cpu()
                .item()
            )
            raise RuntimeError(
                f"{owner} contains a non-finite tensor value: "
                f"name={name!r}, flat_index={start + chunk_index}, dtype={tensor.dtype}."
            )


def _validate_finite_model_tensors(model: nn.Module, *, owner: str) -> None:
    """Reject non-finite floating or complex model state before use."""
    _validate_finite_named_tensors(
        (*model.named_parameters(), *model.named_buffers()),
        owner=owner,
    )


def _runtime_options_from_timebraid_config(config: "TimeBraidConfig") -> dict[str, Any]:
    """Build direct-component initialization options from checkpoint metadata.

    A contract field's name *is* its runtime option name, so this copies
    rather than translates. The hand-maintained rename table it replaced was
    what allowed one knob to carry two spellings and drift apart.
    """
    contract = collect_mot_hf_config_contract(config)
    options: dict[str, Any] = {
        name: contract[name] for name in MOT_HF_CONFIG_CONTRACT_FIELDS
    }
    # Never restored from a checkpoint: inference always runs with gradient
    # checkpointing off.
    options["mot_ts_gradient_checkpointing"] = False
    options["mot_ts_gradient_checkpointing_use_reentrant"] = False
    compute_dtype = getattr(config, "dtype", None) or getattr(
        config.llm_config, "dtype", None
    )
    if compute_dtype is not None:
        options["mot_compute_dtype"] = compute_dtype
    return options


_LLM_MIRROR_FIELDS = (
    "vocab_size",
    "hidden_size",
    "intermediate_size",
    "num_hidden_layers",
    "num_attention_heads",
    "num_key_value_heads",
    "head_dim",
    "max_position_embeddings",
    "tie_word_embeddings",
    "dtype",
    "bos_token_id",
    "eos_token_id",
    "pad_token_id",
    "use_cache",
    "sliding_window",
    "layer_types",
    "_attn_implementation",
    "_attn_implementation_internal",
)
_LLM_SERIALIZED_MIRROR_FIELDS = tuple(
    field for field in _LLM_MIRROR_FIELDS if not field.startswith("_")
)


class TimeBraidConfig(PretrainedConfig):
    r"""Composite config for TimeBraid's language and time-series components."""

    model_type = "timebraid"
    is_composition = True
    has_no_defaults_at_init = True
    sub_configs = {"llm_config": AutoConfig}

    def __init__(
        self,
        llm_config: Optional[PretrainedConfig | dict[str, Any]] = None,
        **kwargs: Any,
    ) -> None:
        if llm_config is None:
            raise ValueError("TimeBraidConfig requires `llm_config`.")
        forbidden_fields = _present_forbidden_config_fields(kwargs)
        if forbidden_fields:
            raise ValueError(
                "TimeBraidConfig does not restore training-only or external "
                f"TimesFM fields: {forbidden_fields}."
            )
        nested_forbidden_fields = _present_forbidden_config_fields(llm_config)
        if nested_forbidden_fields:
            raise ValueError(
                "TimeBraidConfig llm_config does not restore training-only or "
                f"external TimesFM fields: {nested_forbidden_fields}."
            )

        if isinstance(llm_config, PretrainedConfig):
            resolved_llm_config = llm_config
        else:
            llm_config_dict = dict(llm_config)
            llm_model_type = llm_config_dict.pop("model_type", None)
            if llm_model_type is None:
                raise ValueError(
                    "TimeBraidConfig `llm_config` must include `model_type`."
                )
            resolved_llm_config = AutoConfig.for_model(
                llm_model_type, **llm_config_dict
            )

        outer_mirrors = sorted(
            field for field in _LLM_SERIALIZED_MIRROR_FIELDS if field in kwargs
        )
        if outer_mirrors:
            raise ValueError(
                "TimeBraidConfig stores LLM fields only in llm_config; remove outer fields "
                f"{outer_mirrors}."
            )
        kwargs.setdefault("architectures", ["TimeBraid"])
        # PretrainedConfig applies dtype and attention overrides during its
        # initializer. Register the nested config first so those composite
        # controls reach Qwen instead of remaining only on the outer wrapper.
        self.llm_config = resolved_llm_config
        super().__init__(**kwargs)
        self._sync_llm_surface()

    @classmethod
    def from_llm_config(
        cls, llm_config: PretrainedConfig, **kwargs: Any
    ) -> "TimeBraidConfig":
        return cls(llm_config=llm_config, **kwargs)

    def _sync_llm_surface(self) -> None:
        """Derive the non-serialized wrapper surface generic HF helpers consume."""
        for field in _LLM_MIRROR_FIELDS:
            if hasattr(self.llm_config, field):
                setattr(self, field, getattr(self.llm_config, field))

    def to_dict(self) -> dict[str, Any]:
        """Serialize only inference restore state, never training objectives."""
        payload = super().to_dict()
        forbidden_fields = _present_forbidden_config_fields(payload)
        if forbidden_fields:
            raise RuntimeError(
                "TimeBraidConfig contains forbidden training-only or external "
                f"TimesFM fields: {forbidden_fields}."
            )
        for field in _LLM_SERIALIZED_MIRROR_FIELDS:
            payload.pop(field, None)
        nested = payload.get("llm_config")
        if isinstance(nested, dict):
            nested_forbidden_fields = _present_forbidden_config_fields(nested)
            if nested_forbidden_fields:
                raise RuntimeError(
                    "TimeBraidConfig llm_config contains forbidden training-only, "
                    f"or external TimesFM fields: {nested_forbidden_fields}."
                )
        return payload


def _labels_with_language_supervision(
    labels: Optional[torch.LongTensor],
) -> Optional[torch.LongTensor]:
    if isinstance(labels, torch.Tensor) and (
        labels.numel() == 0 or not bool(torch.any(labels.ne(IGNORE_INDEX)).item())
    ):
        return None
    return labels


def _require_binary_plain_attention_mask(attention_mask: object) -> None:
    """Reject packed segment ids before delegating a plain call to native Qwen."""
    # Rank-4 prepared masks remain part of Qwen's public ABI. TimeBraid packing
    # uses positive segment ids only in the rank-2 mask produced by its
    # processor/collator, so the plain route must accept exactly binary values
    # there instead of letting Qwen collapse every nonzero id to one segment.
    if not isinstance(attention_mask, torch.Tensor) or attention_mask.ndim != 2:
        return
    binary_values = torch.logical_or(attention_mask.eq(0), attention_mask.eq(1))
    if not bool(torch.all(binary_values).item()):
        raise RuntimeError(
            "Plain Qwen fallback requires a binary 2D attention_mask with values "
            "in {0, 1}; packed segment ids require the TimeBraid processor/collator "
            f"payload, got dtype={attention_mask.dtype}, shape={tuple(attention_mask.shape)}."
        )


_TIMEBRAID_KWARG_PREFIXES = ("mot_", "ts_")
_KNOWN_TIMEBRAID_KWARGS = frozenset(
    TIMEBRAID_TS_PAYLOAD_FIELDS + _MOT_GENERATION_CONTROL_FIELDS
)


def _reject_unknown_timebraid_kwargs(generation_kwargs: Mapping[str, object]) -> None:
    """Reject misspelled TimeBraid arguments instead of forwarding them.

    `mot_` and `ts_` are TimeBraid's own argument namespace, so an unrecognized
    name there is a mistake we can name. Unprefixed names belong to Hugging
    Face and must keep flowing through, or this package would break whenever
    transformers adds a generation argument.

    Without this, a typo such as `mot_target_horizon` was silently forwarded,
    the request routed to plain text, and the caller received prose with no
    forecast and no error.
    """
    unknown = sorted(
        name
        for name in generation_kwargs
        if name.startswith(_TIMEBRAID_KWARG_PREFIXES)
        and name not in _KNOWN_TIMEBRAID_KWARGS
    )
    if unknown:
        raise TypeError(
            f"Unknown TimeBraid generation argument(s) {unknown}. Supported "
            f"names are {sorted(_KNOWN_TIMEBRAID_KWARGS)}."
        )


def _fixed_loss_breakdown(
    breakdown: Optional[Mapping[str, torch.Tensor]],
) -> Optional[TimeBraidLossBreakdown]:
    if breakdown is None:
        return None
    reference = next(
        (value for value in breakdown.values() if isinstance(value, torch.Tensor)),
        None,
    )
    if reference is None:
        raise RuntimeError("TimeBraid loss accounting returned no tensor values.")
    zero = reference.detach().new_zeros(())
    values: dict[str, torch.Tensor] = {}
    for output_field in fields(TimeBraidLossBreakdown):
        value = breakdown.get(output_field.name)
        if value is None:
            value = zero.clone()
        if not isinstance(value, torch.Tensor) or value.numel() != 1:
            raise RuntimeError(
                "TimeBraid loss accounting values must be scalar tensors: "
                f"field={output_field.name!r}, value={value!r}."
            )
        values[output_field.name] = value.detach().reshape(())
    return TimeBraidLossBreakdown(**values)


def _as_timebraid_output(
    outputs: object,
    breakdown: Optional[Mapping[str, torch.Tensor]],
) -> TimeBraidOutput:
    return TimeBraidOutput(
        loss=getattr(outputs, "loss", None),
        logits=getattr(outputs, "logits", None),
        past_key_values=getattr(outputs, "past_key_values", None),
        hidden_states=getattr(outputs, "hidden_states", None),
        attentions=getattr(outputs, "attentions", None),
        token_accuracy=getattr(outputs, "token_accuracy", None),
        predicted_tokens=getattr(outputs, "predicted_tokens", None),
        loss_breakdown=_fixed_loss_breakdown(breakdown),
    )


def _build_lm_output(
    *,
    llm: PreTrainedModel,
    hidden_states: torch.Tensor,
    past_key_values: object,
    decoder_hidden_states: object,
    decoder_attentions: object,
    labels: Optional[torch.LongTensor],
    labels_for_loss: Optional[torch.LongTensor],
    shift_labels: Optional[torch.LongTensor],
    logits_to_keep: Union[int, torch.Tensor],
    skip_logits: Optional[bool],
    loss_kwargs: Mapping[str, object],
) -> TimeBraidOutput:
    """Project decoder hidden states and own the complete language-loss contract."""
    if skip_logits is not None and type(skip_logits) is not bool:
        raise TypeError(
            f"TimeBraid forward skip_logits must be a bool or None, got {skip_logits!r}."
        )

    # `skip_logits=True` only ever meant "the fused Liger loss will consume the
    # hidden states instead of logits". That path was unreachable — the flag
    # enabling it was hardcoded False with no setter — so it is gone, and the
    # request is refused rather than silently ignored.
    if skip_logits:
        raise RuntimeError(
            "skip_logits=True is not supported: TimeBraid always projects logits "
            "through the language head."
        )

    slice_indices = (
        slice(-logits_to_keep, None)
        if isinstance(logits_to_keep, int)
        else logits_to_keep
    )
    kept_hidden_states = hidden_states[:, slice_indices, :]
    has_effective_supervision = labels_for_loss is not None or shift_labels is not None
    loss = None
    # Only the removed fused-loss path produced these; the language head does
    # not, so they stay None and keep TimeBraidOutput's shape stable.
    token_accuracy = None
    predicted_tokens = None
    logits = llm.lm_head(kept_hidden_states)
    if has_effective_supervision:
        loss = llm.loss_function(
            logits=logits,
            labels=labels_for_loss,
            shift_labels=shift_labels,
            vocab_size=llm.config.vocab_size,
            **loss_kwargs,
        )

    return TimeBraidOutput(
        loss=loss,
        logits=logits,
        past_key_values=past_key_values,
        hidden_states=decoder_hidden_states,
        attentions=decoder_attentions,
        token_accuracy=token_accuracy,
        predicted_tokens=predicted_tokens,
    )


class TimeBraid(PreTrainedModel):
    r"""Top-level model with direct language, time-series, and attention components."""

    # This is the complete trainable/inference component graph. Keeping the
    # ownership list on the public model makes optional topology readable
    # without inspecting the numeric execution helpers.
    llm: PreTrainedModel
    generation_tsfm: nn.Module
    understanding_tsfm: Optional[nn.Module]
    global_residual_attention: nn.ModuleDict
    packed_attention: nn.Module
    understanding_head: Optional[nn.Module]

    config_class = TimeBraidConfig
    base_model_prefix = "llm"
    main_input_name = "input_ids"
    _supports_flash_attn_2 = True
    _supports_sdpa = True
    supports_gradient_checkpointing = True
    _tied_weights_keys = {"llm.lm_head.weight": "llm.model.embed_tokens.weight"}

    def __init__(
        self,
        config: TimeBraidConfig,
        llm: Optional[PreTrainedModel] = None,
        runtime_options: Optional[dict[str, Any]] = None,
    ) -> None:
        if not isinstance(config, TimeBraidConfig):
            raise ValueError(
                f"TimeBraid requires TimeBraidConfig, got {type(config).__name__}."
            )
        super().__init__(config)
        self.llm = (
            llm
            if llm is not None
            else AutoModelForCausalLM.from_config(config.llm_config)
        )
        self.config.llm_config = self.llm.config
        self.config._sync_llm_surface()
        self.generation_config = self.llm.generation_config

        resolved_runtime_options = _runtime_options_from_timebraid_config(config)
        if runtime_options:
            resolved_runtime_options.update(runtime_options)
        initialize_timebraid_components(
            self,
            self.llm.model.config,
            resolved_runtime_options,
        )
        # Optional training integration may enable the fused hidden-to-loss tail
        # after construction; that tail does not replace either Qwen forward method.
        self.generation_tsfm.eval()
        if self.understanding_tsfm is not None:
            self.understanding_tsfm.eval()
        self._sync_hf_loading_contracts()

    def _sync_hf_loading_contracts(self) -> None:
        """Synchronize tied-weight and parallelism metadata without generic post-init."""
        self._tied_weights_keys = {
            "llm.lm_head.weight": "llm.model.embed_tokens.weight"
        }
        self.all_tied_weights_keys = dict(self._tied_weights_keys)
        tied_keys = getattr(self.llm, "all_tied_weights_keys", None)
        if tied_keys:
            self.all_tied_weights_keys.update(
                {f"llm.{key}": f"llm.{value}" for key, value in tied_keys.items()}
            )
        # Transformers expects composite wrappers to expose concrete empty
        # parallelism plans during distributed `from_pretrained()` allocator
        # warmup, so mirror the child plans here.
        for plan_attr in ("_tp_plan", "_ep_plan", "_pp_plan"):
            merged_plan = {}
            child_plan = getattr(self.llm, plan_attr, None)
            if child_plan:
                merged_plan.update(
                    {f"llm.{key}": value for key, value in child_plan.copy().items()}
                )
            setattr(self, plan_attr, merged_plan)

    def train(self, mode: bool = True) -> "TimeBraid":
        super().train(mode)
        # TimesFM towers stay in eval mode during full-parameter training. Module
        # mode controls stochastic behavior independently of `requires_grad`.
        self.generation_tsfm.eval()
        if self.understanding_tsfm is not None:
            self.understanding_tsfm.eval()
        return self

    def gradient_checkpointing_enable(
        self, gradient_checkpointing_kwargs: Optional[dict[str, Any]] = None
    ) -> None:
        super().gradient_checkpointing_enable(
            gradient_checkpointing_kwargs=gradient_checkpointing_kwargs
        )
        sync_gradient_checkpointing_from_decoder_layers(self, self.llm.model.layers)

    def gradient_checkpointing_disable(self) -> None:
        super().gradient_checkpointing_disable()
        sync_gradient_checkpointing_from_decoder_layers(self, self.llm.model.layers)

    def forward(
        self,
        input_ids: Optional[torch.LongTensor] = None,
        attention_mask: Optional[torch.Tensor] = None,
        position_ids: Optional[torch.LongTensor] = None,
        past_key_values: Optional[Cache] = None,
        inputs_embeds: Optional[torch.FloatTensor] = None,
        labels: Optional[torch.LongTensor] = None,
        use_cache: Optional[bool] = None,
        cache_position: Optional[torch.LongTensor] = None,
        logits_to_keep: Union[int, torch.Tensor] = 0,
        output_attentions: Optional[bool] = None,
        output_hidden_states: Optional[bool] = None,
        return_dict: Optional[bool] = None,
        skip_logits: Optional[bool] = None,
        shift_labels: Optional[torch.LongTensor] = None,
        ts_values: object = None,
        ts_lengths: object = None,
        ts_loss_start_idxs: object = None,
        ts_loss_roi_masks: object = None,
        ts_roles: object = None,
        ts_segment_ids: object = None,
        ts_span_mask: object = None,
        ts_text_start_token_idxs: object = None,
        ts_text_end_token_idxs: object = None,
        active_rows: Optional[torch.Tensor] = None,
        num_items_in_batch: Optional[object] = None,
        mot_ts_num_records_by_horizon: Optional[object] = None,
        mot_ts_num_roi_targets_by_horizon: Optional[object] = None,
        mot_ts_understanding_num_points: Optional[object] = None,
        mot_ts_understanding_num_fft_elements: Optional[object] = None,
        **kwargs: Any,
    ) -> TimeBraidOutput | tuple[Any, ...]:
        # Generation-only controls can accompany processor batches, but never
        # participate in the causal forward computation.
        for key in _MOT_GENERATION_CONTROL_FIELDS:
            kwargs.pop(key, None)

        payload = TimeBraidPayload(
            ts_values=ts_values,
            ts_lengths=ts_lengths,
            ts_loss_start_idxs=ts_loss_start_idxs,
            ts_loss_roi_masks=ts_loss_roi_masks,
            ts_roles=ts_roles,
            ts_segment_ids=ts_segment_ids,
            ts_span_mask=ts_span_mask,
            ts_text_start_token_idxs=ts_text_start_token_idxs,
            ts_text_end_token_idxs=ts_text_end_token_idxs,
        )
        use_timebraid_decoder = (
            isinstance(past_key_values, MoTDynamicCache) or payload.has_runtime_inputs()
        )
        if not use_timebraid_decoder and isinstance(input_ids, torch.Tensor):
            use_timebraid_decoder = bool(
                torch.logical_or(
                    input_ids.eq(int(self.ts_open_token_id)),
                    input_ids.eq(int(self.ts_close_token_id)),
                )
                .any()
                .item()
            )
        if active_rows is not None and not isinstance(past_key_values, MoTDynamicCache):
            raise RuntimeError("active_rows requires an existing MoTDynamicCache.")

        if return_dict is None:
            resolved_return_dict = bool(self.llm.config.use_return_dict)
        elif type(return_dict) is bool:
            resolved_return_dict = return_dict
        else:
            raise TypeError(
                f"TimeBraid forward return_dict must be a bool or None, got {return_dict!r}."
            )
        labels_for_loss = _labels_with_language_supervision(labels)
        if not use_timebraid_decoder:
            _require_binary_plain_attention_mask(attention_mask)
            if skip_logits is not None and type(skip_logits) is not bool:
                raise TypeError(
                    "TimeBraid forward skip_logits must be a bool or None, got "
                    f"{skip_logits!r}."
                )
            use_outer_lm_head = shift_labels is not None and labels_for_loss is None
            if skip_logits and not use_outer_lm_head:
                raise ValueError(
                    "skip_logits=True requires fused TimeBraid language loss."
                )
            plain_loss_kwargs = dict(kwargs)
            if num_items_in_batch is not None:
                plain_loss_kwargs["num_items_in_batch"] = num_items_in_batch
            plain_forward_kwargs = dict(plain_loss_kwargs)
            if shift_labels is not None:
                plain_forward_kwargs["shift_labels"] = shift_labels
            if use_outer_lm_head:
                # Run Qwen's ordinary decoder, then let TimeBraid consume its
                # hidden states. This outer tail exists for explicit
                # shift-label-only loss, where Qwen cannot build the labels.
                decoder_outputs = self.llm.model(
                    input_ids=input_ids,
                    attention_mask=attention_mask,
                    position_ids=position_ids,
                    past_key_values=past_key_values,
                    inputs_embeds=inputs_embeds,
                    use_cache=use_cache,
                    cache_position=cache_position,
                    output_attentions=output_attentions,
                    output_hidden_states=output_hidden_states,
                    return_dict=True,
                    **kwargs,
                )
                outputs = _build_lm_output(
                    llm=self.llm,
                    hidden_states=decoder_outputs.last_hidden_state,
                    past_key_values=decoder_outputs.past_key_values,
                    decoder_hidden_states=decoder_outputs.hidden_states,
                    decoder_attentions=decoder_outputs.attentions,
                    labels=labels,
                    labels_for_loss=labels_for_loss,
                    shift_labels=shift_labels,
                    logits_to_keep=logits_to_keep,
                    skip_logits=skip_logits,
                    loss_kwargs=plain_loss_kwargs,
                )
            else:
                outputs = self.llm(
                    input_ids=input_ids,
                    attention_mask=attention_mask,
                    position_ids=position_ids,
                    past_key_values=past_key_values,
                    inputs_embeds=inputs_embeds,
                    labels=labels_for_loss,
                    use_cache=use_cache,
                    cache_position=cache_position,
                    logits_to_keep=logits_to_keep,
                    output_attentions=output_attentions,
                    output_hidden_states=output_hidden_states,
                    return_dict=True,
                    **plain_forward_kwargs,
                )
            outputs, breakdown = merge_timebraid_losses(
                model=self,
                mot_runtime=None,
                outputs=outputs,
            )
            timebraid_output = _as_timebraid_output(outputs, breakdown)
            if resolved_return_dict:
                return timebraid_output
            # Preserve Qwen's tuple prefix and ordering. Logits keeps its slot
            # when fused execution leaves it as None, followed by optional decoder
            # outputs; TimeBraid's token statistics append afterward.
            tuple_output = (timebraid_output.logits,)
            tuple_output += tuple(
                value
                for value in (
                    timebraid_output.past_key_values,
                    timebraid_output.hidden_states,
                    timebraid_output.attentions,
                )
                if value is not None
            )
            if timebraid_output.loss is not None:
                tuple_output = (timebraid_output.loss,) + tuple_output
            if timebraid_output.token_accuracy is not None:
                tuple_output += (timebraid_output.token_accuracy,)
            if timebraid_output.predicted_tokens is not None:
                tuple_output += (timebraid_output.predicted_tokens,)
            return tuple_output

        if not resolved_return_dict:
            raise RuntimeError("TS-routed TimeBraid forward requires return_dict=True.")
        cache_implementation = kwargs.pop("cache_implementation", None)
        if use_cache is None:
            configured_use_cache = self.llm.model.config.use_cache
            if type(configured_use_cache) is not bool:
                raise TypeError(
                    "Qwen config use_cache must be a bool, got "
                    f"{configured_use_cache!r}."
                )
            resolved_use_cache = configured_use_cache and not self.llm.model.training
        elif type(use_cache) is bool:
            resolved_use_cache = use_cache
        else:
            raise TypeError(
                f"TimeBraid forward use_cache must be a bool or None, got {use_cache!r}."
            )
        decoder_result = run_timebraid_decoder(
            owner=self,
            text_model=self.llm.model,
            payload=payload,
            input_ids=input_ids,
            attention_mask=attention_mask,
            position_ids=position_ids,
            past_key_values=past_key_values,
            inputs_embeds=inputs_embeds,
            use_cache=resolved_use_cache,
            cache_position=cache_position,
            output_attentions=output_attentions,
            output_hidden_states=output_hidden_states,
            return_dict=True,
            active_rows=active_rows,
            cache_implementation=cache_implementation,
            collect_incremental_kv_cache=resolved_use_cache,
        )

        loss_kwargs = dict(kwargs)
        if num_items_in_batch is not None:
            loss_kwargs["num_items_in_batch"] = num_items_in_batch
        outputs = _build_lm_output(
            llm=self.llm,
            hidden_states=decoder_result.hidden_states,
            past_key_values=decoder_result.past_key_values,
            decoder_hidden_states=None,
            decoder_attentions=None,
            labels=labels,
            labels_for_loss=labels_for_loss,
            shift_labels=shift_labels,
            logits_to_keep=logits_to_keep,
            skip_logits=skip_logits,
            loss_kwargs=loss_kwargs,
        )
        outputs, breakdown = merge_timebraid_losses(
            model=self,
            mot_runtime=decoder_result.mot_runtime,
            outputs=outputs,
            mot_ts_num_records_by_horizon=mot_ts_num_records_by_horizon,
            mot_ts_num_roi_targets_by_horizon=mot_ts_num_roi_targets_by_horizon,
            mot_ts_understanding_num_points=mot_ts_understanding_num_points,
            mot_ts_understanding_num_fft_elements=(
                mot_ts_understanding_num_fft_elements
            ),
        )
        return _as_timebraid_output(outputs, breakdown)

    def save_pretrained(self, *args: Any, **kwargs: Any) -> None:
        # Match Transformers' own dtype authority, but update both composite
        # config surfaces before the base implementation serializes either one.
        if self.config.llm_config is not self.llm.config:
            raise RuntimeError(
                "TimeBraid save requires config.llm_config to be the live LLM config."
            )
        # The checkpoint is self-contained, so machine-local source paths are
        # neither restore semantics nor useful public provenance.
        self.config._name_or_path = ""
        self.config.llm_config._name_or_path = ""
        self.config.llm_config.dtype = get_parameter_dtype(self)
        self.config._sync_llm_surface()
        return super().save_pretrained(*args, **kwargs)

    def prepare_inputs_for_generation(
        self, *args: Any, **kwargs: Any
    ) -> dict[str, Any]:
        return self.llm.prepare_inputs_for_generation(*args, **kwargs)

    def generate_mixed(
        self,
        inputs: "TimeBraidBatchFeature",
        *,
        max_new_tokens: Optional[int] = None,
        max_length: Optional[int] = None,
        mot_target_total_lengths: Optional[Sequence[int]] = None,
        mot_forecast_head_len: Optional[int] = None,
        mot_return_forecast_quantiles: bool = False,
        eos_token_id: Optional[Union[int, Sequence[int]]] = None,
        pad_token_id: Optional[int] = None,
        use_cache: Optional[bool] = None,
        cache_implementation: Optional[str] = None,
    ) -> TimeBraidGenerateOutput:
        """Generate mixed text and time series, or raise.

        `inputs` is the mapping returned by
        `TimeBraidProcessor.apply_chat_template`, which carries `input_ids`,
        `attention_mask`, the complete TS payload, and the target horizons.
        Everything the processor cannot know is a keyword argument here.

        This is the explicit counterpart to `generate`. It states its whole
        contract in its signature, so a misspelled argument is a `TypeError`
        rather than a silent downgrade, and an argument this scheduler cannot
        honour — `temperature`, `logits_processor`, anything sampling-related —
        is simply not a parameter. Unlike `generate`, it never falls back to
        plain text: if the request does not resolve to mixed generation it
        raises and says which signal was missing.

        Supply exactly one of `max_new_tokens` or `max_length`.

        TimeBraid's own arguments keep the `mot_` prefix they carry everywhere
        else — in `config.json`, in the processor's output, and in `generate`'s
        keyword arguments — so one name means one thing at every boundary.
        """
        require_mixed_generation_inputs(inputs)
        input_ids = validate_mot_generation_input_ids(inputs["input_ids"])
        batch_size = int(input_ids.shape[0])

        payload = TimeBraidPayload(
            **{name: inputs.get(name) for name in TIMEBRAID_TS_PAYLOAD_FIELDS}
        )
        controls = NormalizedMoTGenerationControls.normalize(
            batch_size=batch_size,
            # Supplied by the processor, which owns the prompt surface.
            target_horizons=inputs.get("mot_target_horizons"),
            target_history_span_idxs=inputs.get("mot_target_history_span_idxs"),
            # Supplied by the caller: the processor cannot know these.
            target_total_lengths=mot_target_total_lengths,
            forecast_head_len=mot_forecast_head_len,
            return_forecast_quantiles=mot_return_forecast_quantiles,
        )

        # No route resolution here: `require_mixed_generation_inputs` above has
        # already established the one signal that matters, a complete TS
        # payload. Inferring intent is the compatibility shim's problem.
        if controls.target_horizons is None:
            raise RuntimeError(
                "generate_mixed requires `mot_target_horizons` in `inputs`. Pass "
                "`horizon=` to TimeBraidProcessor.apply_chat_template, which "
                "supplies it; use 0 to forbid TS rollout."
            )

        generation_kwargs: dict[str, Any] = {}
        for name, value in (
            ("max_new_tokens", max_new_tokens),
            ("max_length", max_length),
            ("eos_token_id", eos_token_id),
            ("pad_token_id", pad_token_id),
            ("use_cache", use_cache),
            ("cache_implementation", cache_implementation),
        ):
            if value is not None:
                generation_kwargs[name] = value

        return run_timebraid_generate(
            model=self,
            text_model=self.llm.model,
            payload=payload,
            input_ids=input_ids,
            attention_mask=inputs.get("attention_mask"),
            controls=controls,
            kwargs=generation_kwargs,
        )

    def generate(
        self, *args: Any, **kwargs: Any
    ) -> TimeBraidGenerateOutput | torch.Tensor | ModelOutput:
        """Hugging Face-compatible dispatch over `**kwargs`.

        Kept so `model.generate(**processor_output, max_new_tokens=...)` keeps
        working, and so plain-text requests still reach the host language
        model. It resolves a route and delegates; the contract for mixed
        generation lives on `generate_mixed`.
        """
        generation_kwargs = dict(kwargs)
        payload = TimeBraidPayload.pop_from_kwargs(generation_kwargs)
        generation_controls = {
            key: generation_kwargs.pop(key)
            for key in _MOT_GENERATION_CONTROL_FIELDS
            if key in generation_kwargs
        }
        _reject_unknown_timebraid_kwargs(generation_kwargs)

        positional_input = args[0] if args else None
        keyword_input = generation_kwargs.get("input_ids")
        hf_input = generation_kwargs.get("inputs")
        route_input = (
            positional_input
            if positional_input is not None
            else (keyword_input if keyword_input is not None else hf_input)
        )
        if isinstance(route_input, torch.Tensor) and route_input.ndim == 2:
            control_batch_size = int(route_input.shape[0])
        elif isinstance(payload.ts_values, torch.Tensor) and payload.ts_values.ndim > 0:
            control_batch_size = int(payload.ts_values.shape[0])
        else:
            control_batch_size = 1
        controls = normalize_mot_generation_controls(
            generation_controls,
            batch_size=control_batch_size,
        )

        if isinstance(generation_kwargs.get("past_key_values"), MoTDynamicCache):
            raise RuntimeError(
                "MoT native mixed generate accepts a fresh prompt and owns its full "
                "phase scheduler; continue a public forward cache through "
                "model.forward instead."
            )

        decision = resolve_generation_route(
            payload=payload,
            route_input=route_input,
            target_horizons=controls.target_horizons,
            target_total_lengths=controls.target_total_lengths,
            ts_open_token_id=int(self.ts_open_token_id),
            ts_close_token_id=int(self.ts_close_token_id),
        )
        if decision.route is GenerationRoute.PLAIN_TEXT:
            _require_binary_plain_attention_mask(
                generation_kwargs.get("attention_mask")
            )
            return self.llm.generate(*args, **generation_kwargs)
        if controls.target_horizons is None:
            raise RuntimeError(
                "Mixed generation requires explicit `mot_target_horizons`. Pass "
                "`horizon=` to TimeBraidProcessor.apply_chat_template, which "
                "supplies it; use 0 to forbid TS rollout, or a positive integer "
                "to allow one generated TS span."
            )

        if len(args) > 1:
            raise TypeError(
                "TS generation accepts at most one positional input tensor."
            )
        provided_inputs = [
            value
            for value in (positional_input, keyword_input, hf_input)
            if value is not None
        ]
        if len(provided_inputs) != 1:
            raise ValueError(
                "TS generation requires exactly one of positional inputs, "
                "input_ids, or inputs."
            )
        input_ids = validate_mot_generation_input_ids(provided_inputs[0])
        generation_kwargs.pop("input_ids", None)
        generation_kwargs.pop("inputs", None)
        attention_mask = generation_kwargs.pop("attention_mask", None)
        return run_timebraid_generate(
            model=self,
            text_model=self.llm.model,
            payload=payload,
            input_ids=input_ids,
            attention_mask=attention_mask,
            controls=controls,
            kwargs=generation_kwargs,
        )

    def get_input_embeddings(self) -> nn.Module:
        return self.llm.get_input_embeddings()

    def set_input_embeddings(self, value: nn.Module) -> None:
        self.llm.set_input_embeddings(value)

    def get_output_embeddings(self) -> Optional[nn.Module]:
        return self.llm.get_output_embeddings()

    def set_output_embeddings(self, value: nn.Module) -> None:
        self.llm.set_output_embeddings(value)

    def tie_weights(self) -> None:
        super().tie_weights()
        self._sync_hf_loading_contracts()

    def can_generate(self) -> bool:
        llm = self._modules.get("llm") if hasattr(self, "_modules") else None
        return True if llm is None else llm.can_generate()


def register_timebraid_auto_classes() -> None:
    """Register TimeBraid with HF auto classes in the active Python process."""
    from transformers import AutoProcessor

    from ..processing_timebraid import TimeBraidProcessor

    AutoConfig.register(TimeBraidConfig.model_type, TimeBraidConfig, exist_ok=True)
    AutoProcessor.register(TimeBraidConfig, TimeBraidProcessor, exist_ok=True)
    # Deliberately not registered with plain `AutoModel`: that class means
    # "base model without a task head", and TimeBraid is a causal LM. The
    # published auto_map must not advertise it either — see AUTO_MAP in
    # scripts/build-huggingface-timebraid-model.py.
    AutoModelForCausalLM.register(TimeBraidConfig, TimeBraid, exist_ok=True)


__all__ = [
    "TimeBraid",
    "TimeBraidConfig",
    "TimeBraidLossBreakdown",
    "TimeBraidOutput",
    "register_timebraid_auto_classes",
]