code-loader 1.0.193.dev4__py3-none-any.whl → 1.0.193.dev5__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.
code_loader/leaploader.py CHANGED
@@ -7,7 +7,7 @@ import sys
7
7
  from contextlib import redirect_stdout
8
8
  from functools import lru_cache
9
9
  from pathlib import Path
10
- from typing import Dict, List, Iterable, Set, FrozenSet, Union, Any, Type, Optional, Callable, Tuple
10
+ from typing import Dict, List, Iterable, Set, Union, Any, Type, Optional, Callable, Tuple
11
11
 
12
12
  import numpy as np
13
13
  import numpy.typing as npt
@@ -27,8 +27,7 @@ from code_loader.contract.sim_config import FloatBounds, IntBounds, CategoricalB
27
27
  from code_loader.inner_leap_binder import global_leap_binder
28
28
  from code_loader.inner_leap_binder.leapbinder import mapping_runtime_mode_env_var_mame
29
29
  from code_loader.leaploaderbase import LeapLoaderBase
30
- from code_loader.utils import get_root_exception_file_and_line_number, get_metadata_type_from_variable, \
31
- validate_autoregressive_state_types, autoregressive_nests_equal
30
+ from code_loader.utils import get_root_exception_file_and_line_number, get_metadata_type_from_variable
32
31
 
33
32
 
34
33
  def _serialize_sim_bounds(bounds) -> dict:
@@ -60,14 +59,35 @@ class LeapLoader(LeapLoaderBase):
60
59
 
61
60
  @lru_cache()
62
61
  def exec_script(self) -> None:
62
+ from code_loader.inner_leap_binder import leapbinder_decorators as _leap_dec
63
63
  try:
64
64
  os.environ[mapping_runtime_mode_env_var_mame] = 'TRUE'
65
65
  self.evaluate_module()
66
+
67
+ # A grouped integration test may index a grouped input/gt (image_list[i]) to wire a
68
+ # single sample of a group; that indexing is only valid when the mapping graph is
69
+ # built in grouped mode. Detect grouped-ness so the placeholder pass below matches
70
+ # the dataset. Preprocess is stubbed while mapping mode is on, so run it with mapping
71
+ # mode momentarily disabled, and cache it so _preprocess_result() doesn't re-run it.
72
+ is_grouped = False
66
73
  if global_leap_binder.integration_test_func is not None:
67
- from code_loader.inner_leap_binder.leapbinder_decorators import \
68
- _reset_model_loop_state
69
- _reset_model_loop_state()
70
- global_leap_binder.integration_test_func(None, PreprocessResponse(state=DataStateType.training, length=0))
74
+ del os.environ[mapping_runtime_mode_env_var_mame]
75
+ try:
76
+ preprocess_result = global_leap_binder.get_preprocess_result()
77
+ self._preprocess_result_cached = preprocess_result
78
+ is_grouped = any(r.is_grouped for r in preprocess_result.values())
79
+ except Exception:
80
+ is_grouped = False
81
+ finally:
82
+ os.environ[mapping_runtime_mode_env_var_mame] = 'TRUE'
83
+
84
+ _leap_dec._mapping_dataset_is_grouped = is_grouped
85
+ if global_leap_binder.integration_test_func is not None:
86
+ mapping_preprocess = (
87
+ PreprocessResponse(sample_ids=[["__mapping_placeholder__"]], state=DataStateType.training)
88
+ if is_grouped else
89
+ PreprocessResponse(state=DataStateType.training, length=0))
90
+ global_leap_binder.integration_test_func(None, mapping_preprocess)
71
91
  except TypeError as e:
72
92
  import traceback
73
93
  global_leap_binder.setup_container = DatasetIntegrationSetup()
@@ -80,6 +100,7 @@ class LeapLoader(LeapLoaderBase):
80
100
  raise DatasetScriptException(getattr(e, 'message', repr(e))) from e
81
101
  finally:
82
102
  # ensure that the environment variable is removed after the script execution
103
+ _leap_dec._mapping_dataset_is_grouped = False
83
104
  if mapping_runtime_mode_env_var_mame in os.environ:
84
105
  del os.environ[mapping_runtime_mode_env_var_mame]
85
106
 
@@ -250,16 +271,20 @@ class LeapLoader(LeapLoaderBase):
250
271
  )
251
272
 
252
273
  preprocess_result = self._preprocess_result()
253
- if state == DataStateEnum.unlabeled and sample_id not in preprocess_result[state].sample_ids:
274
+ if state == DataStateEnum.unlabeled and sample_id not in preprocess_result[state].flat_sample_ids:
254
275
  self._preprocess_result(update_unlabeled_preprocess=True)
255
276
 
256
277
  metadata, metadata_is_none = self.get_metadata(state, sample_id)
257
278
 
258
279
  custom_latent_space = None
