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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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, Union, Any, Type, Optional, Callable, Tuple
10
+ from typing import Dict, List, Iterable, Set, FrozenSet, Union, Any, Type, Optional, Callable, Tuple
11
11
 
12
12
  import numpy as np
13
13
  import numpy.typing as npt
@@ -27,7 +27,8 @@ 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
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
31
32
 
32
33
 
33
34
  def _serialize_sim_bounds(bounds) -> dict:
@@ -59,35 +60,14 @@ class LeapLoader(LeapLoaderBase):
59
60
 
60
61
  @lru_cache()
61
62
  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
73
- if global_leap_binder.integration_test_func is not None:
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
66
  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)
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))
91
71
  except TypeError as e:
92
72
  import traceback
93
73
  global_leap_binder.setup_container = DatasetIntegrationSetup()
@@ -100,7 +80,6 @@ class LeapLoader(LeapLoaderBase):
100
80
  raise DatasetScriptException(getattr(e, 'message', repr(e))) from e
101
81
  finally:
102
82
  # ensure that the environment variable is removed after the script execution
103
- _leap_dec._mapping_dataset_is_grouped = False
104
83
  if mapping_runtime_mode_env_var_mame in os.environ:
105
84
  del os.environ[mapping_runtime_mode_env_var_mame]
106
85
 
@@ -271,20 +250,12 @@ class LeapLoader(LeapLoaderBase):
271
250
  )
272
251
 
273
252
  preprocess_result = self._preprocess_result()
274
- if state == DataStateEnum.unlabeled and sample_id not in preprocess_result[state].flat_sample_ids:
253
+ if state == DataStateEnum.unlabeled and sample_id not in preprocess_result[state].sample_ids:
275
254
  self._preprocess_result(update_unlabeled_preprocess=True)
276
255
 
277
256
  metadata, metadata_is_none = self.get_metadata(state, sample_id)
278
257
 
279
- custom_latent_space = None
280
- if global_leap_binder.setup_container.custom_latent_space is not None:
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)
258
+ custom_latent_spaces = self._get_custom_latent_spaces(sample_id, preprocess_result[state])
288
259
  instance_mask = self._get_instances_masks(state, sample_id, instance_id)
289
260
  sample = DatasetSample(inputs=self._get_inputs(state, sample_id),
290
261
  gt=None if state == DataStateEnum.unlabeled else self._get_gt(state, sample_id),
@@ -292,7 +263,7 @@ class LeapLoader(LeapLoaderBase):
292
263
  metadata_is_none=metadata_is_none,
293
264
  index=sample_id,
294
265
  state=state,
295
- custom_latent_space=custom_latent_space,
266
+ custom_latent_spaces=custom_latent_spaces,
296
267
  instance_masks=instance_mask)
297
268
  return sample
298
269
 
@@ -323,11 +294,7 @@ class LeapLoader(LeapLoaderBase):
323
294
  handler.name, handler_result
324
295
  )
325
296
 
326
- custom_latent_space = None
327
- if global_leap_binder.setup_container.custom_latent_space is not None:
328
- custom_latent_space = global_leap_binder.setup_container.custom_latent_space.function(
329
- original_sample_id, preprocess
330
- )
297
+ custom_latent_spaces = self._get_custom_latent_spaces(original_sample_id, preprocess)
331
298
 
332
299
  return DatasetSample(
333
300
  inputs=inputs,
@@ -336,7 +303,7 @@ class LeapLoader(LeapLoaderBase):
336
303
  metadata_is_none=metadata_is_none,
337
304
  index=synthetic_index,
338
305
  state=DataStateEnum.additional,
339
- custom_latent_space=custom_latent_space,
306
+ custom_latent_spaces=custom_latent_spaces,
340
307
  instance_masks=None,
341
308
  )
342
309
 
@@ -415,7 +382,7 @@ class LeapLoader(LeapLoaderBase):
415
382
  if self.get_sample_id_type() is str:
416
383
  max_allowed_item_size = np.dtype('<U256').itemsize
417
384
  for state, preprocess_response in preprocess_result.items():
418
- sample_ids_array = np.array(preprocess_response.flat_sample_ids)
385
+ sample_ids_array = np.array(preprocess_response.sample_ids)
419
386
  if sample_ids_array.dtype.itemsize > max_allowed_item_size:
420
387
  raise Exception(f"Sample id are too long. Max allowed length is 256 charecters.")
421
388
 
