code-loader 1.0.191.dev1__tar.gz → 1.0.193.dev0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. {code_loader-1.0.191.dev1 → code_loader-1.0.193.dev0}/PKG-INFO +1 -1
  2. {code_loader-1.0.191.dev1 → code_loader-1.0.193.dev0}/code_loader/contract/datasetclasses.py +70 -55
  3. {code_loader-1.0.191.dev1 → code_loader-1.0.193.dev0}/code_loader/inner_leap_binder/leapbinder.py +164 -17
  4. {code_loader-1.0.191.dev1 → code_loader-1.0.193.dev0}/code_loader/inner_leap_binder/leapbinder_decorators.py +1063 -192
  5. {code_loader-1.0.191.dev1 → code_loader-1.0.193.dev0}/code_loader/leaploader.py +286 -187
  6. {code_loader-1.0.191.dev1 → code_loader-1.0.193.dev0}/code_loader/leaploaderbase.py +5 -0
  7. {code_loader-1.0.191.dev1 → code_loader-1.0.193.dev0}/code_loader/utils.py +36 -5
  8. {code_loader-1.0.191.dev1 → code_loader-1.0.193.dev0}/pyproject.toml +1 -1
  9. {code_loader-1.0.191.dev1 → code_loader-1.0.193.dev0}/LICENSE +0 -0
  10. {code_loader-1.0.191.dev1 → code_loader-1.0.193.dev0}/README.md +0 -0
  11. {code_loader-1.0.191.dev1 → code_loader-1.0.193.dev0}/code_loader/__init__.py +0 -0
  12. {code_loader-1.0.191.dev1 → code_loader-1.0.193.dev0}/code_loader/contract/__init__.py +0 -0
  13. {code_loader-1.0.191.dev1 → code_loader-1.0.193.dev0}/code_loader/contract/enums.py +0 -0
  14. {code_loader-1.0.191.dev1 → code_loader-1.0.193.dev0}/code_loader/contract/exceptions.py +0 -0
  15. {code_loader-1.0.191.dev1 → code_loader-1.0.193.dev0}/code_loader/contract/mapping.py +0 -0
  16. {code_loader-1.0.191.dev1 → code_loader-1.0.193.dev0}/code_loader/contract/responsedataclasses.py +0 -0
  17. {code_loader-1.0.191.dev1 → code_loader-1.0.193.dev0}/code_loader/contract/sim_config.py +0 -0
  18. {code_loader-1.0.191.dev1 → code_loader-1.0.193.dev0}/code_loader/contract/visualizer_classes.py +0 -0
  19. {code_loader-1.0.191.dev1 → code_loader-1.0.193.dev0}/code_loader/default_losses.py +0 -0
  20. {code_loader-1.0.191.dev1 → code_loader-1.0.193.dev0}/code_loader/default_metrics.py +0 -0
  21. {code_loader-1.0.191.dev1 → code_loader-1.0.193.dev0}/code_loader/experiment_api/__init__.py +0 -0
  22. {code_loader-1.0.191.dev1 → code_loader-1.0.193.dev0}/code_loader/experiment_api/api.py +0 -0
  23. {code_loader-1.0.191.dev1 → code_loader-1.0.193.dev0}/code_loader/experiment_api/cli_config_utils.py +0 -0
  24. {code_loader-1.0.191.dev1 → code_loader-1.0.193.dev0}/code_loader/experiment_api/client.py +0 -0
  25. {code_loader-1.0.191.dev1 → code_loader-1.0.193.dev0}/code_loader/experiment_api/epoch.py +0 -0
  26. {code_loader-1.0.191.dev1 → code_loader-1.0.193.dev0}/code_loader/experiment_api/experiment.py +0 -0
  27. {code_loader-1.0.191.dev1 → code_loader-1.0.193.dev0}/code_loader/experiment_api/experiment_context.py +0 -0
  28. {code_loader-1.0.191.dev1 → code_loader-1.0.193.dev0}/code_loader/experiment_api/types.py +0 -0
  29. {code_loader-1.0.191.dev1 → code_loader-1.0.193.dev0}/code_loader/experiment_api/utils.py +0 -0
  30. {code_loader-1.0.191.dev1 → code_loader-1.0.193.dev0}/code_loader/experiment_api/workingspace_config_utils.py +0 -0
  31. {code_loader-1.0.191.dev1 → code_loader-1.0.193.dev0}/code_loader/inner_leap_binder/__init__.py +0 -0
  32. {code_loader-1.0.191.dev1 → code_loader-1.0.193.dev0}/code_loader/mixpanel_tracker.py +0 -0
  33. {code_loader-1.0.191.dev1 → code_loader-1.0.193.dev0}/code_loader/plot_functions/__init__.py +0 -0
  34. {code_loader-1.0.191.dev1 → code_loader-1.0.193.dev0}/code_loader/plot_functions/plot_functions.py +0 -0
  35. {code_loader-1.0.191.dev1 → code_loader-1.0.193.dev0}/code_loader/plot_functions/visualize.py +0 -0
  36. {code_loader-1.0.191.dev1 → code_loader-1.0.193.dev0}/code_loader/visualizers/__init__.py +0 -0
  37. {code_loader-1.0.191.dev1 → code_loader-1.0.193.dev0}/code_loader/visualizers/default_visualizers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: code-loader
