code-loader 1.0.195.dev1__py3-none-any.whl → 1.0.197.dev0__py3-none-any.whl

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.
@@ -1,4 +1,6 @@
1
1
  # mypy: ignore-errors
2
+ import copy
3
+ import hashlib
2
4
  import inspect
3
5
  import os
4
6
  import warnings
@@ -13,7 +15,8 @@ from typing import Optional, Union, Callable, List, Dict, get_args, get_origin,
13
15
  import numpy as np
14
16
  import numpy.typing as npt
15
17
 
16
- from code_loader.utils import get_metadata_type_from_variable, map_dict_to_metadata_types
18
+ from code_loader.utils import get_metadata_type_from_variable, map_dict_to_metadata_types, \
19
+ validate_autoregressive_state_types, autoregressive_nests_equal
17
20
 
18
21
  logger = logging.getLogger(__name__)
19
22
 
@@ -26,13 +29,17 @@ from code_loader.contract.enums import MetricDirection, LeapDataType, DatasetMet
26
29
  from code_loader import leap_binder, LeapLoader
27
30
  from code_loader.contract.mapping import NodeMapping, NodeMappingType, NodeConnection
28
31
  from code_loader.contract.visualizer_classes import LeapImage, LeapImageMask, LeapTextMask, LeapText, LeapGraph, \
29
- LeapHorizontalBar, LeapImageWithBBox, LeapImageWithHeatmap, LeapVideo, LeapValidationError
32
+ LeapHorizontalBar, LeapImageWithBBox, LeapImageWithHeatmap, LeapVideo, LeapValidationError, \
33
+ map_leap_data_type_to_visualizer_class
30
34
  from code_loader.inner_leap_binder.leapbinder import mapping_runtime_mode_env_var_mame
31
35
  from code_loader.mixpanel_tracker import clear_integration_events, AnalyticsEvent, emit_integration_event_once
32
36
 
33
37
  _called_from_inside_tl_decorator = 0
34
38
  _called_from_inside_tl_integration_test_decorator = False
35
- _mapping_dataset_is_grouped = False
39
+ # Whether any integration test wrapper was entered in this process. The crash reporter uses it
40
+ # to tell a raise in the test body (report as code-flow failure) from a module-level crash
41
+ # before any test ran (report as a plain script crash).
42
+ _integration_test_started = False
36
43
  _call_from_tl_platform = os.environ.get('IS_TENSORLEAP_PLATFORM') == 'true'
37
44
 
38
45
  # ---- warnings store (module-level) ----
@@ -206,50 +213,6 @@ def batch_warning(result, func_name):
206
213
  )
207
214
 
208
215
 
209
- def _validate_id_or_group(sample_id, preprocess_response, func_name):
210
- """Accept either a single sample id (flat dataset) or a group — a list of ids (grouped
211
- dataset). The argument shape must match the dataset: a group requires a grouped
212
- PreprocessResponse and a single id requires a flat one. Every id must match the declared
213
- sample_id_type."""
214
- is_group = isinstance(sample_id, list)
215
- if is_group and not preprocess_response.is_grouped:
216
- raise AssertionError(
217
- f'{func_name}() validation failed: got a group (list of sample ids) but the '
218
- f'PreprocessResponse is not grouped. Pass a single sample id for a flat dataset.')
219
- if not is_group and preprocess_response.is_grouped:
220
- raise AssertionError(
221
- f'{func_name}() validation failed: got a single sample id but the PreprocessResponse '
222
- f'is grouped. Pass a group (list of sample ids) for a grouped dataset.')
223
- ids = sample_id if is_group else [sample_id]
224
- for sid in ids:
225
- assert type(sid) == preprocess_response.sample_id_type, \
226
- (f'{func_name}() validation failed: '
227
- f'Argument sample_id should be as the same type as defined in the preprocess response '
228
- f'{preprocess_response.sample_id_type}. Got {type(sid)}.')
229
-
230
-
231
- def _validate_grouped_result(result, group_size, func_name, validate_single):
232
- """A grouped (group) encoder result must be either a single ndarray with leading dim
233
- == group_size, or a list of `group_size` per-sample results (each validated by
234
- validate_single). Metadata validates its grouped result separately (list only)."""
235
- if isinstance(result, np.ndarray):
236
- assert len(result.shape) > 0 and result.shape[0] == group_size, \
237
- (f'{func_name}() validation failed: expected a grouped output for a group of '
238
- f'{group_size}: an array with leading dim {group_size}, got shape {result.shape}.')
239
- validate_single(result)
240
- elif isinstance(result, list):
241
- assert len(result) == group_size, \
242
- (f'{func_name}() validation failed: expected a grouped output for a group of '
243
- f'{group_size}: a list of {group_size} arrays, got {len(result)}.')
244
- for r in result:
245
- validate_single(r)
246
- else:
247
- raise AssertionError(
248
- f'{func_name}() validation failed: expected a grouped output for a group of '
249
- f'{group_size}: either an array with leading dim {group_size} or a list of '
250
- f'{group_size} arrays, got {type(result)}.')
251
-
252
-
253
216
  def _add_mapping_connection(user_unique_name, connection_destinations, arg_names, name, node_mapping_type):
254
217
  connection_destinations = [connection_destination for connection_destination in connection_destinations
255
218
  if not isinstance(connection_destination, SamplePreprocessResponse)]
@@ -327,16 +290,32 @@ def tensorleap_integration_test():
327
290
 
328
291
  def _validate_input_args(*args, **kwargs):
329
292
  sample_id, preprocess_response = args
330
- _validate_id_or_group(sample_id, preprocess_response, 'tensorleap_integration_test')
293
+ assert type(sample_id) == preprocess_response.sample_id_type, (
294
+ f"tensorleap_integration_test validation failed: "
295
+ f"sample_id type ({type(sample_id).__name__}) does not match the expected "
296
+ f"type ({preprocess_response.sample_id_type}) from the PreprocessResponse."
297
+ )
331
298
 
332
299
  def inner(*args, **kwargs):
333
300
  if not _call_from_tl_platform:
334
301
  set_current('tensorleap_integration_test')
335
- validate_args_structure(*args, types_order=[Union[int, str, list], PreprocessResponse],
336
- func_name='integration_test', expected_names=["idx", "preprocess"], **kwargs)
302
+ # Keyword calls must work with the user function's OWN parameter names — the local
303
+ # run template calls integration_test(sample_id=..., preprocess=...) against a
304
+ # signature the user chose, not the canonical placeholder names.
305
+ expected_names = inspect.getfullargspec(integration_test_function)[0][:2]
306
+ if len(expected_names) < 2:
307
+ expected_names = ['idx', 'preprocess']
308
+ validate_args_structure(*args, types_order=[Union[int, str], PreprocessResponse],
309
+ func_name='integration_test', expected_names=expected_names,
310
+ **kwargs)
311
+ sample_id, preprocess_response = (
312
+ args[i] if i < len(args) else kwargs[name]
313
+ for i, name in enumerate(expected_names))
314
+ args, kwargs = (sample_id, preprocess_response), {}
337
315
  _validate_input_args(*args, **kwargs)
338
316
 
339
- global _called_from_inside_tl_integration_test_decorator
317
+ global _called_from_inside_tl_integration_test_decorator, _integration_test_started
318
+ _integration_test_started = True
340
319
  # Clear integration test events for new test
341
320
  try:
342
321
  clear_integration_events()
@@ -347,12 +326,12 @@ def tensorleap_integration_test():
347
326
  if not _call_from_tl_platform:
348
327
  update_env_params_func("tensorleap_integration_test",
349
328
  "v") # put here because otherwise it will become v only if it finishes all the script
329
+ _reset_model_loop_state()
350
330
  ret = integration_test_function(*args, **kwargs)
351
331
 
352
- global _mapping_dataset_is_grouped
353
- _mapping_dataset_is_grouped = args[1].is_grouped
354
332
  try:
355
333
  os.environ[mapping_runtime_mode_env_var_mame] = 'True'
334
+ _reset_model_loop_state()
356
335
  integration_test_function(None, PreprocessResponse(state=DataStateType.training, length=0))
357
336
  except Exception as e:
358
337
  import traceback
@@ -370,7 +349,6 @@ def tensorleap_integration_test():
370
349
  f'Integration test is only allowed to call Tensorleap decorators. '
371
350
  f'Ensure any arithmetics, external library use, Python logic is placed within Tensorleap decoders')
372
351
  finally:
373
- _mapping_dataset_is_grouped = False
374
352
  if mapping_runtime_mode_env_var_mame in os.environ:
375
353
  del os.environ[mapping_runtime_mode_env_var_mame]
376
354
  finally:
@@ -466,7 +444,7 @@ def tensorleap_load_model(prediction_types: Optional[List[PredictionTypeHandler]
466
444
  pass
467
445
 
468
446
  @lru_cache()
469
- def inner(*args, **kwargs):
447
+ def _cached_inner(*args, **kwargs):
470
448
  if not _call_from_tl_platform:
471
449
  set_current('tensorleap_load_model')
472
450
  validate_args_structure(*args, types_order=[],
@@ -624,14 +602,32 @@ def tensorleap_load_model(prediction_types: Optional[List[PredictionTypeHandler]
624
602
  update_env_params_func("tensorleap_load_model", "v")
625
603
  return model_placeholder
626
604
 
605
+ def inner(*args, **kwargs):
606
+ model_placeholder = _cached_inner(*args, **kwargs)
607
+ # Re-assert on every call, outside the cache: each mapping rerun's load_model
608
+ # overwrites this global with its mapping placeholder, and on a cache hit the
609
+ # body above never runs — a second integration test would otherwise fail the
610
+ # model loop's identity check against the stale mapping placeholder.
611
+ global _last_loaded_model
612
+ _last_loaded_model = model_placeholder
613
+ return model_placeholder
614
+
627
615
  def mapping_inner():
628
616
  class ModelOutputPlaceholder:
629
617
  def __init__(self):
630
618
  self.node_mapping = NodeMapping('', NodeMappingType.Prediction0)
631
619
 
632
620
  def __getitem__(self, key):
633
- assert isinstance(key, int), \
634
- f'Expected key to be an int, got {type(key)} instead.'
621
+ assert isinstance(key, (int, str)), \
622
+ f'Expected key to be an int or a prediction-type name, got {type(key)} instead.'
623
+ if isinstance(key, str):
624
+ declared_names = [prediction_type.name for prediction_type
625
+ in leap_binder.setup_container.prediction_types]
626
+ if key not in declared_names:
627
+ raise Exception(
628
+ f'Unknown prediction name "{key}" — declared prediction types: '
629
+ f'{declared_names}.')
630
+ key = declared_names.index(key)
635
631
 
636
632
  ret = TempMapping()
637
633
  try:
@@ -685,7 +681,10 @@ def tensorleap_load_model(prediction_types: Optional[List[PredictionTypeHandler]
685
681
 
686
682
  return FollowInputIndex()
687
683
 
688
- return ModelPlaceholder()
684
+ model_placeholder = ModelPlaceholder()
685
+ global _last_loaded_model
686
+ _last_loaded_model = model_placeholder
687
+ return model_placeholder
689
688
 
690
689
  def final_inner(*args, **kwargs):
691
690
  if os.environ.get(mapping_runtime_mode_env_var_mame):
@@ -1562,10 +1561,13 @@ def tensorleap_metadata(
1562
1561
  raise Exception(f'Metadata with name {name} already exists. '
1563
1562
  f'Please choose another')
1564
1563
 
1565
- def _validate_input_args(sample_id: Union[int, str, list], preprocess_response: PreprocessResponse):
1566
- _validate_id_or_group(sample_id, preprocess_response, user_function.__name__)
1564
+ def _validate_input_args(sample_id: Union[int, str], preprocess_response: PreprocessResponse):
1565
+ assert type(sample_id) == preprocess_response.sample_id_type, \
1566
+ (f'{user_function.__name__}() validation failed: '
1567
+ f'Argument sample_id should be as the same type as defined in the preprocess response '
1568
+ f'{preprocess_response.sample_id_type}. Got {type(sample_id)}.')
1567
1569
 
1568
- def _validate_single(result):
1570
+ def _validate_result(result):
1569
1571
  supported_result_types = (type(None), int, str, bool, float, dict, np.floating,
1570
1572
  np.bool_, np.unsignedinteger, np.signedinteger, np.integer)
1571
1573
  if isinstance(result, tuple):
@@ -1583,20 +1585,6 @@ def tensorleap_metadata(
1583
1585
  (f'{user_function.__name__}() validation failed: '
1584
1586
  f'Values in the return dict should be of type {str(supported_result_types)}. Got {type(value)}.')
1585
1587
 
1586
- def _validate_result(result, grouped=False, group_size=None):
1587
- if not grouped:
1588
- _validate_single(result)
1589
- return
1590
- # Grouped metadata: a list of `group_size` per-sample scalars/dicts.
1591
- assert isinstance(result, list), \
1592
- (f'{user_function.__name__}() validation failed: expected a grouped output for a group '
1593
- f'of {group_size}: a list of {group_size} metadata values, got {type(result)}.')
1594
- assert len(result) == group_size, \
1595
- (f'{user_function.__name__}() validation failed: expected a grouped output for a group '
1596
- f'of {group_size}: a list of {group_size} metadata values, got {len(result)}.')
1597
- for value in result:
1598
- _validate_single(value)
1599
-
1600
1588
  def inner_without_validate(sample_id, preprocess_response):
1601
1589
 
1602
1590
  global _called_from_inside_tl_decorator
@@ -1616,16 +1604,14 @@ def tensorleap_metadata(
1616
1604
  set_current('tensorleap_metadata')
1617
1605
  if os.environ.get(mapping_runtime_mode_env_var_mame):
1618
1606
  return None
1619
- validate_args_structure(*args, types_order=[Union[int, str, list], PreprocessResponse],
1607
+ validate_args_structure(*args, types_order=[Union[int, str], PreprocessResponse],
1620
1608
  func_name=user_function.__name__, expected_names=["idx", "preprocess"], **kwargs)
1621
1609
  sample_id, preprocess_response = args if len(args) != 0 else kwargs.values()
1622
1610
  _validate_input_args(sample_id, preprocess_response)
1623
1611
 
1624
- grouped = isinstance(sample_id, list)
1625
- group_size = len(sample_id) if grouped else None
1626
1612
  result = inner_without_validate(sample_id, preprocess_response)
1627
1613
 
1628
- _validate_result(result, grouped, group_size)
1614
+ _validate_result(result)
1629
1615
  if not _call_from_tl_platform:
1630
1616
  update_env_params_func("tensorleap_metadata", "v")
1631
1617
  return result
@@ -1635,52 +1621,38 @@ def tensorleap_metadata(
1635
1621
  return decorating_function
1636
1622
 
1637
1623
 
1638
- def tensorleap_custom_latent_space():
1624
+ def tensorleap_custom_latent_space(name: Optional[str] = None):
1639
1625
  def decorating_function(user_function: SectionCallableInterface):
1640
- def _validate_input_args(sample_id: Union[int, str, list], preprocess_response: PreprocessResponse):
1641
- _validate_id_or_group(sample_id, preprocess_response, 'tensorleap_custom_latent_space')
1626
+ ls_name = name if name is not None else user_function.__name__
1627
+ def _validate_input_args(sample_id: Union[int, str], preprocess_response: PreprocessResponse):
1628
+ assert isinstance(sample_id, (int, str)), \
1629
+ (f'tensorleap_custom_latent_space validation failed: '
1630
+ f'Argument sample_id should be either int or str. Got {type(sample_id)}.')
1631
+ assert isinstance(preprocess_response, PreprocessResponse), \
1632
+ (f'tensorleap_custom_latent_space validation failed: '
1633
+ f'Argument preprocess_response should be a PreprocessResponse. Got {type(preprocess_response)}.')
1634
+ assert type(sample_id) == preprocess_response.sample_id_type, \
1635
+ (f'tensorleap_custom_latent_space validation failed: '
1636
+ f'Argument sample_id should be as the same type as defined in the preprocess response '
1637
+ f'{preprocess_response.sample_id_type}. Got {type(sample_id)}.')
1642
1638
 
1643
- def _validate_single(single_result):
1644
- assert isinstance(single_result, np.ndarray), \
1639
+ def _validate_result(result):
1640
+ assert isinstance(result, np.ndarray), \
1645
1641
  (f'tensorleap_custom_latent_space validation failed: '
1646
- f'The return type should be a numpy array. Got {type(single_result)}.')
1647
- if single_result.ndim > 1:
1648
- flat_dim = int(np.prod(single_result.shape))
1642
+ f'The return type should be a numpy array. Got {type(result)}.')
1643
+ if result.ndim > 1:
1644
+ flat_dim = int(np.prod(result.shape))
1649
1645
  store_general_warning(
1650
- key=("tensorleap_custom_latent_space_flatten", tuple(single_result.shape)),
1646
+ key=("tensorleap_custom_latent_space_flatten", ls_name, tuple(result.shape)),
1651
1647
  message=(
1652
- f"tensorleap_custom_latent_space returned per-sample shape {tuple(single_result.shape)} "
1653
- f"(ndim={single_result.ndim}). Tensorleap assumes per-sample shape (d, ...) and will "
1648
+ f"tensorleap_custom_latent_space '{ls_name}' returned per-sample shape {tuple(result.shape)} "
1649
+ f"(ndim={result.ndim}). Tensorleap assumes per-sample shape (d, ...) and will "
1654
1650
  f"flatten to ({flat_dim},) before downstream visualization and clustering. "
1655
1651
  f"If you want a different aggregation (e.g. global average pooling), do it "
1656
1652
  f"inside your function."
1657
1653
  ),
1658
1654
  )
1659
1655
 
1660
- def _validate_result(result, grouped=False, group_size=None):
1661
- if not grouped:
1662
- _validate_single(result)
1663
- return
1664
- # Grouped: (B, *) array or list of B per-sample arrays. Validate each per-sample slice so
1665
- # the per-sample flatten check/warning uses the sample shape, not the (B, *) batch shape.
1666
- if isinstance(result, np.ndarray):
1667
- assert result.ndim >= 1 and result.shape[0] == group_size, \
1668
- (f'tensorleap_custom_latent_space validation failed: expected a grouped output for a '
1669
- f'group of {group_size}: an array with leading dim {group_size}, got shape {result.shape}.')
1670
- for single in result:
1671
- _validate_single(single)
1672
- elif isinstance(result, list):
1673
- assert len(result) == group_size, \
1674
- (f'tensorleap_custom_latent_space validation failed: expected a grouped output for a '
1675
- f'group of {group_size}: a list of {group_size} arrays, got {len(result)}.')
1676
- for single in result:
1677
- _validate_single(single)
1678
- else:
1679
- raise AssertionError(
1680
- f'tensorleap_custom_latent_space validation failed: expected a grouped output for a '
1681
- f'group of {group_size}: either an array with leading dim {group_size} or a list of '
1682
- f'{group_size} arrays, got {type(result)}.')
1683
-
1684
1656
  def inner_without_validate(sample_id, preprocess_response):
1685
1657
  global _called_from_inside_tl_decorator
1686
1658
  _called_from_inside_tl_decorator += 1
@@ -1692,7 +1664,7 @@ def tensorleap_custom_latent_space():
1692
1664
 
1693
1665
  return result
1694
1666
 
1695
- leap_binder.set_custom_latent_space(inner_without_validate)
1667
+ leap_binder.set_custom_latent_space(inner_without_validate, ls_name)
1696
1668
 
1697
1669
  def inner(sample_id, preprocess_response):
1698
1670
  if os.environ.get(mapping_runtime_mode_env_var_mame):
@@ -1700,11 +1672,9 @@ def tensorleap_custom_latent_space():
1700
1672
 
1701
1673
  _validate_input_args(sample_id, preprocess_response)
1702
1674
 
1703
- grouped = isinstance(sample_id, list)
1704
- group_size = len(sample_id) if grouped else None
1705
1675
  result = inner_without_validate(sample_id, preprocess_response)
1706
1676
 
1707
- _validate_result(result, grouped, group_size)
1677
+ _validate_result(result)
1708
1678
  return result
1709
1679
 
1710
1680
  return inner
@@ -1712,18 +1682,263 @@ def tensorleap_custom_latent_space():
1712
1682
  return decorating_function
1713
1683
 
1714
1684
 
1685
+ _MODEL_LOOP_MAX_STEPS = 1000
1686
+
1687
+ _active_model_loop = None
1688
+ _model_loop_run_in_current_test = False
1689
+ _last_loaded_model = None
1690
+ _chain_artifact_provenance: Dict[int, tuple] = {}
1691
+
1692
+ _validate_state_types = validate_autoregressive_state_types
1693
+ _nest_equal = autoregressive_nests_equal
1694
+
1695
+
1696
+ def _nest_fingerprint(value):
1697
+ digest = hashlib.sha1()
1698
+
1699
+ def _feed(item):
1700
+ if isinstance(item, dict):
1701
+ digest.update(b'{')
1702
+ for key in sorted(item):
1703
+ digest.update(str(key).encode())
1704
+ digest.update(b':')
1705
+ _feed(item[key])
1706
+ digest.update(b'}')
1707
+ elif isinstance(item, (list, tuple)):
1708
+ digest.update(b'[')
1709
+ for element in item:
1710
+ _feed(element)
1711
+ digest.update(b']')
1712
+ elif isinstance(item, np.ndarray):
1713
+ digest.update(str(item.dtype).encode())
1714
+ digest.update(str(item.shape).encode())
1715
+ digest.update(np.ascontiguousarray(item).tobytes())
1716
+ else:
1717
+ digest.update(repr(item).encode())
1718
+
1719
+ _feed(value)
1720
+ return digest.hexdigest()
1721
+
1722
+
1723
+ def _ndarray_leaves(value):
1724
+ if isinstance(value, np.ndarray):
1725
+ return [value]
1726
+ if isinstance(value, dict):
1727
+ return [leaf for item in value.values() for leaf in _ndarray_leaves(item)]
1728
+ if isinstance(value, (list, tuple)):
1729
+ return [leaf for item in value for leaf in _ndarray_leaves(item)]
1730
+ return []
1731
+
1732
+
1733
+ def _squeeze_leading_batch_axis(tensors_dict):
1734
+ if tensors_dict is None:
1735
+ return None
1736
+ squeezed = {}
1737
+ for key, value in tensors_dict.items():
1738
+ if isinstance(value, np.ndarray) and value.ndim > 0 and value.shape[0] == 1:
1739
+ squeezed[key] = value[0]
1740
+ else:
1741
+ squeezed[key] = value
1742
+ return squeezed
1743
+
1744
+
1745
+ def _register_chain_artifact(obj, kind):
1746
+ # The object reference is kept in the registry entry so its id() cannot be recycled by the GC.
1747
+ _chain_artifact_provenance[id(obj)] = (kind, _nest_fingerprint(obj), obj)
1748
+
1749
+
1750
+ def _lookup_chain_artifact(obj):
1751
+ entry = _chain_artifact_provenance.get(id(obj))
1752
+ if entry is None or entry[2] is not obj:
1753
+ return None
1754
+ return entry
1755
+
1756
+
1757
+ def _reset_model_loop_state():
1758
+ global _active_model_loop, _model_loop_run_in_current_test
1759
+ _active_model_loop = None
1760
+ _model_loop_run_in_current_test = False
1761
+ _chain_artifact_provenance.clear()
1762
+
1763
+
1764
+ class _MappingStatePlaceholder:
1765
+ def _unsupported(self):
1766
+ raise LeapValidationError(
1767
+ 'The autoregressive state is opaque inside the integration test — pass it whole '
1768
+ 'between the hook, the model loop and the autoregressive metrics. Read or derive '
1769
+ 'state content inside the hook or inside the metric/visualizer body.')
1770
+
1771
+ def __getitem__(self, key):
1772
+ self._unsupported()
1773
+
1774
+ def __getattr__(self, name):
1775
+ raise AttributeError(name)
1776
+
1777
+ def __iter__(self):
1778
+ self._unsupported()
1779
+
1780
+
1781
+ class _ModelLoopContext:
1782
+ def __init__(self, sample_id, preprocess_response, prediction_names, is_mapping):
1783
+ self.sample_id = sample_id
1784
+ self.preprocess_response = preprocess_response
1785
+ self.prediction_names = prediction_names
1786
+ self.is_mapping = is_mapping
1787
+ self.phase = 'awaiting_first_hook'
1788
+ self.steps = 0
1789
+ self.fed_inputs = None
1790
+ self.fed_inputs_fingerprint = None
1791
+ self.last_outputs = None
1792
+ self.last_outputs_fingerprint = None
1793
+ self.last_state = None
1794
+ self.last_state_fingerprint = None
1795
+
1796
+ def _fail(self, message):
1797
+ raise LeapValidationError(f'tensorleap_model_loop validation failed: {message}')
1798
+
1799
+ def on_hook_call(self, sample_id, prev_inputs, prev_outputs, state, preprocess_response):
1800
+ if self.phase == 'ended':
1801
+ self._fail('the chain already ended — once the autoregressive step returns None for '
1802
+ 'next_inputs the loop must exit without further hook or model calls.')
1803
+ if self.phase == 'awaiting_model':
1804
+ self._fail('two autoregressive step calls in a row — the model must run exactly once '
1805
+ 'between hook calls.')
1806
+ if sample_id != self.sample_id:
1807
+ self._fail('the hook must be called with the sample_id the model loop received.')
1808
+ if preprocess_response is not self.preprocess_response:
1809
+ self._fail('the hook must be called with the preprocess response the model loop '
1810
+ 'received.')
1811
+ if self.phase == 'awaiting_first_hook':
1812
+ if prev_inputs is not None or prev_outputs is not None or state is not None:
1813
+ self._fail('the first hook call inside the model loop must be the initial call: '
1814
+ 'prev_inputs=None, prev_outputs=None, state=None.')
1815
+ return
1816
+ if prev_inputs is not self.fed_inputs:
1817
+ self._fail('prev_inputs must be exactly the inputs dict the previous hook call '
1818
+ 'returned (the one fed to the model) — do not copy, rebuild or slice it.')
1819
+ if prev_outputs is not self.last_outputs:
1820
+ self._fail('prev_outputs must be exactly the outputs dict the model call returned — '
1821
+ 'do not copy, rebuild or slice it.')
1822
+ if state is not self.last_state:
1823
+ self._fail('state must be exactly the state object the previous hook call returned.')
1824
+ if _nest_fingerprint(prev_inputs) != self.fed_inputs_fingerprint:
1825
+ self._fail('the model inputs were mutated in place after being fed to the model — '
1826
+ 'the platform re-reads the stored step tensors, so local mutations diverge '
1827
+ 'from platform results. Compute new tensors inside the hook instead.')
1828
+ if _nest_fingerprint(prev_outputs) != self.last_outputs_fingerprint:
1829
+ self._fail('the model outputs were mutated in place after the model call — the '
1830
+ 'platform re-reads the stored step tensors, so local mutations diverge '
1831
+ 'from platform results. Compute derived tensors inside the hook instead.')
1832
+ if _nest_fingerprint(state) != self.last_state_fingerprint:
1833
+ self._fail('the state object was mutated between hook calls — state may only change '
1834
+ 'inside the hook body.')
1835
+
1836
+ def on_hook_return(self, next_inputs, state):
1837
+ self.steps += 1
1838
+ if self.steps > _MODEL_LOOP_MAX_STEPS:
1839
+ self._fail(f'the chain exceeded {_MODEL_LOOP_MAX_STEPS} steps without the '
1840
+ f'autoregressive step returning None — the loop would never terminate on '
1841
+ f'the platform.')
1842
+ self.last_state = state
1843
+ self.last_state_fingerprint = _nest_fingerprint(state)
1844
+ if next_inputs is None:
1845
+ self.phase = 'ended'
1846
+ else:
1847
+ self.fed_inputs = next_inputs
1848
+ self.fed_inputs_fingerprint = _nest_fingerprint(next_inputs)
1849
+ self.phase = 'awaiting_model'
1850
+
1851
+ def mapping_hook_call(self, inputs_placeholder, state_placeholder):
1852
+ if self.phase == 'ended':
1853
+ self._fail('the chain already ended — once the autoregressive step returns None for '
1854
+ 'next_inputs the loop must exit without further hook or model calls.')
1855
+ if self.phase == 'awaiting_model':
1856
+ self._fail('two autoregressive step calls in a row — the model must run exactly once '
1857
+ 'between hook calls.')
1858
+ self.last_state = state_placeholder
1859
+ if self.phase == 'awaiting_first_hook':
1860
+ self.fed_inputs = inputs_placeholder
1861
+ self.phase = 'awaiting_model'
1862
+ return inputs_placeholder, state_placeholder
1863
+ self.phase = 'ended'
1864
+ return None, state_placeholder
1865
+
1866
+ def on_model_call(self, fed):
1867
+ if self.phase == 'awaiting_first_hook':
1868
+ self._fail('the model was called before the autoregressive step produced the initial '
1869
+ 'inputs — the loop must start with the initial hook call '
1870
+ '(prev_inputs=None, prev_outputs=None, state=None).')
1871
+ if self.phase == 'awaiting_hook':
1872
+ self._fail('the model was called twice in a row — the autoregressive step must run '
1873
+ 'between model calls.')
1874
+ if self.phase == 'ended':
1875
+ self._fail('the chain already ended — once the autoregressive step returns None for '
1876
+ 'next_inputs the loop must exit without further hook or model calls.')
1877
+ if self.is_mapping:
1878
+ return
1879
+ fed_ids = {id(leaf) for leaf in _ndarray_leaves(fed)}
1880
+ hook_ids = {id(value) for value in self.fed_inputs.values()}
1881
+ if fed_ids != hook_ids:
1882
+ self._fail('the model must be fed exactly the tensors the last hook call returned — '
1883
+ 'any computation between the hook and the model is invisible to the '
1884
+ 'platform. Move it into the hook.')
1885
+ if _nest_fingerprint(self.fed_inputs) != self.fed_inputs_fingerprint:
1886
+ self._fail('the model inputs were mutated in place after the hook returned them — '
1887
+ 'the platform feeds the model the tensors exactly as the hook returned '
1888
+ 'them. Compute new tensors inside the hook instead.')
1889
+
1890
+ def on_model_return(self, raw_outputs):
1891
+ self.phase = 'awaiting_hook'
1892
+ if self.is_mapping:
1893
+ self.last_outputs = raw_outputs
1894
+ return raw_outputs
1895
+ outputs_list = raw_outputs if isinstance(raw_outputs, list) else [raw_outputs]
1896
+ if len(outputs_list) != len(self.prediction_names):
1897
+ self._fail(f'the model returned {len(outputs_list)} outputs but '
1898
+ f'{len(self.prediction_names)} prediction types are declared on '
1899
+ f'tensorleap_load_model — declare one prediction type per model output.')
1900
+ named_outputs = {name: np.asarray(output)
1901
+ for name, output in zip(self.prediction_names, outputs_list)}
1902
+ self.last_outputs = named_outputs
1903
+ self.last_outputs_fingerprint = _nest_fingerprint(named_outputs)
1904
+ return named_outputs
1905
+
1906
+
1907
+ class _ModelLoopModelProxy:
1908
+ def __init__(self, model, context):
1909
+ self._model = model
1910
+ self._context = context
1911
+
1912
+ def __call__(self, inputs):
1913
+ self._context.on_model_call(inputs)
1914
+ return self._context.on_model_return(self._model(inputs))
1915
+
1916
+ def run(self, output_names, input_dict):
1917
+ self._context.on_model_call(input_dict)
1918
+ return self._context.on_model_return(self._model.run(output_names, input_dict))
1919
+
1920
+ def get_inputs(self):
1921
+ return self._model.get_inputs()
1922
+
1923
+
1715
1924
  def tensorleap_autoregressive_step(latent_space_aggregation: str = 'last_step'):
1716
1925
  """The feedback hook that drives an autoregressive chain.
1717
1926
 
1718
1927
  Signature of the decorated function:
1719
- (sample_id, prev_inputs, prev_outputs, preprocess) -> Optional[Dict[str, np.ndarray]]
1720
-
1721
- First call per chain: prev_inputs=None, prev_outputs=None — returns the initial model inputs.
1722
- Later calls receive the previous step's model inputs and outputs (unbatched, per-sample) and
1723
- return the next step's full input dict; returning None ends the chain. Keys starting with '_'
1724
- are passthrough state: never fed to the model, handed back verbatim in prev_inputs next call.
1725
- The hook must be stateless and deterministically seeded (seed stochasticity from sample_id)
1726
- the engine replays steps after crash recovery and relies on identical results.
1928
+ (sample_id, prev_inputs, prev_outputs, state, preprocess)
1929
+ -> (Optional[Dict[str, np.ndarray]], state)
1930
+
1931
+ First call per chain: prev_inputs=None, prev_outputs=None, state=None returns the initial
1932
+ model inputs and the initial state. Later calls receive the previous step's model inputs and
1933
+ outputs (unbatched, per-sample; outputs keyed by the declared prediction-type names) plus the
1934
+ state returned by the previous call, and return the next step's full input dict together with
1935
+ the updated state; returning (None, state) ends the chain the terminating call still
1936
+ returns state, so the final step participates in state aggregation. State is never fed to
1937
+ the model; any picklable nest of builtin/numpy values works (dicts, lists, tuples, sets,
1938
+ numbers, strings, arrays — not user-defined classes, lambdas or open resources, since the
1939
+ platform deserializes state without the integration code), and it rides the platform queue
1940
+ on every step — accumulate reductions, not raw per-step tensors. The hook must be stateless and deterministically seeded (seed stochasticity from
1941
+ sample_id) — the engine replays steps after crash recovery and relies on identical results.
1727
1942
 
1728
1943
  latent_space_aggregation ('last_step' | 'mean') declares how the chain's latent-space
1729
1944
  vectors are derived from its per-step forward passes. 'last_step' (default): every latent
@@ -1742,62 +1957,95 @@ def tensorleap_autoregressive_step(latent_space_aggregation: str = 'last_step'):
1742
1957
  first_call_record: Dict[str, Any] = {'keys': None, 'shapes': None}
1743
1958
 
1744
1959
  def _normalize_args(args, kwargs):
1745
- expected_names = ["sample_id", "prev_inputs", "prev_outputs", "preprocess"]
1746
- validate_args_structure(
1747
- *args,
1748
- types_order=[Union[int, str], Union[dict, type(None)], Union[dict, type(None)],
1749
- PreprocessResponse],
1750
- func_name=user_function.__name__, expected_names=expected_names, **kwargs)
1960
+ expected_names = ["sample_id", "prev_inputs", "prev_outputs", "state", "preprocess"]
1751
1961
  normalized = []
1752
- for i, name in enumerate(expected_names):
1753
- normalized.append(args[i] if i < len(args) else kwargs[name])
1962
+ for i, arg_name in enumerate(expected_names):
1963
+ if i < len(args):
1964
+ normalized.append(args[i])
1965
+ elif arg_name in kwargs:
1966
+ normalized.append(kwargs[arg_name])
1967
+ else:
1968
+ raise AssertionError(
1969
+ f'{user_function.__name__} validation failed: missing required argument '
1970
+ f"'{arg_name}'. Expected arguments: {expected_names}.")
1971
+ if len(args) + len(kwargs) != len(expected_names):
1972
+ raise AssertionError(
1973
+ f'{user_function.__name__} validation failed: expected exactly '
1974
+ f'{len(expected_names)} arguments {expected_names}, got '
1975
+ f'{len(args) + len(kwargs)}.')
1976
+ sample_id, prev_inputs, prev_outputs, state, preprocess_response = normalized
1977
+ assert isinstance(sample_id, (int, str)), \
1978
+ (f'{user_function.__name__}() validation failed: '
1979
+ f'sample_id must be an int or str. Got {type(sample_id).__name__}.')
1980
+ assert prev_inputs is None or isinstance(prev_inputs, dict), \
1981
+ (f'{user_function.__name__}() validation failed: '
1982
+ f'prev_inputs must be a dict or None. Got {type(prev_inputs).__name__}.')
1983
+ assert prev_outputs is None or isinstance(prev_outputs, dict), \
1984
+ (f'{user_function.__name__}() validation failed: '
1985
+ f'prev_outputs must be a dict or None. Got {type(prev_outputs).__name__}.')
1986
+ assert isinstance(preprocess_response, PreprocessResponse), \
1987
+ (f'{user_function.__name__}() validation failed: '
1988
+ f'preprocess must be a PreprocessResponse. Got '
1989
+ f'{type(preprocess_response).__name__}.')
1754
1990
  return normalized
1755
1991
 
1756
- def _validate_input_args(sample_id, prev_inputs, prev_outputs, preprocess_response):
1992
+ def _validate_input_args(sample_id, prev_inputs, prev_outputs, state, preprocess_response):
1757
1993
  assert (prev_inputs is None) == (prev_outputs is None), \
1758
1994
  (f'{user_function.__name__}() validation failed: '
1759
1995
  f'prev_inputs and prev_outputs must both be None (first call) or both be dicts '
1760
1996
  f'(subsequent calls). Got prev_inputs={type(prev_inputs).__name__}, '
1761
1997
  f'prev_outputs={type(prev_outputs).__name__}.')
1998
+ assert not (prev_inputs is None and state is not None), \
1999
+ (f'{user_function.__name__}() validation failed: '
2000
+ f'the first call (prev_inputs=None, prev_outputs=None) must pass state=None. '
2001
+ f'Got state of type {type(state).__name__}.')
1762
2002
  assert type(sample_id) == preprocess_response.sample_id_type, \
1763
2003
  (f'{user_function.__name__}() validation failed: '
1764
2004
  f'Argument sample_id should be as the same type as defined in the preprocess response '
1765
2005
  f'{preprocess_response.sample_id_type}. Got {type(sample_id)}.')
1766
2006
 
1767
2007
  def _validate_result(result, is_first_call):
1768
- if result is None:
2008
+ assert isinstance(result, tuple) and len(result) == 2, \
2009
+ (f'{user_function.__name__}() validation failed: '
2010
+ f'the hook must return a (next_inputs, state) tuple — next_inputs is a dict of '
2011
+ f'named model input tensors, or None to end the chain; state rides to the next '
2012
+ f'call. Got {type(result).__name__}.')
2013
+ next_inputs, state = result
2014
+ if next_inputs is None:
1769
2015
  assert not is_first_call, \
1770
2016
  (f'{user_function.__name__}() validation failed: '
1771
- f'the first call (prev_inputs=None, prev_outputs=None) must return the initial '
1772
- f'model inputs dict — returning None on the first call would produce an empty chain.')
2017
+ f'the first call (prev_inputs=None, prev_outputs=None, state=None) must return '
2018
+ f'the initial model inputs dict — returning None on the first call would '
2019
+ f'produce an empty chain.')
1773
2020
  return
1774
- assert isinstance(result, dict), \
2021
+ assert isinstance(next_inputs, dict), \
1775
2022
  (f'{user_function.__name__}() validation failed: '
1776
- f'the return value must be a dict of named model input tensors, or None to end the '
1777
- f'chain. Got {type(result)}.')
1778
- model_input_keys = [key for key in result if not key.startswith('_')]
1779
- assert model_input_keys, \
2023
+ f'next_inputs must be a dict of named model input tensors, or None to end the '
2024
+ f'chain. Got {type(next_inputs)}.')
2025
+ assert next_inputs, \
1780
2026
  (f'{user_function.__name__}() validation failed: '
1781
- f'the returned dict contains no model inputs every key starts with "_" '
1782
- f'(underscore keys are passthrough state, never fed to the model).')
1783
- for key, value in result.items():
2027
+ f'next_inputs is an empty dict return the full model input dict, or None to '
2028
+ f'end the chain.')
2029
+ for key, value in next_inputs.items():
1784
2030
  assert isinstance(key, str), \
1785
2031
  (f'{user_function.__name__}() validation failed: '
1786
- f'all keys in the returned dict must be strings. Got {type(key)}.')
1787
- if key.startswith('_'):
1788
- continue
2032
+ f'all keys in next_inputs must be strings. Got {type(key)}.')
2033
+ assert not key.startswith('_'), \
2034
+ (f'{user_function.__name__}() validation failed: '
2035
+ f'next_inputs key "{key}" starts with "_" — underscore passthrough keys were '
2036
+ f'replaced by the explicit state channel. Move passthrough values into the '
2037
+ f'returned state.')
1789
2038
  assert isinstance(value, np.ndarray), \
1790
2039
  (f'{user_function.__name__}() validation failed: '
1791
2040
  f'value for model input "{key}" must be a numpy array. Got {type(value)}.')
1792
2041
 
1793
- def _validate_consistency(result):
1794
- # Rules 2-3 of the autoregressive contract: identical key set and identical
1795
- # (non-passthrough) shapes on every call — compiled graphs need static shapes.
1796
- if result is None:
2042
+ def _validate_consistency(next_inputs):
2043
+ # Rules 2-3 of the autoregressive contract: identical key set and identical shapes on
2044
+ # every call — compiled graphs need static shapes.
2045
+ if next_inputs is None:
1797
2046
  return
1798
- keys = set(result.keys())
1799
- shapes = {key: tuple(value.shape) for key, value in result.items()
1800
- if not key.startswith('_') and isinstance(value, np.ndarray)}
2047
+ keys = set(next_inputs.keys())
2048
+ shapes = {key: tuple(value.shape) for key, value in next_inputs.items()}
1801
2049
  if first_call_record['keys'] is None:
1802
2050
  first_call_record['keys'] = keys
1803
2051
  first_call_record['shapes'] = shapes
@@ -1819,36 +2067,26 @@ def tensorleap_autoregressive_step(latent_space_aggregation: str = 'last_step'):
1819
2067
  f'the hook is not deterministic — two calls with identical arguments returned different '
1820
2068
  f'outputs. Seed any stochasticity (e.g. initial noise) from sample_id: the engine '
1821
2069
  f'replays steps after crash recovery and relies on identical results.')
1822
- if first_result is None or second_result is None:
1823
- assert first_result is None and second_result is None, error_message
1824
- return
1825
- assert set(first_result.keys()) == set(second_result.keys()), error_message
1826
- for key, value in first_result.items():
1827
- if key.startswith('_') or not isinstance(value, np.ndarray):
1828
- continue
1829
- assert np.array_equal(value, second_result[key]), error_message
1830
-
1831
- def _squeeze_test_batch_dim(tensors_dict):
1832
- # Inside the integration test the model works on batch-1 tensors (this wrapper adds the
1833
- # batch axis to returned inputs, and model outputs come back batched). The user's hook
1834
- # body must see the same unbatched per-sample tensors it will see at engine runtime, so
1835
- # strip the leading batch-1 axis before calling it.
1836
- if tensors_dict is None:
1837
- return None
1838
- squeezed = {}
1839
- for key, value in tensors_dict.items():
1840
- if not key.startswith('_') and isinstance(value, np.ndarray) \
1841
- and value.ndim > 0 and value.shape[0] == 1:
1842
- squeezed[key] = value[0]
1843
- else:
1844
- squeezed[key] = value
1845
- return squeezed
1846
-
1847
- def inner_without_validate(sample_id, prev_inputs, prev_outputs, preprocess_response):
2070
+ assert isinstance(first_result, tuple) and isinstance(second_result, tuple) and \
2071
+ len(first_result) == 2 and len(second_result) == 2, error_message
2072
+ first_inputs, first_state = first_result
2073
+ second_inputs, second_state = second_result
2074
+ if first_inputs is None or second_inputs is None:
2075
+ assert first_inputs is None and second_inputs is None, error_message
2076
+ else:
2077
+ assert set(first_inputs.keys()) == set(second_inputs.keys()), error_message
2078
+ for key, value in first_inputs.items():
2079
+ if not isinstance(value, np.ndarray):
2080
+ continue
2081
+ assert _nest_equal(value, second_inputs[key]), error_message
2082
+ assert _nest_equal(first_state, second_state), error_message
2083
+
2084
+ def inner_without_validate(sample_id, prev_inputs, prev_outputs, state, preprocess_response):
1848
2085
  global _called_from_inside_tl_decorator
1849
2086
  _called_from_inside_tl_decorator += 1
1850
2087
  try:
1851
- result = user_function(sample_id, prev_inputs, prev_outputs, preprocess_response)
2088
+ result = user_function(sample_id, prev_inputs, prev_outputs, state,
2089
+ preprocess_response)
1852
2090
  finally:
1853
2091
  _called_from_inside_tl_decorator -= 1
1854
2092
  return result
@@ -1859,43 +2097,69 @@ def tensorleap_autoregressive_step(latent_space_aggregation: str = 'last_step'):
1859
2097
  def inner(*args, **kwargs):
1860
2098
  if not _call_from_tl_platform:
1861
2099
  set_current('tensorleap_autoregressive_step')
1862
- sample_id, prev_inputs, prev_outputs, preprocess_response = _normalize_args(args, kwargs)
2100
+ sample_id, prev_inputs, prev_outputs, state, preprocess_response = \
2101
+ _normalize_args(args, kwargs)
1863
2102
 
1864
- is_top_level_in_test = (_called_from_inside_tl_decorator == 0
2103
+ is_top_level = _called_from_inside_tl_decorator == 0
2104
+ is_top_level_in_test = (is_top_level
1865
2105
  and _called_from_inside_tl_integration_test_decorator)
2106
+ loop_context = _active_model_loop
2107
+ if is_top_level_in_test and loop_context is None:
2108
+ raise LeapValidationError(
2109
+ f'{user_function.__name__}() validation failed: inside the integration '
2110
+ f'test the autoregressive step may only be called from within the '
2111
+ f'tensorleap_model_loop function — including the initial call '
2112
+ f'(prev_inputs=None). The platform drives the chain through the model '
2113
+ f'loop only, so a hook call outside it would not run on the platform.')
2114
+ # Outside the integration test a model loop may still be running (local debugging);
2115
+ # the loop's phase machine must advance there too, or its model proxy always raises.
2116
+ notify_loop = is_top_level and loop_context is not None
2117
+ if notify_loop:
2118
+ loop_context.on_hook_call(sample_id, prev_inputs, prev_outputs, state,
2119
+ preprocess_response)
1866
2120
  if is_top_level_in_test:
1867
- prev_inputs = _squeeze_test_batch_dim(prev_inputs)
1868
- prev_outputs = _squeeze_test_batch_dim(prev_outputs)
2121
+ # The wrapper adds the batch axis to returned inputs and model outputs come back
2122
+ # batched; the hook body must see the same unbatched per-sample tensors it will
2123
+ # see at engine runtime.
2124
+ prev_inputs = _squeeze_leading_batch_axis(prev_inputs)
2125
+ prev_outputs = _squeeze_leading_batch_axis(prev_outputs)
1869
2126
 
1870
- _validate_input_args(sample_id, prev_inputs, prev_outputs, preprocess_response)
2127
+ _validate_input_args(sample_id, prev_inputs, prev_outputs, state, preprocess_response)
1871
2128
 
1872
- result = inner_without_validate(sample_id, prev_inputs, prev_outputs, preprocess_response)
2129
+ # The deep copy keeps the determinism double-call independent: a hook that
2130
+ # accumulates into state in place would otherwise see its own first-call mutations.
2131
+ state_for_second_call = copy.deepcopy(state) if is_top_level_in_test else None
2132
+ result = inner_without_validate(sample_id, prev_inputs, prev_outputs, state,
2133
+ preprocess_response)
1873
2134
 
1874
2135
  if is_top_level_in_test:
1875
2136
  # Rule 7: determinism double-call. An unseeded sampler fails here, on the user's
1876
2137
  # machine, instead of silently forking a chain on engine crash-replay.
1877
2138
  second_result = inner_without_validate(sample_id, prev_inputs, prev_outputs,
1878
- preprocess_response)
2139
+ state_for_second_call, preprocess_response)
1879
2140
  _assert_deterministic(result, second_result)
1880
2141
 
1881
2142
  _validate_result(result, is_first_call=prev_inputs is None)
1882
- _validate_consistency(result)
2143
+ next_inputs, new_state = result
2144
+ _validate_consistency(next_inputs)
2145
+ _validate_state_types(new_state)
1883
2146
 
1884
- if is_top_level_in_test and result is not None:
1885
- result = {key: (np.expand_dims(value, axis=0)
1886
- if not key.startswith('_') and isinstance(value, np.ndarray) else value)
1887
- for key, value in result.items()}
2147
+ if is_top_level_in_test and next_inputs is not None:
2148
+ next_inputs = {key: np.expand_dims(value, axis=0)
2149
+ for key, value in next_inputs.items()}
2150
+ if notify_loop:
2151
+ loop_context.on_hook_return(next_inputs, new_state)
1888
2152
 
1889
2153
  if not _call_from_tl_platform:
1890
2154
  update_env_params_func("tensorleap_autoregressive_step", "v")
1891
- return result
2155
+ return next_inputs, new_state
1892
2156
 
1893
2157
  class _MappingInputsPlaceholder:
1894
- """Mapping-mode stand-in for the hook's returned dict.
2158
+ """Mapping-mode stand-in for the hook's returned inputs dict.
1895
2159
 
1896
2160
  Indexing it by a model-input name yields a placeholder whose node_mapping the model
1897
2161
  placeholder tags Input0/Input1/... — exactly how input encoders wire the graph. One
1898
- NodeConnection is recorded per non-passthrough key, once.
2162
+ NodeConnection is recorded per key, once.
1899
2163
  """
1900
2164
  def __init__(self):
1901
2165
  self._items: Dict[str, Any] = {}
@@ -1908,8 +2172,7 @@ def tensorleap_autoregressive_step(latent_space_aggregation: str = 'last_step'):
1908
2172
  pass
1909
2173
  ret = TempMapping()
1910
2174
  ret.node_mapping = NodeMapping(key, NodeMappingType.Input)
1911
- if not key.startswith('_'):
1912
- leap_binder.mapping_connections.append(NodeConnection(ret.node_mapping, None))
2175
+ leap_binder.mapping_connections.append(NodeConnection(ret.node_mapping, None))
1913
2176
  self._items[key] = ret
1914
2177
  return self._items[key]
1915
2178
 
@@ -1934,9 +2197,17 @@ def tensorleap_autoregressive_step(latent_space_aggregation: str = 'last_step'):
1934
2197
  self._unsupported_iteration()
1935
2198
 
1936
2199
  mapping_placeholder = _MappingInputsPlaceholder()
2200
+ mapping_state_placeholder = _MappingStatePlaceholder()
1937
2201
 
1938
2202
  def mapping_inner(*args, **kwargs):
1939
- return mapping_placeholder
2203
+ loop_context = _active_model_loop
2204
+ if loop_context is None:
2205
+ raise LeapValidationError(
2206
+ f'{user_function.__name__}() validation failed: inside the integration test '
2207
+ f'the autoregressive step may only be called from within the '
2208
+ f'tensorleap_model_loop function — including the initial call '
2209
+ f'(prev_inputs=None).')
2210
+ return loop_context.mapping_hook_call(mapping_placeholder, mapping_state_placeholder)
1940
2211
 
1941
2212
  def final_inner(*args, **kwargs):
1942
2213
  if os.environ.get(mapping_runtime_mode_env_var_mame):
@@ -1952,6 +2223,396 @@ def tensorleap_autoregressive_step(latent_space_aggregation: str = 'last_step'):
1952
2223
  return decorating_function
1953
2224
 
1954
2225
 
2226
+ def tensorleap_model_loop():
2227
+ """The user-authored autoregressive rollout loop.
2228
+
2229
+ Signature of the decorated function:
2230
+ (model, sample_id, preprocess) -> (final_inputs, final_outputs, final_state)
2231
+
2232
+ The function drives the chain locally, exactly as the platform will: it makes the initial
2233
+ hook call (prev_inputs=None, prev_outputs=None, state=None) inside the loop, alternates
2234
+ model forwards with hook calls, feeds the model exactly the tensors the hook returned, and
2235
+ exits when the hook returns None for next_inputs — returning the last inputs dict fed to the
2236
+ model, the last model outputs (a dict keyed by the declared prediction-type names) and the
2237
+ final state. The loop body itself never runs on the platform — the engine drives the chain —
2238
+ so the decorator validates, while the loop runs, that its behavior matches the engine loop;
2239
+ any computation in the loop body outside the hook and the model is invisible to the platform
2240
+ and fails validation. Canonical shape:
2241
+
2242
+ @tensorleap_model_loop()
2243
+ def model_loop(model, sample_id, preprocess):
2244
+ inputs, state = generate_next_inputs(sample_id, None, None, None, preprocess)
2245
+ while True:
2246
+ outputs = model(inputs)
2247
+ next_inputs, state = generate_next_inputs(sample_id, inputs, outputs, state,
2248
+ preprocess)
2249
+ if next_inputs is None:
2250
+ return inputs, outputs, state
2251
+ inputs = next_inputs
2252
+ """
2253
+
2254
+ def decorating_function(user_function):
2255
+
2256
+ def _normalize_args(args, kwargs):
2257
+ expected_names = ["model", "sample_id", "preprocess"]
2258
+ normalized = []
2259
+ for i, arg_name in enumerate(expected_names):
2260
+ if i < len(args):
2261
+ normalized.append(args[i])
2262
+ elif arg_name in kwargs:
2263
+ normalized.append(kwargs[arg_name])
2264
+ else:
2265
+ raise AssertionError(
2266
+ f'{user_function.__name__} validation failed: missing required argument '
2267
+ f"'{arg_name}'. Expected arguments: {expected_names}.")
2268
+ if len(args) + len(kwargs) != len(expected_names):
2269
+ raise AssertionError(
2270
+ f'{user_function.__name__} validation failed: expected exactly '
2271
+ f'{len(expected_names)} arguments {expected_names}, got '
2272
+ f'{len(args) + len(kwargs)}.')
2273
+ return normalized
2274
+
2275
+ def _fail(message):
2276
+ raise LeapValidationError(f'tensorleap_model_loop validation failed: {message}')
2277
+
2278
+ def _run_with_context(model, sample_id, preprocess_response, is_mapping):
2279
+ global _active_model_loop, _model_loop_run_in_current_test
2280
+ in_test = bool(_called_from_inside_tl_integration_test_decorator)
2281
+ if _active_model_loop is not None:
2282
+ _fail('model loops cannot nest or run concurrently.')
2283
+ if in_test and _model_loop_run_in_current_test:
2284
+ _fail('only one model loop run is allowed per integration test.')
2285
+ if model is None or model is not _last_loaded_model:
2286
+ _fail('the model argument must be the object returned by the '
2287
+ 'tensorleap_load_model function, unchanged.')
2288
+ if leap_binder.setup_container.autoregressive_step is None:
2289
+ _fail('tensorleap_model_loop requires a tensorleap_autoregressive_step hook.')
2290
+ prediction_names = [prediction_type.name for prediction_type
2291
+ in leap_binder.setup_container.prediction_types]
2292
+ if not prediction_names:
2293
+ _fail('tensorleap_model_loop requires prediction types declared on '
2294
+ 'tensorleap_load_model — the loop keys the model outputs by their names.')
2295
+ context = _ModelLoopContext(sample_id, preprocess_response, prediction_names,
2296
+ is_mapping)
2297
+ _active_model_loop = context
2298
+ try:
2299
+ result = user_function(_ModelLoopModelProxy(model, context), sample_id,
2300
+ preprocess_response)
2301
+ finally:
2302
+ _active_model_loop = None
2303
+ if in_test:
2304
+ _model_loop_run_in_current_test = True
2305
+ if context.phase == 'awaiting_first_hook':
2306
+ _fail('the loop never called the autoregressive step — it must start with the '
2307
+ 'initial hook call (prev_inputs=None, prev_outputs=None, state=None).')
2308
+ if context.phase != 'ended':
2309
+ _fail('the loop returned before the autoregressive step signaled the chain end — '
2310
+ 'it must run until the hook returns None for next_inputs and then return '
2311
+ '(final_inputs, final_outputs, final_state).')
2312
+ if not isinstance(result, tuple) or len(result) != 3:
2313
+ _fail('the loop must return (final_inputs, final_outputs, final_state). Got '
2314
+ f'{type(result).__name__}.')
2315
+ final_inputs, final_outputs, final_state = result
2316
+ if final_inputs is not context.fed_inputs:
2317
+ _fail('final_inputs must be exactly the last inputs dict fed to the model — the '
2318
+ 'one the hook returned before the terminating call.')
2319
+ if final_outputs is not context.last_outputs:
2320
+ _fail('final_outputs must be exactly the outputs dict of the last model call.')
2321
+ if final_state is not context.last_state:
2322
+ _fail('final_state must be exactly the state returned by the terminating hook '
2323
+ 'call.')
2324
+ return context, final_inputs, final_outputs, final_state
2325
+
2326
+ def inner(*args, **kwargs):
2327
+ if not _call_from_tl_platform:
2328
+ set_current('tensorleap_model_loop')
2329
+ model, sample_id, preprocess_response = _normalize_args(args, kwargs)
2330
+ in_test = _called_from_inside_tl_integration_test_decorator
2331
+ context, final_inputs, final_outputs, final_state = _run_with_context(
2332
+ model, sample_id, preprocess_response, is_mapping=False)
2333
+ if _nest_fingerprint(final_inputs) != context.fed_inputs_fingerprint:
2334
+ _fail('the final inputs were mutated in place after the last model call.')
2335
+ if _nest_fingerprint(final_outputs) != context.last_outputs_fingerprint:
2336
+ _fail('the final outputs were mutated in place after the last model call.')
2337
+ if _nest_fingerprint(final_state) != context.last_state_fingerprint:
2338
+ _fail('the final state was mutated in place after the terminating hook call.')
2339
+ if not in_test:
2340
+ if not _call_from_tl_platform:
2341
+ update_env_params_func("tensorleap_model_loop", "v")
2342
+ return final_inputs, final_outputs, final_state
2343
+ # Inside the integration test the chain tensors carry a batch-1 axis; the platform
2344
+ # feeds autoregressive metrics per-chain unbatched dicts, so strip it here.
2345
+ squeezed_inputs = _squeeze_leading_batch_axis(final_inputs)
2346
+ squeezed_outputs = _squeeze_leading_batch_axis(final_outputs)
2347
+ _register_chain_artifact(squeezed_inputs, 'final_inputs')
2348
+ _register_chain_artifact(squeezed_outputs, 'final_outputs')
2349
+ _register_chain_artifact(final_state, 'final_state')
2350
+ if not _call_from_tl_platform:
2351
+ update_env_params_func("tensorleap_model_loop", "v")
2352
+ return squeezed_inputs, squeezed_outputs, final_state
2353
+
2354
+ def mapping_inner(*args, **kwargs):
2355
+ model, sample_id, preprocess_response = _normalize_args(args, kwargs)
2356
+ context, final_inputs, final_outputs, final_state = _run_with_context(
2357
+ model, sample_id, preprocess_response, is_mapping=True)
2358
+ _register_chain_artifact(final_inputs, 'final_inputs')
2359
+ _register_chain_artifact(final_outputs, 'final_outputs')
2360
+ _register_chain_artifact(final_state, 'final_state')
2361
+ return final_inputs, final_outputs, final_state
2362
+
2363
+ def final_inner(*args, **kwargs):
2364
+ if os.environ.get(mapping_runtime_mode_env_var_mame):
2365
+ return mapping_inner(*args, **kwargs)
2366
+ else:
2367
+ return inner(*args, **kwargs)
2368
+
2369
+ if not _call_from_tl_platform:
2370
+ register_decorator_row("tensorleap_model_loop")
2371
+
2372
+ return final_inner
2373
+
2374
+ return decorating_function
2375
+
2376
+
2377
+ _IMPLICIT_ARG_TO_ARTIFACT_KIND = {'inputs': 'final_inputs', 'outputs': 'final_outputs',
2378
+ 'state': 'final_state'}
2379
+ _ARTIFACT_KIND_DESCRIPTIONS = {
2380
+ 'final_inputs': 'final inputs dict returned by the model loop',
2381
+ 'final_outputs': 'final outputs dict returned by the model loop',
2382
+ 'final_state': 'final state returned by the model loop',
2383
+ 'ground_truth': 'direct return value of a ground-truth encoder',
2384
+ }
2385
+ _AUTOREGRESSIVE_SCALAR_TYPES = (int, float, np.floating, np.integer, np.bool_)
2386
+
2387
+
2388
+ def _normalize_autoregressive_call_args(func_name, full_arg_names, args, kwargs):
2389
+ if len(args) + len(kwargs) != len(full_arg_names):
2390
+ raise AssertionError(
2391
+ f'{func_name} validation failed: expected exactly {len(full_arg_names)} arguments '
2392
+ f'{full_arg_names}, got {len(args) + len(kwargs)}.')
2393
+ normalized = {}
2394
+ for i, arg_name in enumerate(full_arg_names):
2395
+ if i < len(args):
2396
+ normalized[arg_name] = args[i]
2397
+ elif arg_name in kwargs:
2398
+ normalized[arg_name] = kwargs[arg_name]
2399
+ else:
2400
+ raise AssertionError(
2401
+ f'{func_name} validation failed: missing required argument \'{arg_name}\'. '
2402
+ f'Expected arguments: {full_arg_names}.')
2403
+ return normalized
2404
+
2405
+
2406
+ def _validate_autoregressive_arg_provenance(func_name, normalized):
2407
+ for arg_name, value in normalized.items():
2408
+ expected_kind = _IMPLICIT_ARG_TO_ARTIFACT_KIND.get(arg_name, 'ground_truth')
2409
+ entry = _lookup_chain_artifact(value)
2410
+ if entry is None:
2411
+ raise LeapValidationError(
2412
+ f'{func_name} validation failed: argument "{arg_name}" must be the '
2413
+ f'{_ARTIFACT_KIND_DESCRIPTIONS[expected_kind]}, passed whole and unmodified — '
2414
+ f'the platform feeds it from the finished chain, so anything else would not '
2415
+ f'exist at platform runtime. Slice or derive inside the function body instead.')
2416
+ kind, fingerprint, _ = entry
2417
+ if kind != expected_kind:
2418
+ raise LeapValidationError(
2419
+ f'{func_name} validation failed: argument "{arg_name}" received the '
2420
+ f'{_ARTIFACT_KIND_DESCRIPTIONS[kind]} but must be the '
2421
+ f'{_ARTIFACT_KIND_DESCRIPTIONS[expected_kind]}.')
2422
+ if fingerprint != _nest_fingerprint(value):
2423
+ raise LeapValidationError(
2424
+ f'{func_name} validation failed: argument "{arg_name}" was mutated in place '
2425
+ f'after it was produced — the platform feeds the original values, so local '
2426
+ f'mutations diverge from platform results.')
2427
+
2428
+
2429
+ def _squeeze_wired_autoregressive_args(normalized):
2430
+ squeezed = {}
2431
+ for arg_name, value in normalized.items():
2432
+ if arg_name not in _IMPLICIT_ARG_TO_ARTIFACT_KIND and isinstance(value, np.ndarray) \
2433
+ and value.ndim > 0 and value.shape[0] == 1:
2434
+ squeezed[arg_name] = value[0]
2435
+ else:
2436
+ squeezed[arg_name] = value
2437
+ return squeezed
2438
+
2439
+
2440
+ def _validate_autoregressive_scalar_result(func_name, result, allow_dict):
2441
+ def _is_scalar(value):
2442
+ return isinstance(value, _AUTOREGRESSIVE_SCALAR_TYPES) or \
2443
+ (isinstance(value, np.ndarray) and value.ndim == 0)
2444
+
2445
+ if _is_scalar(result):
2446
+ return
2447
+ if allow_dict and isinstance(result, dict) and result and \
2448
+ all(isinstance(key, str) for key in result) and \
2449
+ all(_is_scalar(value) for value in result.values()):
2450
+ return
2451
+ expected = 'a scalar number' + (' or a dict of named scalar numbers' if allow_dict else '')
2452
+ raise AssertionError(
2453
+ f'{func_name} validation failed: autoregressive results are per-chain — the function '
2454
+ f'must return {expected} for the single chain it received. Got {type(result).__name__}.')
2455
+
2456
+
2457
+ def _autoregressive_mapping_call(mapping_inner_func, args, kwargs, node_mapping_type):
2458
+ user_unique_name = mapping_inner_func.name
2459
+ if 'user_unique_name' in kwargs:
2460
+ kwargs = dict(kwargs)
2461
+ user_unique_name = kwargs.pop('user_unique_name')
2462
+ if user_unique_name in mapping_inner_func.name_to_unique_name[mapping_inner_func.name]:
2463
+ user_unique_name = \
2464
+ f'{user_unique_name}_{len(mapping_inner_func.name_to_unique_name[mapping_inner_func.name])}'
2465
+ mapping_inner_func.name_to_unique_name[mapping_inner_func.name].add(user_unique_name)
2466
+
2467
+ normalized = _normalize_autoregressive_call_args(mapping_inner_func.name,
2468
+ mapping_inner_func.full_arg_names, args,
2469
+ kwargs)
2470
+ for arg_name, value in normalized.items():
2471
+ if arg_name in _IMPLICIT_ARG_TO_ARTIFACT_KIND:
2472
+ entry = _lookup_chain_artifact(value)
2473
+ expected_kind = _IMPLICIT_ARG_TO_ARTIFACT_KIND[arg_name]
2474
+ if entry is None or entry[0] != expected_kind:
2475
+ raise LeapValidationError(
2476
+ f'{mapping_inner_func.name} validation failed: argument "{arg_name}" must '
2477
+ f'be the {_ARTIFACT_KIND_DESCRIPTIONS[expected_kind]}, passed whole.')
2478
+ elif not hasattr(value, 'node_mapping'):
2479
+ raise LeapValidationError(
2480
+ f'{mapping_inner_func.name} validation failed: argument "{arg_name}" must be '
2481
+ f'the direct return value of a ground-truth encoder.')
2482
+ ordered_connections = [normalized[arg_name] for arg_name in mapping_inner_func.arg_names]
2483
+ _add_mapping_connection(user_unique_name, ordered_connections, mapping_inner_func.arg_names,
2484
+ mapping_inner_func.name, node_mapping_type)
2485
+ return None
2486
+
2487
+
2488
+ def _make_autoregressive_decorator_inner(user_function, name, decorator_env_name, handler_data,
2489
+ node_mapping_type, validate_result):
2490
+ full_arg_names = inspect.getfullargspec(user_function)[0]
2491
+
2492
+ def inner_without_validate(*args, **kwargs):
2493
+ global _called_from_inside_tl_decorator
2494
+ _called_from_inside_tl_decorator += 1
2495
+ try:
2496
+ result = user_function(*args, **kwargs)
2497
+ finally:
2498
+ _called_from_inside_tl_decorator -= 1
2499
+ return result
2500
+
2501
+ def inner(*args, **kwargs):
2502
+ if not _call_from_tl_platform:
2503
+ set_current(decorator_env_name)
2504
+ normalized = _normalize_autoregressive_call_args(user_function.__name__, full_arg_names,
2505
+ args, kwargs)
2506
+ is_top_level_in_test = (_called_from_inside_tl_decorator == 0
2507
+ and _called_from_inside_tl_integration_test_decorator)
2508
+ if is_top_level_in_test:
2509
+ _validate_autoregressive_arg_provenance(user_function.__name__, normalized)
2510
+ normalized = _squeeze_wired_autoregressive_args(normalized)
2511
+ result = inner_without_validate(**normalized)
2512
+ validate_result(user_function.__name__, result)
2513
+ if not _call_from_tl_platform:
2514
+ update_env_params_func(decorator_env_name, "v")
2515
+ return result
2516
+
2517
+ def mapping_inner(*args, **kwargs):
2518
+ return _autoregressive_mapping_call(mapping_inner, args, kwargs, node_mapping_type)
2519
+
2520
+ mapping_inner.name = name
2521
+ mapping_inner.full_arg_names = full_arg_names
2522
+ mapping_inner.arg_names = handler_data.arg_names
2523
+ mapping_inner.name_to_unique_name = defaultdict(set)
2524
+
2525
+ def final_inner(*args, **kwargs):
2526
+ if os.environ.get(mapping_runtime_mode_env_var_mame):
2527
+ return mapping_inner(*args, **kwargs)
2528
+ else:
2529
+ return inner(*args, **kwargs)
2530
+
2531
+ if not _call_from_tl_platform:
2532
+ register_decorator_row(decorator_env_name)
2533
+
2534
+ return final_inner
2535
+
2536
+
2537
+ def tensorleap_autoregressive_metric(name: str,
2538
+ direction: Union[MetricDirection, Dict[str, MetricDirection]]
2539
+ = MetricDirection.Downward,
2540
+ compute_insights: Optional[Union[bool, Dict[str, bool]]] = None):
2541
+ """A per-chain metric over a finished autoregressive chain.
2542
+
2543
+ The reserved arguments inputs / outputs / state are fed implicitly by the platform from the
2544
+ chain's final step (unbatched dicts; outputs keyed by the declared prediction-type names;
2545
+ state as the terminating hook call returned it). Declare whichever of them the metric needs.
2546
+ Every other argument is wired to a ground-truth encoder through the integration test. Runs
2547
+ once per chain and must return a scalar number (or a dict of named scalar numbers).
2548
+ """
2549
+
2550
+ def decorating_function(user_function):
2551
+ leap_binder.add_autoregressive_metric(user_function, name, direction, compute_insights)
2552
+ handler_data = \
2553
+ leap_binder.setup_container.autoregressive_metrics[-1].metric_handler_data
2554
+
2555
+ def validate_result(func_name, result):
2556
+ _validate_autoregressive_scalar_result(func_name, result, allow_dict=True)
2557
+
2558
+ return _make_autoregressive_decorator_inner(
2559
+ user_function, name, 'tensorleap_autoregressive_metric', handler_data,
2560
+ NodeMappingType.Metric, validate_result)
2561
+
2562
+ return decorating_function
2563
+
2564
+
2565
+ def tensorleap_autoregressive_loss(name: str):
2566
+ """A per-chain loss over a finished autoregressive chain.
2567
+
2568
+ Same argument contract as tensorleap_autoregressive_metric; must return a single scalar
2569
+ number for the chain it received.
2570
+ """
2571
+
2572
+ def decorating_function(user_function):
2573
+ leap_binder.add_autoregressive_loss(user_function, name)
2574
+ handler_data = \
2575
+ leap_binder.setup_container.autoregressive_losses[-1].custom_loss_handler_data
2576
+
2577
+ def validate_result(func_name, result):
2578
+ _validate_autoregressive_scalar_result(func_name, result, allow_dict=False)
2579
+
2580
+ return _make_autoregressive_decorator_inner(
2581
+ user_function, name, 'tensorleap_autoregressive_loss', handler_data,
2582
+ NodeMappingType.CustomLoss, validate_result)
2583
+
2584
+ return decorating_function
2585
+
2586
+
2587
+ def tensorleap_autoregressive_visualizer(name: str, visualizer_type: LeapDataType):
2588
+ """A per-chain visualizer over a finished autoregressive chain.
2589
+
2590
+ Same argument contract as tensorleap_autoregressive_metric; must return a LeapData object
2591
+ matching visualizer_type.
2592
+ """
2593
+
2594
+ def decorating_function(user_function):
2595
+ assert isinstance(visualizer_type, LeapDataType), \
2596
+ (f'{user_function.__name__} validation failed: visualizer_type should be of type '
2597
+ f'{LeapDataType.__name__} but got {type(visualizer_type)}')
2598
+ leap_binder.add_autoregressive_visualizer(user_function, name, visualizer_type)
2599
+ handler_data = \
2600
+ leap_binder.setup_container.autoregressive_visualizers[-1].visualizer_handler_data
2601
+
2602
+ def validate_result(func_name, result):
2603
+ expected_class = map_leap_data_type_to_visualizer_class[visualizer_type.value]
2604
+ assert isinstance(result, expected_class), \
2605
+ (f'{func_name} validation failed: the return type should be '
2606
+ f'{expected_class.__name__} (visualizer_type={visualizer_type}). '
2607
+ f'Got {type(result).__name__}.')
2608
+
2609
+ return _make_autoregressive_decorator_inner(
2610
+ user_function, name, 'tensorleap_autoregressive_visualizer', handler_data,
2611
+ NodeMappingType.Visualizer, validate_result)
2612
+
2613
+ return decorating_function
2614
+
2615
+
1955
2616
  def tensorleap_preprocess():
1956
2617
  def decorating_function(user_function: Callable[[], List[PreprocessResponse]]):
1957
2618
  leap_binder.set_preprocess(user_function)
@@ -1982,15 +2643,6 @@ def tensorleap_preprocess():
1982
2643
  assert len(set(result)) == len(result), \
1983
2644
  (f'{user_function.__name__}() validation failed: '
1984
2645
  f'The return list should not contain duplicate PreprocessResponse objects.')
1985
- # All-or-none: a dataset is either all grouped (nested sample_ids) or all flat.
1986
- if len({response.is_grouped for response in result}) != 1:
1987
- shapes = ', '.join(
1988
- f"#{i}={'grouped' if response.is_grouped else 'flat'}"
1989
- for i, response in enumerate(result))
1990
- raise AssertionError(
1991
- f'{user_function.__name__}() validation failed: '
1992
- f'All PreprocessResponses must be either all grouped (nested sample_ids) '
1993
- f'or all flat, but got a mix: {shapes}.')
1994
2646
 
1995
2647
  def inner(*args, **kwargs):
1996
2648
  if not _call_from_tl_platform:
@@ -2328,10 +2980,14 @@ def tensorleap_input_encoder(name: str, channel_dim=_UNSET, model_input_index=No
2328
2980
  f"channel axis in the batched tensor (batch = axis 0), so 0 is invalid. Use 1 "
2329
2981
  f"for channel-first (NCHW) and -1 for channel-last (NHWC).")
2330
2982
 
2331
- def _validate_input_args(sample_id: Union[int, str, list], preprocess_response: PreprocessResponse):
2332
- _validate_id_or_group(sample_id, preprocess_response, user_function.__name__)
2983
+ def _validate_input_args(sample_id: Union[int, str], preprocess_response: PreprocessResponse):
2984
+ assert type(sample_id) == preprocess_response.sample_id_type, \
2985
+ (f'{user_function.__name__}() validation failed: '
2986
+ f'Argument sample_id should be as the same type as defined in the preprocess response '
2987
+ f'{preprocess_response.sample_id_type}. Got {type(sample_id)}.')
2333
2988
 
2334
- def _validate_single(result):
2989
+ def _validate_result(result):
2990
+ validate_output_structure(result, func_name=user_function.__name__, expected_type_name="np.ndarray")
2335
2991
  assert isinstance(result, np.ndarray), \
2336
2992
  (f'{user_function.__name__}() validation failed: '
2337
2993
  f'Unsupported return type. Should be a numpy array. Got {type(result)}.')
@@ -2341,13 +2997,6 @@ def tensorleap_input_encoder(name: str, channel_dim=_UNSET, model_input_index=No
2341
2997
  assert channel_dim - 1 <= len(result.shape), (f'{user_function.__name__}() validation failed: '
2342
2998
  f'The channel_dim ({channel_dim}) should be <= to the rank of the resulting input rank ({len(result.shape)}).')
2343
2999
 
2344
- def _validate_result(result, grouped=False, group_size=None):
2345
- if not grouped:
2346
- validate_output_structure(result, func_name=user_function.__name__, expected_type_name="np.ndarray")
2347
- _validate_single(result)
2348
- return
2349
- _validate_grouped_result(result, group_size, user_function.__name__, _validate_single)
2350
-
2351
3000
  def inner_without_validate(sample_id, preprocess_response):
2352
3001
  global _called_from_inside_tl_decorator
2353
3002
  _called_from_inside_tl_decorator += 1
@@ -2364,27 +3013,18 @@ def tensorleap_input_encoder(name: str, channel_dim=_UNSET, model_input_index=No
2364
3013
  def inner(*args, **kwargs):
2365
3014
  if not _call_from_tl_platform:
2366
3015
  set_current("tensorleap_input_encoder")
2367
- validate_args_structure(*args, types_order=[Union[int, str, list], PreprocessResponse],
3016
+ validate_args_structure(*args, types_order=[Union[int, str], PreprocessResponse],
2368
3017
  func_name=user_function.__name__, expected_names=["idx", "preprocess"], **kwargs)
2369
3018
  sample_id, preprocess_response = args if len(args) != 0 else kwargs.values()
2370
3019
  _validate_input_args(sample_id, preprocess_response)
2371
3020
 
2372
- grouped = isinstance(sample_id, list)
2373
- group_size = len(sample_id) if grouped else None
2374
3021
  result = inner_without_validate(sample_id, preprocess_response)
2375
3022
 
2376
- _validate_result(result, grouped, group_size)
3023
+ _validate_result(result)
2377
3024
 
2378
3025
  if _called_from_inside_tl_decorator == 0 and _called_from_inside_tl_integration_test_decorator:
2379
- if grouped:
2380
- # A grouped result is a list of per-sample arrays (never stacked, so a
2381
- # B=1-only model can be fed one sample at a time). Add a batch dim to each
2382
- # sample, mirroring the flat path's single-sample expand_dims.
2383
- if isinstance(result, list):
2384
- result = [np.expand_dims(r, axis=0) for r in result]
2385
- else:
2386
- batch_warning(result, user_function.__name__)
2387
- result = np.expand_dims(result, axis=0)
3026
+ batch_warning(result, user_function.__name__)
3027
+ result = np.expand_dims(result, axis=0)
2388
3028
  # Emit integration test event once per test
2389
3029
  try:
2390
3030
  emit_integration_event_once(AnalyticsEvent.INPUT_ENCODER_INTEGRATION_TEST, {
@@ -2405,15 +3045,8 @@ def tensorleap_input_encoder(name: str, channel_dim=_UNSET, model_input_index=No
2405
3045
  inner.node_mapping = NodeMapping(name, node_mapping_type)
2406
3046
 
2407
3047
  def mapping_inner(*args, **kwargs):
2408
- if _mapping_dataset_is_grouped:
2409
- class TempMapping:
2410
- def __getitem__(self, key):
2411
- # Indexing a grouped input group (input_list[i]) selects a sample from the
2412
- # same input source; the model wiring is identical to the un-grouped input.
2413
- return self
2414
- else:
2415
- class TempMapping:
2416
- pass
3048
+ class TempMapping:
3049
+ pass
2417
3050
 
2418
3051
  ret = TempMapping()
2419
3052
  ret.node_mapping = mapping_inner.node_mapping
@@ -2443,10 +3076,15 @@ def tensorleap_gt_encoder(name: str):
2443
3076
  raise Exception(f'GT with name {name} already exists. '
2444
3077
  f'Please choose another')
2445
3078
 
2446
- def _validate_input_args(sample_id: Union[int, str, list], preprocess_response: PreprocessResponse):
2447
- _validate_id_or_group(sample_id, preprocess_response, user_function.__name__)
3079
+ def _validate_input_args(sample_id: Union[int, str], preprocess_response: PreprocessResponse):
3080
+ assert type(sample_id) == preprocess_response.sample_id_type, \
3081
+ (f'{user_function.__name__}() validation failed: '
3082
+ f'Argument sample_id should be as the same type as defined in the preprocess response '
3083
+ f'{preprocess_response.sample_id_type}. Got {type(sample_id)}.')
2448
3084
 
2449
- def _validate_single(result):
3085
+ def _validate_result(result):
3086
+ validate_output_structure(result, func_name=user_function.__name__, expected_type_name="np.ndarray",
3087
+ gt_flag=True)
2450
3088
  assert isinstance(result, np.ndarray), \
2451
3089
  (f'{user_function.__name__}() validation failed: '
2452
3090
  f'Unsupported return type. Should be a numpy array. Got {type(result)}.')
@@ -2454,14 +3092,6 @@ def tensorleap_gt_encoder(name: str):
2454
3092
  (f'{user_function.__name__}() validation failed: '
2455
3093
  f'The return type should be a numpy array of type float32. Got {result.dtype}.')
2456
3094
 
2457
- def _validate_result(result, grouped=False, group_size=None):
2458
- if not grouped:
2459
- validate_output_structure(result, func_name=user_function.__name__, expected_type_name="np.ndarray",
2460
- gt_flag=True)
2461
- _validate_single(result)
2462
- return
2463
- _validate_grouped_result(result, group_size, user_function.__name__, _validate_single)
2464
-
2465
3095
  def inner_without_validate(sample_id, preprocess_response):
2466
3096
  global _called_from_inside_tl_decorator
2467
3097
  _called_from_inside_tl_decorator += 1
@@ -2478,27 +3108,19 @@ def tensorleap_gt_encoder(name: str):
2478
3108
  def inner(*args, **kwargs):
2479
3109
  if not _call_from_tl_platform:
2480
3110
  set_current("tensorleap_gt_encoder")
2481
- validate_args_structure(*args, types_order=[Union[int, str, list], PreprocessResponse],
3111
+ validate_args_structure(*args, types_order=[Union[int, str], PreprocessResponse],
2482
3112
  func_name=user_function.__name__, expected_names=["idx", "preprocess"], **kwargs)
2483
3113
  sample_id, preprocess_response = args
2484
3114
  _validate_input_args(sample_id, preprocess_response)
2485
3115
 
2486
- grouped = isinstance(sample_id, list)
2487
- group_size = len(sample_id) if grouped else None
2488
3116
  result = inner_without_validate(sample_id, preprocess_response)
2489
3117
 
2490
- _validate_result(result, grouped, group_size)
3118
+ _validate_result(result)
2491
3119
 
2492
3120
  if _called_from_inside_tl_decorator == 0 and _called_from_inside_tl_integration_test_decorator:
2493
- if grouped:
2494
- # A grouped result is a list of per-sample arrays (never stacked, so a
2495
- # B=1-only model can be fed one sample at a time). Add a batch dim to each
2496
- # sample, mirroring the flat path's single-sample expand_dims.
2497
- if isinstance(result, list):
2498
- result = [np.expand_dims(r, axis=0) for r in result]
2499
- else:
2500
- batch_warning(result, user_function.__name__)
2501
- result = np.expand_dims(result, axis=0)
3121
+ batch_warning(result, user_function.__name__)
3122
+ result = np.expand_dims(result, axis=0)
3123
+ _register_chain_artifact(result, 'ground_truth')
2502
3124
  # Emit integration test event once per test
2503
3125
  try:
2504
3126
  emit_integration_event_once(AnalyticsEvent.GT_ENCODER_INTEGRATION_TEST, {
@@ -2513,15 +3135,8 @@ def tensorleap_gt_encoder(name: str):
2513
3135
  inner.node_mapping = NodeMapping(name, NodeMappingType.GroundTruth)
2514
3136
 
2515
3137
  def mapping_inner(*args, **kwargs):
2516
- if _mapping_dataset_is_grouped:
2517
- class TempMapping:
2518
- def __getitem__(self, key):
2519
- # Indexing a grouped GT group (gt_list[i]) selects a sample from the same
2520
- # ground-truth source; wiring is identical to the un-grouped GT node.
2521
- return self
2522
- else:
2523
- class TempMapping:
2524
- pass
3138
+ class TempMapping:
3139
+ pass
2525
3140
 
2526
3141
  ret = TempMapping()
2527
3142
  ret.node_mapping = mapping_inner.node_mapping
@@ -2772,6 +3387,12 @@ def tensorleap_status_table():
2772
3387
  if not started_from("leap_integration.py"):
2773
3388
  return
2774
3389
 
3390
+ if leap_binder.setup_container.autoregressive_step is not None:
3391
+ # Input encoders are forbidden in autoregressive integrations — the step hook is
3392
+ # the sole source of model inputs — so the row is not applicable: it must not be
3393
+ # printed, gate readiness, or be recommended as a next step.
3394
+ table[:] = [row for row in table if row["name"] != "tensorleap_input_encoder"]
3395
+
2775
3396
  ready_mess = "\nAll parts have been successfully set. If no errors accured, you can now push the project to the Tensorleap system."
2776
3397
  not_ready_mess = "\nSome mandatory components have not yet been added to the Integration test. Recommended next interface to add is: "
2777
3398
  mandatory_ready_mess = "\nAll mandatory parts have been successfully set. If no errors accured, you can now push the project to the Tensorleap system or continue to the next optional reccomeded interface,adding: "
@@ -2803,7 +3424,11 @@ def tensorleap_status_table():
2803
3424
  return
2804
3425
 
2805
3426
  if _crashed["value"]:
2806
- print(f"\nScript crashed before completing all steps. crashed at function '{_current_func['name']}'.")
3427
+ crashed_at = _current_func['name']
3428
+ if crashed_at:
3429
+ print(f"\nScript crashed before completing all steps. crashed at function '{crashed_at}'.")
3430
+ else:
3431
+ print("\nScript crashed before completing all steps.")
2807
3432
  return
2808
3433
 
2809
3434
  print(ready_mess) if ready else print(
@@ -2878,9 +3503,11 @@ def tensorleap_status_table():
2878
3503
  row = _find_row(crashed_name)
2879
3504
  if row:
2880
3505
  row["Added to integration"] = CROSS
2881
- elif not crashed_name:
2882
- # Crash with no decorator currently running = the integration-test body /
2883
- # code flow itself failed; report that rather than blaming a decorator.
3506
+ elif not crashed_name and _integration_test_started:
3507
+ # Crash with no decorator currently running while an integration test has been
3508
+ # entered = the test body / code flow itself failed; report that rather than
3509
+ # blaming a decorator. Without a started test (a module-level crash between
3510
+ # decorator registrations) the plain script-crash message is the truthful one.
2884
3511
  code_mapping_failure[0] = 1
2885
3512
 
2886
3513
  traceback.print_exception(exc_type, exc_value, exc_traceback)