@@ -505,31 +472,49 @@ class LeapLoader(LeapLoaderBase):
505
472
  try:
506
473
  preprocess_result = self._preprocess_result()
507
474
  input_shapes: Dict[str, List[int]] = {}
475
+ expected_keys: Optional[FrozenSet[str]] = None
476
+ expected_keys_state = ''
508
477
  for state, preprocess_response in preprocess_result.items():
509
478
  if preprocess_response.sample_ids_to_instance_mappings:
510
479
  raise Exception('Element instances are not supported together with '
511
480
  'tensorleap_autoregressive_step.')
512
481
  sample_id = preprocess_response.sample_ids[0]
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):
482
+ first_result = handler.function(sample_id, None, None, None, preprocess_response)
483
+ second_result = handler.function(sample_id, None, None, None, preprocess_response)
484
+ if not isinstance(first_result, tuple) or len(first_result) != 2:
485
+ raise Exception('The autoregressive step hook must return a (next_inputs, '
486
+ f'state) tuple, got {type(first_result)} '
487
+ f'(state: {state.name}).')
488
+ first_inputs, first_state = first_result
489
+ if not isinstance(first_inputs, dict) or not first_inputs:
516
490
  raise Exception('The autoregressive step hook must return a dict of initial model '
517
- f'inputs on its first call, got {type(first_result)} '
491
+ f'inputs on its first call, got {type(first_inputs)} '
518
492
  f'(state: {state.name}).')
519
- if not isinstance(second_result, dict) or \
520
- set(second_result.keys()) != set(first_result.keys()):
493
+ validate_autoregressive_state_types(first_state)
494
+ if not isinstance(second_result, tuple) or len(second_result) != 2 or \
495
+ not isinstance(second_result[0], dict) or \
496
+ set(second_result[0].keys()) != set(first_inputs.keys()) or \
497
+ not autoregressive_nests_equal(first_state, second_result[1]):
521
498
  raise Exception('The autoregressive step hook is not deterministic — two calls '
522
499
  'with identical arguments returned different results '
523
500
  f'(state: {state.name}). Seed any stochasticity from '
524
501
  'sample_id: the engine replays steps after crash recovery '
525
502
  'and relies on identical results.')
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():
503
+ if expected_keys is None:
504
+ expected_keys = frozenset(first_inputs)
505
+ expected_keys_state = state.name
506
+ elif frozenset(first_inputs) != expected_keys:
507
+ raise Exception(f'The autoregressive step hook returned different model input '
508
+ f'keys across states: {sorted(expected_keys)} '
509
+ f'({expected_keys_state}) vs {sorted(first_inputs)} '
510
+ f'({state.name}). The key set must be identical for every '
511
+ f'state.')
512
+ for key, value in first_inputs.items():
513
+ if key.startswith('_'):
514
+ raise Exception(f'The autoregressive step hook returned model input key '
515
+ f'"{key}" — underscore passthrough keys were replaced by '
516
+ f'the explicit state channel; move passthrough values '
517
+ f'into the returned state. State: {state.name}.')
533
518
  if not isinstance(value, np.ndarray):
534
519
  raise Exception(f'The autoregressive step hook returned a non-numpy value for '
535
520
  f'model input "{key}" ({type(value)}). State: {state.name}.')
@@ -538,7 +523,7 @@ class LeapLoader(LeapLoaderBase):
538
523
  f'model input "{key}" across states: {input_shapes[key]} vs '
539
524
  f'{list(value.shape)}. Shapes must be fixed.')
540
525
  input_shapes[key] = list(value.shape)
