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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,
@@ -846,21 +833,6 @@ class LeapLoader(LeapLoaderBase):
846
833
  metric_inst = MetricInstance(metric.metric_handler_data.name, metric.metric_handler_data.arg_names)
847
834
  metrics.append(metric_inst)
848
835
 
849
- # Autoregressive decorators are reported alongside the regular ones so the platform sees
850
- # their names; the engine tells them apart by name via get_autoregressive_decorator_names.
851
- for autoregressive_metric in setup.autoregressive_metrics:
852
- metrics.append(MetricInstance(autoregressive_metric.metric_handler_data.name,
853
- autoregressive_metric.metric_handler_data.arg_names))
854
- for autoregressive_loss in setup.autoregressive_losses:
855
- custom_losses.append(CustomLossInstance(
856
- autoregressive_loss.custom_loss_handler_data.name,
857
- autoregressive_loss.custom_loss_handler_data.arg_names))
858
- for autoregressive_visualizer in setup.autoregressive_visualizers:
859
- visualizers.append(VisualizerInstance(
860
- autoregressive_visualizer.visualizer_handler_data.name,
861
- autoregressive_visualizer.visualizer_handler_data.type,
862
- autoregressive_visualizer.visualizer_handler_data.arg_names))
863
-
864
836
  simulations = []
865
837
  for sim in setup.simulations:
