code-loader 1.0.191.dev0__tar.gz → 1.0.192__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.dev0 → code_loader-1.0.192}/PKG-INFO +1 -1
  2. {code_loader-1.0.191.dev0 → code_loader-1.0.192}/code_loader/contract/datasetclasses.py +30 -2
  3. {code_loader-1.0.191.dev0 → code_loader-1.0.192}/code_loader/inner_leap_binder/leapbinder.py +73 -22
  4. {code_loader-1.0.191.dev0 → code_loader-1.0.192}/code_loader/inner_leap_binder/leapbinder_decorators.py +293 -29
  5. {code_loader-1.0.191.dev0 → code_loader-1.0.192}/code_loader/leaploader.py +173 -20
  6. {code_loader-1.0.191.dev0 → code_loader-1.0.192}/code_loader/leaploaderbase.py +4 -3
  7. {code_loader-1.0.191.dev0 → code_loader-1.0.192}/pyproject.toml +1 -1
  8. {code_loader-1.0.191.dev0 → code_loader-1.0.192}/LICENSE +0 -0
  9. {code_loader-1.0.191.dev0 → code_loader-1.0.192}/README.md +0 -0
  10. {code_loader-1.0.191.dev0 → code_loader-1.0.192}/code_loader/__init__.py +0 -0
  11. {code_loader-1.0.191.dev0 → code_loader-1.0.192}/code_loader/contract/__init__.py +0 -0
  12. {code_loader-1.0.191.dev0 → code_loader-1.0.192}/code_loader/contract/enums.py +0 -0
  13. {code_loader-1.0.191.dev0 → code_loader-1.0.192}/code_loader/contract/exceptions.py +0 -0
  14. {code_loader-1.0.191.dev0 → code_loader-1.0.192}/code_loader/contract/mapping.py +0 -0
  15. {code_loader-1.0.191.dev0 → code_loader-1.0.192}/code_loader/contract/responsedataclasses.py +0 -0
  16. {code_loader-1.0.191.dev0 → code_loader-1.0.192}/code_loader/contract/sim_config.py +0 -0
  17. {code_loader-1.0.191.dev0 → code_loader-1.0.192}/code_loader/contract/visualizer_classes.py +0 -0
  18. {code_loader-1.0.191.dev0 → code_loader-1.0.192}/code_loader/default_losses.py +0 -0
  19. {code_loader-1.0.191.dev0 → code_loader-1.0.192}/code_loader/default_metrics.py +0 -0
  20. {code_loader-1.0.191.dev0 → code_loader-1.0.192}/code_loader/experiment_api/__init__.py +0 -0
  21. {code_loader-1.0.191.dev0 → code_loader-1.0.192}/code_loader/experiment_api/api.py +0 -0
  22. {code_loader-1.0.191.dev0 → code_loader-1.0.192}/code_loader/experiment_api/cli_config_utils.py +0 -0
  23. {code_loader-1.0.191.dev0 → code_loader-1.0.192}/code_loader/experiment_api/client.py +0 -0
  24. {code_loader-1.0.191.dev0 → code_loader-1.0.192}/code_loader/experiment_api/epoch.py +0 -0
  25. {code_loader-1.0.191.dev0 → code_loader-1.0.192}/code_loader/experiment_api/experiment.py +0 -0
  26. {code_loader-1.0.191.dev0 → code_loader-1.0.192}/code_loader/experiment_api/experiment_context.py +0 -0
  27. {code_loader-1.0.191.dev0 → code_loader-1.0.192}/code_loader/experiment_api/types.py +0 -0
  28. {code_loader-1.0.191.dev0 → code_loader-1.0.192}/code_loader/experiment_api/utils.py +0 -0
  29. {code_loader-1.0.191.dev0 → code_loader-1.0.192}/code_loader/experiment_api/workingspace_config_utils.py +0 -0
  30. {code_loader-1.0.191.dev0 → code_loader-1.0.192}/code_loader/inner_leap_binder/__init__.py +0 -0
  31. {code_loader-1.0.191.dev0 → code_loader-1.0.192}/code_loader/mixpanel_tracker.py +0 -0
  32. {code_loader-1.0.191.dev0 → code_loader-1.0.192}/code_loader/plot_functions/__init__.py +0 -0
  33. {code_loader-1.0.191.dev0 → code_loader-1.0.192}/code_loader/plot_functions/plot_functions.py +0 -0
  34. {code_loader-1.0.191.dev0 → code_loader-1.0.192}/code_loader/plot_functions/visualize.py +0 -0
  35. {code_loader-1.0.191.dev0 → code_loader-1.0.192}/code_loader/utils.py +0 -0
  36. {code_loader-1.0.191.dev0 → code_loader-1.0.192}/code_loader/visualizers/__init__.py +0 -0
  37. {code_loader-1.0.191.dev0 → code_loader-1.0.192}/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.dev0
3
+ Version: 1.0.192
4
4
  Summary:
5
5
  Home-page: https://github.com/tensorleap/code-loader
6
6
  License: MIT