541
- if not np.array_equal(value, second_result[key]):
526
+ if not autoregressive_nests_equal(value, second_result[0][key]):
542
527
  raise Exception('The autoregressive step hook is not deterministic — two calls '
543
528
  'with identical arguments returned different outputs for model '
544
529
  f'input "{key}". Seed any stochasticity from sample_id: the '
@@ -552,19 +537,39 @@ class LeapLoader(LeapLoaderBase):
552
537
  test_result.is_passed = False
553
538
  return test_result
554
539
 
540
+ @staticmethod
541
+ def _validate_autoregressive_step_inputs(step_inputs: Dict[str, Any],
542
+ sample_id: Union[int, str]) -> None:
543
+ # The parse-time check only covers the first sample of each state; the engine runtime
544
+ # paths must enforce the same key/value contract for every sample they touch.
545
+ for key, value in step_inputs.items():
546
+ if key.startswith('_'):
547
+ raise Exception(f'The autoregressive step hook returned model input key "{key}" '
548
+ f'for sample {sample_id} — underscore passthrough keys were '
549
+ 'replaced by the explicit state channel; move passthrough values '
550
+ 'into the returned state.')
551
+ if not isinstance(value, np.ndarray):
552
+ raise Exception('The autoregressive step hook returned a non-numpy value for '
553
+ f'model input "{key}" ({type(value).__name__}) for sample '
554
+ f'{sample_id}.')
555
+
555
556
  def _ensure_autoregressive_input_shapes(self) -> Dict[str, List[int]]:
556
557
  handler = global_leap_binder.setup_container.autoregressive_step
557
558
  assert handler is not None
558
559
  if handler.input_shapes is None:
559
560
  preprocess_result = self._preprocess_result()
560
561
  first_state_response = next(iter(preprocess_result.values()))
561
- first_result = handler.function(first_state_response.sample_ids[0], None, None,
562
+ first_sample_id = first_state_response.sample_ids[0]
563
+ first_result = handler.function(first_sample_id, None, None, None,
562
564
  first_state_response)
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)}
565
+ if not isinstance(first_result, tuple) or len(first_result) != 2 or \
566
+ not isinstance(first_result[0], dict):
567
+ raise Exception('The autoregressive step hook must return a (next_inputs, state) '
568
+ 'tuple with a dict of initial model inputs on its first call, '
569
+ f'got {type(first_result)}.')
570
+ self._validate_autoregressive_step_inputs(first_result[0], first_sample_id)
571
+ handler.input_shapes = {key: list(value.shape)
572
+ for key, value in first_result[0].items()}
568
573
  return handler.input_shapes
569
574
 
570
575
  def run_simulation(self, sim_name, params=None, n_samples=1, seed=0,
@@ -833,6 +838,21 @@ class LeapLoader(LeapLoaderBase):
833
838
  metric_inst = MetricInstance(metric.metric_handler_data.name, metric.metric_handler_data.arg_names)
834
839
  metrics.append(metric_inst)
835
840
 
841
+ # Autoregressive decorators are reported alongside the regular ones so the platform sees
842
+ # their names; the engine tells them apart by name via get_autoregressive_decorator_names.
843
+ for autoregressive_metric in setup.autoregressive_metrics:
844
+ metrics.append(MetricInstance(autoregressive_metric.metric_handler_data.name,
845
+ autoregressive_metric.metric_handler_data.arg_names))
846
+ for autoregressive_loss in setup.autoregressive_losses:
847
+ custom_losses.append(CustomLossInstance(
848
+ autoregressive_loss.custom_loss_handler_data.name,
849
+ autoregressive_loss.custom_loss_handler_data.arg_names))
850
+ for autoregressive_visualizer in setup.autoregressive_visualizers:
851
+ visualizers.append(VisualizerInstance(
852
+ autoregressive_visualizer.visualizer_handler_data.name,
853
+ autoregressive_visualizer.visualizer_handler_data.type,
854
+ autoregressive_visualizer.visualizer_handler_data.arg_names))
855
+
836
856
  simulations = []
837
857
  for sim in setup.simulations:
838
858
  sim_config_serialized = {
@@ -878,58 +898,6 @@ class LeapLoader(LeapLoaderBase):
878
898
 
879
899
  return sample_ids
880
900
 
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
-
933
901
  def _get_dataset_handlers(self, handlers: Iterable[DatasetBaseHandler],
934
902
  state: DataStateEnum, sample_id: Union[int, str]) -> Dict[str, npt.NDArray[np.float32]]:
935
903
  result_agg = {}
@@ -941,71 +909,12 @@ class LeapLoader(LeapLoaderBase):
941
909
  sample_id = original_local_id
942
910
  else:
943
911
  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
951
912
  for handler in handlers:
952
913
  handler_result = handler.function(sample_id, preprocess_state)
953
914
  handler_name = handler.name
954
915
  result_agg[handler_name] = handler_result
955
916
  return result_agg
956
917
 
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
-
1009
918
  def _get_inputs(self, state: DataStateEnum, sample_id: Union[int, str]) -> Dict[str, npt.NDArray[np.float32]]:
1010
919
  inputs = self._get_dataset_handlers(global_leap_binder.setup_container.inputs, state, sample_id)
1011
920
  autoregressive_handler = global_leap_binder.setup_container.autoregressive_step
@@ -1014,13 +923,16 @@ class LeapLoader(LeapLoaderBase):
1014
923
  # the sample's (step-0) model inputs, so every sample-fetching flow (graph validation,
1015
924
  # analysis, import checks) sees a fully-populated inputs dict.
1016
925
  preprocess_response = self._preprocess_result()[state]
1017
- step_zero_inputs = autoregressive_handler.function(sample_id, None, None, preprocess_response)
1018
- if not isinstance(step_zero_inputs, dict):
926
+ step_zero_result = autoregressive_handler.function(sample_id, None, None, None,
927
+ preprocess_response)
928
+ if not isinstance(step_zero_result, tuple) or len(step_zero_result) != 2 or \
929
+ not isinstance(step_zero_result[0], dict):
1019
930
  raise Exception(
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('_')})
931
+ 'The autoregressive step hook must return a (next_inputs, state) tuple with a '
932
+ f'dict of initial model inputs on its first call, got {type(step_zero_result)} '
933
+ f'for sample {sample_id}.')
934
+ self._validate_autoregressive_step_inputs(step_zero_result[0], sample_id)
935
+ inputs.update(step_zero_result[0])
1024
936
  return inputs