866
838
  sim_config_serialized = {
@@ -906,6 +878,58 @@ class LeapLoader(LeapLoaderBase):
906
878
 
907
879
  return sample_ids
908
880
 
881
+ @staticmethod
882
+ def _grouped_dict_keys(rows: List[Dict[str, Any]], handler_name: str) -> List[str]:
883
+ """Keys for a group of dict-metadata rows, requiring every row to share the same keys.
884
+ A group must expose a uniform metadata schema; otherwise the per-key lists would silently
885
+ drop or KeyError on rows that differ from row 0."""
886
+ keys = list(rows[0].keys())
887
+ key_set = set(keys)
888
+ for pos, row in enumerate(rows):
889
+ if set(row.keys()) != key_set:
890
+ raise ValueError(
891
+ f"Metadata handler {handler_name!r} returned inconsistent dict keys across the "
892
+ f"group: row 0 has {sorted(key_set)} but row {pos} has {sorted(row.keys())}. "
893
+ f"All rows in a group must share the same metadata keys.")
894
+ return keys
895
+
896
+ @staticmethod
897
+ def _to_grouped_list(raw: Any) -> List[npt.NDArray[np.float32]]:
898
+ """Normalize a grouped encoder result to a list of per-sample arrays.
899
+
900
+ Grouped results are NEVER stacked into a (B, *) batch. Grouping is only a storage
901
+ read-unit (one file load per group); assembling the model batch (B) is the engine's
902
+ single responsibility, so it stays the only place samples get batched. Keeping
903
+ per-sample arrays lets the engine batch to any B — including a B=1-only model — without
904
+ an intermediate stack/unstack. A user encoder that already returned a stacked (B, *)
905
+ array is split back into its per-sample rows so the contract is a list either way."""
906
+ if isinstance(raw, list):
907
+ return raw
908
+ return [row for row in np.asarray(raw)]
909
+
910
+ def _locate_group(self, preprocess_state: PreprocessResponse,
911
+ sample_id: Union[int, str]) -> Tuple[List[Union[int, str]], int]:
912
+ """Map a sample id to (its group, its position in that group). Cached per
913
+ PreprocessResponse so grouped single-sample fetches don't rescan the groups."""
914
+ cache = getattr(self, '_group_pos_cache', None)
915
+ if cache is None:
916
+ cache = {}
917
+ self._group_pos_cache = cache
918
+ mapping = cache.get(id(preprocess_state))
919
+ if mapping is None:
920
+ mapping = {}
921
+ for group in preprocess_state.groups:
922
+ for pos, sid in enumerate(group):
923
+ if sid in mapping:
924
+ raise ValueError(
925
+ f"Duplicate sample id {sid!r} across groups in the preprocess response; "
926
+ f"sample ids must be unique within and across groups.")
927
+ mapping[sid] = (group, pos)
928
+ cache[id(preprocess_state)] = mapping
929
+ if sample_id not in mapping:
930
+ raise KeyError(f"Sample id {sample_id!r} is not present in any group of this preprocess response.")
931
+ return mapping[sample_id]
932
+
909
933
  def _get_dataset_handlers(self, handlers: Iterable[DatasetBaseHandler],
910
934
  state: DataStateEnum, sample_id: Union[int, str]) -> Dict[str, npt.NDArray[np.float32]]:
911
935
  result_agg = {}
@@ -917,12 +941,71 @@ class LeapLoader(LeapLoaderBase):
917
941
  sample_id = original_local_id
918
942
  else:
919
943
  preprocess_state = preprocess_result[state]
944
+ if preprocess_state.is_grouped:
945
+ # Grouped dataset: call the encoder once with the whole group, index out the sample.
946
+ group_ids, pos = self._locate_group(preprocess_state, sample_id)
947
+ for handler in handlers:
948
+ grouped = self._to_grouped_list(handler.function(group_ids, preprocess_state))
949
+ result_agg[handler.name] = grouped[pos]
950
+ return result_agg
920
951
  for handler in handlers:
921
952
  handler_result = handler.function(sample_id, preprocess_state)
922
953
  handler_name = handler.name
923
954
  result_agg[handler_name] = handler_result
924
955
  return result_agg
925
956
 
957
+ def get_samples(self, state: DataStateEnum, group_ids: List[Union[int, str]]) -> DatasetSample:
958
+ """Group-aware fetch: hand the whole group to each encoder in a single call and return a
959
+ grouped DatasetSample (inputs/gt as per-sample lists of length B, metadata as per-key lists
960
+ of length B, index = the group). Grouped results are never stacked here — batching the
961
+ samples into the model's B is the engine's job. This is the path a group-aware engine calls
962
+ to read a file once per group. Row order matches group_ids exactly."""
963
+ self.exec_script()
964
+ preprocess_state = self._preprocess_result()[state]
965
+ if preprocess_state.is_grouped:
966
+ # All requested ids must belong to a single group (one file). A cross-group request
967
+ # would force the encoder to load multiple files, defeating the one-load-per-group
968
+ # contract; partitioning a scattered request by group is the engine's responsibility.
969
+ distinct_groups = {id(self._locate_group(preprocess_state, sid)[0]) for sid in group_ids}
970
+ assert len(distinct_groups) == 1, (
971
+ f"get_samples received sample ids spanning {len(distinct_groups)} groups; a request "
972
+ f"must be confined to a single group. The engine partitions cross-group requests and "
973
+ f"issues one get_samples call per group.")
974
+ inputs = {handler.name: self._to_grouped_list(handler.function(group_ids, preprocess_state))
975
+ for handler in global_leap_binder.setup_container.inputs}
976
+ gt = None
977
+ if state != DataStateEnum.unlabeled:
978
+ gt = {handler.name: self._to_grouped_list(handler.function(group_ids, preprocess_state))
979
+ for handler in global_leap_binder.setup_container.ground_truths}
980
+
981
+ metadata: Dict[str, Any] = {}
982
+ metadata_is_none: Dict[str, Any] = {}
983
+ for handler in global_leap_binder.setup_container.metadata:
984
+ rows = handler.function(group_ids, preprocess_state) # list of B scalars/dicts
985
+ if rows and isinstance(rows[0], dict):
986
+ for key in self._grouped_dict_keys(rows, handler.name):
987
+ name = "{}_{}".format(handler.name, key)
988
+ converted = [self._convert_metadata_to_correct_type(name, row[key]) for row in rows]
989
+ metadata[name] = [v for v, _ in converted]
990
+ metadata_is_none[name] = [is_none for _, is_none in converted]
991
+ else:
992
+ converted = [self._convert_metadata_to_correct_type(handler.name, row) for row in rows]
993
+ metadata[handler.name] = [v for v, _ in converted]
994
+ metadata_is_none[handler.name] = [is_none for _, is_none in converted]
995
+
996
+ # custom_latent_space is group-aware like the input/GT encoders: hand it the whole group in a
997
+ # single call so a file-backed latent fn loads the file once, and normalize to (B, d). This
998
+ # keeps the mandatory group path non-lossy. instance_masks stay None here: they are a
999
+ # per-(sample, instance) concern that needs an instance_id the group fetch does not carry.
1000
+ custom_latent_space = None
1001
+ if global_leap_binder.setup_container.custom_latent_space is not None:
1002
+ latent_fn = global_leap_binder.setup_container.custom_latent_space.function
1003
+ custom_latent_space = self._to_grouped_list(latent_fn(group_ids, preprocess_state))
1004
+
1005
+ return DatasetSample(inputs=inputs, gt=gt, metadata=metadata, metadata_is_none=metadata_is_none,
1006
+ index=list(group_ids), state=state, custom_latent_space=custom_latent_space,
1007
+ instance_masks=None)
1008
+
926
1009
  def _get_inputs(self, state: DataStateEnum, sample_id: Union[int, str]) -> Dict[str, npt.NDArray[np.float32]]:
927
1010
  inputs = self._get_dataset_handlers(global_leap_binder.setup_container.inputs, state, sample_id)
928
1011
  autoregressive_handler = global_leap_binder.setup_container.autoregressive_step
@@ -931,16 +1014,13 @@ class LeapLoader(LeapLoaderBase):
931
1014
  # the sample's (step-0) model inputs, so every sample-fetching flow (graph validation,
932
1015
  # analysis, import checks) sees a fully-populated inputs dict.
933
1016
  preprocess_response = self._preprocess_result()[state]
934
- step_zero_result = autoregressive_handler.function(sample_id, None, None, None,
935
- preprocess_response)
936
- if not isinstance(step_zero_result, tuple) or len(step_zero_result) != 2 or \
937
- not isinstance(step_zero_result[0], dict):
1017
+ step_zero_inputs = autoregressive_handler.function(sample_id, None, None, preprocess_response)
1018
+ if not isinstance(step_zero_inputs, dict):
938
1019
  raise Exception(
939
- 'The autoregressive step hook must return a (next_inputs, state) tuple with a '
940
- f'dict of initial model inputs on its first call, got {type(step_zero_result)} '
941
- f'for sample {sample_id}.')
942
- self._validate_autoregressive_step_inputs(step_zero_result[0], sample_id)
943
- inputs.update(step_zero_result[0])
1020
+ 'The autoregressive step hook must return a dict of initial model inputs on its '
1021
+ f'first call, got {type(step_zero_inputs)} for sample {sample_id}.')
1022
+ inputs.update({key: value for key, value in step_zero_inputs.items()
1023
+ if not key.startswith('_')})
944
1024
  return inputs