259
280
  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])
281
+ latent_fn = global_leap_binder.setup_container.custom_latent_space.function
282
+ preprocess_state = preprocess_result[state]
283
+ if preprocess_state.is_grouped:
284
+ group_ids, pos = self._locate_group(preprocess_state, sample_id)
285
+ custom_latent_space = self._to_grouped_list(latent_fn(group_ids, preprocess_state))[pos]
286
+ else:
287
+ custom_latent_space = latent_fn(sample_id, preprocess_state)
263
288
  instance_mask = self._get_instances_masks(state, sample_id, instance_id)
264
289
  sample = DatasetSample(inputs=self._get_inputs(state, sample_id),
265
290
  gt=None if state == DataStateEnum.unlabeled else self._get_gt(state, sample_id),
@@ -390,7 +415,7 @@ class LeapLoader(LeapLoaderBase):
390
415
  if self.get_sample_id_type() is str:
391
416
  max_allowed_item_size = np.dtype('<U256').itemsize
392
417
  for state, preprocess_response in preprocess_result.items():
393
- sample_ids_array = np.array(preprocess_response.sample_ids)
418
+ sample_ids_array = np.array(preprocess_response.flat_sample_ids)
394
419
  if sample_ids_array.dtype.itemsize > max_allowed_item_size:
395
420
  raise Exception(f"Sample id are too long. Max allowed length is 256 charecters.")
396
421
 
@@ -480,49 +505,31 @@ class LeapLoader(LeapLoaderBase):
480
505
  try:
481
506
  preprocess_result = self._preprocess_result()
482
507
  input_shapes: Dict[str, List[int]] = {}
483
- expected_keys: Optional[FrozenSet[str]] = None
484
- expected_keys_state = ''
485
508
  for state, preprocess_response in preprocess_result.items():
486
509
  if preprocess_response.sample_ids_to_instance_mappings:
487
510
  raise Exception('Element instances are not supported together with '
488
511
  'tensorleap_autoregressive_step.')
489
512
  sample_id = preprocess_response.sample_ids[0]
490
- first_result = handler.function(sample_id, None, None, None, preprocess_response)
491
- second_result = handler.function(sample_id, None, None, None, preprocess_response)
492
- if not isinstance(first_result, tuple) or len(first_result) != 2:
493
- raise Exception('The autoregressive step hook must return a (next_inputs, '
494
- f'state) tuple, got {type(first_result)} '
495
- f'(state: {state.name}).')
496
- first_inputs, first_state = first_result
497
- if not isinstance(first_inputs, dict) or not first_inputs:
513
+ first_result = handler.function(sample_id, None, None, preprocess_response)
514
+ second_result = handler.function(sample_id, None, None, preprocess_response)
515
+ if not isinstance(first_result, dict):
498
516
  raise Exception('The autoregressive step hook must return a dict of initial model '
499
- f'inputs on its first call, got {type(first_inputs)} '
517
+ f'inputs on its first call, got {type(first_result)} '
500
518
  f'(state: {state.name}).')
501
- validate_autoregressive_state_types(first_state)
502
- if not isinstance(second_result, tuple) or len(second_result) != 2 or \
503
- not isinstance(second_result[0], dict) or \
504
- set(second_result[0].keys()) != set(first_inputs.keys()) or \
505
- not autoregressive_nests_equal(first_state, second_result[1]):
519
+ if not isinstance(second_result, dict) or \
520
+ set(second_result.keys()) != set(first_result.keys()):
506
521
  raise Exception('The autoregressive step hook is not deterministic — two calls '
507
522
  'with identical arguments returned different results '
508
523
  f'(state: {state.name}). Seed any stochasticity from '
509
524
  'sample_id: the engine replays steps after crash recovery '
510
525
  'and relies on identical results.')
511
- if expected_keys is None:
512
- expected_keys = frozenset(first_inputs)
513
- expected_keys_state = state.name
514
- elif frozenset(first_inputs) != expected_keys:
515
- raise Exception(f'The autoregressive step hook returned different model input '
516
- f'keys across states: {sorted(expected_keys)} '
517
- f'({expected_keys_state}) vs {sorted(first_inputs)} '
518
- f'({state.name}). The key set must be identical for every '
519
- f'state.')
520
- for key, value in first_inputs.items():
521
- if key.startswith('_'):
522
- raise Exception(f'The autoregressive step hook returned model input key '
523
- f'"{key}" — underscore passthrough keys were replaced by '
524
- f'the explicit state channel; move passthrough values '
525
- f'into the returned state. State: {state.name}.')
526
+ model_input_items = {key: value for key, value in first_result.items()
527
+ if not key.startswith('_')}
528
+ if not model_input_items:
529
+ raise Exception('The autoregressive step hook returned no model inputs on its first '
530
+ 'call every key starts with "_" (underscore keys are passthrough '
531
+ f'state, never fed to the model). State: {state.name}.')
532
+ for key, value in model_input_items.items():
526
533
  if not isinstance(value, np.ndarray):
527
534
  raise Exception(f'The autoregressive step hook returned a non-numpy value for '
528
535
  f'model input "{key}" ({type(value)}). State: {state.name}.')
@@ -531,7 +538,7 @@ class LeapLoader(LeapLoaderBase):
531
538
  f'model input "{key}" across states: {input_shapes[key]} vs '
532
539
  f'{list(value.shape)}. Shapes must be fixed.')