1025
937
 
1026
938
  def _get_instances_masks(self, state: DataStateEnum, sample_id: Union[int, str], instance_id: int) -> Optional[Dict[str, ElementInstance]]:
@@ -1106,18 +1018,12 @@ class LeapLoader(LeapLoaderBase):
1106
1018
  sample_id = original_local_id
1107
1019
  else:
1108
1020
  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)
1112
1021
  for handler in global_leap_binder.setup_container.metadata:
1113
1022
  if requested_metadata_names:
1114
1023
  if not is_metadata_name_starts_with_handler_name(handler):
1115
1024
  continue
1116
1025
 
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)
1026
+ handler_result = handler.function(sample_id, preprocess_state)
1121
1027
  if isinstance(handler_result, dict):
1122
1028
  for single_metadata_name, single_metadata_result in handler_result.items():
1123
1029
  handler_name = f'{handler.name}_{single_metadata_name}'
@@ -1136,66 +1042,6 @@ class LeapLoader(LeapLoaderBase):
1136
1042
 
1137
1043
  return result_agg, is_none
1138
1044
 
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
-
1199
1045
  @lru_cache()
1200
1046
  def get_sample_id_type(self) -> Type:
1201
1047
  preprocess_results = list(self._preprocess_result().values())
@@ -1206,10 +1052,29 @@ class LeapLoader(LeapLoaderBase):
1206
1052
 
1207
1053
  return id_type
1208
1054
 
1055
+ @staticmethod
1056
+ def _get_custom_latent_spaces(
1057
+ sample_id: Union[int, str],
1058
+ preprocess: "PreprocessResponse") -> Optional[Dict[str, npt.NDArray[np.float32]]]:
1059
+ handlers = global_leap_binder.setup_container.custom_latent_spaces
1060
+ if not handlers:
1061
+ return None
1062
+ return {name: handler.function(sample_id, preprocess) for name, handler in handlers.items()}
1063
+
1209
1064
  @lru_cache()
1210
1065
  def has_custom_latent_space_decorator(self) -> bool:
1211
1066
  self.exec_script()
1212
- return global_leap_binder.setup_container.custom_latent_space is not None
1067
+ return len(global_leap_binder.setup_container.custom_latent_spaces) > 0
1068
+
1069
+ @lru_cache()
1070
+ def get_custom_latent_space_names(self) -> Tuple[str, ...]:
1071
+ """Ordered names of all registered custom latent spaces.
1072
+
1073
+ Registration order is the canonical index mapping consumed by the engine
1074
+ (name i -> user_custom_i). Returns a tuple so the lru_cache value is hashable.
1075
+ """
1076
+ self.exec_script()
1077
+ return tuple(global_leap_binder.setup_container.custom_latent_spaces.keys())
1213
1078
 
1214
1079
  @lru_cache()
1215
1080
  def has_autoregressive_step(self) -> bool:
@@ -1227,12 +1092,15 @@ class LeapLoader(LeapLoaderBase):
1227
1092
  def run_autoregressive_step(self, sample_id: Union[int, str],
1228
1093
  prev_inputs: Optional[Dict[str, npt.NDArray[np.float32]]],
1229
1094
  prev_outputs: Optional[Dict[str, npt.NDArray[np.float32]]],
1230
- state: DataStateEnum) -> Optional[Dict[str, npt.NDArray[np.float32]]]:
1095
+ chain_state: Any,
1096
+ state: DataStateEnum
1097
+ ) -> Tuple[Optional[Dict[str, npt.NDArray[np.float32]]], Any]:
1231
1098
  """Execute the user's autoregressive step hook against the in-process preprocess result.
1232
1099
 
1233
1100
  This is the engine rollout's per-step entry point (invoked on the generic pod). The first
1234
- call per chain passes prev_inputs=None, prev_outputs=None and returns the initial model
1235
- inputs; a None return ends the chain.
1101
+ call per chain passes prev_inputs=None, prev_outputs=None, chain_state=None and returns
1102
+ the initial model inputs plus the initial state; a None next_inputs ends the chain — the
1103
+ terminating call still returns the final state.
1236
1104
  """
1237
1105
  self.exec_script()
1238
1106
  handler = global_leap_binder.setup_container.autoregressive_step
@@ -1240,28 +1108,119 @@ class LeapLoader(LeapLoaderBase):
1240
1108
  raise Exception('run_autoregressive_step was called but the integration has no '
1241
1109
  'tensorleap_autoregressive_step hook.')
1242
1110
  preprocess_response = self._preprocess_result()[state]
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):
1111
+ result = handler.function(sample_id, prev_inputs, prev_outputs, chain_state,
1112
+ preprocess_response)
1113
+ if not isinstance(result, tuple) or len(result) != 2:
1114
+ raise Exception('The autoregressive step hook must return a (next_inputs, state) '
1115
+ f'tuple, got {type(result)} for sample {sample_id}.')
1116
+ next_inputs, new_chain_state = result
1117
+ validate_autoregressive_state_types(new_chain_state)
1118
+ if next_inputs is None:
1119
+ if prev_inputs is None:
1120
+ raise Exception('The autoregressive step hook returned None for next_inputs on '
1121
+ f'the first call of the chain (sample {sample_id}) — the first '
1122
+ 'call must return the initial model inputs; None would produce '
1123
+ 'an empty chain with no forward pass.')
1124
+ return None, new_chain_state
1125
+ if not isinstance(next_inputs, dict):
1247
1126
  raise Exception('The autoregressive step hook must return a dict of model inputs or '
1248
- f'None to end the chain, got {type(result)} for sample {sample_id}.')
1127
+ f'None to end the chain, got {type(next_inputs)} for sample '
1128
+ f'{sample_id}.')
1249
1129
  # Parse-time validation only exercises step 0; enforce the fixed key-set/shape contract
1250
1130
  # on every step here so drift fails with an actionable message instead of a cryptic
1251
1131
  # engine error on a compiled graph.
1252
1132
  expected_shapes = self._ensure_autoregressive_input_shapes()
1253
- model_inputs = {key: value for key, value in result.items() if not key.startswith('_')}
1254
- if set(model_inputs) != set(expected_shapes):
1133
+ if set(next_inputs) != set(expected_shapes):
1255
1134
  raise Exception('The autoregressive step hook must return the same model input keys '
1256
1135
  f'on every call. Expected {sorted(expected_shapes)}, got '
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]:
1136
+ f'{sorted(next_inputs)} for sample {sample_id}.')
1137
+ for key, value in next_inputs.items():
1138
+ if not isinstance(value, np.ndarray):
1139
+ raise Exception('The autoregressive step hook must return numpy arrays for every '
1140
+ f'model input on every call — input "{key}" is a '
1141
+ f'{type(value).__name__} for sample {sample_id} (forgot .numpy() '
1142
+ 'on a fed-back model output?).')
1143
+ if list(value.shape) != expected_shapes[key]:
1260
1144
  raise Exception('The autoregressive step hook must return fixed tensor shapes on '
1261
1145
  f'every call (use fixed-length padding + mask for growing '
1262
1146
  f'sequences). Input "{key}" was {expected_shapes[key]} on the '
1263
1147
  f'first call and {list(value.shape)} now (sample {sample_id}).')