945
1025
 
946
1026
  def _get_instances_masks(self, state: DataStateEnum, sample_id: Union[int, str], instance_id: int) -> Optional[Dict[str, ElementInstance]]:
@@ -1026,12 +1106,18 @@ class LeapLoader(LeapLoaderBase):
1026
1106
  sample_id = original_local_id
1027
1107
  else:
1028
1108
  preprocess_state = preprocess_result[state]
1109
+ group_ids, pos = (None, None)
1110
+ if preprocess_state.is_grouped:
1111
+ group_ids, pos = self._locate_group(preprocess_state, sample_id)
1029
1112
  for handler in global_leap_binder.setup_container.metadata:
1030
1113
  if requested_metadata_names:
1031
1114
  if not is_metadata_name_starts_with_handler_name(handler):
1032
1115
  continue
1033
1116
 
1034
- handler_result = handler.function(sample_id, preprocess_state)
1117
+ if preprocess_state.is_grouped:
1118
+ handler_result = handler.function(group_ids, preprocess_state)[pos]
1119
+ else:
1120
+ handler_result = handler.function(sample_id, preprocess_state)
1035
1121
  if isinstance(handler_result, dict):
1036
1122
  for single_metadata_name, single_metadata_result in handler_result.items():
1037
1123
  handler_name = f'{handler.name}_{single_metadata_name}'
@@ -1050,6 +1136,66 @@ class LeapLoader(LeapLoaderBase):
1050
1136
 