@@ -92,6 +92,16 @@ SectionCallableInterface = Callable[[Union[int, str], PreprocessResponse], npt.N
92
92
  InstanceCallableInterface = Callable[[Union[int, str], PreprocessResponse, int], Optional[ElementInstance]]
93
93
  InstanceLengthCallableInterface = Callable[[Union[int, str], PreprocessResponse], int]
94
94
 
95
+ # (sample_id, prev_inputs, prev_outputs, preprocess) -> next model inputs, or None to end the chain.
96
+ # First call per chain receives prev_inputs=None, prev_outputs=None and returns the initial inputs.
97
+ # Keys starting with '_' are passthrough state: never fed to the model, handed back verbatim in
98
+ # prev_inputs on the next call. The hook must be stateless and deterministically seeded (the engine
99
+ # may replay a step after crash recovery and relies on identical results).
100
+ AutoregressiveStepCallableInterface = Callable[
101
+ [Union[int, str], Optional[Dict[str, npt.NDArray[np.float32]]], Optional[Dict[str, npt.NDArray[np.float32]]],
102
+ PreprocessResponse],
103
+ Optional[Dict[str, npt.NDArray[np.float32]]]]
104
+
95
105
  MetadataSectionCallableInterface = Union[
96
106
  Callable[[Union[int, str], PreprocessResponse], int],
97
107
  Callable[[Union[int, str], PreprocessResponse], Dict[str, int]],
@@ -239,6 +249,23 @@ class CustomLatentSpaceHandler:
239
249
  function: SectionCallableInterface
240
250
  name: str = 'custom_latent_space'
241
251
 
252
+
253
+ # How a chain's latent-space vectors are derived from its per-step forward passes.
254
+ # 'last_step': every latent space comes from the final step's forward pass, except input-kind
255
+ # latent spaces which come from the first step (the original sample, before generated content
256
+ # dominates the model inputs). 'mean': every latent space is the elementwise mean over all steps.
257
+ AUTOREGRESSIVE_LATENT_SPACE_AGGREGATIONS = ('last_step', 'mean')
258
+
259
+
260
+ @dataclass
261
+ class AutoregressiveStepHandler:
262
+ function: AutoregressiveStepCallableInterface
263
+ name: str = 'autoregressive_step'
264
+ # Per-model-input shapes (unbatched, underscore passthrough keys excluded), discovered by running
265
+ # the hook's first call at parse time. Fills the role InputHandler.shape plays for input encoders.
266
+ input_shapes: Optional[Dict[str, List[int]]] = None
267
+ latent_space_aggregation: str = 'last_step'
268
+
242
269
  @dataclass
243
270
  class PredictionTypeHandler:
244
271
  name: str
@@ -277,8 +304,9 @@ class DatasetIntegrationSetup:
277
304
  metrics: List[MetricHandler] = field(default_factory=list)
278
305
  instance_metrics: List[InstanceMetricHandler] = field(default_factory=list)
279
306
  custom_layers: Dict[str, CustomLayerHandler] = field(default_factory=dict)
280
- custom_latent_spaces: Dict[str, CustomLatentSpaceHandler] = field(default_factory=dict)
307
+ custom_latent_space: Optional[CustomLatentSpaceHandler] = None
281
308
  simulations: List[SimulationHandler] = field(default_factory=list)
309
+ autoregressive_step: Optional[AutoregressiveStepHandler] = None
282
310
 
283
311
 
284
312
  @dataclass
@@ -289,6 +317,6 @@ class DatasetSample:
289
317
  metadata_is_none: Dict[str, bool]
290
318
  index: Union[int, str]
291
319
  state: DataStateEnum
292
- custom_latent_spaces: Optional[Dict[str, npt.NDArray[np.float32]]] = None
320
+ custom_latent_space: Optional[npt.NDArray[np.float32]] = None
293
321
  instance_masks: Optional[Dict[str, ElementInstance]] = None
294
322
 
@@ -13,7 +13,8 @@ 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
17
18
  from code_loader.contract.enums import LeapDataType, DataStateEnum, DataStateType, MetricDirection, DatasetMetadataType
18
19
  from code_loader.contract.mapping import NodeConnection, NodeMapping, NodeMappingType
19
20
  from code_loader.contract.responsedataclasses import DatasetTestResultPayload, LeapAnalysisConfiguration
@@ -511,34 +512,46 @@ class LeapBinder:
511
512
  """
512
513
  self.setup_container.metadata.append(MetadataHandler(name, function, metadata_type))
513
514
 
514
- def set_custom_latent_space(self, function: SectionCallableInterface,
515
- name: Optional[str] = None) -> None:
515
+ def set_custom_latent_space(self, function: SectionCallableInterface) -> None:
516
516
  """
517
- Register a custom latent space function.
518
-
519
- Multiple custom latent spaces may be registered as long as each has a
520
- unique name. They are stored name-keyed (insertion order preserved), so a
521
- later registration no longer overrides an earlier one.
517
+ Set a custom latent space function.
522
518
 
523
519
  Args:
524
- function (SectionCallableInterface): The latent-space handler function.
520
+ function (SectionCallableInterface): The metadata handler function.
525
521
  This function receives:
526
522
  subset (PreprocessResponse): The subset of the data.
527
523
  index (int): The index of the sample within the subset.
528
- This function should return a numpy float32 array containing the latent
529
- space vec of the sample.
530
- name (Optional[str]): Unique name for this custom latent space. Defaults to
531
- the reserved single-LS name for backward compatibility.
524
+ This function should numpy float32 array contains the latent space vec of the sample.
532
525
  """
533
- if name is None:
534
- name = custom_latent_space_attribute
535
- if name in self.setup_container.custom_latent_spaces:
536
- raise Exception(
537
- f"A custom latent space named '{name}' is already registered. Each "
538
- f"@tensorleap_custom_latent_space must have a unique name "
539
- f"(pass name='...' to distinguish them)."
540
- )
541
- self.setup_container.custom_latent_spaces[name] = CustomLatentSpaceHandler(function, name)
526
+ self.setup_container.custom_latent_space = CustomLatentSpaceHandler(function)
527
+
528
+ def set_autoregressive_step(self, function: AutoregressiveStepCallableInterface,
529
+ latent_space_aggregation: str = 'last_step') -> None:
530
+ """
531
+ Set the autoregressive step hook the feedback function that drives a chain:
532
+ it supplies the model's initial inputs on its first call (prev_inputs=None, prev_outputs=None)
533
+ and turns each step's inputs/outputs into the next step's inputs until it returns None.
534
+ An autoregressive integration has no input encoders; the hook is the sole input source.
535
+ latent_space_aggregation declares how the chain's latent-space vectors are derived from
536
+ its steps (see AUTOREGRESSIVE_LATENT_SPACE_AGGREGATIONS).
537
+ """
538
+ if self.setup_container.autoregressive_step is not None:
539
+ raise Exception('tensorleap_autoregressive_step is already defined. '
540
+ 'Only one autoregressive step hook is allowed per integration.')
541
+ if latent_space_aggregation not in AUTOREGRESSIVE_LATENT_SPACE_AGGREGATIONS:
542
+ raise Exception(f'tensorleap_autoregressive_step: unknown latent_space_aggregation '
543
+ f'{latent_space_aggregation!r}. Supported values: '
544
+ f'{", ".join(AUTOREGRESSIVE_LATENT_SPACE_AGGREGATIONS)}.')
545
+ self.setup_container.autoregressive_step = AutoregressiveStepHandler(
546
+ function, latent_space_aggregation=latent_space_aggregation)
547
+
548
+ # Builtin chain metadata, declared at parse time so it survives the reporter's
549
+ # metadata type mapping; the placeholder values are overwritten by the engine when a
550
+ # chain finalizes (realized length, truncated-by-safety-cap flag).
551
+ def builtin_chain_metadata(idx: Any, preprocess: PreprocessResponse) -> Dict[str, Any]:
552
+ return {'length': -1, 'truncated': False}
553
+
554
+ self.set_metadata(builtin_chain_metadata, 'builtin_chain')
542
555
 
543
556
  def set_custom_layer(self, custom_layer: Type[Any], name: str, inspect_layer: bool = False,
544
557
  kernel_index: Optional[int] = None, use_custom_latent_space: bool = False) -> None:
@@ -750,6 +763,7 @@ class LeapBinder:
750
763
  )
751
764
 
752
765
  def check(self) -> None:
766
+ self.validate_autoregressive_setup()
753
767
  preprocess_result = self.get_preprocess_result()
754
768
  self.check_preprocess(preprocess_result)
755
769
  self.check_handlers(preprocess_result)
@@ -757,6 +771,43 @@ class LeapBinder:
757
771
  self.validate_ignore_latent_spaces()
758
772
  print("Successful!")
759
773
 
774
+ def validate_autoregressive_setup(self) -> None:
775
+ """Structural rules for autoregressive integrations (no-op when the hook is absent).
776
+
777
+ Order-independent: runs after the whole script registered, because decorators may
778
+ appear in any order in the integration file.
779
+ """
780
+ if self.setup_container.autoregressive_step is None:
781
+ return
782
+ if self.setup_container.inputs:
783
+ raise Exception(
784
+ "An autoregressive integration must not declare input encoders — "
785
+ "tensorleap_autoregressive_step is the sole source of model inputs "
786
+ "(its first call returns the initial inputs). Found input encoders: "
787
+ f"{[handler.name for handler in self.setup_container.inputs]}.")
788
+ if self.setup_container.simulations:
789
+ raise Exception(
790
+ "Simulations are not supported together with tensorleap_autoregressive_step yet: "
791
+ "the autoregressive entry points do not resolve synthetic sample ids. Remove the "
792
+ "simulation decorators or the autoregressive hook.")
793
+ if self.setup_container.instance_masks:
794
+ raise Exception(
795
+ "Element instances are not supported together with tensorleap_autoregressive_step: "
796
+ "instance generation masks the sample's encoded inputs, which do not exist in an "
797
+ "autoregressive integration. Remove the instance encoders or the autoregressive hook.")
798
+ if self.setup_container.custom_latent_space is not None:
799
+ raise Exception(
800
+ "tensorleap_custom_latent_space is not supported together with "
801
+ "tensorleap_autoregressive_step: the custom latent space function only sees "
802
+ "(sample_id, preprocess) — it runs before generation and cannot observe the chain. "
803
+ "The chain's latent is the final step's model latent.")
804
+ if not self.setup_container.prediction_types:
805
+ raise Exception(
806
+ "An autoregressive integration must declare prediction types "
807
+ "(tensorleap_load_model(prediction_types=[...])): the engine keys each step's "
808
+ "model outputs by the declared names to build the hook's prev_outputs dict. "
809
+ "Without them, prev_outputs would be empty at runtime.")
810
+
760
811
  def validate_ignore_latent_spaces(self) -> None:
761
812
  """Validate leap_analysis_configuration.ignore_latent_spaces against this binder.
762
813
 
@@ -21,12 +21,12 @@ from code_loader.contract.datasetclasses import CustomCallableInterfaceMultiArgs
21
21
  CustomMultipleReturnCallableInterfaceMultiArgs, ConfusionMatrixCallableInterfaceMultiArgs, CustomCallableInterface, \
22
22
  VisualizerCallableInterface, MetadataSectionCallableInterface, PreprocessResponse, SectionCallableInterface, \
23
23
  ConfusionMatrixElement, SamplePreprocessResponse, PredictionTypeHandler, InstanceCallableInterface, ElementInstance, \
24
- InstanceLengthCallableInterface
24
+ InstanceLengthCallableInterface, AutoregressiveStepCallableInterface
25
25
  from code_loader.contract.enums import MetricDirection, LeapDataType, DatasetMetadataType, DataStateType
26
26
  from code_loader import leap_binder, LeapLoader
27
27
  from code_loader.contract.mapping import NodeMapping, NodeMappingType, NodeConnection
28
28
  from code_loader.contract.visualizer_classes import LeapImage, LeapImageMask, LeapTextMask, LeapText, LeapGraph, \
29
- LeapHorizontalBar, LeapImageWithBBox, LeapImageWithHeatmap, LeapVideo
29
+ LeapHorizontalBar, LeapImageWithBBox, LeapImageWithHeatmap, LeapVideo, LeapValidationError
30
30
  from code_loader.inner_leap_binder.leapbinder import mapping_runtime_mode_env_var_mame
31
31
  from code_loader.mixpanel_tracker import clear_integration_events, AnalyticsEvent, emit_integration_event_once
32
32
 
@@ -197,11 +197,11 @@ def validate_output_structure(result, func_name: str, expected_type_name="np.nda
197
197
  def batch_warning(result, func_name):
198
198
  if len(result.shape) > 0 and result.shape[0] == 1:
199
199
  warnings.warn(
200
- f"{func_name} warning: Tensorleap will add a batch dimension at axis 0 to the output of {func_name}, "
201
- f"although the detected size of axis 0 is already 1. "
202
- f"This may lead to an extra batch dimension (e.g., shape (1, 1, ...)). "
203
- f"Please ensure that the output of '{func_name}' is not already batched "
204
- f"to avoid computation errors."
200
+ f"{func_name} warning: Tensorleap will add a batch dimension at axis 0 of "
201
+ f"{func_name}'s output, but axis 0 is already size 1 — this can produce a "
202
+ f"doubly-batched shape (e.g. (1, 1, ...)). If axis 0 is a real dimension "
203
+ f"(e.g. a single channel) and not an accidental batch, you can ignore this; "
204
+ f"only fix it if you pre-batched the output."
205
205
  )
206
206
 
207
207
 
@@ -316,16 +316,16 @@ def tensorleap_integration_test():
316
316
  first_tb = traceback.extract_tb(e.__traceback__)[-1]
317
317
  file_name = Path(first_tb.filename).name
318
318
  line_number = first_tb.lineno
319
- if isinstance(e, TypeError) and 'is not subscriptable' in str(e):
320
- update_env_params_func("code_mapping", "x")
321
- raise (f'Invalid integration code. File {file_name}, line {line_number}: '
322
- f"indexing is supported only on the model's predictions inside the integration test. Please remove this indexing operation usage from the integration test code.")
319
+ update_env_params_func("code_mapping", "x")
320
+ if isinstance(e, LeapValidationError):
321
+ raise Exception(f'Invalid integration code. File {file_name}, line {line_number}: {e}')
322
+ elif isinstance(e, TypeError) and 'is not subscriptable' in str(e):
323
+ raise Exception(f'Invalid integration code. File {file_name}, line {line_number}: '
324
+ f"indexing is supported only on the model's predictions inside the integration test. Please remove this indexing operation usage from the integration test code.")
323
325
  else:
324
- update_env_params_func("code_mapping", "x")
325
-
326
- raise (f'Invalid integration code. File {file_name}, line {line_number}: '
327
- f'Integration test is only allowed to call Tensorleap decorators. '
328
- f'Ensure any arithmetics, external library use, Python logic is placed within Tensorleap decoders')
326
+ raise Exception(f'Invalid integration code. File {file_name}, line {line_number}: '
327
+ f'Integration test is only allowed to call Tensorleap decorators. '
328
+ f'Ensure any arithmetics, external library use, Python logic is placed within Tensorleap decoders')
329
329
  finally:
330
330
  if mapping_runtime_mode_env_var_mame in os.environ:
331
331
  del os.environ[mapping_runtime_mode_env_var_mame]
@@ -349,6 +349,14 @@ def _safe_get_item(key):
349
349
 
350
350
 
351
351
  def tensorleap_load_model(prediction_types: Optional[List[PredictionTypeHandler]] = _UNSET):
352
+ """Register the model-loading function.
353
+
354
+ ``prediction_types``: declare one ``PredictionTypeHandler`` per model output, in
355
+ output order. Multi-output models must declare an entry for EVERY output — the
356
+ count is validated against the model's outputs — so for an output you don't consume
357
+ as a prediction, pass a throwaway handler to satisfy the count. Omit the argument
358
+ only when the model has a single prediction output.
359
+ """
352
360
  prediction_types_was_provided = prediction_types is not _UNSET
353
361
 
354
362
  if not prediction_types_was_provided:
@@ -603,7 +611,9 @@ def tensorleap_load_model(prediction_types: Optional[List[PredictionTypeHandler]
603
611
 
604
612
  # onnx runtime interface
605
613
  def run(self, output_names, input_dict):
606
- assert output_names is None
614
+ if output_names is not None:
615
+ raise LeapValidationError("Tensorleap doesn't support selecting outputs by name — "
616
+ "use model.run(None, inputs) to return all outputs.")
607
617
  assert isinstance(input_dict, dict), \
608
618
  f'Expected input_dict to be a dict, got {type(input_dict)} instead.'
609
619
  for i, (input_key, elem) in enumerate(input_dict.items()):
@@ -1568,9 +1578,8 @@ def tensorleap_metadata(
1568
1578
  return decorating_function
1569
1579
 
1570
1580
 
1571
- def tensorleap_custom_latent_space(name: Optional[str] = None):
1581
+ def tensorleap_custom_latent_space():
1572
1582
  def decorating_function(user_function: SectionCallableInterface):
1573
- ls_name = name if name is not None else user_function.__name__
1574
1583
  def _validate_input_args(sample_id: Union[int, str], preprocess_response: PreprocessResponse):
1575
1584
  assert isinstance(sample_id, (int, str)), \
1576
1585
  (f'tensorleap_custom_latent_space validation failed: '
@@ -1590,9 +1599,9 @@ def tensorleap_custom_latent_space(name: Optional[str] = None):
1590
1599
  if result.ndim > 1:
1591
1600
  flat_dim = int(np.prod(result.shape))
1592
1601
  store_general_warning(
1593
- key=("tensorleap_custom_latent_space_flatten", ls_name, tuple(result.shape)),
1602
+ key=("tensorleap_custom_latent_space_flatten", tuple(result.shape)),
1594
1603
  message=(
1595
- f"tensorleap_custom_latent_space '{ls_name}' returned per-sample shape {tuple(result.shape)} "
1604
+ f"tensorleap_custom_latent_space returned per-sample shape {tuple(result.shape)} "
1596
1605
  f"(ndim={result.ndim}). Tensorleap assumes per-sample shape (d, ...) and will "
1597
1606
  f"flatten to ({flat_dim},) before downstream visualization and clustering. "
1598
1607
  f"If you want a different aggregation (e.g. global average pooling), do it "
@@ -1611,7 +1620,7 @@ def tensorleap_custom_latent_space(name: Optional[str] = None):
1611
1620
 
1612
1621
  return result
1613
1622
 
1614
- leap_binder.set_custom_latent_space(inner_without_validate, ls_name)
1623
+ leap_binder.set_custom_latent_space(inner_without_validate)
1615
1624
 
1616
1625
  def inner(sample_id, preprocess_response):
1617
1626
  if os.environ.get(mapping_runtime_mode_env_var_mame):
@@ -1629,6 +1638,246 @@ def tensorleap_custom_latent_space(name: Optional[str] = None):
1629
1638
  return decorating_function
1630
1639
 
1631
1640
 
1641
+ def tensorleap_autoregressive_step(latent_space_aggregation: str = 'last_step'):
1642
+ """The feedback hook that drives an autoregressive chain.
1643
+
1644
+ Signature of the decorated function:
1645
+ (sample_id, prev_inputs, prev_outputs, preprocess) -> Optional[Dict[str, np.ndarray]]
1646
+
1647
+ First call per chain: prev_inputs=None, prev_outputs=None — returns the initial model inputs.
1648
+ Later calls receive the previous step's model inputs and outputs (unbatched, per-sample) and
1649
+ return the next step's full input dict; returning None ends the chain. Keys starting with '_'
1650
+ are passthrough state: never fed to the model, handed back verbatim in prev_inputs next call.
1651
+ The hook must be stateless and deterministically seeded (seed stochasticity from sample_id) —
1652
+ the engine replays steps after crash recovery and relies on identical results.
1653
+
1654
+ latent_space_aggregation ('last_step' | 'mean') declares how the chain's latent-space
1655
+ vectors are derived from its per-step forward passes. 'last_step' (default): every latent
1656
+ space comes from the final step's forward pass, except input-kind latent spaces which come
1657
+ from the first step — the original sample, before generated content dominates the model
1658
+ inputs. 'mean': every latent space is the elementwise mean over all steps of the chain.
1659
+ """
1660
+ assert isinstance(latent_space_aggregation, str), \
1661
+ ('tensorleap_autoregressive_step must be called with parentheses: '
1662
+ '@tensorleap_autoregressive_step() or '
1663
+ "@tensorleap_autoregressive_step(latent_space_aggregation='mean').")
1664
+
1665
+ def decorating_function(user_function: AutoregressiveStepCallableInterface):
1666
+ # Contract: shapes and key set are fixed across all calls AND all chains (compiled graphs
1667
+ # need static shapes), so one record shared across samples is the correct scope.
1668
+ first_call_record: Dict[str, Any] = {'keys': None, 'shapes': None}
1669
+
1670
+ def _normalize_args(args, kwargs):
1671
+ expected_names = ["sample_id", "prev_inputs", "prev_outputs", "preprocess"]
1672
+ validate_args_structure(
1673
+ *args,
1674
+ types_order=[Union[int, str], Union[dict, type(None)], Union[dict, type(None)],
1675
+ PreprocessResponse],
1676
+ func_name=user_function.__name__, expected_names=expected_names, **kwargs)
1677
+ normalized = []
1678
+ for i, name in enumerate(expected_names):
1679
+ normalized.append(args[i] if i < len(args) else kwargs[name])
1680
+ return normalized
1681
+
1682
+ def _validate_input_args(sample_id, prev_inputs, prev_outputs, preprocess_response):
1683
+ assert (prev_inputs is None) == (prev_outputs is None), \
1684
+ (f'{user_function.__name__}() validation failed: '
1685
+ f'prev_inputs and prev_outputs must both be None (first call) or both be dicts '
1686
+ f'(subsequent calls). Got prev_inputs={type(prev_inputs).__name__}, '
1687
+ f'prev_outputs={type(prev_outputs).__name__}.')
1688
+ assert type(sample_id) == preprocess_response.sample_id_type, \
1689
+ (f'{user_function.__name__}() validation failed: '
1690
+ f'Argument sample_id should be as the same type as defined in the preprocess response '
1691
+ f'{preprocess_response.sample_id_type}. Got {type(sample_id)}.')
1692
+
1693
+ def _validate_result(result, is_first_call):
1694
+ if result is None:
1695
+ assert not is_first_call, \
1696
+ (f'{user_function.__name__}() validation failed: '
1697
+ f'the first call (prev_inputs=None, prev_outputs=None) must return the initial '
1698
+ f'model inputs dict — returning None on the first call would produce an empty chain.')
1699
+ return
1700
+ assert isinstance(result, dict), \
1701
+ (f'{user_function.__name__}() validation failed: '
1702
+ f'the return value must be a dict of named model input tensors, or None to end the '
1703
+ f'chain. Got {type(result)}.')
1704
+ model_input_keys = [key for key in result if not key.startswith('_')]
1705
+ assert model_input_keys, \
1706
+ (f'{user_function.__name__}() validation failed: '
1707
+ f'the returned dict contains no model inputs — every key starts with "_" '
1708
+ f'(underscore keys are passthrough state, never fed to the model).')
1709
+ for key, value in result.items():
1710
+ assert isinstance(key, str), \
1711
+ (f'{user_function.__name__}() validation failed: '
1712
+ f'all keys in the returned dict must be strings. Got {type(key)}.')
1713
+ if key.startswith('_'):
1714
+ continue
1715
+ assert isinstance(value, np.ndarray), \
1716
+ (f'{user_function.__name__}() validation failed: '
1717
+ f'value for model input "{key}" must be a numpy array. Got {type(value)}.')
1718
+
1719
+ def _validate_consistency(result):
1720
+ # Rules 2-3 of the autoregressive contract: identical key set and identical
1721
+ # (non-passthrough) shapes on every call — compiled graphs need static shapes.
1722
+ if result is None:
1723
+ return
1724
+ keys = set(result.keys())
1725
+ shapes = {key: tuple(value.shape) for key, value in result.items()
1726
+ if not key.startswith('_') and isinstance(value, np.ndarray)}
1727
+ if first_call_record['keys'] is None:
1728
+ first_call_record['keys'] = keys
1729
+ first_call_record['shapes'] = shapes
1730
+ return
1731
+ assert keys == first_call_record['keys'], \
1732
+ (f'{user_function.__name__}() validation failed: '
1733
+ f'every call must return the same key set as the first call. '
1734
+ f'First call keys: {sorted(first_call_record["keys"])}, this call: {sorted(keys)}.')
1735
+ for key, shape in shapes.items():
1736
+ assert shape == first_call_record['shapes'][key], \
1737
+ (f'{user_function.__name__}() validation failed: '
1738
+ f'tensor shapes must be identical on every call (fixed-length padding + mask for '
1739
+ f'growing sequences). Input "{key}" was {first_call_record["shapes"][key]} on the '
1740
+ f'first call and {shape} now.')
1741
+
1742
+ def _assert_deterministic(first_result, second_result):
1743
+ error_message = (
1744
+ f'{user_function.__name__}() validation failed: '
1745
+ f'the hook is not deterministic — two calls with identical arguments returned different '
1746
+ f'outputs. Seed any stochasticity (e.g. initial noise) from sample_id: the engine '
1747
+ f'replays steps after crash recovery and relies on identical results.')
1748
+ if first_result is None or second_result is None:
1749
+ assert first_result is None and second_result is None, error_message
1750
+ return
1751
+ assert set(first_result.keys()) == set(second_result.keys()), error_message
1752
+ for key, value in first_result.items():
1753
+ if key.startswith('_') or not isinstance(value, np.ndarray):
1754
+ continue
1755
+ assert np.array_equal(value, second_result[key]), error_message
1756
+
1757
+ def _squeeze_test_batch_dim(tensors_dict):
1758
+ # Inside the integration test the model works on batch-1 tensors (this wrapper adds the
1759
+ # batch axis to returned inputs, and model outputs come back batched). The user's hook
1760
+ # body must see the same unbatched per-sample tensors it will see at engine runtime, so
1761
+ # strip the leading batch-1 axis before calling it.
1762
+ if tensors_dict is None:
1763
+ return None
1764
+ squeezed = {}
1765
+ for key, value in tensors_dict.items():
1766
+ if not key.startswith('_') and isinstance(value, np.ndarray) \
1767
+ and value.ndim > 0 and value.shape[0] == 1:
1768
+ squeezed[key] = value[0]
1769
+ else:
1770
+ squeezed[key] = value
1771
+ return squeezed
1772
+
1773
+ def inner_without_validate(sample_id, prev_inputs, prev_outputs, preprocess_response):
1774
+ global _called_from_inside_tl_decorator
1775
+ _called_from_inside_tl_decorator += 1
1776
+ try:
1777
+ result = user_function(sample_id, prev_inputs, prev_outputs, preprocess_response)
1778
+ finally:
1779
+ _called_from_inside_tl_decorator -= 1
1780
+ return result
1781
+
1782
+ leap_binder.set_autoregressive_step(inner_without_validate,
1783
+ latent_space_aggregation=latent_space_aggregation)
1784
+
1785
+ def inner(*args, **kwargs):
1786
+ if not _call_from_tl_platform:
1787
+ set_current('tensorleap_autoregressive_step')
1788
+ sample_id, prev_inputs, prev_outputs, preprocess_response = _normalize_args(args, kwargs)
1789
+
1790
+ is_top_level_in_test = (_called_from_inside_tl_decorator == 0
1791
+ and _called_from_inside_tl_integration_test_decorator)
1792
+ if is_top_level_in_test:
1793
+ prev_inputs = _squeeze_test_batch_dim(prev_inputs)
1794
+ prev_outputs = _squeeze_test_batch_dim(prev_outputs)
1795
+
1796
+ _validate_input_args(sample_id, prev_inputs, prev_outputs, preprocess_response)
1797
+
1798
+ result = inner_without_validate(sample_id, prev_inputs, prev_outputs, preprocess_response)
1799
+
1800
+ if is_top_level_in_test:
1801
+ # Rule 7: determinism double-call. An unseeded sampler fails here, on the user's
1802
+ # machine, instead of silently forking a chain on engine crash-replay.
1803
+ second_result = inner_without_validate(sample_id, prev_inputs, prev_outputs,
1804
+ preprocess_response)
1805
+ _assert_deterministic(result, second_result)
1806
+
1807
+ _validate_result(result, is_first_call=prev_inputs is None)
1808
+ _validate_consistency(result)
1809
+
1810
+ if is_top_level_in_test and result is not None:
1811
+ result = {key: (np.expand_dims(value, axis=0)
1812
+ if not key.startswith('_') and isinstance(value, np.ndarray) else value)
1813
+ for key, value in result.items()}
1814
+
1815
+ if not _call_from_tl_platform:
1816
+ update_env_params_func("tensorleap_autoregressive_step", "v")
1817
+ return result
1818
+
1819
+ class _MappingInputsPlaceholder:
1820
+ """Mapping-mode stand-in for the hook's returned dict.
1821
+
1822
+ Indexing it by a model-input name yields a placeholder whose node_mapping the model
1823
+ placeholder tags Input0/Input1/... — exactly how input encoders wire the graph. One
1824
+ NodeConnection is recorded per non-passthrough key, once.
1825
+ """
1826
+ def __init__(self):
1827
+ self._items: Dict[str, Any] = {}
1828
+
1829
+ def __getitem__(self, key):
1830
+ assert isinstance(key, str), \
1831
+ f'Expected a string model input name, got {type(key)} instead.'
1832
+ if key not in self._items:
1833
+ class TempMapping:
1834
+ pass
1835
+ ret = TempMapping()
1836
+ ret.node_mapping = NodeMapping(key, NodeMappingType.Input)
1837
+ if not key.startswith('_'):
1838
+ leap_binder.mapping_connections.append(NodeConnection(ret.node_mapping, None))
1839
+ self._items[key] = ret
1840
+ return self._items[key]
1841
+
1842
+ def get(self, key, default=None):
1843
+ # Deliberate alias of [] — the fixed-key-set contract makes a defaulted lookup
1844
+ # meaningless in mapping mode, and every accessed key must wire into the graph.
1845
+ return self[key]
1846
+
1847
+ def _unsupported_iteration(self):
1848
+ raise Exception(
1849
+ 'Iterating over the autoregressive hook result is not supported inside the '
1850
+ 'integration test. Pass the model its inputs with explicit keys, e.g. '
1851
+ "model.run(None, {'x': inputs['x'], 't': inputs['t']}).")
1852
+
1853
+ def items(self):
1854
+ self._unsupported_iteration()
1855
+
1856
+ def keys(self):
1857
+ self._unsupported_iteration()
1858
+
1859
+ def values(self):
1860
+ self._unsupported_iteration()
1861
+
1862
+ mapping_placeholder = _MappingInputsPlaceholder()
1863
+
1864
+ def mapping_inner(*args, **kwargs):
1865
+ return mapping_placeholder
1866
+
1867
+ def final_inner(*args, **kwargs):
1868
+ if os.environ.get(mapping_runtime_mode_env_var_mame):
1869
+ return mapping_inner(*args, **kwargs)
1870
+ else:
1871
+ return inner(*args, **kwargs)
1872
+
1873
+ if not _call_from_tl_platform:
1874
+ register_decorator_row("tensorleap_autoregressive_step")
1875
+
1876
+ return final_inner
1877
+
1878
+ return decorating_function
1879
+
1880
+
1632
1881
  def tensorleap_preprocess():
1633
1882
  def decorating_function(user_function: Callable[[], List[PreprocessResponse]]):
1634
1883
  leap_binder.set_preprocess(user_function)
@@ -2428,12 +2677,12 @@ def tensorleap_status_table():
2428
2677
  ready = False
2429
2678
  next_step = row["name"]
2430
2679
 
2431
- if _crashed["value"]:
2432
- print(f"\nScript crashed before completing all steps. crashed at function '{_current_func['name']}'.")
2680
+ if code_mapping_failure[0]:
2681
+ print(f"\n{CROSS} {code_mapping_failure_mes}")
2433
2682
  return
2434
2683
 
2435
- if code_mapping_failure[0]:
2436
- print(f"\n{CROSS + code_mapping_failure_mes}.")
2684
+ if _crashed["value"]:
2685
+ print(f"\nScript crashed before completing all steps. crashed at function '{_current_func['name']}'.")
2437
2686
  return
2438
2687
 
2439
2688
  print(ready_mess) if ready else print(
@@ -2448,6 +2697,10 @@ def tensorleap_status_table():
2448
2697
  code_mapping_failure[0] = 1
2449
2698
  if status == "v":
2450
2699
  _set_status(name, CHECK)
2700
+ # A decorator that reports success is no longer the "currently running"
2701
+ # one, so clear the pointer. This keeps a later body-level raise from
2702
+ # being blamed on the last decorator that actually succeeded.
2703
+ _current_func["name"] = None
2451
2704
  else:
2452
2705
  _set_status(name, CROSS)
2453
2706
 
@@ -2459,6 +2712,12 @@ def tensorleap_status_table():
2459
2712
  table.append({"name": "tensorleap simulation (optional)", "Added to integration": UNKNOWN})
2460
2713
  _sim_tracking[sim_name] = "registered"
2461
2714
 
2715
+ def register_decorator_row(name: str):
2716
+ # Rows for decorators that only apply to some integration kinds (e.g. the autoregressive
2717
+ # step hook) are appended on registration, so unrelated integrations never show them.
2718
+ if _find_row(_remove_suffix(name, " (optional)")) is None:
2719
+ table.append({"name": name, "Added to integration": UNKNOWN})
2720
+
2462
2721
  def mark_sim_result(sim_name: str, passed: bool):
2463
2722
  _sim_tracking[sim_name] = "passed" if passed else "failed"
2464
2723
 
@@ -2494,10 +2753,14 @@ def tensorleap_status_table():
2494
2753
  def handle_exception(exc_type, exc_value, exc_traceback):
2495
2754
  _crashed["value"] = True
2496
2755
  crashed_name = _current_func["name"]
2497
- if crashed_name:
2756
+ if crashed_name and not code_mapping_failure[0]:
2498
2757
  row = _find_row(crashed_name)
2499
2758
  if row:
2500
2759
  row["Added to integration"] = CROSS
2760
+ elif not crashed_name:
2761
+ # Crash with no decorator currently running = the integration-test body /
2762
+ # code flow itself failed; report that rather than blaming a decorator.
2763
+ code_mapping_failure[0] = 1
2501
2764
 
2502
2765
  traceback.print_exception(exc_type, exc_value, exc_traceback)
2503
2766
  run_on_exit()
@@ -2505,11 +2768,12 @@ def tensorleap_status_table():
2505
2768
  atexit.register(run_on_exit)
2506
2769
  sys.excepthook = handle_exception
2507
2770
 
2508
- return set_current, update_env_params, register_sim, mark_sim_result
2771
+ return set_current, update_env_params, register_sim, mark_sim_result, register_decorator_row
2509
2772
 
2510
2773
 
2511
2774
  if not _call_from_tl_platform:
2512
- set_current, update_env_params_func, register_sim, mark_sim_result = tensorleap_status_table()
2775
+ set_current, update_env_params_func, register_sim, mark_sim_result, register_decorator_row = \
2776
+ tensorleap_status_table()
2513
2777
 
2514
2778
 
2515
2779
 
@@ -251,7 +251,11 @@ class LeapLoader(LeapLoaderBase):
251
251
 
252
252
  metadata, metadata_is_none = self.get_metadata(state, sample_id)
253
253
 
254
- custom_latent_spaces = self._get_custom_latent_spaces(sample_id, preprocess_result[state])
254
+ custom_latent_space = None
255
+ if global_leap_binder.setup_container.custom_latent_space is not None:
256
+ custom_latent_space = global_leap_binder.setup_container.custom_latent_space.function(sample_id,
257
+ preprocess_result[
258
+ state])
255
259
  instance_mask = self._get_instances_masks(state, sample_id, instance_id)
256
260
  sample = DatasetSample(inputs=self._get_inputs(state, sample_id),
257
261
  gt=None if state == DataStateEnum.unlabeled else self._get_gt(state, sample_id),
@@ -259,7 +263,7 @@ class LeapLoader(LeapLoaderBase):
259
263
  metadata_is_none=metadata_is_none,
260
264
  index=sample_id,
261
265
  state=state,
262
- custom_latent_spaces=custom_latent_spaces,
266
+ custom_latent_space=custom_latent_space,
263
267
  instance_masks=instance_mask)
264
268
  return sample
265
269
 
@@ -290,7 +294,11 @@ class LeapLoader(LeapLoaderBase):
290
294
  handler.name, handler_result
291
295
  )
292
296
 
293
- custom_latent_spaces = self._get_custom_latent_spaces(original_sample_id, preprocess)
297
+ custom_latent_space = None
298
+ if global_leap_binder.setup_container.custom_latent_space is not None:
299
+ custom_latent_space = global_leap_binder.setup_container.custom_latent_space.function(
300
+ original_sample_id, preprocess
301
+ )
294
302
 
295
303
  return DatasetSample(
296
304
  inputs=inputs,
@@ -299,7 +307,7 @@ class LeapLoader(LeapLoaderBase):
299
307
  metadata_is_none=metadata_is_none,
300
308
  index=synthetic_index,
301
309
  state=DataStateEnum.additional,
302
- custom_latent_spaces=custom_latent_spaces,
310
+ custom_latent_space=custom_latent_space,
303
311
  instance_masks=None,
304
312
  )
305
313
 
@@ -314,12 +322,16 @@ class LeapLoader(LeapLoaderBase):
314
322
 
315
323
  integration_test_test_payload = self._check_integration_test_exists()
316
324
  test_payloads.append(integration_test_test_payload)
325
+ global_leap_binder.validate_autoregressive_setup()
317
326
  preprocess_test_payload = self._check_preprocess()
318
327
  test_payloads.append(preprocess_test_payload)
319
328
  handlers_test_payloads = self._check_handlers()
320
329
  test_payloads.extend(handlers_test_payloads)
321
330
  simulation_test_payloads = self._check_simulations()
322
331
  test_payloads.extend(simulation_test_payloads)
332
+ autoregressive_test_payload = self._check_autoregressive_step()
333
+ if autoregressive_test_payload is not None:
334
+ test_payloads.append(autoregressive_test_payload)
323
335
  is_valid = all([payload.is_passed for payload in test_payloads])
324
336
  setup_response = self.get_dataset_setup_response(handlers_test_payloads)
325
337
 
@@ -456,6 +468,76 @@ class LeapLoader(LeapLoaderBase):
456
468
  result_payloads.append(test_result)
457
469
  return result_payloads
458
470
 
471
+ def _check_autoregressive_step(self) -> Optional[DatasetTestResultPayload]:
472
+ handler = global_leap_binder.setup_container.autoregressive_step
473
+ if handler is None:
474
+ return None
475
+ test_result = DatasetTestResultPayload(handler.name)
476
+ try:
477
+ preprocess_result = self._preprocess_result()
478
+ input_shapes: Dict[str, List[int]] = {}
479
+ for state, preprocess_response in preprocess_result.items():
480
+ if preprocess_response.sample_ids_to_instance_mappings:
481
+ raise Exception('Element instances are not supported together with '
482
+ 'tensorleap_autoregressive_step.')
483
+ sample_id = preprocess_response.sample_ids[0]
484
+ first_result = handler.function(sample_id, None, None, preprocess_response)
485
+ second_result = handler.function(sample_id, None, None, preprocess_response)
486
+ if not isinstance(first_result, dict):
487
+ raise Exception('The autoregressive step hook must return a dict of initial model '
488
+ f'inputs on its first call, got {type(first_result)} '
489
+ f'(state: {state.name}).')
490
+ if not isinstance(second_result, dict) or \
491
+ set(second_result.keys()) != set(first_result.keys()):
492
+ raise Exception('The autoregressive step hook is not deterministic — two calls '
493
+ 'with identical arguments returned different results '
494
+ f'(state: {state.name}). Seed any stochasticity from '
495
+ 'sample_id: the engine replays steps after crash recovery '
496
+ 'and relies on identical results.')
497
+ model_input_items = {key: value for key, value in first_result.items()
498
+ if not key.startswith('_')}
499
+ if not model_input_items:
500
+ raise Exception('The autoregressive step hook returned no model inputs on its first '
501
+ 'call — every key starts with "_" (underscore keys are passthrough '
502
+ f'state, never fed to the model). State: {state.name}.')
503
+ for key, value in model_input_items.items():
504
+ if not isinstance(value, np.ndarray):
505
+ raise Exception(f'The autoregressive step hook returned a non-numpy value for '
506
+ f'model input "{key}" ({type(value)}). State: {state.name}.')
507
+ if key in input_shapes and input_shapes[key] != list(value.shape):
508
+ raise Exception(f'The autoregressive step hook returned different shapes for '
509
+ f'model input "{key}" across states: {input_shapes[key]} vs '
510
+ f'{list(value.shape)}. Shapes must be fixed.')
511
+ input_shapes[key] = list(value.shape)
512
+ if not np.array_equal(value, second_result[key]):
513
+ raise Exception('The autoregressive step hook is not deterministic — two calls '
514
+ 'with identical arguments returned different outputs for model '
515
+ f'input "{key}". Seed any stochasticity from sample_id: the '
516
+ 'engine replays steps after crash recovery and relies on '
517
+ 'identical results.')
518
+ handler.input_shapes = input_shapes
519
+ except Exception as e:
520
+ line_number, file_name, stacktrace = get_root_exception_file_and_line_number()
521
+ test_result.display[TestingSectionEnum.Errors.name] = \
522
+ f"{repr(e)} in file {file_name}, line_number: {line_number}\nStacktrace:\n{stacktrace}"
523
+ test_result.is_passed = False
524
+ return test_result
525
+
526
+ def _ensure_autoregressive_input_shapes(self) -> Dict[str, List[int]]:
527
+ handler = global_leap_binder.setup_container.autoregressive_step
528
+ assert handler is not None
529
+ if handler.input_shapes is None:
530
+ preprocess_result = self._preprocess_result()
531
+ first_state_response = next(iter(preprocess_result.values()))
532
+ first_result = handler.function(first_state_response.sample_ids[0], None, None,
533
+ first_state_response)
534
+ if not isinstance(first_result, dict):
535
+ raise Exception('The autoregressive step hook must return a dict of initial model '
536
+ f'inputs on its first call, got {type(first_result)}.')
537
+ handler.input_shapes = {key: list(value.shape) for key, value in first_result.items()
538
+ if not key.startswith('_') and isinstance(value, np.ndarray)}
539
+ return handler.input_shapes
540
+
459
541
  def run_simulation(self, sim_name, params=None, n_samples=1, seed=0,
460
542
  sample_ids=None, extend_preprocess=True):
461
543
  # type: (str, Optional[Dict[str, Any]], int, int, Optional[List[str]], bool) -> Dict[str, Any]
@@ -646,6 +728,14 @@ class LeapLoader(LeapLoaderBase):
646
728
  raise Exception(f"cant calculate shape for input, input name:{inp.name}")
647
729
  inputs.append(DatasetInputInstance(name=inp.name, shape=inp.shape, channel_dim=inp.channel_dim))
648
730
 
731
+ if setup.autoregressive_step is not None:
732
+ # The hook is the sole input source; report its step-0 inputs as the dataset inputs so
733
+ # graph building, input-coverage validation and type dictionaries see real specs.
734
+ # channel_dim=-1 is intentional for AR inputs — there is no InputHandler to carry a
735
+ # user-declared value; expose it on the decorator if a use case ever needs otherwise.
736
+ for input_name, input_shape in self._ensure_autoregressive_input_shapes().items():
737
+ inputs.append(DatasetInputInstance(name=input_name, shape=input_shape, channel_dim=-1))
738
+
649
739
  ground_truths = []
650
740
  for gt in setup.ground_truths:
651
741
  if gt.shape is None:
@@ -753,7 +843,21 @@ class LeapLoader(LeapLoaderBase):
753
843
  return result_agg
754
844
 
755
845
  def _get_inputs(self, state: DataStateEnum, sample_id: Union[int, str]) -> Dict[str, npt.NDArray[np.float32]]:
756
- return self._get_dataset_handlers(global_leap_binder.setup_container.inputs, state, sample_id)
846
+ inputs = self._get_dataset_handlers(global_leap_binder.setup_container.inputs, state, sample_id)
847
+ autoregressive_handler = global_leap_binder.setup_container.autoregressive_step
848
+ if autoregressive_handler is not None:
849
+ # Autoregressive integrations have no input encoders — the hook's first call supplies
850
+ # the sample's (step-0) model inputs, so every sample-fetching flow (graph validation,
851
+ # analysis, import checks) sees a fully-populated inputs dict.
852
+ preprocess_response = self._preprocess_result()[state]
853
+ step_zero_inputs = autoregressive_handler.function(sample_id, None, None, preprocess_response)
854
+ if not isinstance(step_zero_inputs, dict):
855
+ raise Exception(
856
+ 'The autoregressive step hook must return a dict of initial model inputs on its '
857
+ f'first call, got {type(step_zero_inputs)} for sample {sample_id}.')
858
+ inputs.update({key: value for key, value in step_zero_inputs.items()
859
+ if not key.startswith('_')})
860
+ return inputs
757
861
 
758
862
  def _get_instances_masks(self, state: DataStateEnum, sample_id: Union[int, str], instance_id: int) -> Optional[Dict[str, ElementInstance]]:
759
863
  if instance_id is None:
@@ -776,6 +880,12 @@ class LeapLoader(LeapLoaderBase):
776
880
  def _get_gt(self, state: DataStateEnum, sample_id: Union[int, str]) -> Dict[str, npt.NDArray[np.float32]]:
777
881
  return self._get_dataset_handlers(global_leap_binder.setup_container.ground_truths, state, sample_id)
778
882
 
883
+ def get_gt(self, state: DataStateEnum, sample_id: Union[int, str]) -> Dict[str, npt.NDArray[np.float32]]:
884
+ """Ground-truth tensors only ({} when no GT encoders are declared) — lets the engine's
885
+ rollout fetch a chain's GT without re-running the autoregressive hook via get_sample."""
886
+ self.exec_script()
887
+ return self._get_gt(state, sample_id)
888
+
779
889
  @lru_cache()
780
890
  def _metadata_name_to_type(self) -> Dict[str, DatasetMetadataType]:
781
891
  global_leap_binder.check_preprocess(self._preprocess_result())
@@ -866,29 +976,72 @@ class LeapLoader(LeapLoaderBase):
866
976
 
867
977
  return id_type
868
978
 
869
- @staticmethod
870
- def _get_custom_latent_spaces(
871
- sample_id: Union[int, str],
872
- preprocess: "PreprocessResponse") -> Optional[Dict[str, npt.NDArray[np.float32]]]:
873
- handlers = global_leap_binder.setup_container.custom_latent_spaces
874
- if not handlers:
875
- return None
876
- return {name: handler.function(sample_id, preprocess) for name, handler in handlers.items()}
877
-
878
979
  @lru_cache()
879
980
  def has_custom_latent_space_decorator(self) -> bool:
880
981
  self.exec_script()
881
- return len(global_leap_binder.setup_container.custom_latent_spaces) > 0
982
+ return global_leap_binder.setup_container.custom_latent_space is not None
882
983
 
883
984
  @lru_cache()
884
- def get_custom_latent_space_names(self) -> Tuple[str, ...]:
885
- """Ordered names of all registered custom latent spaces.
985
+ def has_autoregressive_step(self) -> bool:
986
+ self.exec_script()
987
+ return global_leap_binder.setup_container.autoregressive_step is not None
886
988
 
887
- Registration order is the canonical index mapping consumed by the engine
888
- (name i -> user_custom_i). Returns a tuple so the lru_cache value is hashable.
989
+ @lru_cache()
990
+ def get_autoregressive_latent_space_aggregation(self) -> str:
991
+ self.exec_script()
992
+ handler = global_leap_binder.setup_container.autoregressive_step
993
+ if handler is None:
994
+ return 'last_step'
995
+ return handler.latent_space_aggregation
996
+
997
+ def run_autoregressive_step(self, sample_id: Union[int, str],
998
+ prev_inputs: Optional[Dict[str, npt.NDArray[np.float32]]],
999
+ prev_outputs: Optional[Dict[str, npt.NDArray[np.float32]]],
1000
+ state: DataStateEnum) -> Optional[Dict[str, npt.NDArray[np.float32]]]:
1001
+ """Execute the user's autoregressive step hook against the in-process preprocess result.
1002
+
1003
+ This is the engine rollout's per-step entry point (invoked on the generic pod). The first
1004
+ call per chain passes prev_inputs=None, prev_outputs=None and returns the initial model
1005
+ inputs; a None return ends the chain.
889
1006
  """
890
1007
  self.exec_script()
891
- return tuple(global_leap_binder.setup_container.custom_latent_spaces.keys())
1008
+ handler = global_leap_binder.setup_container.autoregressive_step
1009
+ if handler is None:
1010
+ raise Exception('run_autoregressive_step was called but the integration has no '
1011
+ 'tensorleap_autoregressive_step hook.')
1012
+ preprocess_response = self._preprocess_result()[state]
1013
+ result = handler.function(sample_id, prev_inputs, prev_outputs, preprocess_response)
1014
+ if result is None:
1015
+ return None
1016
+ if not isinstance(result, dict):
1017
+ raise Exception('The autoregressive step hook must return a dict of model inputs or '
1018
+ f'None to end the chain, got {type(result)} for sample {sample_id}.')
1019
+ # Parse-time validation only exercises step 0; enforce the fixed key-set/shape contract
1020
+ # on every step here so drift fails with an actionable message instead of a cryptic
1021
+ # engine error on a compiled graph.
1022
+ expected_shapes = self._ensure_autoregressive_input_shapes()
1023
+ model_inputs = {key: value for key, value in result.items() if not key.startswith('_')}
1024
+ if set(model_inputs) != set(expected_shapes):
1025
+ raise Exception('The autoregressive step hook must return the same model input keys '
1026
+ f'on every call. Expected {sorted(expected_shapes)}, got '
1027
+ f'{sorted(model_inputs)} for sample {sample_id}.')
1028
+ for key, value in model_inputs.items():
1029
+ if isinstance(value, np.ndarray) and list(value.shape) != expected_shapes[key]:
1030
+ raise Exception('The autoregressive step hook must return fixed tensor shapes on '
1031
+ f'every call (use fixed-length padding + mask for growing '
1032
+ f'sequences). Input "{key}" was {expected_shapes[key]} on the '
1033
+ f'first call and {list(value.shape)} now (sample {sample_id}).')
1034
+ return result
1035
+
1036
+ def get_prediction_names_in_order(self) -> List[str]:
1037
+ """Model output names by declared prediction-type order, used to key prev_outputs for the
1038
+ hook. AR integrations must declare prediction types (validate_autoregressive_setup) and
1039
+ the count is checked against the model's outputs at integration-test time; the declared
1040
+ ORDER matching the model's output order is the user's responsibility — it is not
1041
+ validated."""
1042
+ self.exec_script()
1043
+ return [prediction_type.name for prediction_type in
1044
+ global_leap_binder.setup_container.prediction_types]
892
1045
 
893
1046
  def get_instances_data(self, state: DataStateEnum) -> Tuple[Dict[str, List[str]], Dict[str, str]]:
894
1047
  preprocess_result = self._preprocess_result()
@@ -149,9 +149,10 @@ class LeapLoaderBase:
149
149
  def has_custom_latent_space_decorator(self) -> bool:
150
150
  pass
151
151
 
152
- @abstractmethod
153
- def get_custom_latent_space_names(self) -> Tuple[str, ...]:
154
- pass
152
+ def has_autoregressive_step(self) -> bool:
153
+ # Default False so existing LeapLoaderBase implementations stay valid; the concrete
154
+ # loaders that can answer (LeapLoader in-process, LeapLoaderWithRedis via RPC) override.
155
+ return False
155
156
 
156
157
  @abstractmethod
157
158
  def get_heatmap_visualizer_raw_vis_input_arg_name(self, visualizer_name: str) -> Optional[str]:
@@ -1,7 +1,7 @@
1
1
  [tool.poetry]
2
2
  name = "code-loader"
3
3
 
4
- version = "1.0.191.dev0"
4
+ version = "1.0.192"
5
5
  description = ""
6
6
  authors = ["dorhar <doron.harnoy@tensorleap.ai>"]
7
7
  license = "MIT"