1264
- return result
1148
+ return next_inputs, new_chain_state
1149
+
1150
+ @lru_cache()
1151
+ def autoregressive_visualizer_by_name(self) -> Dict[str, VisualizerHandlerData]:
1152
+ self.exec_script()
1153
+ return {handler.visualizer_handler_data.name: handler.visualizer_handler_data
1154
+ for handler in global_leap_binder.setup_container.autoregressive_visualizers}
1155
+
1156
+ @lru_cache()
1157
+ def get_autoregressive_decorator_names(self) -> Dict[str, List[str]]:
1158
+ self.exec_script()
1159
+ setup = global_leap_binder.setup_container
1160
+ return {
1161
+ 'metrics': [handler.metric_handler_data.name
1162
+ for handler in setup.autoregressive_metrics],
1163
+ 'losses': [handler.custom_loss_handler_data.name
1164
+ for handler in setup.autoregressive_losses],
1165
+ 'visualizers': [handler.visualizer_handler_data.name
1166
+ for handler in setup.autoregressive_visualizers],
1167
+ }
1168
+
1169
+ @staticmethod
1170
+ def _autoregressive_callable_kwargs(function: Callable[..., Any],
1171
+ inputs: Dict[str, npt.NDArray[np.float32]],
1172
+ outputs: Dict[str, npt.NDArray[np.float32]],
1173
+ chain_state: Any,
1174
+ wired_args: Dict[str, npt.NDArray[np.float32]]
1175
+ ) -> Dict[str, Any]:
1176
+ implicit_values = {'inputs': inputs, 'outputs': outputs, 'state': chain_state}
1177
+ kwargs: Dict[str, Any] = {}
1178
+ for arg_name in inspect.getfullargspec(function)[0]:
1179
+ if arg_name in implicit_values:
1180
+ kwargs[arg_name] = implicit_values[arg_name]
1181
+ elif arg_name in wired_args:
1182
+ kwargs[arg_name] = wired_args[arg_name]
1183
+ else:
1184
+ raise Exception(f'Autoregressive callable is missing wired argument '
1185
+ f'"{arg_name}" — the platform mapping supplies only '
1186
+ f'{sorted(wired_args)}.')
1187
+ return kwargs
1188
+
1189
+ def run_autoregressive_metric(self, name: str,
1190
+ inputs: Dict[str, npt.NDArray[np.float32]],
1191
+ outputs: Dict[str, npt.NDArray[np.float32]],
1192
+ chain_state: Any,
1193
+ wired_args: Dict[str, npt.NDArray[np.float32]]) -> Any:
1194
+ self.exec_script()
1195
+ for handler in global_leap_binder.setup_container.autoregressive_metrics:
1196
+ if handler.metric_handler_data.name == name:
1197
+ return handler.function(**self._autoregressive_callable_kwargs(
1198
+ handler.function, inputs, outputs, chain_state, wired_args))
1199
+ raise Exception(f'No autoregressive metric named "{name}" is registered.')
1200
+
1201
+ def run_autoregressive_loss(self, name: str,
1202
+ inputs: Dict[str, npt.NDArray[np.float32]],
1203
+ outputs: Dict[str, npt.NDArray[np.float32]],
1204
+ chain_state: Any,
1205
+ wired_args: Dict[str, npt.NDArray[np.float32]]) -> Any:
1206
+ self.exec_script()
1207
+ for handler in global_leap_binder.setup_container.autoregressive_losses:
1208
+ if handler.custom_loss_handler_data.name == name:
1209
+ return handler.function(**self._autoregressive_callable_kwargs(
1210
+ handler.function, inputs, outputs, chain_state, wired_args))
1211
+ raise Exception(f'No autoregressive loss named "{name}" is registered.')
1212
+
1213
+ def run_autoregressive_visualizer(self, name: str,
1214
+ inputs: Dict[str, npt.NDArray[np.float32]],
1215
+ outputs: Dict[str, npt.NDArray[np.float32]],
1216
+ chain_state: Any,
1217
+ wired_args: Dict[str, npt.NDArray[np.float32]]) -> Any:
1218
+ self.exec_script()
1219
+ for handler in global_leap_binder.setup_container.autoregressive_visualizers:
1220
+ if handler.visualizer_handler_data.name == name:
1221
+ return handler.function(**self._autoregressive_callable_kwargs(
1222
+ handler.function, inputs, outputs, chain_state, wired_args))
1223
+ raise Exception(f'No autoregressive visualizer named "{name}" is registered.')
1265
1224
 
1266
1225
  def get_prediction_names_in_order(self) -> List[str]:
1267
1226
  """Model output names by declared prediction-type order, used to key prev_outputs for the