1051
1137
  return result_agg, is_none
1052
1138
 
1139
+ def _grouped_metadata_for_group(self, preprocess_state: PreprocessResponse,
1140
+ group_ids: List[Union[int, str]],
1141
+ requested_metadata_names: Optional[List[str]]
1142
+ ) -> Tuple[Dict[str, List[Any]], Dict[str, List[bool]]]:
1143
+ def is_wanted(_handler):
1144
+ if not requested_metadata_names:
1145
+ return True
1146
+ for metadata_name in requested_metadata_names:
1147
+ if metadata_name.startswith(_handler.name + '_') or metadata_name == _handler.name:
1148
+ return True
1149
+ return False
1150
+
1151
+ results: Dict[str, List[Any]] = {}
1152
+ is_none: Dict[str, List[bool]] = {}
1153
+ for handler in global_leap_binder.setup_container.metadata:
1154
+ if requested_metadata_names and not is_wanted(handler):
1155
+ continue
1156
+ rows = handler.function(group_ids, preprocess_state)
1157
+ if rows and isinstance(rows[0], dict):
1158
+ for key in self._grouped_dict_keys(rows, handler.name):
1159
+ name = f'{handler.name}_{key}'
1160
+ if requested_metadata_names and name not in requested_metadata_names:
1161
+ continue
1162
+ converted = [self._convert_metadata_to_correct_type(name, row[key]) for row in rows]
1163
+ results[name] = [v for v, _ in converted]
1164
+ is_none[name] = [n for _, n in converted]
1165
+ else:
1166
+ name = handler.name
1167
+ if requested_metadata_names and name not in requested_metadata_names:
1168
+ continue
1169
+ converted = [self._convert_metadata_to_correct_type(name, row) for row in rows]
1170
+ results[name] = [v for v, _ in converted]
1171
+ is_none[name] = [n for _, n in converted]
1172
+ return results, is_none
1173
+
1174
+ def get_metadata_multiple_samples(self, state: DataStateEnum, sample_ids: Union[List[int], List[str]],
1175
+ requested_metadata_names: Optional[List[str]] = None
1176
+ ) -> Tuple[Dict[str, Union[List[str], List[int], List[bool], List[float]]],
1177
+ Dict[str, List[bool]]]:
1178
+ preprocess_state = self._preprocess_result().get(state)
1179
+ if preprocess_state is None or not preprocess_state.is_grouped:
1180
+ return super().get_metadata_multiple_samples(state, sample_ids, requested_metadata_names)
1181
+
1182
+ sample_id_type = self.get_sample_id_type()
1183
+ aggregated_results: Dict[str, List[Any]] = {}
1184
+ aggregated_is_none: Dict[str, List[bool]] = {}
1185
+ group_cache: Dict[int, Tuple[Dict[str, List[Any]], Dict[str, List[bool]]]] = {}
1186
+ for sample_id in sample_ids:
1187
+ sample_id = sample_id_type(sample_id)
1188
+ group_ids, pos = self._locate_group(preprocess_state, sample_id)
1189
+ key = id(group_ids)
1190
+ if key not in group_cache:
1191
+ group_cache[key] = self._grouped_metadata_for_group(
1192
+ preprocess_state, group_ids, requested_metadata_names)
1193
+ group_results, group_is_none = group_cache[key]
1194
+ for name, values in group_results.items():
1195
+ aggregated_results.setdefault(name, []).append(values[pos])
1196
+ aggregated_is_none.setdefault(name, []).append(group_is_none[name][pos])
1197
+ return aggregated_results, aggregated_is_none
1198
+
1053
1199
  @lru_cache()
1054
1200
  def get_sample_id_type(self) -> Type:
1055
1201
  preprocess_results = list(self._preprocess_result().values())
