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