3
- Version: 1.0.191.dev1
3
+ Version: 1.0.193.dev0
4
4
  Summary:
5
5
  Home-page: https://github.com/tensorleap/code-loader
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  import warnings
2
2
  from dataclasses import dataclass, field
3
- from typing import Any, Callable, List, Optional, Dict, Union, Type, Literal, cast
3
+ from typing import Any, Callable, List, Optional, Dict, Union, Type, Literal, Tuple
4
4
  import re
5
5
  import numpy as np
6
6
  import numpy.typing as npt
@@ -15,8 +15,6 @@ custom_latent_space_attribute = "custom_latent_space"
15
15
 
16
16
  _simulation_context: Dict[str, bool] = {"active": False}
17
17
 
18
- SampleId = Union[int, str]
19
-
20
18
 
21
19
  @dataclass
22
20
  class PreprocessResponse:
@@ -41,44 +39,27 @@ class PreprocessResponse:
41
39
  """
42
40
  length: Optional[int] = None # Deprecated. Please use sample_ids instead
43
41
  data: Any = None
44
- sample_ids: Optional[Union[List[SampleId], List[List[SampleId]]]] = None
42
+ sample_ids: Optional[Union[List[str], List[int]]] = None
45
43
  state: Optional[DataStateType] = None
46
44
  sample_id_type: Optional[Union[Type[str], Type[int]]] = None
47
45
  sample_ids_to_instance_mappings: Optional[Dict[str, List[str]]] = None # in use only for element instance
48
46
  instance_to_sample_ids_mappings: Optional[Dict[str, str]] = None # in use only for element instance
49
47
  tl_generated: bool = False
50
- _grouped: bool = field(default=False, init=False, repr=False, compare=False)
51
48
 
52
49
  def __post_init__(self) -> None:
53
50
  assert self.sample_ids_to_instance_mappings is None, f"Keep sample_ids_to_instance_mappings None when initializing PreprocessResponse"
54
51
  assert self.instance_to_sample_ids_mappings is None, f"Keep instance_to_sample_ids_mappings None when initializing PreprocessResponse"
55
52
 
56
53
  if self.length is not None and self.sample_ids is None:
57
- self.sample_ids = cast(List[SampleId], [i for i in range(self.length)])
54
+ self.sample_ids = [i for i in range(self.length)]
58
55
  self.sample_id_type = int
59
- self._grouped = False
60
56
  elif self.length is None and self.sample_ids is not None:
61
- self._grouped = len(self.sample_ids) > 0 and isinstance(self.sample_ids[0], list)
62
- if self._grouped:
63
- groups = cast(List[List[SampleId]], self.sample_ids)
64
- assert len(groups) >= 1, "Grouped PreprocessResponse must have at least one group."
65
- for group in groups:
66
- assert isinstance(group, list) and len(group) > 0, \
67
- "Each group in a grouped PreprocessResponse must be a non-empty list."
68
- self.sample_id_type = type(groups[0][0])
69
- for group in groups:
70
- for sample_id in group:
71
- assert isinstance(sample_id, self.sample_id_type), \
72
- f"Sample id should be of type {self.sample_id_type.__name__}. Got: {type(sample_id)}"
73
- self.length = sum(len(group) for group in groups)
74
- else:
75
- flat_ids = cast(List[SampleId], self.sample_ids)
76
- self.length = len(flat_ids)
77
- if self.sample_id_type is None:
78
- self.sample_id_type = str
79
- if self.sample_id_type == str:
80
- for sample_id in flat_ids:
81
- assert isinstance(sample_id, str), f"Sample id should be of type str. Got: {type(sample_id)}"
57
+ self.length = len(self.sample_ids)
58
+ if self.sample_id_type is None:
59
+ self.sample_id_type = str
60
+ if self.sample_id_type == str:
61
+ for sample_id in self.sample_ids:
62
+ assert isinstance(sample_id, str), f"Sample id should be of type str. Got: {type(sample_id)}"
82
63
  else:
83
64
  raise Exception("length is deprecated, please use sample_ids instead.")
84
65
 
@@ -98,36 +79,9 @@ class PreprocessResponse:
98
79
  return id(self)
99
80
 
100
81
  def __len__(self) -> int:
101
- assert self.length is not None
102
- return self.length
103
-
104
- @property
105
- def is_grouped(self) -> bool:
106
- return self._grouped
107
-
108
- @property
109
- def num_groups(self) -> int:
110
- # Grouped: the number of groups. Flat: the (flat) sample count, since sample_ids is the
111
- # flat id list and there is one sample per "group".
112
82
  assert self.sample_ids is not None
113
83
  return len(self.sample_ids)
114
84
 
115
- @property
116
- def groups(self) -> Optional[List[List[SampleId]]]:
117
- # The nested list of groups when grouped, otherwise None. Callers should
118
- # branch on is_grouped before relying on this.
119
- return cast(List[List[SampleId]], self.sample_ids) if self._grouped else None
120
-
121
- @property
122
- def flat_sample_ids(self) -> List[SampleId]:
123
- # All sample ids in order: concatenation of the groups when grouped,
124
- # otherwise the flat sample_ids list as-is.
125
- assert self.sample_ids is not None
126
- if self._grouped:
127
- groups = cast(List[List[SampleId]], self.sample_ids)
128
- return [sample_id for group in groups for sample_id in group]
129
- return cast(List[SampleId], self.sample_ids)
130
-
131
85
  @dataclass
132
86
  class ElementInstance:
133
87
  name: str
@@ -138,6 +92,20 @@ SectionCallableInterface = Callable[[Union[int, str], PreprocessResponse], npt.N
138
92
  InstanceCallableInterface = Callable[[Union[int, str], PreprocessResponse, int], Optional[ElementInstance]]
139
93
  InstanceLengthCallableInterface = Callable[[Union[int, str], PreprocessResponse], int]
140
94
 
95
+ # (sample_id, prev_inputs, prev_outputs, state, preprocess) -> (next model inputs | None, state).
96
+ # First call per chain receives prev_inputs=None, prev_outputs=None, state=None and returns the
97
+ # initial model inputs plus the initial state. Returning next_inputs=None ends the chain — the
98
+ # terminating call still returns state, so the final step participates in state aggregation.
99
+ # State is an arbitrary nest of dicts/lists holding numpy arrays, numbers, strings or bools; it is
100
+ # never fed to the model and rides the engine's redis queue on every step, so accumulate
101
+ # reductions, not raw per-step tensors. The hook must be stateless and deterministically seeded
102
+ # (the engine may replay a step after crash recovery and relies on identical results).
103
+ AutoregressiveChainState = Any
104
+ AutoregressiveStepCallableInterface = Callable[
105
+ [Union[int, str], Optional[Dict[str, npt.NDArray[np.float32]]], Optional[Dict[str, npt.NDArray[np.float32]]],
106
+ AutoregressiveChainState, PreprocessResponse],
107
+ Tuple[Optional[Dict[str, npt.NDArray[np.float32]]], AutoregressiveChainState]]
108
+
141
109
  MetadataSectionCallableInterface = Union[
142
110
  Callable[[Union[int, str], PreprocessResponse], int],
143
111
  Callable[[Union[int, str], PreprocessResponse], Dict[str, int]],
@@ -285,6 +253,49 @@ class CustomLatentSpaceHandler:
285
253
  function: SectionCallableInterface
286
254
  name: str = 'custom_latent_space'
287
255
 
256
+
257
+ # How a chain's latent-space vectors are derived from its per-step forward passes.
258
+ # 'last_step': every latent space comes from the final step's forward pass, except input-kind
259
+ # latent spaces which come from the first step (the original sample, before generated content
260
+ # dominates the model inputs). 'mean': every latent space is the elementwise mean over all steps.
261
+ AUTOREGRESSIVE_LATENT_SPACE_AGGREGATIONS = ('last_step', 'mean')
262
+
263
+ # Reserved argument names of autoregressive metrics/losses/visualizers, fed implicitly by the
264
+ # platform from the finished chain (per-chain dicts, no batch axis). Any other argument is wired
265
+ # to a ground-truth encoder through the integration test.
266
+ AUTOREGRESSIVE_IMPLICIT_ARG_NAMES = ('inputs', 'outputs', 'state')
267
+
268
+
269
+ @dataclass
270
+ class AutoregressiveStepHandler:
271
+ function: AutoregressiveStepCallableInterface
272
+ name: str = 'autoregressive_step'
273
+ # Per-model-input shapes (unbatched), discovered by running the hook's first call at parse
274
+ # time. Fills the role InputHandler.shape plays for input encoders.
275
+ input_shapes: Optional[Dict[str, List[int]]] = None
276
+ latent_space_aggregation: str = 'last_step'
277
+
278
+
279
+ # Per-chain, unbatched callables: called once per finished chain with the final step's tensors —
280
+ # inputs/outputs/state are fed implicitly by the platform (dicts, no batch axis; outputs keyed by
281
+ # prediction-type names), remaining args are wired ground-truth encoder results.
282
+ @dataclass
283
+ class AutoregressiveMetricHandler:
284
+ metric_handler_data: MetricHandlerData
285
+ function: CustomCallableInterfaceMultiArgs
286
+
287
+
288
+ @dataclass
289
+ class AutoregressiveLossHandler:
290
+ custom_loss_handler_data: CustomLossHandlerData
291
+ function: CustomCallableInterface
292
+
293
+
294
+ @dataclass
295
+ class AutoregressiveVisualizerHandler:
296
+ visualizer_handler_data: VisualizerHandlerData
297
+ function: VisualizerCallableInterface
298
+
288
299
  @dataclass
289
300
  class PredictionTypeHandler:
290
301
  name: str
@@ -325,6 +336,10 @@ class DatasetIntegrationSetup:
325
336
  custom_layers: Dict[str, CustomLayerHandler] = field(default_factory=dict)
326
337
  custom_latent_space: Optional[CustomLatentSpaceHandler] = None
327
338
  simulations: List[SimulationHandler] = field(default_factory=list)
339
+ autoregressive_step: Optional[AutoregressiveStepHandler] = None
340
+ autoregressive_metrics: List[AutoregressiveMetricHandler] = field(default_factory=list)
341
+ autoregressive_losses: List[AutoregressiveLossHandler] = field(default_factory=list)
342
+ autoregressive_visualizers: List[AutoregressiveVisualizerHandler] = field(default_factory=list)
328
343
 
329
344
 
330
345
  @dataclass
@@ -13,7 +13,9 @@ from code_loader.contract.datasetclasses import SectionCallableInterface, InputH
13
13
  CustomMultipleReturnCallableInterfaceMultiArgs, DatasetBaseHandler, custom_latent_space_attribute, \
14
14
  RawInputsForHeatmap, VisualizerHandlerData, MetricHandlerData, CustomLossHandlerData, SamplePreprocessResponse, \
15
15
  ElementInstanceMasksHandler, InstanceCallableInterface, CustomLatentSpaceHandler, InstanceMetricHandler, \
16
- SimulationHandler, _simulation_context
16
+ SimulationHandler, _simulation_context, AutoregressiveStepHandler, AutoregressiveStepCallableInterface, \
17
+ AUTOREGRESSIVE_LATENT_SPACE_AGGREGATIONS, AUTOREGRESSIVE_IMPLICIT_ARG_NAMES, \
18
+ AutoregressiveMetricHandler, AutoregressiveLossHandler, AutoregressiveVisualizerHandler
17
19
  from code_loader.contract.enums import LeapDataType, DataStateEnum, DataStateType, MetricDirection, DatasetMetadataType
18
20
  from code_loader.contract.mapping import NodeConnection, NodeMapping, NodeMappingType
19
21
  from code_loader.contract.responsedataclasses import DatasetTestResultPayload, LeapAnalysisConfiguration
@@ -524,6 +526,111 @@ class LeapBinder:
524
526
  """