@@ -1081,15 +1227,12 @@ class LeapLoader(LeapLoaderBase):
1081
1227
  def run_autoregressive_step(self, sample_id: Union[int, str],
1082
1228
  prev_inputs: Optional[Dict[str, npt.NDArray[np.float32]]],
1083
1229
  prev_outputs: Optional[Dict[str, npt.NDArray[np.float32]]],
1084
- chain_state: Any,
1085
- state: DataStateEnum
1086
- ) -> Tuple[Optional[Dict[str, npt.NDArray[np.float32]]], Any]:
1230
+ state: DataStateEnum) -> Optional[Dict[str, npt.NDArray[np.float32]]]:
1087
1231
  """Execute the user's autoregressive step hook against the in-process preprocess result.
1088
1232
 
1089
1233
  This is the engine rollout's per-step entry point (invoked on the generic pod). The first
1090
- call per chain passes prev_inputs=None, prev_outputs=None, chain_state=None and returns
1091
- the initial model inputs plus the initial state; a None next_inputs ends the chain — the
1092
- terminating call still returns the final state.
1234
+ call per chain passes prev_inputs=None, prev_outputs=None and returns the initial model
1235
+ inputs; a None return ends the chain.
1093
1236
  """
1094
1237
  self.exec_script()
1095
1238
  handler = global_leap_binder.setup_container.autoregressive_step
@@ -1097,119 +1240,28 @@ class LeapLoader(LeapLoaderBase):
1097
1240
  raise Exception('run_autoregressive_step was called but the integration has no '
1098
1241
  'tensorleap_autoregressive_step hook.')
1099
1242
  preprocess_response = self._preprocess_result()[state]
1100
- result = handler.function(sample_id, prev_inputs, prev_outputs, chain_state,
1101
- preprocess_response)
1102
- if not isinstance(result, tuple) or len(result) != 2:
1103
- raise Exception('The autoregressive step hook must return a (next_inputs, state) '
1104
- f'tuple, got {type(result)} for sample {sample_id}.')
1105
- next_inputs, new_chain_state = result
1106
- validate_autoregressive_state_types(new_chain_state)
1107
- if next_inputs is None:
1108
- if prev_inputs is None:
1109
- raise Exception('The autoregressive step hook returned None for next_inputs on '
1110
- f'the first call of the chain (sample {sample_id}) — the first '
1111
- 'call must return the initial model inputs; None would produce '
1112
- 'an empty chain with no forward pass.')
1113
- return None, new_chain_state
1114
- if not isinstance(next_inputs, dict):
1243
+ result = handler.function(sample_id, prev_inputs, prev_outputs, preprocess_response)
1244
+ if result is None:
1245
+ return None
1246
+ if not isinstance(result, dict):
1115
1247
  raise Exception('The autoregressive step hook must return a dict of model inputs or '
1116
- f'None to end the chain, got {type(next_inputs)} for sample '
1117
- f'{sample_id}.')
1248
+ f'None to end the chain, got {type(result)} for sample {sample_id}.')
1118
1249
  # Parse-time validation only exercises step 0; enforce the fixed key-set/shape contract
1119
1250
  # on every step here so drift fails with an actionable message instead of a cryptic
1120
1251
  # engine error on a compiled graph.
1121
1252
  expected_shapes = self._ensure_autoregressive_input_shapes()
1122
- if set(next_inputs) != set(expected_shapes):
1253
+ model_inputs = {key: value for key, value in result.items() if not key.startswith('_')}
1254
+ if set(model_inputs) != set(expected_shapes):
1123
1255
  raise Exception('The autoregressive step hook must return the same model input keys '
1124
1256
  f'on every call. Expected {sorted(expected_shapes)}, got '
1125
- f'{sorted(next_inputs)} for sample {sample_id}.')
1126
- for key, value in next_inputs.items():
1127
- if not isinstance(value, np.ndarray):
1128
- raise Exception('The autoregressive step hook must return numpy arrays for every '
1129
- f'model input on every call — input "{key}" is a '
1130
- f'{type(value).__name__} for sample {sample_id} (forgot .numpy() '
1131
- 'on a fed-back model output?).')
1132
- if list(value.shape) != expected_shapes[key]:
1257
+ f'{sorted(model_inputs)} for sample {sample_id}.')
1258
+ for key, value in model_inputs.items():
1259
+ if isinstance(value, np.ndarray) and list(value.shape) != expected_shapes[key]:
1133
1260
  raise Exception('The autoregressive step hook must return fixed tensor shapes on '
1134
1261
  f'every call (use fixed-length padding + mask for growing '
1135
1262
  f'sequences). Input "{key}" was {expected_shapes[key]} on the '
1136
1263
  f'first call and {list(value.shape)} now (sample {sample_id}).')