533
540
  input_shapes[key] = list(value.shape)
534
- if not autoregressive_nests_equal(value, second_result[0][key]):
541
+ if not np.array_equal(value, second_result[key]):
535
542
  raise Exception('The autoregressive step hook is not deterministic — two calls '
536
543
  'with identical arguments returned different outputs for model '
537
544
  f'input "{key}". Seed any stochasticity from sample_id: the '
@@ -545,39 +552,19 @@ class LeapLoader(LeapLoaderBase):
545
552
  test_result.is_passed = False
546
553
  return test_result
547
554
 
548
- @staticmethod
549
- def _validate_autoregressive_step_inputs(step_inputs: Dict[str, Any],
550
- sample_id: Union[int, str]) -> None:
551
- # The parse-time check only covers the first sample of each state; the engine runtime
552
- # paths must enforce the same key/value contract for every sample they touch.
553
- for key, value in step_inputs.items():
554
- if key.startswith('_'):
555
- raise Exception(f'The autoregressive step hook returned model input key "{key}" '
556
- f'for sample {sample_id} — underscore passthrough keys were '
557
- 'replaced by the explicit state channel; move passthrough values '
558
- 'into the returned state.')
559
- if not isinstance(value, np.ndarray):
560
- raise Exception('The autoregressive step hook returned a non-numpy value for '
561
- f'model input "{key}" ({type(value).__name__}) for sample '
562
- f'{sample_id}.')
563
-
564
555
  def _ensure_autoregressive_input_shapes(self) -> Dict[str, List[int]]:
565
556
  handler = global_leap_binder.setup_container.autoregressive_step
566
557
  assert handler is not None
567
558
  if handler.input_shapes is None:
568
559
  preprocess_result = self._preprocess_result()
569
560
  first_state_response = next(iter(preprocess_result.values()))
570
- first_sample_id = first_state_response.sample_ids[0]
571
- first_result = handler.function(first_sample_id, None, None, None,
561
+ first_result = handler.function(first_state_response.sample_ids[0], None, None,
572
562
  first_state_response)
573
- if not isinstance(first_result, tuple) or len(first_result) != 2 or \
574
- not isinstance(first_result[0], dict):
575
- raise Exception('The autoregressive step hook must return a (next_inputs, state) '
576
- 'tuple with a dict of initial model inputs on its first call, '
577
- f'got {type(first_result)}.')
578
- self._validate_autoregressive_step_inputs(first_result[0], first_sample_id)
579
- handler.input_shapes = {key: list(value.shape)
580
- for key, value in first_result[0].items()}
563
+ if not isinstance(first_result, dict):
564
+ raise Exception('The autoregressive step hook must return a dict of initial model '
565
+ f'inputs on its first call, got {type(first_result)}.')
566
+ handler.input_shapes = {key: list(value.shape) for key, value in first_result.items()
567
+ if not key.startswith('_') and isinstance(value, np.ndarray)}
581
568
  return handler.input_shapes
582
569
 
583
570
  def run_simulation(self, sim_name, params=None, n_samples=1, seed=0,
@@ -822,21 +809,6 @@ class LeapLoader(LeapLoaderBase):
822
809
  metric_inst = MetricInstance(metric.metric_handler_data.name, metric.metric_handler_data.arg_names)
823
810
  metrics.append(metric_inst)
824
811
 
825
- # Autoregressive decorators are reported alongside the regular ones so the platform sees
826
- # their names; the engine tells them apart by name via get_autoregressive_decorator_names.
827
- for autoregressive_metric in setup.autoregressive_metrics:
828
- metrics.append(MetricInstance(autoregressive_metric.metric_handler_data.name,
829
- autoregressive_metric.metric_handler_data.arg_names))
830
- for autoregressive_loss in setup.autoregressive_losses:
831
- custom_losses.append(CustomLossInstance(
832
- autoregressive_loss.custom_loss_handler_data.name,
833
- autoregressive_loss.custom_loss_handler_data.arg_names))
834
- for autoregressive_visualizer in setup.autoregressive_visualizers:
835
- visualizers.append(VisualizerInstance(
836
- autoregressive_visualizer.visualizer_handler_data.name,
837
- autoregressive_visualizer.visualizer_handler_data.type,
838
- autoregressive_visualizer.visualizer_handler_data.arg_names))
839
-
840
812
  simulations = []
841
813
  for sim in setup.simulations:
842
814
  sim_config_serialized = {
@@ -882,6 +854,58 @@ class LeapLoader(LeapLoaderBase):
882
854
 
883
855
  return sample_ids
884
856
 
857
+ @staticmethod
858
+ def _grouped_dict_keys(rows: List[Dict[str, Any]], handler_name: str) -> List[str]:
859
+ """Keys for a group of dict-metadata rows, requiring every row to share the same keys.
860
+ A group must expose a uniform metadata schema; otherwise the per-key lists would silently
861
+ drop or KeyError on rows that differ from row 0."""
862
+ keys = list(rows[0].keys())
863
+ key_set = set(keys)
864
+ for pos, row in enumerate(rows):
865
+ if set(row.keys()) != key_set:
866
+ raise ValueError(
867
+ f"Metadata handler {handler_name!r} returned inconsistent dict keys across the "
868
+ f"group: row 0 has {sorted(key_set)} but row {pos} has {sorted(row.keys())}. "
869
+ f"All rows in a group must share the same metadata keys.")
870
+ return keys
871
+
872
+ @staticmethod
873
+ def _to_grouped_list(raw: Any) -> List[npt.NDArray[np.float32]]:
874
+ """Normalize a grouped encoder result to a list of per-sample arrays.
875
+
876
+ Grouped results are NEVER stacked into a (B, *) batch. Grouping is only a storage
877
+ read-unit (one file load per group); assembling the model batch (B) is the engine's
878
+ single responsibility, so it stays the only place samples get batched. Keeping
879
+ per-sample arrays lets the engine batch to any B — including a B=1-only model — without
880
+ an intermediate stack/unstack. A user encoder that already returned a stacked (B, *)
881
+ array is split back into its per-sample rows so the contract is a list either way."""
882
+ if isinstance(raw, list):
883
+ return raw
884
+ return [row for row in np.asarray(raw)]
885
+
886
+ def _locate_group(self, preprocess_state: PreprocessResponse,
887
+ sample_id: Union[int, str]) -> Tuple[List[Union[int, str]], int]:
888
+ """Map a sample id to (its group, its position in that group). Cached per
889
+ PreprocessResponse so grouped single-sample fetches don't rescan the groups."""
890
+ cache = getattr(self, '_group_pos_cache', None)
891
+ if cache is None:
892
+ cache = {}
893
+ self._group_pos_cache = cache
894
+ mapping = cache.get(id(preprocess_state))
895
+ if mapping is None:
896
+ mapping = {}
897
+ for group in preprocess_state.groups:
898
+ for pos, sid in enumerate(group):
899
+ if sid in mapping:
900
+ raise ValueError(
901
+ f"Duplicate sample id {sid!r} across groups in the preprocess response; "
902
+ f"sample ids must be unique within and across groups.")
903
+ mapping[sid] = (group, pos)
904
+ cache[id(preprocess_state)] = mapping
905
+ if sample_id not in mapping:
906
+ raise KeyError(f"Sample id {sample_id!r} is not present in any group of this preprocess response.")
907
+ return mapping[sample_id]
908
+
885
909
  def _get_dataset_handlers(self, handlers: Iterable[DatasetBaseHandler],
886
910
  state: DataStateEnum, sample_id: Union[int, str]) -> Dict[str, npt.NDArray[np.float32]]:
887
911
  result_agg = {}
@@ -893,12 +917,71 @@ class LeapLoader(LeapLoaderBase):
893
917
  sample_id = original_local_id
894
918
  else:
895
919
  preprocess_state = preprocess_result[state]
920
+ if preprocess_state.is_grouped:
921
+ # Grouped dataset: call the encoder once with the whole group, index out the sample.
922
+ group_ids, pos = self._locate_group(preprocess_state, sample_id)
923
+ for handler in handlers:
924
+ grouped = self._to_grouped_list(handler.function(group_ids, preprocess_state))
925
+ result_agg[handler.name] = grouped[pos]
926
+ return result_agg
896
927
  for handler in handlers:
897
928
  handler_result = handler.function(sample_id, preprocess_state)
898
929
  handler_name = handler.name
899
930
  result_agg[handler_name] = handler_result
900
931
  return result_agg
901
932
 
933
+ def get_samples(self, state: DataStateEnum, group_ids: List[Union[int, str]]) -> DatasetSample:
934
+ """Group-aware fetch: hand the whole group to each encoder in a single call and return a
935
+ grouped DatasetSample (inputs/gt as per-sample lists of length B, metadata as per-key lists
936
+ of length B, index = the group). Grouped results are never stacked here — batching the
937
+ samples into the model's B is the engine's job. This is the path a group-aware engine calls
938
+ to read a file once per group. Row order matches group_ids exactly."""
939
+ self.exec_script()
940
+ preprocess_state = self._preprocess_result()[state]
941
+ if preprocess_state.is_grouped:
942
+ # All requested ids must belong to a single group (one file). A cross-group request
943
+ # would force the encoder to load multiple files, defeating the one-load-per-group
944
+ # contract; partitioning a scattered request by group is the engine's responsibility.
945
+ distinct_groups = {id(self._locate_group(preprocess_state, sid)[0]) for sid in group_ids}
946
+ assert len(distinct_groups) == 1, (
947
+ f"get_samples received sample ids spanning {len(distinct_groups)} groups; a request "
948
+ f"must be confined to a single group. The engine partitions cross-group requests and "
949
+ f"issues one get_samples call per group.")
950
+ inputs = {handler.name: self._to_grouped_list(handler.function(group_ids, preprocess_state))
951
+ for handler in global_leap_binder.setup_container.inputs}
952
+ gt = None
953
+ if state != DataStateEnum.unlabeled:
954
+ gt = {handler.name: self._to_grouped_list(handler.function(group_ids, preprocess_state))
955
+ for handler in global_leap_binder.setup_container.ground_truths}
956
+
957
+ metadata: Dict[str, Any] = {}
958
+ metadata_is_none: Dict[str, Any] = {}
959
+ for handler in global_leap_binder.setup_container.metadata:
960
+ rows = handler.function(group_ids, preprocess_state) # list of B scalars/dicts
961
+ if rows and isinstance(rows[0], dict):
962
+ for key in self._grouped_dict_keys(rows, handler.name):
963
+ name = "{}_{}".format(handler.name, key)
964
+ converted = [self._convert_metadata_to_correct_type(name, row[key]) for row in rows]
965
+ metadata[name] = [v for v, _ in converted]
966
+ metadata_is_none[name] = [is_none for _, is_none in converted]
967
+ else:
968
+ converted = [self._convert_metadata_to_correct_type(handler.name, row) for row in rows]
969
+ metadata[handler.name] = [v for v, _ in converted]
970
+ metadata_is_none[handler.name] = [is_none for _, is_none in converted]
971
+
972
+ # custom_latent_space is group-aware like the input/GT encoders: hand it the whole group in a
973
+ # single call so a file-backed latent fn loads the file once, and normalize to (B, d). This
974
+ # keeps the mandatory group path non-lossy. instance_masks stay None here: they are a
975
+ # per-(sample, instance) concern that needs an instance_id the group fetch does not carry.
976
+ custom_latent_space = None
977
+ if global_leap_binder.setup_container.custom_latent_space is not None:
978
+ latent_fn = global_leap_binder.setup_container.custom_latent_space.function
979
+ custom_latent_space = self._to_grouped_list(latent_fn(group_ids, preprocess_state))
980
+
981
+ return DatasetSample(inputs=inputs, gt=gt, metadata=metadata, metadata_is_none=metadata_is_none,
982
+ index=list(group_ids), state=state, custom_latent_space=custom_latent_space,
983
+ instance_masks=None)
984
+
902
985
  def _get_inputs(self, state: DataStateEnum, sample_id: Union[int, str]) -> Dict[str, npt.NDArray[np.float32]]:
903
986
  inputs = self._get_dataset_handlers(global_leap_binder.setup_container.inputs, state, sample_id)
904
987
  autoregressive_handler = global_leap_binder.setup_container.autoregressive_step
@@ -907,16 +990,13 @@ class LeapLoader(LeapLoaderBase):
907
990
  # the sample's (step-0) model inputs, so every sample-fetching flow (graph validation,
908
991
  # analysis, import checks) sees a fully-populated inputs dict.
909
992
  preprocess_response = self._preprocess_result()[state]
910
- step_zero_result = autoregressive_handler.function(sample_id, None, None, None,
911
- preprocess_response)
912
- if not isinstance(step_zero_result, tuple) or len(step_zero_result) != 2 or \
913
- not isinstance(step_zero_result[0], dict):
993
+ step_zero_inputs = autoregressive_handler.function(sample_id, None, None, preprocess_response)
994
+ if not isinstance(step_zero_inputs, dict):
914
995
  raise Exception(
915
- 'The autoregressive step hook must return a (next_inputs, state) tuple with a '
916
- f'dict of initial model inputs on its first call, got {type(step_zero_result)} '
917
- f'for sample {sample_id}.')
918
- self._validate_autoregressive_step_inputs(step_zero_result[0], sample_id)
919
- inputs.update(step_zero_result[0])
996
+ 'The autoregressive step hook must return a dict of initial model inputs on its '
997
+ f'first call, got {type(step_zero_inputs)} for sample {sample_id}.')
998
+ inputs.update({key: value for key, value in step_zero_inputs.items()
999
+ if not key.startswith('_')})
920
1000
  return inputs
921
1001
 
922
1002
  def _get_instances_masks(self, state: DataStateEnum, sample_id: Union[int, str], instance_id: int) -> Optional[Dict[str, ElementInstance]]:
@@ -1002,12 +1082,18 @@ class LeapLoader(LeapLoaderBase):
1002
1082
  sample_id = original_local_id
1003
1083
  else:
1004
1084
  preprocess_state = preprocess_result[state]
1085
+ group_ids, pos = (None, None)
1086
+ if preprocess_state.is_grouped:
1087
+ group_ids, pos = self._locate_group(preprocess_state, sample_id)
1005
1088
  for handler in global_leap_binder.setup_container.metadata:
1006
1089
  if requested_metadata_names:
1007
1090
  if not is_metadata_name_starts_with_handler_name(handler):
1008
1091
  continue
1009
1092
 
1010
- handler_result = handler.function(sample_id, preprocess_state)
1093
+ if preprocess_state.is_grouped:
1094
+ handler_result = handler.function(group_ids, preprocess_state)[pos]
1095
+ else:
1096
+ handler_result = handler.function(sample_id, preprocess_state)
1011
1097
  if isinstance(handler_result, dict):
1012
1098
  for single_metadata_name, single_metadata_result in handler_result.items():
1013
1099
  handler_name = f'{handler.name}_{single_metadata_name}'
@@ -1026,6 +1112,66 @@ class LeapLoader(LeapLoaderBase):
1026
1112
 
1027
1113
  return result_agg, is_none
1028
1114
 
1115
+ def _grouped_metadata_for_group(self, preprocess_state: PreprocessResponse,
1116
+ group_ids: List[Union[int, str]],
1117
+ requested_metadata_names: Optional[List[str]]
1118
+ ) -> Tuple[Dict[str, List[Any]], Dict[str, List[bool]]]:
1119
+ def is_wanted(_handler):
1120
+ if not requested_metadata_names:
1121
+ return True
1122
+ for metadata_name in requested_metadata_names:
1123
+ if metadata_name.startswith(_handler.name + '_') or metadata_name == _handler.name:
1124
+ return True
1125
+ return False
1126
+
1127
+ results: Dict[str, List[Any]] = {}
1128
+ is_none: Dict[str, List[bool]] = {}
1129
+ for handler in global_leap_binder.setup_container.metadata:
1130
+ if requested_metadata_names and not is_wanted(handler):
1131
+ continue
1132
+ rows = handler.function(group_ids, preprocess_state)
1133
+ if rows and isinstance(rows[0], dict):
1134
+ for key in self._grouped_dict_keys(rows, handler.name):
1135
+ name = f'{handler.name}_{key}'
1136
+ if requested_metadata_names and name not in requested_metadata_names:
1137
+ continue
1138
+ converted = [self._convert_metadata_to_correct_type(name, row[key]) for row in rows]
1139
+ results[name] = [v for v, _ in converted]
1140
+ is_none[name] = [n for _, n in converted]
1141
+ else:
1142
+ name = handler.name
1143
+ if requested_metadata_names and name not in requested_metadata_names:
1144
+ continue
1145
+ converted = [self._convert_metadata_to_correct_type(name, row) for row in rows]
1146
+ results[name] = [v for v, _ in converted]
1147
+ is_none[name] = [n for _, n in converted]
1148
+ return results, is_none
1149
+
1150
+ def get_metadata_multiple_samples(self, state: DataStateEnum, sample_ids: Union[List[int], List[str]],
1151
+ requested_metadata_names: Optional[List[str]] = None
1152
+ ) -> Tuple[Dict[str, Union[List[str], List[int], List[bool], List[float]]],
1153
+ Dict[str, List[bool]]]:
1154
+ preprocess_state = self._preprocess_result().get(state)
1155
+ if preprocess_state is None or not preprocess_state.is_grouped:
1156
+ return super().get_metadata_multiple_samples(state, sample_ids, requested_metadata_names)
1157
+
1158
+ sample_id_type = self.get_sample_id_type()
1159
+ aggregated_results: Dict[str, List[Any]] = {}
1160
+ aggregated_is_none: Dict[str, List[bool]] = {}
1161
+ group_cache: Dict[int, Tuple[Dict[str, List[Any]], Dict[str, List[bool]]]] = {}
1162
+ for sample_id in sample_ids:
1163
+ sample_id = sample_id_type(sample_id)
1164
+ group_ids, pos = self._locate_group(preprocess_state, sample_id)
1165
+ key = id(group_ids)
1166
+ if key not in group_cache:
1167
+ group_cache[key] = self._grouped_metadata_for_group(
1168
+ preprocess_state, group_ids, requested_metadata_names)
1169
+ group_results, group_is_none = group_cache[key]
1170
+ for name, values in group_results.items():
1171
+ aggregated_results.setdefault(name, []).append(values[pos])
1172
+ aggregated_is_none.setdefault(name, []).append(group_is_none[name][pos])
1173
+ return aggregated_results, aggregated_is_none
1174
+
1029
1175
  @lru_cache()
1030
1176
  def get_sample_id_type(self) -> Type:
1031
1177
  preprocess_results = list(self._preprocess_result().values())
@@ -1057,15 +1203,12 @@ class LeapLoader(LeapLoaderBase):
1057
1203
  def run_autoregressive_step(self, sample_id: Union[int, str],
1058
1204
  prev_inputs: Optional[Dict[str, npt.NDArray[np.float32]]],
1059
1205
  prev_outputs: Optional[Dict[str, npt.NDArray[np.float32]]],
1060
- chain_state: Any,
1061
- state: DataStateEnum
1062
- ) -> Tuple[Optional[Dict[str, npt.NDArray[np.float32]]], Any]:
1206
+ state: DataStateEnum) -> Optional[Dict[str, npt.NDArray[np.float32]]]:
1063
1207
  """Execute the user's autoregressive step hook against the in-process preprocess result.
1064
1208
 
1065
1209
  This is the engine rollout's per-step entry point (invoked on the generic pod). The first
1066
- call per chain passes prev_inputs=None, prev_outputs=None, chain_state=None and returns
1067
- the initial model inputs plus the initial state; a None next_inputs ends the chain — the
1068
- terminating call still returns the final state.
1210
+ call per chain passes prev_inputs=None, prev_outputs=None and returns the initial model
1211
+ inputs; a None return ends the chain.
1069
1212
  """
1070
1213
  self.exec_script()
1071
1214
  handler = global_leap_binder.setup_container.autoregressive_step
@@ -1073,119 +1216,28 @@ class LeapLoader(LeapLoaderBase):
1073
1216
  raise Exception('run_autoregressive_step was called but the integration has no '
1074
1217
  'tensorleap_autoregressive_step hook.')
1075
1218
  preprocess_response = self._preprocess_result()[state]
1076
- result = handler.function(sample_id, prev_inputs, prev_outputs, chain_state,
1077
- preprocess_response)
1078
- if not isinstance(result, tuple) or len(result) != 2:
1079
- raise Exception('The autoregressive step hook must return a (next_inputs, state) '
1080
- f'tuple, got {type(result)} for sample {sample_id}.')
1081
- next_inputs, new_chain_state = result
1082
- validate_autoregressive_state_types(new_chain_state)
1083
- if next_inputs is None:
1084
- if prev_inputs is None:
1085
- raise Exception('The autoregressive step hook returned None for next_inputs on '
1086
- f'the first call of the chain (sample {sample_id}) — the first '
1087
- 'call must return the initial model inputs; None would produce '
1088
- 'an empty chain with no forward pass.')
1089
- return None, new_chain_state
1090
- if not isinstance(next_inputs, dict):
1219
+ result = handler.function(sample_id, prev_inputs, prev_outputs, preprocess_response)
1220
+ if result is None:
1221
+ return None
1222
+ if not isinstance(result, dict):
1091
1223
  raise Exception('The autoregressive step hook must return a dict of model inputs or '
1092
- f'None to end the chain, got {type(next_inputs)} for sample '
1093
- f'{sample_id}.')
1224
+ f'None to end the chain, got {type(result)} for sample {sample_id}.')
1094
1225
  # Parse-time validation only exercises step 0; enforce the fixed key-set/shape contract
1095
1226
  # on every step here so drift fails with an actionable message instead of a cryptic
1096
1227
  # engine error on a compiled graph.
1097
1228
  expected_shapes = self._ensure_autoregressive_input_shapes()
1098
- if set(next_inputs) != set(expected_shapes):
1229
+ model_inputs = {key: value for key, value in result.items() if not key.startswith('_')}
1230
+ if set(model_inputs) != set(expected_shapes):
1099
1231
  raise Exception('The autoregressive step hook must return the same model input keys '
1100
1232
  f'on every call. Expected {sorted(expected_shapes)}, got '
1101
- f'{sorted(next_inputs)} for sample {sample_id}.')
1102
- for key, value in next_inputs.items():
1103
- if not isinstance(value, np.ndarray):
1104
- raise Exception('The autoregressive step hook must return numpy arrays for every '
1105
- f'model input on every call — input "{key}" is a '
1106
- f'{type(value).__name__} for sample {sample_id} (forgot .numpy() '
1107
- 'on a fed-back model output?).')
1108
- if list(value.shape) != expected_shapes[key]:
1233
+ f'{sorted(model_inputs)} for sample {sample_id}.')
1234
+ for key, value in model_inputs.items():
1235
+ if isinstance(value, np.ndarray) and list(value.shape) != expected_shapes[key]:
1109
1236
  raise Exception('The autoregressive step hook must return fixed tensor shapes on '
1110
1237
  f'every call (use fixed-length padding + mask for growing '
1111
1238
  f'sequences). Input "{key}" was {expected_shapes[key]} on the '
1112
1239
  f'first call and {list(value.shape)} now (sample {sample_id}).')
