code-loader 1.0.195.dev0__py3-none-any.whl → 1.0.196.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,6 +1,6 @@
1
1
  import warnings
2
2
  from dataclasses import dataclass, field
3
- from typing import Any, Callable, List, Optional, Dict, Union, Type, Literal, Tuple
3
+ from typing import Any, Callable, List, Optional, Dict, Union, Type, Literal, Tuple, cast
4
4
  import re
5
5
  import numpy as np
6
6
  import numpy.typing as npt
@@ -15,6 +15,8 @@ custom_latent_space_attribute = "custom_latent_space"
15
15
 
16
16
  _simulation_context: Dict[str, bool] = {"active": False}
17
17
 
18
+ SampleId = Union[int, str]
19
+
18
20
 
19
21
  @dataclass
20
22
  class PreprocessResponse:
@@ -39,27 +41,46 @@ class PreprocessResponse:
39
41
  """
40
42
  length: Optional[int] = None # Deprecated. Please use sample_ids instead
41
43
  data: Any = None
42
- sample_ids: Optional[Union[List[str], List[int]]] = None
44
+ sample_ids: Optional[Union[List[SampleId], List[List[SampleId]]]] = None
43
45
  state: Optional[DataStateType] = None
44
46
  sample_id_type: Optional[Union[Type[str], Type[int]]] = None
45
47
  sample_ids_to_instance_mappings: Optional[Dict[str, List[str]]] = None # in use only for element instance
46
48
  instance_to_sample_ids_mappings: Optional[Dict[str, str]] = None # in use only for element instance
47
49
  tl_generated: bool = False
50
+ _grouped: bool = field(default=False, init=False, repr=False, compare=False)
51
+ _group_pos_cache: Optional[Dict[SampleId, Tuple[List[SampleId], int]]] = field(
52
+ default=None, init=False, repr=False, compare=False)
48
53
 
49
54
  def __post_init__(self) -> None:
50
55
  assert self.sample_ids_to_instance_mappings is None, f"Keep sample_ids_to_instance_mappings None when initializing PreprocessResponse"
51
56
  assert self.instance_to_sample_ids_mappings is None, f"Keep instance_to_sample_ids_mappings None when initializing PreprocessResponse"
52
57
 
53
58
  if self.length is not None and self.sample_ids is None:
54
- self.sample_ids = [i for i in range(self.length)]
59
+ self.sample_ids = cast(List[SampleId], [i for i in range(self.length)])
55
60
  self.sample_id_type = int
61
+ self._grouped = False
56
62
  elif self.length is None and self.sample_ids is not None:
57
- self.length = len(self.sample_ids)
58
- if self.sample_id_type is None:
59
- self.sample_id_type = str
60
- if self.sample_id_type == str:
61
- for sample_id in self.sample_ids:
62
- assert isinstance(sample_id, str), f"Sample id should be of type str. Got: {type(sample_id)}"
63
+ self._grouped = len(self.sample_ids) > 0 and isinstance(self.sample_ids[0], list)
64
+ if self._grouped:
65
+ groups = cast(List[List[SampleId]], self.sample_ids)
66
+ assert len(groups) >= 1, "Grouped PreprocessResponse must have at least one group."
67
+ for group in groups:
68
+ assert isinstance(group, list) and len(group) > 0, \
69
+ "Each group in a grouped PreprocessResponse must be a non-empty list."
70
+ self.sample_id_type = type(groups[0][0])
71
+ for group in groups:
72
+ for sample_id in group:
73
+ assert isinstance(sample_id, self.sample_id_type), \
74
+ f"Sample id should be of type {self.sample_id_type.__name__}. Got: {type(sample_id)}"
75
+ self.length = sum(len(group) for group in groups)
76
+ else:
77
+ flat_ids = cast(List[SampleId], self.sample_ids)
78
+ self.length = len(flat_ids)
79
+ if self.sample_id_type is None:
80
+ self.sample_id_type = str
81
+ if self.sample_id_type == str:
82
+ for sample_id in flat_ids:
83
+ assert isinstance(sample_id, str), f"Sample id should be of type str. Got: {type(sample_id)}"
63
84
  else:
64
85
  raise Exception("length is deprecated, please use sample_ids instead.")
65
86
 
@@ -79,9 +100,36 @@ class PreprocessResponse:
79
100
  return id(self)
80
101
 
81
102
  def __len__(self) -> int:
103
+ assert self.length is not None
104
+ return self.length
105
+
106
+ @property
107
+ def is_grouped(self) -> bool:
108
+ return self._grouped
109
+
110
+ @property
111
+ def num_groups(self) -> int:
112
+ # Grouped: the number of groups. Flat: the (flat) sample count, since sample_ids is the
113
+ # flat id list and there is one sample per "group".
82
114
  assert self.sample_ids is not None
83
115
  return len(self.sample_ids)
84
116
 
117
+ @property
118
+ def groups(self) -> Optional[List[List[SampleId]]]:
119
+ # The nested list of groups when grouped, otherwise None. Callers should
120
+ # branch on is_grouped before relying on this.
121
+ return cast(List[List[SampleId]], self.sample_ids) if self._grouped else None
122
+
123
+ @property
124
+ def flat_sample_ids(self) -> List[SampleId]:
125
+ # All sample ids in order: concatenation of the groups when grouped,
126
+ # otherwise the flat sample_ids list as-is.
127
+ assert self.sample_ids is not None
128
+ if self._grouped:
129
+ groups = cast(List[List[SampleId]], self.sample_ids)
130
+ return [sample_id for group in groups for sample_id in group]
131
+ return cast(List[SampleId], self.sample_ids)
132
+
85
133
  @dataclass
86
134
  class ElementInstance:
87
135
  name: str
@@ -1,6 +1,6 @@
1
1
 
2
2
  import inspect
3
- from typing import Callable, List, Optional, Dict, Any, Type, Union, get_args
3
+ from typing import Callable, List, Optional, Dict, Any, Type, Union, get_args, cast
4
4
 
5
5
  import numpy as np
6
6
  import numpy.typing as npt
@@ -549,7 +549,9 @@ class LeapBinder:
549
549
  # Builtin chain metadata, declared at parse time so it survives the reporter's
550
550
  # metadata type mapping; the placeholder values are overwritten by the engine when a
551
551
  # chain finalizes (realized length, truncated-by-safety-cap flag).
552
- def builtin_chain_metadata(idx: Any, preprocess: PreprocessResponse) -> Dict[str, Any]:
552
+ def builtin_chain_metadata(idx: Any, preprocess: PreprocessResponse) -> Any:
553
+ if isinstance(idx, list):
554
+ return [{'length': -1, 'truncated': False} for _ in idx]
553
555
  return {'length': -1, 'truncated': False}
554
556
 
555
557
  self.set_metadata(builtin_chain_metadata, 'builtin_chain')
@@ -713,6 +715,15 @@ class LeapBinder:
713
715
  if state_enum in preprocess_result_dict:
714
716
  preprocess_result_dict_in_correct_order[state_enum] = preprocess_result_dict[state_enum]
715
717
 
718
+ # All-or-none: every state must agree on grouped (nested sample_ids) vs flat.
719
+ if len({r.is_grouped for r in preprocess_result_dict_in_correct_order.values()}) > 1:
720
+ shapes = ', '.join(
721
+ f"{state.name}={'grouped' if r.is_grouped else 'flat'}"
722
+ for state, r in preprocess_result_dict_in_correct_order.items())
723
+ raise Exception(
724
+ "All preprocess states must be either all grouped (nested sample_ids) or all flat, "
725
+ f"but got a mix: {shapes}.")
726
+
716
727
  return preprocess_result_dict_in_correct_order
717
728
 
718
729
  def get_preprocess_unlabeled_result(self) -> Optional[PreprocessResponse]:
@@ -733,7 +744,15 @@ class LeapBinder:
733
744
  preprocess_response: PreprocessResponse, test_result: List[DatasetTestResultPayload],
734
745
  dataset_base_handler: Union[DatasetBaseHandler, MetadataHandler], state: DataStateEnum) -> List[DatasetTestResultPayload]:
735
746
  assert preprocess_response.sample_ids is not None
736
- raw_result = dataset_base_handler.function(preprocess_response.sample_ids[0], preprocess_response)
747
+ if preprocess_response.is_grouped:
748
+ # Grouped: probe with the first group, then reduce to a single sample's result so the
749
+ # recorded shape/type is per-sample (matching the flat case), not the (B, *) batch.
750
+ # (casts: sample_ids[0] is a group list here, and the grouped result is a per-sample list.)
751
+ group = preprocess_response.sample_ids[0]
752
+ raw_result = cast(Any, dataset_base_handler.function(cast(Any, group), preprocess_response))[0]
753
+ else:
754
+ raw_result = dataset_base_handler.function(
755
+ cast(Union[int, str], preprocess_response.sample_ids[0]), preprocess_response)
737
756
  handler_type = 'metadata' if isinstance(dataset_base_handler, MetadataHandler) else None
738
757
  if isinstance(dataset_base_handler, MetadataHandler):
739
758
  if isinstance(raw_result, dict):
@@ -829,10 +848,11 @@ class LeapBinder:
829
848
  preprocess_response.tl_generated = True
830
849
  if not preprocess_response.length or preprocess_response.length < 1:
831
850
  raise Exception("Simulation '{}' returned PreprocessResponse with length < 1".format(sim.name))
832
- preprocess_response.sample_ids = [0]
851
+ preprocess_response.sample_ids = cast(List[Union[int, str]], [0])
852
+ sim_sample_ids = cast(List[Union[int, str]], preprocess_response.sample_ids)
833
853
  for handler in self.setup_container.inputs:
834
- out1 = handler.function(preprocess_response.sample_ids[0], preprocess_response)
835
- out2 = handler.function(preprocess_response.sample_ids[0], preprocess_response)
854
+ out1 = handler.function(sim_sample_ids[0], preprocess_response)
855
+ out2 = handler.function(sim_sample_ids[0], preprocess_response)
836
856
  if not np.allclose(out1, out2):
837
857
  raise Exception(
838
858
  "Simulation '{}': encoder '{}' is non-deterministic — consecutive calls with seed=0 returned different outputs".format(
@@ -40,6 +40,7 @@ _called_from_inside_tl_integration_test_decorator = False
40
40
  # to tell a raise in the test body (report as code-flow failure) from a module-level crash
41
41
  # before any test ran (report as a plain script crash).
42
42
  _integration_test_started = False
43
+ _mapping_dataset_is_grouped = False
43
44
  _call_from_tl_platform = os.environ.get('IS_TENSORLEAP_PLATFORM') == 'true'
44
45
 
45
46
  # ---- warnings store (module-level) ----
@@ -213,6 +214,53 @@ def batch_warning(result, func_name):
213
214
  )
214
215
 
215
216
 
217
+ def _validate_id_or_group(sample_id, preprocess_response, func_name):
218
+ """Accept either a single sample id (flat dataset) or a group — a list of ids (grouped
219
+ dataset). The argument shape must match the dataset: a group requires a grouped
220
+ PreprocessResponse and a single id requires a flat one. Every id must match the declared
221
+ sample_id_type."""
222
+ is_group = isinstance(sample_id, list)
223
+ if is_group and not preprocess_response.is_grouped:
224
+ raise AssertionError(
225
+ f'{func_name}() validation failed: got a group (list of sample ids) but the '
226
+ f'PreprocessResponse is not grouped. Pass a single sample id for a flat dataset.')
227
+ if not is_group and preprocess_response.is_grouped:
228
+ raise AssertionError(
229
+ f'{func_name}() validation failed: got a single sample id but the PreprocessResponse '
230
+ f'is grouped. Pass a group (list of sample ids) for a grouped dataset.')
231
+ ids = sample_id if is_group else [sample_id]
232
+ for sid in ids:
233
+ assert type(sid) == preprocess_response.sample_id_type, \
234
+ (f'{func_name}() validation failed: '
235
+ f'Argument sample_id should be as the same type as defined in the preprocess response '
236
+ f'{preprocess_response.sample_id_type}. Got {type(sid)}.')
237
+
238
+
239
+ def _validate_grouped_result(result, group_size, func_name, validate_single):
240
+ """A grouped (group) encoder result must be either a single ndarray with leading dim
241
+ == group_size, or a list of `group_size` per-sample results (each validated by
242
+ validate_single). Metadata validates its grouped result separately (list only).
243
+ An `(B, *)` array is validated per-sample slice (not as the whole batched array) so
244
+ per-sample checks (e.g. channel_dim <= rank) use the sample shape, not the batch shape."""
245
+ if isinstance(result, np.ndarray):
246
+ assert len(result.shape) > 0 and result.shape[0] == group_size, \
247
+ (f'{func_name}() validation failed: expected a grouped output for a group of '
248
+ f'{group_size}: an array with leading dim {group_size}, got shape {result.shape}.')
249
+ for single in result:
250
+ validate_single(single)
251
+ elif isinstance(result, list):
252
+ assert len(result) == group_size, \
253
+ (f'{func_name}() validation failed: expected a grouped output for a group of '
254
+ f'{group_size}: a list of {group_size} arrays, got {len(result)}.')
255
+ for r in result:
256
+ validate_single(r)
257
+ else:
258
+ raise AssertionError(
259
+ f'{func_name}() validation failed: expected a grouped output for a group of '
260
+ f'{group_size}: either an array with leading dim {group_size} or a list of '
261
+ f'{group_size} arrays, got {type(result)}.')
262
+
263
+
216
264
  def _add_mapping_connection(user_unique_name, connection_destinations, arg_names, name, node_mapping_type):
217
265
  connection_destinations = [connection_destination for connection_destination in connection_destinations
218
266
  if not isinstance(connection_destination, SamplePreprocessResponse)]
@@ -290,11 +338,7 @@ def tensorleap_integration_test():
290
338
 
291
339
  def _validate_input_args(*args, **kwargs):
292
340
  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
- )
341
+ _validate_id_or_group(sample_id, preprocess_response, 'tensorleap_integration_test')
298
342
 
299
343
  def inner(*args, **kwargs):
300
344
  if not _call_from_tl_platform:
@@ -305,7 +349,7 @@ def tensorleap_integration_test():
305
349
  expected_names = inspect.getfullargspec(integration_test_function)[0][:2]
306
350
  if len(expected_names) < 2:
307
351
  expected_names = ['idx', 'preprocess']
308
- validate_args_structure(*args, types_order=[Union[int, str], PreprocessResponse],
352
+ validate_args_structure(*args, types_order=[Union[int, str, list], PreprocessResponse],
309
353
  func_name='integration_test', expected_names=expected_names,
310
354
  **kwargs)
311
355
  sample_id, preprocess_response = (
@@ -329,6 +373,8 @@ def tensorleap_integration_test():
329
373
  _reset_model_loop_state()
330
374
  ret = integration_test_function(*args, **kwargs)
331
375
 
376
+ global _mapping_dataset_is_grouped
377
+ _mapping_dataset_is_grouped = args[1].is_grouped
332
378
  try:
333
379
  os.environ[mapping_runtime_mode_env_var_mame] = 'True'
334
380
  _reset_model_loop_state()
@@ -349,6 +395,7 @@ def tensorleap_integration_test():
349
395
  f'Integration test is only allowed to call Tensorleap decorators. '
350
396
  f'Ensure any arithmetics, external library use, Python logic is placed within Tensorleap decoders')
351
397
  finally:
398
+ _mapping_dataset_is_grouped = False
352
399
  if mapping_runtime_mode_env_var_mame in os.environ:
353
400
  del os.environ[mapping_runtime_mode_env_var_mame]
354
401
  finally:
@@ -1561,13 +1608,10 @@ def tensorleap_metadata(
1561
1608
  raise Exception(f'Metadata with name {name} already exists. '
1562
1609
  f'Please choose another')
1563
1610
 
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)}.')
1611
+ def _validate_input_args(sample_id: Union[int, str, list], preprocess_response: PreprocessResponse):
1612
+ _validate_id_or_group(sample_id, preprocess_response, user_function.__name__)
1569
1613
 
1570
- def _validate_result(result):
1614
+ def _validate_single(result):
1571
1615
  supported_result_types = (type(None), int, str, bool, float, dict, np.floating,
1572
1616
  np.bool_, np.unsignedinteger, np.signedinteger, np.integer)
1573
1617
  if isinstance(result, tuple):
@@ -1585,6 +1629,20 @@ def tensorleap_metadata(
1585
1629
  (f'{user_function.__name__}() validation failed: '
1586
1630
  f'Values in the return dict should be of type {str(supported_result_types)}. Got {type(value)}.')
1587
1631
 
1632
+ def _validate_result(result, grouped=False, group_size=None):
1633
+ if not grouped:
1634
+ _validate_single(result)
1635
+ return
1636
+ # Grouped metadata: a list of `group_size` per-sample scalars/dicts.
1637
+ assert isinstance(result, list), \
1638
+ (f'{user_function.__name__}() validation failed: expected a grouped output for a group '
1639
+ f'of {group_size}: a list of {group_size} metadata values, got {type(result)}.')
1640
+ assert len(result) == group_size, \
1641
+ (f'{user_function.__name__}() validation failed: expected a grouped output for a group '
1642
+ f'of {group_size}: a list of {group_size} metadata values, got {len(result)}.')
1643
+ for value in result:
1644
+ _validate_single(value)
1645
+
1588
1646
  def inner_without_validate(sample_id, preprocess_response):
1589
1647
 
1590
1648
  global _called_from_inside_tl_decorator
@@ -1604,14 +1662,16 @@ def tensorleap_metadata(
1604
1662
  set_current('tensorleap_metadata')
1605
1663
  if os.environ.get(mapping_runtime_mode_env_var_mame):
1606
1664
  return None
1607
- validate_args_structure(*args, types_order=[Union[int, str], PreprocessResponse],
1665
+ validate_args_structure(*args, types_order=[Union[int, str, list], PreprocessResponse],
1608
1666
  func_name=user_function.__name__, expected_names=["idx", "preprocess"], **kwargs)
1609
1667
  sample_id, preprocess_response = args if len(args) != 0 else kwargs.values()
1610
1668
  _validate_input_args(sample_id, preprocess_response)
1611
1669
 
1670
+ grouped = isinstance(sample_id, list)
1671
+ group_size = len(sample_id) if grouped else None
1612
1672
  result = inner_without_validate(sample_id, preprocess_response)
1613
1673
 
1614
- _validate_result(result)
1674
+ _validate_result(result, grouped, group_size)
1615
1675
  if not _call_from_tl_platform:
1616
1676
  update_env_params_func("tensorleap_metadata", "v")
1617
1677
  return result
@@ -1623,35 +1683,50 @@ def tensorleap_metadata(
1623
1683
 
1624
1684
  def tensorleap_custom_latent_space():
1625
1685
  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)}.')
1686
+ def _validate_input_args(sample_id: Union[int, str, list], preprocess_response: PreprocessResponse):
1687
+ _validate_id_or_group(sample_id, preprocess_response, 'tensorleap_custom_latent_space')
1637
1688
 
1638
- def _validate_result(result):
1639
- assert isinstance(result, np.ndarray), \
1689
+ def _validate_single(single_result):
1690
+ assert isinstance(single_result, np.ndarray), \
1640
1691
  (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))
1692
+ f'The return type should be a numpy array. Got {type(single_result)}.')
1693
+ if single_result.ndim > 1:
1694
+ flat_dim = int(np.prod(single_result.shape))
1644
1695
  store_general_warning(
1645
- key=("tensorleap_custom_latent_space_flatten", tuple(result.shape)),
1696
+ key=("tensorleap_custom_latent_space_flatten", tuple(single_result.shape)),
1646
1697
  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 "
1698
+ f"tensorleap_custom_latent_space returned per-sample shape {tuple(single_result.shape)} "
1699
+ f"(ndim={single_result.ndim}). Tensorleap assumes per-sample shape (d, ...) and will "
1649
1700
  f"flatten to ({flat_dim},) before downstream visualization and clustering. "
1650
1701
  f"If you want a different aggregation (e.g. global average pooling), do it "
1651
1702
  f"inside your function."
1652
1703
  ),
1653
1704
  )
1654
1705
 
1706
+ def _validate_result(result, grouped=False, group_size=None):
1707
+ if not grouped:
1708
+ _validate_single(result)
1709
+ return
1710
+ # Grouped: (B, *) array or list of B per-sample arrays. Validate each per-sample slice so
1711
+ # the per-sample flatten check/warning uses the sample shape, not the (B, *) batch shape.
1712
+ if isinstance(result, np.ndarray):
1713
+ assert result.ndim >= 1 and result.shape[0] == group_size, \
1714
+ (f'tensorleap_custom_latent_space validation failed: expected a grouped output for a '
1715
+ f'group of {group_size}: an array with leading dim {group_size}, got shape {result.shape}.')
1716
+ for single in result:
1717
+ _validate_single(single)
1718
+ elif isinstance(result, list):
1719
+ assert len(result) == group_size, \
1720
+ (f'tensorleap_custom_latent_space validation failed: expected a grouped output for a '
1721
+ f'group of {group_size}: a list of {group_size} arrays, got {len(result)}.')
1722
+ for single in result:
1723
+ _validate_single(single)
1724
+ else:
1725
+ raise AssertionError(
1726
+ f'tensorleap_custom_latent_space validation failed: expected a grouped output for a '
1727
+ f'group of {group_size}: either an array with leading dim {group_size} or a list of '
1728
+ f'{group_size} arrays, got {type(result)}.')
1729
+
1655
1730
  def inner_without_validate(sample_id, preprocess_response):
1656
1731
  global _called_from_inside_tl_decorator
1657
1732
  _called_from_inside_tl_decorator += 1
@@ -1671,9 +1746,11 @@ def tensorleap_custom_latent_space():
1671
1746
 
1672
1747
  _validate_input_args(sample_id, preprocess_response)
1673
1748
 
1749
+ grouped = isinstance(sample_id, list)
1750
+ group_size = len(sample_id) if grouped else None
1674
1751
  result = inner_without_validate(sample_id, preprocess_response)
1675
1752
 
1676
- _validate_result(result)
1753
+ _validate_result(result, grouped, group_size)
1677
1754
  return result
1678
1755
 
1679
1756
  return inner
@@ -1933,10 +2010,9 @@ def tensorleap_autoregressive_step(latent_space_aggregation: str = 'last_step'):
1933
2010
  state returned by the previous call, and return the next step's full input dict together with
1934
2011
  the updated state; returning (None, state) ends the chain — the terminating call still
1935
2012
  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
2013
+ the model; it may be any nest of dicts/lists holding numpy arrays, numbers, strings or bools,
2014
+ and it rides the platform queue on every step accumulate reductions, not raw per-step
2015
+ tensors. The hook must be stateless and deterministically seeded (seed stochasticity from
1940
2016
  sample_id) — the engine replays steps after crash recovery and relies on identical results.
1941
2017
 
1942
2018
  latent_space_aggregation ('last_step' | 'mean') declares how the chain's latent-space
@@ -2642,6 +2718,15 @@ def tensorleap_preprocess():
2642
2718
  assert len(set(result)) == len(result), \
2643
2719
  (f'{user_function.__name__}() validation failed: '
2644
2720
  f'The return list should not contain duplicate PreprocessResponse objects.')
2721
+ # All-or-none: a dataset is either all grouped (nested sample_ids) or all flat.
2722
+ if len({response.is_grouped for response in result}) != 1:
2723
+ shapes = ', '.join(
2724
+ f"#{i}={'grouped' if response.is_grouped else 'flat'}"
2725
+ for i, response in enumerate(result))
2726
+ raise AssertionError(
2727
+ f'{user_function.__name__}() validation failed: '
2728
+ f'All PreprocessResponses must be either all grouped (nested sample_ids) '
2729
+ f'or all flat, but got a mix: {shapes}.')
2645
2730
 
2646
2731
  def inner(*args, **kwargs):
2647
2732
  if not _call_from_tl_platform:
@@ -2979,14 +3064,10 @@ def tensorleap_input_encoder(name: str, channel_dim=_UNSET, model_input_index=No
2979
3064
  f"channel axis in the batched tensor (batch = axis 0), so 0 is invalid. Use 1 "
2980
3065
  f"for channel-first (NCHW) and -1 for channel-last (NHWC).")
2981
3066
 
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)}.')
3067
+ def _validate_input_args(sample_id: Union[int, str, list], preprocess_response: PreprocessResponse):
3068
+ _validate_id_or_group(sample_id, preprocess_response, user_function.__name__)
2987
3069
 
2988
- def _validate_result(result):
2989
- validate_output_structure(result, func_name=user_function.__name__, expected_type_name="np.ndarray")
3070
+ def _validate_single(result):
2990
3071
  assert isinstance(result, np.ndarray), \
2991
3072
  (f'{user_function.__name__}() validation failed: '
2992
3073
  f'Unsupported return type. Should be a numpy array. Got {type(result)}.')
@@ -2996,6 +3077,13 @@ def tensorleap_input_encoder(name: str, channel_dim=_UNSET, model_input_index=No
2996
3077
  assert channel_dim - 1 <= len(result.shape), (f'{user_function.__name__}() validation failed: '
2997
3078
  f'The channel_dim ({channel_dim}) should be <= to the rank of the resulting input rank ({len(result.shape)}).')
2998
3079
 
3080
+ def _validate_result(result, grouped=False, group_size=None):
3081
+ if not grouped:
3082
+ validate_output_structure(result, func_name=user_function.__name__, expected_type_name="np.ndarray")
3083
+ _validate_single(result)
3084
+ return
3085
+ _validate_grouped_result(result, group_size, user_function.__name__, _validate_single)
3086
+
2999
3087
  def inner_without_validate(sample_id, preprocess_response):
3000
3088
  global _called_from_inside_tl_decorator
3001
3089
  _called_from_inside_tl_decorator += 1
@@ -3012,18 +3100,27 @@ def tensorleap_input_encoder(name: str, channel_dim=_UNSET, model_input_index=No
3012
3100
  def inner(*args, **kwargs):
3013
3101
  if not _call_from_tl_platform:
3014
3102
  set_current("tensorleap_input_encoder")
3015
- validate_args_structure(*args, types_order=[Union[int, str], PreprocessResponse],
3103
+ validate_args_structure(*args, types_order=[Union[int, str, list], PreprocessResponse],
3016
3104
  func_name=user_function.__name__, expected_names=["idx", "preprocess"], **kwargs)
3017
3105
  sample_id, preprocess_response = args if len(args) != 0 else kwargs.values()
3018
3106
  _validate_input_args(sample_id, preprocess_response)
3019
3107
 
3108
+ grouped = isinstance(sample_id, list)
3109
+ group_size = len(sample_id) if grouped else None
3020
3110
  result = inner_without_validate(sample_id, preprocess_response)
3021
3111
 
3022
- _validate_result(result)
3112
+ _validate_result(result, grouped, group_size)
3023
3113
 
3024
3114
  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)
3115
+ if grouped:
3116
+ # A grouped result is a list of per-sample arrays (never stacked, so a
3117
+ # B=1-only model can be fed one sample at a time). Add a batch dim to each
3118
+ # sample, mirroring the flat path's single-sample expand_dims.
3119
+ if isinstance(result, list):
3120
+ result = [np.expand_dims(r, axis=0) for r in result]
3121
+ else:
3122
+ batch_warning(result, user_function.__name__)
3123
+ result = np.expand_dims(result, axis=0)
3027
3124
  # Emit integration test event once per test
3028
3125
  try:
3029
3126
  emit_integration_event_once(AnalyticsEvent.INPUT_ENCODER_INTEGRATION_TEST, {
@@ -3044,8 +3141,15 @@ def tensorleap_input_encoder(name: str, channel_dim=_UNSET, model_input_index=No
3044
3141
  inner.node_mapping = NodeMapping(name, node_mapping_type)
3045
3142
 
3046
3143
  def mapping_inner(*args, **kwargs):
3047
- class TempMapping:
3048
- pass
3144
+ if _mapping_dataset_is_grouped:
3145
+ class TempMapping:
3146
+ def __getitem__(self, key):
3147
+ # Indexing a grouped input group (input_list[i]) selects a sample from the
3148
+ # same input source; the model wiring is identical to the un-grouped input.
3149
+ return self
3150
+ else:
3151
+ class TempMapping:
3152
+ pass
3049
3153
 
3050
3154
  ret = TempMapping()
3051
3155
  ret.node_mapping = mapping_inner.node_mapping
@@ -3075,15 +3179,10 @@ def tensorleap_gt_encoder(name: str):
3075
3179
  raise Exception(f'GT with name {name} already exists. '
3076
3180
  f'Please choose another')
3077
3181
 
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)}.')
3182
+ def _validate_input_args(sample_id: Union[int, str, list], preprocess_response: PreprocessResponse):
3183
+ _validate_id_or_group(sample_id, preprocess_response, user_function.__name__)
3083
3184
 
3084
- def _validate_result(result):
3085
- validate_output_structure(result, func_name=user_function.__name__, expected_type_name="np.ndarray",
3086
- gt_flag=True)
3185
+ def _validate_single(result):
3087
3186
  assert isinstance(result, np.ndarray), \
3088
3187
  (f'{user_function.__name__}() validation failed: '
3089
3188
  f'Unsupported return type. Should be a numpy array. Got {type(result)}.')
@@ -3091,6 +3190,14 @@ def tensorleap_gt_encoder(name: str):
3091
3190
  (f'{user_function.__name__}() validation failed: '
3092
3191
  f'The return type should be a numpy array of type float32. Got {result.dtype}.')
3093
3192
 
3193
+ def _validate_result(result, grouped=False, group_size=None):
3194
+ if not grouped:
3195
+ validate_output_structure(result, func_name=user_function.__name__, expected_type_name="np.ndarray",
3196
+ gt_flag=True)
3197
+ _validate_single(result)
3198
+ return
3199
+ _validate_grouped_result(result, group_size, user_function.__name__, _validate_single)
3200
+
3094
3201
  def inner_without_validate(sample_id, preprocess_response):
3095
3202
  global _called_from_inside_tl_decorator
3096
3203
  _called_from_inside_tl_decorator += 1
@@ -3107,18 +3214,27 @@ def tensorleap_gt_encoder(name: str):
3107
3214
  def inner(*args, **kwargs):
3108
3215
  if not _call_from_tl_platform:
3109
3216
  set_current("tensorleap_gt_encoder")
3110
- validate_args_structure(*args, types_order=[Union[int, str], PreprocessResponse],
3217
+ validate_args_structure(*args, types_order=[Union[int, str, list], PreprocessResponse],
3111
3218
  func_name=user_function.__name__, expected_names=["idx", "preprocess"], **kwargs)
3112
3219
  sample_id, preprocess_response = args
3113
3220
  _validate_input_args(sample_id, preprocess_response)
3114
3221
 
3222
+ grouped = isinstance(sample_id, list)
3223
+ group_size = len(sample_id) if grouped else None
3115
3224
  result = inner_without_validate(sample_id, preprocess_response)
3116
3225
 
3117
- _validate_result(result)
3226
+ _validate_result(result, grouped, group_size)
3118
3227
 
3119
3228
  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)
3229
+ if grouped:
3230
+ # A grouped result is a list of per-sample arrays (never stacked, so a
3231
+ # B=1-only model can be fed one sample at a time). Add a batch dim to each
3232
+ # sample, mirroring the flat path's single-sample expand_dims.
3233
+ if isinstance(result, list):
3234
+ result = [np.expand_dims(r, axis=0) for r in result]
3235
+ else:
3236
+ batch_warning(result, user_function.__name__)
3237
+ result = np.expand_dims(result, axis=0)
3122
3238
  _register_chain_artifact(result, 'ground_truth')
3123
3239
  # Emit integration test event once per test
3124
3240
  try:
@@ -3134,8 +3250,15 @@ def tensorleap_gt_encoder(name: str):
3134
3250
  inner.node_mapping = NodeMapping(name, NodeMappingType.GroundTruth)
3135
3251
 
3136
3252
  def mapping_inner(*args, **kwargs):
3137
- class TempMapping:
3138
- pass
3253
+ if _mapping_dataset_is_grouped:
3254
+ class TempMapping:
3255
+ def __getitem__(self, key):
3256
+ # Indexing a grouped GT group (gt_list[i]) selects a sample from the same
3257
+ # ground-truth source; wiring is identical to the un-grouped GT node.
3258
+ return self
3259
+ else:
3260
+ class TempMapping:
3261
+ pass
3139
3262
 
3140
3263
  ret = TempMapping()
3141
3264
  ret.node_mapping = mapping_inner.node_mapping
code_loader/leaploader.py CHANGED
@@ -60,14 +60,38 @@ class LeapLoader(LeapLoaderBase):
60
60
 
61
61
  @lru_cache()
62
62
  def exec_script(self) -> None:
63
+ from code_loader.inner_leap_binder import leapbinder_decorators as _leap_dec
63
64
  try:
64
65
  os.environ[mapping_runtime_mode_env_var_mame] = 'TRUE'
65
66
  self.evaluate_module()
67
+
68
+ # A grouped integration test may index a grouped input/gt (image_list[i]) to wire a
69
+ # single sample of a group; that indexing is only valid when the mapping graph is
70
+ # built in grouped mode. Detect grouped-ness so the placeholder pass below matches
71
+ # the dataset. Preprocess is stubbed while mapping mode is on, so run it with mapping
72
+ # mode momentarily disabled, and cache it so _preprocess_result() doesn't re-run it.
73
+ is_grouped = False
74
+ if global_leap_binder.integration_test_func is not None:
75
+ del os.environ[mapping_runtime_mode_env_var_mame]
76
+ try:
77
+ preprocess_result = global_leap_binder.get_preprocess_result()
78
+ self._preprocess_result_cached = preprocess_result
79
+ is_grouped = any(r.is_grouped for r in preprocess_result.values())
80
+ except Exception:
81
+ is_grouped = False
82
+ finally:
83
+ os.environ[mapping_runtime_mode_env_var_mame] = 'TRUE'
84
+
85
+ _leap_dec._mapping_dataset_is_grouped = is_grouped
66
86
  if global_leap_binder.integration_test_func is not None:
67
87
  from code_loader.inner_leap_binder.leapbinder_decorators import \
68
88
  _reset_model_loop_state
69
89
  _reset_model_loop_state()
70
- global_leap_binder.integration_test_func(None, PreprocessResponse(state=DataStateType.training, length=0))
90
+ mapping_preprocess = (
91
+ PreprocessResponse(sample_ids=[["__mapping_placeholder__"]], state=DataStateType.training)
92
+ if is_grouped else
93
+ PreprocessResponse(state=DataStateType.training, length=0))
94
+ global_leap_binder.integration_test_func(None, mapping_preprocess)
71
95
  except TypeError as e:
72
96
  import traceback
73
97
  global_leap_binder.setup_container = DatasetIntegrationSetup()
@@ -80,6 +104,7 @@ class LeapLoader(LeapLoaderBase):
80
104
  raise DatasetScriptException(getattr(e, 'message', repr(e))) from e
81
105
  finally:
82
106
  # ensure that the environment variable is removed after the script execution
107
+ _leap_dec._mapping_dataset_is_grouped = False
83
108
  if mapping_runtime_mode_env_var_mame in os.environ:
84
109
  del os.environ[mapping_runtime_mode_env_var_mame]
85
110
 
@@ -209,7 +234,7 @@ class LeapLoader(LeapLoaderBase):
209
234
  additional = self._preprocess_result().get(DataStateEnum.additional)
210
235
  if additional is None:
211
236
  return set()
212
- return set(additional.sample_ids)
237
+ return set(additional.flat_sample_ids)
213
238
 
214
239
  def _resolve_synthetic(self, sample_id: Union[int, str],
215
240
  state: Optional[DataStateEnum] = None
@@ -250,16 +275,20 @@ class LeapLoader(LeapLoaderBase):
250
275
  )
251
276
 
252
277
  preprocess_result = self._preprocess_result()
253
- if state == DataStateEnum.unlabeled and sample_id not in preprocess_result[state].sample_ids:
278
+ if state == DataStateEnum.unlabeled and sample_id not in preprocess_result[state].flat_sample_ids:
254
279
  self._preprocess_result(update_unlabeled_preprocess=True)
255
280
 
256
281
  metadata, metadata_is_none = self.get_metadata(state, sample_id)
257
282
 
258
283
  custom_latent_space = None
259
284
  if global_leap_binder.setup_container.custom_latent_space is not None:
260
- custom_latent_space = global_leap_binder.setup_container.custom_latent_space.function(sample_id,
261
- preprocess_result[
262
- state])
285
+ latent_fn = global_leap_binder.setup_container.custom_latent_space.function
286
+ preprocess_state = preprocess_result[state]
287
+ if preprocess_state.is_grouped:
288
+ group_ids, pos = self._locate_group(preprocess_state, sample_id)
289
+ custom_latent_space = self._to_grouped_list(latent_fn(group_ids, preprocess_state))[pos]
290
+ else:
291
+ custom_latent_space = latent_fn(sample_id, preprocess_state)
263
292
  instance_mask = self._get_instances_masks(state, sample_id, instance_id)
264
293
  sample = DatasetSample(inputs=self._get_inputs(state, sample_id),
265
294
  gt=None if state == DataStateEnum.unlabeled else self._get_gt(state, sample_id),
@@ -390,7 +419,7 @@ class LeapLoader(LeapLoaderBase):
390
419
  if self.get_sample_id_type() is str:
391
420
  max_allowed_item_size = np.dtype('<U256').itemsize
392
421
  for state, preprocess_response in preprocess_result.items():
393
- sample_ids_array = np.array(preprocess_response.sample_ids)
422
+ sample_ids_array = np.array(preprocess_response.flat_sample_ids)
394
423
  if sample_ids_array.dtype.itemsize > max_allowed_item_size:
395
424
  raise Exception(f"Sample id are too long. Max allowed length is 256 charecters.")
396
425
 
@@ -486,7 +515,10 @@ class LeapLoader(LeapLoaderBase):
486
515
  if preprocess_response.sample_ids_to_instance_mappings:
487
516
  raise Exception('Element instances are not supported together with '
488
517
  'tensorleap_autoregressive_step.')
489
- sample_id = preprocess_response.sample_ids[0]
518
+ # Grouped preprocess is fine: grouping only amortizes the shared base reads
519
+ # (GT/metadata/custom_latent) — the AR hook always drives a single scalar chain
520
+ # and is never handed a group.
521
+ sample_id = preprocess_response.flat_sample_ids[0]
490
522
  first_result = handler.function(sample_id, None, None, None, preprocess_response)
491
523
  second_result = handler.function(sample_id, None, None, None, preprocess_response)
492
524
  if not isinstance(first_result, tuple) or len(first_result) != 2:
@@ -567,7 +599,7 @@ class LeapLoader(LeapLoaderBase):
567
599
  if handler.input_shapes is None:
568
600
  preprocess_result = self._preprocess_result()
569
601
  first_state_response = next(iter(preprocess_result.values()))
570
- first_sample_id = first_state_response.sample_ids[0]
602
+ first_sample_id = first_state_response.flat_sample_ids[0]
571
603
  first_result = handler.function(first_sample_id, None, None, None,
572
604
  first_state_response)
573
605
  if not isinstance(first_result, tuple) or len(first_result) != 2 or \
@@ -906,6 +938,57 @@ class LeapLoader(LeapLoaderBase):
906
938
 
907
939
  return sample_ids
908
940
 
941
+ @staticmethod
942
+ def _grouped_dict_keys(rows: List[Dict[str, Any]], handler_name: str) -> List[str]:
943
+ """Keys for a group of dict-metadata rows, requiring every row to share the same keys.
944
+ A group must expose a uniform metadata schema; otherwise the per-key lists would silently
945
+ drop or KeyError on rows that differ from row 0."""
946
+ keys = list(rows[0].keys())
947
+ key_set = set(keys)
948
+ for pos, row in enumerate(rows):
949
+ if set(row.keys()) != key_set:
950
+ raise ValueError(
951
+ f"Metadata handler {handler_name!r} returned inconsistent dict keys across the "
952
+ f"group: row 0 has {sorted(key_set)} but row {pos} has {sorted(row.keys())}. "
953
+ f"All rows in a group must share the same metadata keys.")
954
+ return keys
955
+
956
+ @staticmethod
957
+ def _to_grouped_list(raw: Any) -> List[npt.NDArray[np.float32]]:
958
+ """Normalize a grouped encoder result to a list of per-sample arrays.
959
+
960
+ Grouped results are NEVER stacked into a (B, *) batch. Grouping is only a storage
961
+ read-unit (one file load per group); assembling the model batch (B) is the engine's
962
+ single responsibility, so it stays the only place samples get batched. Keeping
963
+ per-sample arrays lets the engine batch to any B — including a B=1-only model — without
964
+ an intermediate stack/unstack. A user encoder that already returned a stacked (B, *)
965
+ array is split back into its per-sample rows so the contract is a list either way."""
966
+ if isinstance(raw, list):
967
+ return raw
968
+ return [row for row in np.asarray(raw)]
969
+
970
+ def _locate_group(self, preprocess_state: PreprocessResponse,
971
+ sample_id: Union[int, str]) -> Tuple[List[Union[int, str]], int]:
972
+ """Map a sample id to (its group, its position in that group). Cached on the
973
+ PreprocessResponse itself (not keyed by id() on this loader) so grouped single-sample
974
+ fetches don't rescan the groups, and the cache can never go stale: when the response is
975
+ replaced (e.g. unlabeled preprocess refresh), the old object and its cache are simply
976
+ discarded together instead of lingering under a possibly-reused id()."""
977
+ mapping = preprocess_state._group_pos_cache
978
+ if mapping is None:
979
+ mapping = {}
980
+ for group in preprocess_state.groups:
981
+ for pos, sid in enumerate(group):
982
+ if sid in mapping:
983
+ raise ValueError(
984
+ f"Duplicate sample id {sid!r} across groups in the preprocess response; "
985
+ f"sample ids must be unique within and across groups.")
986
+ mapping[sid] = (group, pos)
987
+ preprocess_state._group_pos_cache = mapping
988
+ if sample_id not in mapping:
989
+ raise KeyError(f"Sample id {sample_id!r} is not present in any group of this preprocess response.")
990
+ return mapping[sample_id]
991
+
909
992
  def _get_dataset_handlers(self, handlers: Iterable[DatasetBaseHandler],
910
993
  state: DataStateEnum, sample_id: Union[int, str]) -> Dict[str, npt.NDArray[np.float32]]:
911
994
  result_agg = {}
@@ -917,12 +1000,100 @@ class LeapLoader(LeapLoaderBase):
917
1000
  sample_id = original_local_id
918
1001
  else:
919
1002
  preprocess_state = preprocess_result[state]
1003
+ if preprocess_state.is_grouped:
1004
+ # Grouped dataset: call the encoder once with the whole group, index out the sample.
1005
+ group_ids, pos = self._locate_group(preprocess_state, sample_id)
1006
+ for handler in handlers:
1007
+ grouped = self._to_grouped_list(handler.function(group_ids, preprocess_state))
1008
+ result_agg[handler.name] = grouped[pos]
1009
+ return result_agg
920
1010
  for handler in handlers:
921
1011
  handler_result = handler.function(sample_id, preprocess_state)
922
1012
  handler_name = handler.name
923
1013
  result_agg[handler_name] = handler_result
924
1014
  return result_agg
925
1015
 
1016
+ def get_samples(self, state: DataStateEnum, group_ids: List[Union[int, str]]) -> DatasetSample:
1017
+ """Group-aware fetch: hand the whole group to each encoder in a single call and return a
1018
+ grouped DatasetSample (inputs/gt as per-sample lists of length B, metadata as per-key lists
1019
+ of length B, index = the group). Grouped results are never stacked here — batching the
1020
+ samples into the model's B is the engine's job. This is the path a group-aware engine calls
1021
+ to read a file once per group. Row order matches group_ids exactly."""
1022
+ self.exec_script()
1023
+ preprocess_result = self._preprocess_result()
1024
+ if state == DataStateEnum.unlabeled and any(
1025
+ sid not in preprocess_result[state].flat_sample_ids for sid in group_ids):
1026
+ # Mirrors get_sample's refresh: the unlabeled preprocess can grow between calls, so a
1027
+ # group_id absent from the current snapshot may just not have been generated yet.
1028
+ self._preprocess_result(update_unlabeled_preprocess=True)
1029
+ preprocess_state = preprocess_result[state]
1030
+ assert preprocess_state.is_grouped, (
1031
+ "get_samples is the group-aware fetch path and requires a grouped preprocess "
1032
+ "response; call get_sample for a flat (non-grouped) dataset.")
1033
+ # All requested ids must belong to a single group (one file). A cross-group request
1034
+ # would force the encoder to load multiple files, defeating the one-load-per-group
1035
+ # contract; partitioning a scattered request by group is the engine's responsibility.
1036
+ distinct_groups = {id(self._locate_group(preprocess_state, sid)[0]) for sid in group_ids}
1037
+ assert len(distinct_groups) == 1, (
1038
+ f"get_samples received sample ids spanning {len(distinct_groups)} groups; a request "
1039
+ f"must be confined to a single group. The engine partitions cross-group requests and "
1040
+ f"issues one get_samples call per group.")
1041
+ inputs = {handler.name: self._to_grouped_list(handler.function(group_ids, preprocess_state))
1042
+ for handler in global_leap_binder.setup_container.inputs}
1043
+ autoregressive_handler = global_leap_binder.setup_container.autoregressive_step
1044
+ if autoregressive_handler is not None:
1045
+ # AR integrations have no input encoders — the hook's first call supplies each
1046
+ # chain's step-0 model inputs. The hook is strictly per-scalar-chain (never handed
1047
+ # a group), so it is called once per flat id in the group and the per-sample results
1048
+ # are collected into per-key lists of length B, matching the grouped (never-stacked)
1049
+ # contract used for every other encoder here.
1050
+ step_zero_per_sample = []
1051
+ for sid in group_ids:
1052
+ step_zero_result = autoregressive_handler.function(sid, None, None, None,
1053
+ preprocess_state)
1054
+ if not isinstance(step_zero_result, tuple) or len(step_zero_result) != 2 or \
1055
+ not isinstance(step_zero_result[0], dict):
1056
+ raise Exception(
1057
+ 'The autoregressive step hook must return a (next_inputs, state) tuple '
1058
+ f'with a dict of initial model inputs on its first call, got '
1059
+ f'{type(step_zero_result)} for sample {sid}.')
1060
+ self._validate_autoregressive_step_inputs(step_zero_result[0], sid)
1061
+ step_zero_per_sample.append(step_zero_result[0])
1062
+ for key in (step_zero_per_sample[0].keys() if step_zero_per_sample else []):
1063
+ inputs[key] = [row[key] for row in step_zero_per_sample]
1064
+ gt = None
1065
+ if state != DataStateEnum.unlabeled:
1066
+ gt = {handler.name: self._to_grouped_list(handler.function(group_ids, preprocess_state))
1067
+ for handler in global_leap_binder.setup_container.ground_truths}
1068
+
1069
+ metadata: Dict[str, Any] = {}
1070
+ metadata_is_none: Dict[str, Any] = {}
1071
+ for handler in global_leap_binder.setup_container.metadata:
1072
+ rows = handler.function(group_ids, preprocess_state) # list of B scalars/dicts
1073
+ if rows and isinstance(rows[0], dict):
1074
+ for key in self._grouped_dict_keys(rows, handler.name):
1075
+ name = "{}_{}".format(handler.name, key)
1076
+ converted = [self._convert_metadata_to_correct_type(name, row[key]) for row in rows]
1077
+ metadata[name] = [v for v, _ in converted]
1078
+ metadata_is_none[name] = [is_none for _, is_none in converted]
1079
+ else:
1080
+ converted = [self._convert_metadata_to_correct_type(handler.name, row) for row in rows]
1081
+ metadata[handler.name] = [v for v, _ in converted]
1082
+ metadata_is_none[handler.name] = [is_none for _, is_none in converted]
1083
+
1084
+ # custom_latent_space is group-aware like the input/GT encoders: hand it the whole group in a
1085
+ # single call so a file-backed latent fn loads the file once, and normalize to (B, d). This
1086
+ # keeps the mandatory group path non-lossy. instance_masks stay None here: they are a
1087
+ # per-(sample, instance) concern that needs an instance_id the group fetch does not carry.
1088
+ custom_latent_space = None
1089
+ if global_leap_binder.setup_container.custom_latent_space is not None:
1090
+ latent_fn = global_leap_binder.setup_container.custom_latent_space.function
1091
+ custom_latent_space = self._to_grouped_list(latent_fn(group_ids, preprocess_state))
1092
+
1093
+ return DatasetSample(inputs=inputs, gt=gt, metadata=metadata, metadata_is_none=metadata_is_none,
1094
+ index=list(group_ids), state=state, custom_latent_space=custom_latent_space,
1095
+ instance_masks=None)
1096
+
926
1097
  def _get_inputs(self, state: DataStateEnum, sample_id: Union[int, str]) -> Dict[str, npt.NDArray[np.float32]]:
927
1098
  inputs = self._get_dataset_handlers(global_leap_binder.setup_container.inputs, state, sample_id)
928
1099
  autoregressive_handler = global_leap_binder.setup_container.autoregressive_step
@@ -1026,12 +1197,18 @@ class LeapLoader(LeapLoaderBase):
1026
1197
  sample_id = original_local_id
1027
1198
  else:
1028
1199
  preprocess_state = preprocess_result[state]
1200
+ group_ids, pos = (None, None)
1201
+ if preprocess_state.is_grouped:
1202
+ group_ids, pos = self._locate_group(preprocess_state, sample_id)
1029
1203
  for handler in global_leap_binder.setup_container.metadata:
1030
1204
  if requested_metadata_names:
1031
1205
  if not is_metadata_name_starts_with_handler_name(handler):
1032
1206
  continue
1033
1207
 
1034
- handler_result = handler.function(sample_id, preprocess_state)
1208
+ if preprocess_state.is_grouped:
1209
+ handler_result = handler.function(group_ids, preprocess_state)[pos]
1210
+ else:
1211
+ handler_result = handler.function(sample_id, preprocess_state)
1035
1212
  if isinstance(handler_result, dict):
1036
1213
  for single_metadata_name, single_metadata_result in handler_result.items():
1037
1214
  handler_name = f'{handler.name}_{single_metadata_name}'
@@ -1050,6 +1227,66 @@ class LeapLoader(LeapLoaderBase):
1050
1227
 
1051
1228
  return result_agg, is_none
1052
1229
 
1230
+ def _grouped_metadata_for_group(self, preprocess_state: PreprocessResponse,
1231
+ group_ids: List[Union[int, str]],
1232
+ requested_metadata_names: Optional[List[str]]
1233
+ ) -> Tuple[Dict[str, List[Any]], Dict[str, List[bool]]]:
1234
+ def is_wanted(_handler):
1235
+ if not requested_metadata_names:
1236
+ return True
1237
+ for metadata_name in requested_metadata_names:
1238
+ if metadata_name.startswith(_handler.name + '_') or metadata_name == _handler.name:
1239
+ return True
1240
+ return False
1241
+
1242
+ results: Dict[str, List[Any]] = {}
1243
+ is_none: Dict[str, List[bool]] = {}
1244
+ for handler in global_leap_binder.setup_container.metadata:
1245
+ if requested_metadata_names and not is_wanted(handler):
1246
+ continue
1247
+ rows = handler.function(group_ids, preprocess_state)
1248
+ if rows and isinstance(rows[0], dict):
1249
+ for key in self._grouped_dict_keys(rows, handler.name):
1250
+ name = f'{handler.name}_{key}'
1251
+ if requested_metadata_names and name not in requested_metadata_names:
1252
+ continue
1253
+ converted = [self._convert_metadata_to_correct_type(name, row[key]) for row in rows]
1254
+ results[name] = [v for v, _ in converted]
1255
+ is_none[name] = [n for _, n in converted]
1256
+ else:
1257
+ name = handler.name
1258
+ if requested_metadata_names and name not in requested_metadata_names:
1259
+ continue
1260
+ converted = [self._convert_metadata_to_correct_type(name, row) for row in rows]
1261
+ results[name] = [v for v, _ in converted]
1262
+ is_none[name] = [n for _, n in converted]
1263
+ return results, is_none
1264
+
1265
+ def get_metadata_multiple_samples(self, state: DataStateEnum, sample_ids: Union[List[int], List[str]],
1266
+ requested_metadata_names: Optional[List[str]] = None
1267
+ ) -> Tuple[Dict[str, Union[List[str], List[int], List[bool], List[float]]],
1268
+ Dict[str, List[bool]]]:
1269
+ preprocess_state = self._preprocess_result().get(state)
1270
+ if preprocess_state is None or not preprocess_state.is_grouped:
1271
+ return super().get_metadata_multiple_samples(state, sample_ids, requested_metadata_names)
1272
+
1273
+ sample_id_type = self.get_sample_id_type()
1274
+ aggregated_results: Dict[str, List[Any]] = {}
1275
+ aggregated_is_none: Dict[str, List[bool]] = {}
1276
+ group_cache: Dict[int, Tuple[Dict[str, List[Any]], Dict[str, List[bool]]]] = {}
1277
+ for sample_id in sample_ids:
1278
+ sample_id = sample_id_type(sample_id)
1279
+ group_ids, pos = self._locate_group(preprocess_state, sample_id)
1280
+ key = id(group_ids)
1281
+ if key not in group_cache:
1282
+ group_cache[key] = self._grouped_metadata_for_group(
1283
+ preprocess_state, group_ids, requested_metadata_names)
1284
+ group_results, group_is_none = group_cache[key]
1285
+ for name, values in group_results.items():
1286
+ aggregated_results.setdefault(name, []).append(values[pos])
1287
+ aggregated_is_none.setdefault(name, []).append(group_is_none[name][pos])
1288
+ return aggregated_results, aggregated_is_none
1289
+
1053
1290
  @lru_cache()
1054
1291
  def get_sample_id_type(self) -> Type:
1055
1292
  preprocess_results = list(self._preprocess_result().values())
code_loader/utils.py CHANGED
@@ -1,6 +1,4 @@
1
- import io
2
1
  import math
3
- import pickle
4
2
  import sys
5
3
  from pathlib import Path
6
4
  from types import TracebackType
@@ -17,6 +15,11 @@ from code_loader.contract.enums import DatasetMetadataType
17
15
  def to_numpy_return_wrapper(encoder_function: SectionCallableInterface) -> SectionCallableInterface:
18
16
  def numpy_encoder_function(idx: Union[int, str], samples: PreprocessResponse) -> npt.NDArray[np.float32]:
19
17
  result = encoder_function(idx, samples)
18
+ if isinstance(result, list) and len(result) > 0 and all(isinstance(sample, np.ndarray) for sample in result):
19
+ # A list of per-sample arrays only comes from a grouped encoder (a flat encoder returns
20
+ # a single array). Never stack it into a (B, *) batch — grouping is only a storage
21
+ # read-unit; batching the group to the model's B is the engine's single responsibility.
22
+ return result
20
23
  numpy_result: npt.NDArray[np.float32] = np.array(result)
21
24
  return numpy_result
22
25
 
@@ -106,31 +109,27 @@ def get_metadata_type_from_variable(variable: Union[Optional[str], int, bool, Op
106
109
 
107
110
 
108
111
 
109
- # The platform queue transports state as pickled payloads which the engine deserializes
110
- # WITHOUT the integration code loaded, so a class defined in the user's modules round-trips
111
- # locally but crashes on the engine. Only modules importable on the engine may resolve.
112
- _STATE_SAFE_PICKLE_MODULES = ('builtins', 'copyreg', 'collections', 'numpy', 'datetime')
113
-
114
-
115
- class _EngineSafeUnpickler(pickle.Unpickler):
116
- def find_class(self, module: str, name: str) -> Any:
117
- if module.split('.')[0] in _STATE_SAFE_PICKLE_MODULES:
118
- return super().find_class(module, name)
119
- raise pickle.UnpicklingError(
120
- f'{module}.{name} is defined in integration code, which is not importable on the '
121
- f'platform engine')
112
+ AUTOREGRESSIVE_STATE_LEAF_TYPES = (np.ndarray, np.generic, int, float, str, bool, type(None))
122
113
 
123
114
 
124
115
  def validate_autoregressive_state_types(value: Any, path: str = 'state') -> None:
125
- try:
126
- _EngineSafeUnpickler(io.BytesIO(pickle.dumps(value))).load()
127
- except Exception as e:
116
+ if isinstance(value, dict):
117
+ for key, item in value.items():
118
+ if not isinstance(key, str):
119
+ raise AssertionError(
120
+ f'autoregressive state validation failed: dict key {key!r} at {path} must be '
121
+ f'a string. Got {type(key).__name__}.')
122
+ validate_autoregressive_state_types(item, f'{path}[{key!r}]')
123
+ return
124
+ if isinstance(value, (list, tuple)):
125
+ for i, item in enumerate(value):
126
+ validate_autoregressive_state_types(item, f'{path}[{i}]')
127
+ return
128
+ if not isinstance(value, AUTOREGRESSIVE_STATE_LEAF_TYPES):
128
129
  raise AssertionError(
129
- f'autoregressive state validation failed: {path} is not serializable ({e}). State '
130
- 'is serialized to the platform queue on every chain step and deserialized without '
131
- 'the integration codeany picklable nest of builtin/numpy values works (dicts, '
132
- 'lists, tuples, sets, numbers, strings, arrays), but user-defined classes, lambdas '
133
- 'and open resources do not.') from e
130
+ f'autoregressive state validation failed: {path} holds a {type(value).__name__}. '
131
+ f'State must be built only from numpy arrays, numbers, strings, bools and nests of '
132
+ f'dicts/lists of thoseit is serialized to the platform queue on every chain step.')
134
133
 
135
134
 
136
135
  def autoregressive_nests_equal(a: Any, b: Any) -> bool:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: code-loader
3
- Version: 1.0.195.dev0
3
+ Version: 1.0.196.dev0
4
4
  Summary:
5
5
  Home-page: https://github.com/tensorleap/code-loader
6
6
  License: MIT
@@ -1,7 +1,7 @@
1
1
  LICENSE,sha256=qIwWjdspQeSMTtnFZBC8MuT-95L02FPvzRUdWFxrwJY,1067
2
2
  code_loader/__init__.py,sha256=outxRQ0M-zMfV0QGVJmAed5qWfRmyD0TV6-goEGAzBw,406
3
3
  code_loader/contract/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- code_loader/contract/datasetclasses.py,sha256=jsVwxrE3X1CbR62JFR2xGNLe_Xe1O52p0kFFXkT1mSM,13751
4
+ code_loader/contract/datasetclasses.py,sha256=CX4HO3xiuBUiXaeSBEyjAFV7i1NGS7KZ3JnLGxofH28,16164
5
5
  code_loader/contract/enums.py,sha256=2q-IV_5g9lLE306DIbWA1c0tn5IhDtxsKxyV1x_Lreg,1671
6
6
  code_loader/contract/exceptions.py,sha256=jWqu5i7t-0IG0jGRsKF4DjJdrsdpJjIYpUkN1F4RiyQ,51
7
7
  code_loader/contract/mapping.py,sha256=sWJhpng-IkOzQnWQdMT5w2ZZ3X1Z_OOzSwCLXIS7oxE,1446
@@ -21,18 +21,18 @@ code_loader/experiment_api/types.py,sha256=MY8xFARHwdVA7p4dxyhD60ShmttgTvb4qdp1o
21
21
  code_loader/experiment_api/utils.py,sha256=XZHtxge12TS4H4-8PjV3sKuhp8Ud6ojAiIzTZJEqBqc,3304
22
22
  code_loader/experiment_api/workingspace_config_utils.py,sha256=DLzXQCg4dgTV_YgaSbeTVzq-2ja_SQw4zi7LXwKL9cY,990
23
23
  code_loader/inner_leap_binder/__init__.py,sha256=koOlJyMNYzGbEsoIbXathSmQ-L38N_pEXH_HvL7beXU,99
24
- code_loader/inner_leap_binder/leapbinder.py,sha256=4cbSEbN6vVwjFUMxDEksxk1VSCip2ntLB32RxA81_JE,53194
25
- code_loader/inner_leap_binder/leapbinder_decorators.py,sha256=eHdSRCYtfkHtq7NSlS4bF92TU8RGRzZ1gbb3ULeEV2c,176145
26
- code_loader/leaploader.py,sha256=ME1Ypq9VV3o9XXOCV6voDWWVP0gB4Z0LoJh6u79qDYY,68370
24
+ code_loader/inner_leap_binder/leapbinder.py,sha256=hTkOL-JLtIQSaCqg8EVxnWWaHMbChrnTkgrT1Tj5AxE,54488
25
+ code_loader/inner_leap_binder/leapbinder_decorators.py,sha256=1zKGCW-TgC9AYv8oc_jEplIVlnpJEwqM9o2IpGxOwTI,183070
26
+ code_loader/leaploader.py,sha256=k1yfKV6SBqxggUp3Yw1sNGfd50xsCaNb8-CXBsgsVY8,83459
27
27
  code_loader/leaploaderbase.py,sha256=PvBU_2OQqBfJLDMsbK1s372954eYr4Y3O4r18y5S7uY,10739
28
28
  code_loader/mixpanel_tracker.py,sha256=rNwRmFifNbdUoqLQvvhhgpKczWpWiEmd8MfyJe27sxw,9131
29
29
  code_loader/plot_functions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
30
  code_loader/plot_functions/plot_functions.py,sha256=2DC-zlVaN13P4VNx5d8csgs80C6SisaeP1-Kq2LW7iM,16075
31
31
  code_loader/plot_functions/visualize.py,sha256=gsBAYYkwMh7jIpJeDMPS8G4CW-pxwx6LznoQIvi4vpo,657
32
- code_loader/utils.py,sha256=gEJCKpDWf6p9_KoEz-0EGiJqxD6QfPgZN6zdKdtSAz8,6628
32
+ code_loader/utils.py,sha256=mt1V191TL-6goWPnsqjW76wffGFGNdx8xdQ0J4ZT9bU,6779
33
33
  code_loader/visualizers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
34
  code_loader/visualizers/default_visualizers.py,sha256=onRnLE_TXfgLN4o52hQIOOhUcFexGlqJ3xSpQDVLuZM,2604
35
- code_loader-1.0.195.dev0.dist-info/LICENSE,sha256=qIwWjdspQeSMTtnFZBC8MuT-95L02FPvzRUdWFxrwJY,1067
36
- code_loader-1.0.195.dev0.dist-info/METADATA,sha256=q8UczKAxuYThEseqP4aXcS5ET0DfpjPaRmIy4aKDl1I,1095
37
- code_loader-1.0.195.dev0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
38
- code_loader-1.0.195.dev0.dist-info/RECORD,,
35
+ code_loader-1.0.196.dev0.dist-info/LICENSE,sha256=qIwWjdspQeSMTtnFZBC8MuT-95L02FPvzRUdWFxrwJY,1067
36
+ code_loader-1.0.196.dev0.dist-info/METADATA,sha256=okTBjK87MBiyLc7eEgcuXgtl2XjCMCk_FSOtBTAnXQw,1095
37
+ code_loader-1.0.196.dev0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
38
+ code_loader-1.0.196.dev0.dist-info/RECORD,,