1137
- return next_inputs, new_chain_state
1138
-
1139
- @lru_cache()
1140
- def autoregressive_visualizer_by_name(self) -> Dict[str, VisualizerHandlerData]:
1141
- self.exec_script()
1142
- return {handler.visualizer_handler_data.name: handler.visualizer_handler_data
1143
- for handler in global_leap_binder.setup_container.autoregressive_visualizers}
1144
-
1145
- @lru_cache()
1146
- def get_autoregressive_decorator_names(self) -> Dict[str, List[str]]:
1147
- self.exec_script()
1148
- setup = global_leap_binder.setup_container
1149
- return {
1150
- 'metrics': [handler.metric_handler_data.name
1151
- for handler in setup.autoregressive_metrics],
1152
- 'losses': [handler.custom_loss_handler_data.name
1153
- for handler in setup.autoregressive_losses],
1154
- 'visualizers': [handler.visualizer_handler_data.name
1155
- for handler in setup.autoregressive_visualizers],
1156
- }
1157
-
1158
- @staticmethod
1159
- def _autoregressive_callable_kwargs(function: Callable[..., Any],
1160
- inputs: Dict[str, npt.NDArray[np.float32]],
1161
- outputs: Dict[str, npt.NDArray[np.float32]],
1162
- chain_state: Any,
1163
- wired_args: Dict[str, npt.NDArray[np.float32]]
1164
- ) -> Dict[str, Any]:
1165
- implicit_values = {'inputs': inputs, 'outputs': outputs, 'state': chain_state}
1166
- kwargs: Dict[str, Any] = {}
1167
- for arg_name in inspect.getfullargspec(function)[0]:
1168
- if arg_name in implicit_values:
1169
- kwargs[arg_name] = implicit_values[arg_name]
1170
- elif arg_name in wired_args:
1171
- kwargs[arg_name] = wired_args[arg_name]
1172
- else:
1173
- raise Exception(f'Autoregressive callable is missing wired argument '
1174
- f'"{arg_name}" — the platform mapping supplies only '
1175
- f'{sorted(wired_args)}.')
1176
- return kwargs
1177
-
1178
- def run_autoregressive_metric(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_metrics:
1185
- if handler.metric_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 metric named "{name}" is registered.')
1189
-
1190
- def run_autoregressive_loss(self, name: str,
1191
- inputs: Dict[str, npt.NDArray[np.float32]],
1192
- outputs: Dict[str, npt.NDArray[np.float32]],
1193
- chain_state: Any,
1194
- wired_args: Dict[str, npt.NDArray[np.float32]]) -> Any:
1195
- self.exec_script()
1196
- for handler in global_leap_binder.setup_container.autoregressive_losses:
1197
- if handler.custom_loss_handler_data.name == name:
1198
- return handler.function(**self._autoregressive_callable_kwargs(
1199
- handler.function, inputs, outputs, chain_state, wired_args))
1200
- raise Exception(f'No autoregressive loss named "{name}" is registered.')
1201
-
1202
- def run_autoregressive_visualizer(self, name: str,
1203
- inputs: Dict[str, npt.NDArray[np.float32]],
1204
- outputs: Dict[str, npt.NDArray[np.float32]],
1205
- chain_state: Any,
1206
- wired_args: Dict[str, npt.NDArray[np.float32]]) -> Any:
1207
- self.exec_script()
1208
- for handler in global_leap_binder.setup_container.autoregressive_visualizers:
1209
- if handler.visualizer_handler_data.name == name:
1210
- return handler.function(**self._autoregressive_callable_kwargs(
1211
- handler.function, inputs, outputs, chain_state, wired_args))
1212
- raise Exception(f'No autoregressive visualizer named "{name}" is registered.')
1264
+ return result
1213
1265
 
1214
1266
  def get_prediction_names_in_order(self) -> List[str]:
1215
1267
  """Model output names by declared prediction-type order, used to key prev_outputs for the