525
527
  self.setup_container.custom_latent_space = CustomLatentSpaceHandler(function)
526
528
 
529
+ def set_autoregressive_step(self, function: AutoregressiveStepCallableInterface,
530
+ latent_space_aggregation: str = 'last_step') -> None:
531
+ """
532
+ Set the autoregressive step hook — the feedback function that drives a chain:
533
+ it supplies the model's initial inputs on its first call (prev_inputs=None, prev_outputs=None)
534
+ and turns each step's inputs/outputs into the next step's inputs until it returns None.
535
+ An autoregressive integration has no input encoders; the hook is the sole input source.
536
+ latent_space_aggregation declares how the chain's latent-space vectors are derived from
537
+ its steps (see AUTOREGRESSIVE_LATENT_SPACE_AGGREGATIONS).
538
+ """
539
+ if self.setup_container.autoregressive_step is not None:
540
+ raise Exception('tensorleap_autoregressive_step is already defined. '
541
+ 'Only one autoregressive step hook is allowed per integration.')
542
+ if latent_space_aggregation not in AUTOREGRESSIVE_LATENT_SPACE_AGGREGATIONS:
543
+ raise Exception(f'tensorleap_autoregressive_step: unknown latent_space_aggregation '
544
+ f'{latent_space_aggregation!r}. Supported values: '
545
+ f'{", ".join(AUTOREGRESSIVE_LATENT_SPACE_AGGREGATIONS)}.')
546
+ self.setup_container.autoregressive_step = AutoregressiveStepHandler(
547
+ function, latent_space_aggregation=latent_space_aggregation)
548
+
549
+ # Builtin chain metadata, declared at parse time so it survives the reporter's
550
+ # metadata type mapping; the placeholder values are overwritten by the engine when a
551
+ # chain finalizes (realized length, truncated-by-safety-cap flag).
552
+ def builtin_chain_metadata(idx: Any, preprocess: PreprocessResponse) -> Dict[str, Any]:
553
+ return {'length': -1, 'truncated': False}
554
+
555
+ self.set_metadata(builtin_chain_metadata, 'builtin_chain')
556
+
557
+ def _autoregressive_arg_names(self, function: Callable[..., Any], decorator_name: str,
558
+ name: str) -> List[str]:
559
+ """Split an autoregressive callable's signature into implicit chain args (validated
560
+ present) and wired arg names (returned) — the wired args connect to ground-truth
561
+ encoders through the integration test."""
562
+ argspec = inspect.getfullargspec(function)
563
+ for arg_name, arg_type in argspec.annotations.items():
564
+ _reject_stringized_sample_preprocess_response(function, arg_name, arg_type)
565
+ if arg_type == SamplePreprocessResponse:
566
+ raise Exception(
567
+ f'{decorator_name} "{name}": SamplePreprocessResponse arguments are not '
568
+ f'supported for autoregressive decorators yet. Derive what you need inside '
569
+ f'the autoregressive step hook and carry it in the state.')
570
+ implicit = [arg_name for arg_name in argspec[0]
571
+ if arg_name in AUTOREGRESSIVE_IMPLICIT_ARG_NAMES]
572
+ if not implicit:
573
+ raise Exception(
574
+ f'{decorator_name} "{name}": the function must declare at least one of the '
575
+ f'implicit chain arguments {list(AUTOREGRESSIVE_IMPLICIT_ARG_NAMES)} — they are '
576
+ f'fed by the platform from the finished chain.')
577
+ return [arg_name for arg_name in argspec[0]
578
+ if arg_name not in AUTOREGRESSIVE_IMPLICIT_ARG_NAMES]
579
+
580
+ def _assert_unique_metric_name(self, name: str) -> None:
581
+ existing = [handler.metric_handler_data.name for handler in self.setup_container.metrics] \
582
+ + [handler.metric_handler_data.name
583
+ for handler in self.setup_container.autoregressive_metrics]
584
+ if name in existing:
585
+ raise Exception(f'Metric with name {name} already exists. Please choose another')
586
+
587
+ def _assert_unique_loss_name(self, name: str) -> None:
588
+ existing = [handler.custom_loss_handler_data.name
589
+ for handler in self.setup_container.custom_loss_handlers] \
590
+ + [handler.custom_loss_handler_data.name
591
+ for handler in self.setup_container.autoregressive_losses]
592
+ if name in existing:
593
+ raise Exception(f'Custom loss with name {name} already exists. Please choose another')
594
+
595
+ def _assert_unique_visualizer_name(self, name: str) -> None:
596
+ existing = [handler.visualizer_handler_data.name
597
+ for handler in self.setup_container.visualizers] \
598
+ + [handler.visualizer_handler_data.name
599
+ for handler in self.setup_container.autoregressive_visualizers]
600
+ if name in existing:
601
+ raise Exception(f'Visualizer with name {name} already exists. Please choose another')
602
+
603
+ def add_autoregressive_metric(self, function: CustomCallableInterfaceMultiArgs, name: str,
604
+ direction: Optional[Union[MetricDirection, Dict[str, MetricDirection]]]
605
+ = MetricDirection.Downward,
606
+ compute_insights: Optional[Union[bool, Dict[str, bool]]] = None) -> None:
607
+ self._assert_unique_metric_name(name)
608
+ arg_names = self._autoregressive_arg_names(function, 'tensorleap_autoregressive_metric',
609
+ name)
610
+ metric_handler_data = MetricHandlerData(name, arg_names, direction, compute_insights)
611
+ self.setup_container.autoregressive_metrics.append(
612
+ AutoregressiveMetricHandler(metric_handler_data, function))
613
+
614
+ def add_autoregressive_loss(self, function: CustomCallableInterface, name: str) -> None:
615
+ self._assert_unique_loss_name(name)
616
+ arg_names = self._autoregressive_arg_names(function, 'tensorleap_autoregressive_loss',
617
+ name)
618
+ self.setup_container.autoregressive_losses.append(
619
+ AutoregressiveLossHandler(CustomLossHandlerData(name, arg_names), function))
620
+
621
+ def add_autoregressive_visualizer(self, function: VisualizerCallableInterface, name: str,
622
+ visualizer_type: LeapDataType) -> None:
623
+ self._assert_unique_visualizer_name(name)
624
+ if visualizer_type.value not in map_leap_data_type_to_visualizer_class:
625
+ raise Exception(
626
+ f'The visualizer_type is invalid. current visualizer_type: {visualizer_type}, '
627
+ f'should be one of : {", ".join([arg.__name__ for arg in get_args(LeapData)])}')
628
+ arg_names = self._autoregressive_arg_names(
629
+ function, 'tensorleap_autoregressive_visualizer', name)
630
+ self.setup_container.autoregressive_visualizers.append(
631
+ AutoregressiveVisualizerHandler(VisualizerHandlerData(name, visualizer_type,
632
+ arg_names), function))
633
+
527
634
  def set_custom_layer(self, custom_layer: Type[Any], name: str, inspect_layer: bool = False,
528
635
  kernel_index: Optional[int] = None, use_custom_latent_space: bool = False) -> None:
529
636
  """
@@ -606,15 +713,6 @@ class LeapBinder:
606
713
  if state_enum in preprocess_result_dict:
607
714
  preprocess_result_dict_in_correct_order[state_enum] = preprocess_result_dict[state_enum]
608
715
 
609
- # All-or-none: every state must agree on grouped (nested sample_ids) vs flat.
610
- if len({r.is_grouped for r in preprocess_result_dict_in_correct_order.values()}) > 1:
611
- shapes = ', '.join(
612
- f"{state.name}={'grouped' if r.is_grouped else 'flat'}"
613
- for state, r in preprocess_result_dict_in_correct_order.items())
614
- raise Exception(
615
- "All preprocess states must be either all grouped (nested sample_ids) or all flat, "
616
- f"but got a mix: {shapes}.")
617
-
618
716
  return preprocess_result_dict_in_correct_order
619
717
 
620
718
  def get_preprocess_unlabeled_result(self) -> Optional[PreprocessResponse]:
@@ -635,13 +733,7 @@ class LeapBinder:
635
733
  preprocess_response: PreprocessResponse, test_result: List[DatasetTestResultPayload],
636
734
  dataset_base_handler: Union[DatasetBaseHandler, MetadataHandler], state: DataStateEnum) -> List[DatasetTestResultPayload]:
637
735
  assert preprocess_response.sample_ids is not None
638
- if preprocess_response.is_grouped:
639
- # Grouped: probe with the first group, then reduce to a single sample's result so the
640
- # recorded shape/type is per-sample (matching the flat case), not the (B, *) batch.
641
- group = preprocess_response.sample_ids[0]
642
- raw_result = dataset_base_handler.function(group, preprocess_response)[0]
643
- else:
644
- raw_result = dataset_base_handler.function(preprocess_response.sample_ids[0], preprocess_response)
736
+ raw_result = dataset_base_handler.function(preprocess_response.sample_ids[0], preprocess_response)
645
737
  handler_type = 'metadata' if isinstance(dataset_base_handler, MetadataHandler) else None
646
738
  if isinstance(dataset_base_handler, MetadataHandler):
647
739
  if isinstance(raw_result, dict):
@@ -749,6 +841,7 @@ class LeapBinder:
749
841
  )
750
842
 
751
843
  def check(self) -> None:
844
+ self.validate_autoregressive_setup()
752
845
  preprocess_result = self.get_preprocess_result()
753
846
  self.check_preprocess(preprocess_result)
754
847
  self.check_handlers(preprocess_result)
@@ -756,6 +849,60 @@ class LeapBinder:
756
849
  self.validate_ignore_latent_spaces()
757
850
  print("Successful!")
758
851
 
852
+ def validate_autoregressive_setup(self) -> None:
853
+ """Structural rules for autoregressive integrations (no-op when the hook is absent).
854
+
855
+ Order-independent: runs after the whole script registered, because decorators may
856
+ appear in any order in the integration file.
857
+ """
858
+ if self.setup_container.autoregressive_step is None:
859
+ declared_autoregressive = [
860
+ ('tensorleap_autoregressive_metric',
861
+ [handler.metric_handler_data.name
862
+ for handler in self.setup_container.autoregressive_metrics]),
863
+ ('tensorleap_autoregressive_loss',
864
+ [handler.custom_loss_handler_data.name
865
+ for handler in self.setup_container.autoregressive_losses]),
866
+ ('tensorleap_autoregressive_visualizer',
867
+ [handler.visualizer_handler_data.name
868
+ for handler in self.setup_container.autoregressive_visualizers]),
869
+ ]
870
+ for decorator_name, names in declared_autoregressive:
871
+ if names:
872
+ raise Exception(
873
+ f'{decorator_name} {names} requires a tensorleap_autoregressive_step '
874
+ f'hook — autoregressive decorators consume the finished chain the hook '
875
+ f'drives.')
876
+ return
877
+ if self.setup_container.inputs:
878
+ raise Exception(
879
+ "An autoregressive integration must not declare input encoders — "
880
+ "tensorleap_autoregressive_step is the sole source of model inputs "
881
+ "(its first call returns the initial inputs). Found input encoders: "
882
+ f"{[handler.name for handler in self.setup_container.inputs]}.")
883
+ if self.setup_container.simulations:
884
+ raise Exception(
885
+ "Simulations are not supported together with tensorleap_autoregressive_step yet: "
886
+ "the autoregressive entry points do not resolve synthetic sample ids. Remove the "
887
+ "simulation decorators or the autoregressive hook.")
888
+ if self.setup_container.instance_masks:
889
+ raise Exception(
890
+ "Element instances are not supported together with tensorleap_autoregressive_step: "
891
+ "instance generation masks the sample's encoded inputs, which do not exist in an "
892
+ "autoregressive integration. Remove the instance encoders or the autoregressive hook.")
893
+ if self.setup_container.custom_latent_space is not None:
894
+ raise Exception(
895
+ "tensorleap_custom_latent_space is not supported together with "
896
+ "tensorleap_autoregressive_step: the custom latent space function only sees "
897
+ "(sample_id, preprocess) — it runs before generation and cannot observe the chain. "
898
+ "The chain's latent is the final step's model latent.")
899
+ if not self.setup_container.prediction_types:
900
+ raise Exception(
901
+ "An autoregressive integration must declare prediction types "
902
+ "(tensorleap_load_model(prediction_types=[...])): the engine keys each step's "
903
+ "model outputs by the declared names to build the hook's prev_outputs dict. "
904
+ "Without them, prev_outputs would be empty at runtime.")
905
+
759
906
  def validate_ignore_latent_spaces(self) -> None:
760
907
  """Validate leap_analysis_configuration.ignore_latent_spaces against this binder.
761
908