1113
- return next_inputs, new_chain_state
1114
-
1115
- @lru_cache()
1116
- def autoregressive_visualizer_by_name(self) -> Dict[str, VisualizerHandlerData]:
1117
- self.exec_script()
1118
- return {handler.visualizer_handler_data.name: handler.visualizer_handler_data
1119
- for handler in global_leap_binder.setup_container.autoregressive_visualizers}
1120
-
1121
- @lru_cache()
1122
- def get_autoregressive_decorator_names(self) -> Dict[str, List[str]]:
1123
- self.exec_script()
1124
- setup = global_leap_binder.setup_container
1125
- return {
1126
- 'metrics': [handler.metric_handler_data.name
1127
- for handler in setup.autoregressive_metrics],
1128
- 'losses': [handler.custom_loss_handler_data.name
1129
- for handler in setup.autoregressive_losses],
1130
- 'visualizers': [handler.visualizer_handler_data.name
1131
- for handler in setup.autoregressive_visualizers],
1132
- }
1133
-
1134
- @staticmethod
1135
- def _autoregressive_callable_kwargs(function: Callable[..., Any],
1136
- inputs: Dict[str, npt.NDArray[np.float32]],
1137
- outputs: Dict[str, npt.NDArray[np.float32]],
1138
- chain_state: Any,
1139
- wired_args: Dict[str, npt.NDArray[np.float32]]
1140
- ) -> Dict[str, Any]:
1141
- implicit_values = {'inputs': inputs, 'outputs': outputs, 'state': chain_state}
1142
- kwargs: Dict[str, Any] = {}
1143
- for arg_name in inspect.getfullargspec(function)[0]:
1144
- if arg_name in implicit_values:
1145
- kwargs[arg_name] = implicit_values[arg_name]
1146
- elif arg_name in wired_args:
1147
- kwargs[arg_name] = wired_args[arg_name]
1148
- else:
1149
- raise Exception(f'Autoregressive callable is missing wired argument '
1150
- f'"{arg_name}" — the platform mapping supplies only '
1151
- f'{sorted(wired_args)}.')
1152
- return kwargs
1153
-
1154
- def run_autoregressive_metric(self, name: str,
1155
- inputs: Dict[str, npt.NDArray[np.float32]],
1156
- outputs: Dict[str, npt.NDArray[np.float32]],
1157
- chain_state: Any,
1158
- wired_args: Dict[str, npt.NDArray[np.float32]]) -> Any:
1159
- self.exec_script()
1160
- for handler in global_leap_binder.setup_container.autoregressive_metrics:
1161
- if handler.metric_handler_data.name == name:
1162
- return handler.function(**self._autoregressive_callable_kwargs(
1163
- handler.function, inputs, outputs, chain_state, wired_args))
1164
- raise Exception(f'No autoregressive metric named "{name}" is registered.')
1165
-
1166
- def run_autoregressive_loss(self, name: str,
1167
- inputs: Dict[str, npt.NDArray[np.float32]],
1168
- outputs: Dict[str, npt.NDArray[np.float32]],
1169
- chain_state: Any,
1170
- wired_args: Dict[str, npt.NDArray[np.float32]]) -> Any:
1171
- self.exec_script()
1172
- for handler in global_leap_binder.setup_container.autoregressive_losses:
1173
- if handler.custom_loss_handler_data.name == name:
1174
- return handler.function(**self._autoregressive_callable_kwargs(
1175
- handler.function, inputs, outputs, chain_state, wired_args))
1176
- raise Exception(f'No autoregressive loss named "{name}" is registered.')
1177
-
1178
- def run_autoregressive_visualizer(self, name: str,
1179
- inputs: Dict[str, npt.NDArray[np.float32]],
1180
- outputs: Dict[str, npt.NDArray[np.float32]],
1181
- chain_state: Any,
1182
- wired_args: Dict[str, npt.NDArray[np.float32]]) -> Any:
1183
- self.exec_script()
1184
- for handler in global_leap_binder.setup_container.autoregressive_visualizers:
1185
- if handler.visualizer_handler_data.name == name:
1186
- return handler.function(**self._autoregressive_callable_kwargs(
1187
- handler.function, inputs, outputs, chain_state, wired_args))
1188
- raise Exception(f'No autoregressive visualizer named "{name}" is registered.')
1240
+ return result
1189
1241
 
1190
1242
  def get_prediction_names_in_order(self) -> List[str]:
1191
1243
  """Model output names by declared prediction-type order, used to key prev_outputs for the