code-loader 1.0.191.dev1__py3-none-any.whl → 1.0.193.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
@@ -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:
@@ -63,6 +64,9 @@ class LeapLoader(LeapLoaderBase):
63
64
  os.environ[mapping_runtime_mode_env_var_mame] = 'TRUE'
64
65
  self.evaluate_module()
65
66
  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()
66
70
  global_leap_binder.integration_test_func(None, PreprocessResponse(state=DataStateType.training, length=0))
67
71
  except TypeError as e:
68
72
  import traceback
@@ -246,20 +250,16 @@ class LeapLoader(LeapLoaderBase):
246
250
  )
247
251
 
248
252
  preprocess_result = self._preprocess_result()
249
- 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:
250
254
  self._preprocess_result(update_unlabeled_preprocess=True)
251
255
 
252
256
  metadata, metadata_is_none = self.get_metadata(state, sample_id)
253
257
 
254
258
  custom_latent_space = None
255
259
  if global_leap_binder.setup_container.custom_latent_space is not None:
256
- latent_fn = global_leap_binder.setup_container.custom_latent_space.function
257
- preprocess_state = preprocess_result[state]
258
- if preprocess_state.is_grouped:
259
- group_ids, pos = self._locate_group(preprocess_state, sample_id)
260
- custom_latent_space = self._to_grouped_array(latent_fn(group_ids, preprocess_state))[pos]
261
- else:
262
- custom_latent_space = latent_fn(sample_id, preprocess_state)
260
+ custom_latent_space = global_leap_binder.setup_container.custom_latent_space.function(sample_id,
261
+ preprocess_result[
262
+ state])
263
263
  instance_mask = self._get_instances_masks(state, sample_id, instance_id)
264
264
  sample = DatasetSample(inputs=self._get_inputs(state, sample_id),
265
265
  gt=None if state == DataStateEnum.unlabeled else self._get_gt(state, sample_id),
@@ -326,12 +326,16 @@ class LeapLoader(LeapLoaderBase):
326
326
 
327
327
  integration_test_test_payload = self._check_integration_test_exists()
328
328
  test_payloads.append(integration_test_test_payload)
329
+ global_leap_binder.validate_autoregressive_setup()
329
330
  preprocess_test_payload = self._check_preprocess()
330
331
  test_payloads.append(preprocess_test_payload)
331
332
  handlers_test_payloads = self._check_handlers()
332
333
  test_payloads.extend(handlers_test_payloads)
333
334
  simulation_test_payloads = self._check_simulations()
334
335
  test_payloads.extend(simulation_test_payloads)
336
+ autoregressive_test_payload = self._check_autoregressive_step()
337
+ if autoregressive_test_payload is not None:
338
+ test_payloads.append(autoregressive_test_payload)
335
339
  is_valid = all([payload.is_passed for payload in test_payloads])
336
340
  setup_response = self.get_dataset_setup_response(handlers_test_payloads)
337
341
 
@@ -386,7 +390,7 @@ class LeapLoader(LeapLoaderBase):
386
390
  if self.get_sample_id_type() is str:
387
391
  max_allowed_item_size = np.dtype('<U256').itemsize
388
392
  for state, preprocess_response in preprocess_result.items():
389
- sample_ids_array = np.array(preprocess_response.flat_sample_ids)
393
+ sample_ids_array = np.array(preprocess_response.sample_ids)
390
394
  if sample_ids_array.dtype.itemsize > max_allowed_item_size:
391
395
  raise Exception(f"Sample id are too long. Max allowed length is 256 charecters.")
392
396
 
@@ -468,6 +472,85 @@ class LeapLoader(LeapLoaderBase):
468
472
  result_payloads.append(test_result)
469
473
  return result_payloads
470
474
 
475
+ def _check_autoregressive_step(self) -> Optional[DatasetTestResultPayload]:
476
+ handler = global_leap_binder.setup_container.autoregressive_step
477
+ if handler is None:
478
+ return None
479
+ test_result = DatasetTestResultPayload(handler.name)
480
+ try:
481
+ preprocess_result = self._preprocess_result()
482
+ input_shapes: Dict[str, List[int]] = {}
483
+ for state, preprocess_response in preprocess_result.items():
484
+ if preprocess_response.sample_ids_to_instance_mappings:
485
+ raise Exception('Element instances are not supported together with '
486
+ 'tensorleap_autoregressive_step.')
487
+ sample_id = preprocess_response.sample_ids[0]
488
+ first_result = handler.function(sample_id, None, None, None, preprocess_response)
489
+ second_result = handler.function(sample_id, None, None, None, preprocess_response)
490
+ if not isinstance(first_result, tuple) or len(first_result) != 2:
491
+ raise Exception('The autoregressive step hook must return a (next_inputs, '
492
+ f'state) tuple, got {type(first_result)} '
493
+ f'(state: {state.name}).')
494
+ first_inputs, first_state = first_result
495
+ if not isinstance(first_inputs, dict) or not first_inputs:
496
+ raise Exception('The autoregressive step hook must return a dict of initial model '
497
+ f'inputs on its first call, got {type(first_inputs)} '
498
+ f'(state: {state.name}).')
499
+ validate_autoregressive_state_types(first_state)
500
+ if not isinstance(second_result, tuple) or len(second_result) != 2 or \
501
+ not isinstance(second_result[0], dict) or \
502
+ set(second_result[0].keys()) != set(first_inputs.keys()) or \
503
+ not autoregressive_nests_equal(first_state, second_result[1]):
504
+ raise Exception('The autoregressive step hook is not deterministic — two calls '
505
+ 'with identical arguments returned different results '
506
+ f'(state: {state.name}). Seed any stochasticity from '
507
+ 'sample_id: the engine replays steps after crash recovery '
508
+ 'and relies on identical results.')
509
+ for key, value in first_inputs.items():
510
+ if key.startswith('_'):
511
+ raise Exception(f'The autoregressive step hook returned model input key '
512
+ f'"{key}" — underscore passthrough keys were replaced by '
513
+ f'the explicit state channel; move passthrough values '
514
+ f'into the returned state. State: {state.name}.')
515
+ if not isinstance(value, np.ndarray):
516
+ raise Exception(f'The autoregressive step hook returned a non-numpy value for '
517
+ f'model input "{key}" ({type(value)}). State: {state.name}.')
518
+ if key in input_shapes and input_shapes[key] != list(value.shape):
519
+ raise Exception(f'The autoregressive step hook returned different shapes for '
520
+ f'model input "{key}" across states: {input_shapes[key]} vs '
521
+ f'{list(value.shape)}. Shapes must be fixed.')
522
+ input_shapes[key] = list(value.shape)
523
+ if not np.array_equal(value, second_result[0][key]):
524
+ raise Exception('The autoregressive step hook is not deterministic — two calls '
525
+ 'with identical arguments returned different outputs for model '
526
+ f'input "{key}". Seed any stochasticity from sample_id: the '
527
+ 'engine replays steps after crash recovery and relies on '
528
+ 'identical results.')
529
+ handler.input_shapes = input_shapes
530
+ except Exception as e:
531
+ line_number, file_name, stacktrace = get_root_exception_file_and_line_number()
532
+ test_result.display[TestingSectionEnum.Errors.name] = \
533
+ f"{repr(e)} in file {file_name}, line_number: {line_number}\nStacktrace:\n{stacktrace}"
534
+ test_result.is_passed = False
535
+ return test_result
536
+
537
+ def _ensure_autoregressive_input_shapes(self) -> Dict[str, List[int]]:
538
+ handler = global_leap_binder.setup_container.autoregressive_step
539
+ assert handler is not None
540
+ if handler.input_shapes is None:
541
+ preprocess_result = self._preprocess_result()
542
+ first_state_response = next(iter(preprocess_result.values()))
543
+ first_result = handler.function(first_state_response.sample_ids[0], None, None, None,
544
+ first_state_response)
545
+ if not isinstance(first_result, tuple) or len(first_result) != 2 or \
546
+ not isinstance(first_result[0], dict):
547
+ raise Exception('The autoregressive step hook must return a (next_inputs, state) '
548
+ 'tuple with a dict of initial model inputs on its first call, '
549
+ f'got {type(first_result)}.')
550
+ handler.input_shapes = {key: list(value.shape) for key, value in first_result[0].items()
551
+ if isinstance(value, np.ndarray)}
552
+ return handler.input_shapes
553
+
471
554
  def run_simulation(self, sim_name, params=None, n_samples=1, seed=0,
472
555
  sample_ids=None, extend_preprocess=True):
473
556
  # type: (str, Optional[Dict[str, Any]], int, int, Optional[List[str]], bool) -> Dict[str, Any]
@@ -658,6 +741,14 @@ class LeapLoader(LeapLoaderBase):
658
741
  raise Exception(f"cant calculate shape for input, input name:{inp.name}")
659
742
  inputs.append(DatasetInputInstance(name=inp.name, shape=inp.shape, channel_dim=inp.channel_dim))
660
743
 
744
+ if setup.autoregressive_step is not None:
745
+ # The hook is the sole input source; report its step-0 inputs as the dataset inputs so
746
+ # graph building, input-coverage validation and type dictionaries see real specs.
747
+ # channel_dim=-1 is intentional for AR inputs — there is no InputHandler to carry a
748
+ # user-declared value; expose it on the decorator if a use case ever needs otherwise.
749
+ for input_name, input_shape in self._ensure_autoregressive_input_shapes().items():
750
+ inputs.append(DatasetInputInstance(name=input_name, shape=input_shape, channel_dim=-1))
751
+
661
752
  ground_truths = []
662
753
  for gt in setup.ground_truths:
663
754
  if gt.shape is None:
@@ -702,6 +793,21 @@ class LeapLoader(LeapLoaderBase):
702
793
  metric_inst = MetricInstance(metric.metric_handler_data.name, metric.metric_handler_data.arg_names)
703
794
  metrics.append(metric_inst)
704
795
 
796
+ # Autoregressive decorators are reported alongside the regular ones so the platform sees
797
+ # their names; the engine tells them apart by name via get_autoregressive_decorator_names.
798
+ for autoregressive_metric in setup.autoregressive_metrics:
799
+ metrics.append(MetricInstance(autoregressive_metric.metric_handler_data.name,
800
+ autoregressive_metric.metric_handler_data.arg_names))
801
+ for autoregressive_loss in setup.autoregressive_losses:
802
+ custom_losses.append(CustomLossInstance(
803
+ autoregressive_loss.custom_loss_handler_data.name,
804
+ autoregressive_loss.custom_loss_handler_data.arg_names))
805
+ for autoregressive_visualizer in setup.autoregressive_visualizers:
806
+ visualizers.append(VisualizerInstance(
807
+ autoregressive_visualizer.visualizer_handler_data.name,
808
+ autoregressive_visualizer.visualizer_handler_data.type,
809
+ autoregressive_visualizer.visualizer_handler_data.arg_names))
810
+
705
811
  simulations = []
706
812
  for sim in setup.simulations:
707
813
  sim_config_serialized = {
@@ -747,57 +853,6 @@ class LeapLoader(LeapLoaderBase):
747
853
 
748
854
  return sample_ids
749
855
 
750
- @staticmethod
751
- def _grouped_dict_keys(rows: List[Dict[str, Any]], handler_name: str) -> List[str]:
752
- """Keys for a group of dict-metadata rows, requiring every row to share the same keys.
753
- A group must expose a uniform metadata schema; otherwise the per-key lists would silently
754
- drop or KeyError on rows that differ from row 0."""
755
- keys = list(rows[0].keys())
756
- key_set = set(keys)
757
- for pos, row in enumerate(rows):
758
- if set(row.keys()) != key_set:
759
- raise ValueError(
760
- f"Metadata handler {handler_name!r} returned inconsistent dict keys across the "
761
- f"group: row 0 has {sorted(key_set)} but row {pos} has {sorted(row.keys())}. "
762
- f"All rows in a group must share the same metadata keys.")
763
- return keys
764
-
765
- @staticmethod
766
- def _to_grouped_array(raw: Any) -> Union[npt.NDArray[np.float32], List[npt.NDArray[np.float32]]]:
767
- """Normalize a grouped encoder result. A list of uniformly-shaped per-sample arrays is
768
- stacked into a single (B, *) array; a list whose samples differ in shape is kept as a
769
- list of per-sample arrays (variable-dim groups); an already-grouped array is returned
770
- as-is."""
771
- if isinstance(raw, list):
772
- shapes = {np.asarray(sample).shape for sample in raw}
773
- if len(shapes) == 1:
774
- return np.stack(raw, axis=0)
775
- return raw
776
- return raw
777
-
778
- def _locate_group(self, preprocess_state: PreprocessResponse,
779
- sample_id: Union[int, str]) -> Tuple[List[Union[int, str]], int]:
780
- """Map a sample id to (its group, its position in that group). Cached per
781
- PreprocessResponse so grouped single-sample fetches don't rescan the groups."""
782
- cache = getattr(self, '_group_pos_cache', None)
783
- if cache is None:
784
- cache = {}
785
- self._group_pos_cache = cache
786
- mapping = cache.get(id(preprocess_state))
787
- if mapping is None:
788
- mapping = {}
789
- for group in preprocess_state.groups:
790
- for pos, sid in enumerate(group):
791
- if sid in mapping:
792
- raise ValueError(
793
- f"Duplicate sample id {sid!r} across groups in the preprocess response; "
794
- f"sample ids must be unique within and across groups.")
795
- mapping[sid] = (group, pos)
796
- cache[id(preprocess_state)] = mapping
797
- if sample_id not in mapping:
798
- raise KeyError(f"Sample id {sample_id!r} is not present in any group of this preprocess response.")
799
- return mapping[sample_id]
800
-
801
856
  def _get_dataset_handlers(self, handlers: Iterable[DatasetBaseHandler],
802
857
  state: DataStateEnum, sample_id: Union[int, str]) -> Dict[str, npt.NDArray[np.float32]]:
803
858
  result_agg = {}
@@ -809,72 +864,30 @@ class LeapLoader(LeapLoaderBase):
809
864
  sample_id = original_local_id
810
865
  else:
811
866
  preprocess_state = preprocess_result[state]
812
- if preprocess_state.is_grouped:
813
- # Grouped dataset: call the encoder once with the whole group, index out the sample.
814
- group_ids, pos = self._locate_group(preprocess_state, sample_id)
815
- for handler in handlers:
816
- grouped = self._to_grouped_array(handler.function(group_ids, preprocess_state))
817
- result_agg[handler.name] = grouped[pos]
818
- return result_agg
819
867
  for handler in handlers:
820
868
  handler_result = handler.function(sample_id, preprocess_state)
821
869
  handler_name = handler.name
822
870
  result_agg[handler_name] = handler_result
823
871
  return result_agg
824
872
 
825
- def get_samples(self, state: DataStateEnum, group_ids: List[Union[int, str]]) -> DatasetSample:
826
- """Group-aware fetch: hand the whole group to each encoder in a single call and return a
827
- grouped DatasetSample (inputs/gt as (B, *), metadata as per-key lists of length B, index =
828
- the group). This is the path a group-aware engine calls to read a file once per group.
829
- Row order matches group_ids exactly."""
830
- self.exec_script()
831
- preprocess_state = self._preprocess_result()[state]
832
- if preprocess_state.is_grouped:
833
- # All requested ids must belong to a single group (one file). A cross-group request
834
- # would force the encoder to load multiple files, defeating the one-load-per-group
835
- # contract; partitioning a scattered request by group is the engine's responsibility.
836
- distinct_groups = {id(self._locate_group(preprocess_state, sid)[0]) for sid in group_ids}
837
- assert len(distinct_groups) == 1, (
838
- f"get_samples received sample ids spanning {len(distinct_groups)} groups; a request "
839
- f"must be confined to a single group. The engine partitions cross-group requests and "
840
- f"issues one get_samples call per group.")
841
- inputs = {handler.name: self._to_grouped_array(handler.function(group_ids, preprocess_state))
842
- for handler in global_leap_binder.setup_container.inputs}
843
- gt = None
844
- if state != DataStateEnum.unlabeled:
845
- gt = {handler.name: self._to_grouped_array(handler.function(group_ids, preprocess_state))
846
- for handler in global_leap_binder.setup_container.ground_truths}
847
-
848
- metadata: Dict[str, Any] = {}
849
- metadata_is_none: Dict[str, Any] = {}
850
- for handler in global_leap_binder.setup_container.metadata:
851
- rows = handler.function(group_ids, preprocess_state) # list of B scalars/dicts
852
- if rows and isinstance(rows[0], dict):
853
- for key in self._grouped_dict_keys(rows, handler.name):
854
- name = "{}_{}".format(handler.name, key)
855
- converted = [self._convert_metadata_to_correct_type(name, row[key]) for row in rows]
856
- metadata[name] = [v for v, _ in converted]
857
- metadata_is_none[name] = [is_none for _, is_none in converted]
858
- else:
859
- converted = [self._convert_metadata_to_correct_type(handler.name, row) for row in rows]
860
- metadata[handler.name] = [v for v, _ in converted]
861
- metadata_is_none[handler.name] = [is_none for _, is_none in converted]
862
-
863
- # custom_latent_space is group-aware like the input/GT encoders: hand it the whole group in a
864
- # single call so a file-backed latent fn loads the file once, and normalize to (B, d). This
865
- # keeps the mandatory group path non-lossy. instance_masks stay None here: they are a
866
- # per-(sample, instance) concern that needs an instance_id the group fetch does not carry.
867
- custom_latent_space = None
868
- if global_leap_binder.setup_container.custom_latent_space is not None:
869
- latent_fn = global_leap_binder.setup_container.custom_latent_space.function
870
- custom_latent_space = self._to_grouped_array(latent_fn(group_ids, preprocess_state))
871
-
872
- return DatasetSample(inputs=inputs, gt=gt, metadata=metadata, metadata_is_none=metadata_is_none,
873
- index=list(group_ids), state=state, custom_latent_space=custom_latent_space,
874
- instance_masks=None)
875
-
876
873
  def _get_inputs(self, state: DataStateEnum, sample_id: Union[int, str]) -> Dict[str, npt.NDArray[np.float32]]:
877
- return self._get_dataset_handlers(global_leap_binder.setup_container.inputs, state, sample_id)
874
+ inputs = self._get_dataset_handlers(global_leap_binder.setup_container.inputs, state, sample_id)
875
+ autoregressive_handler = global_leap_binder.setup_container.autoregressive_step
876
+ if autoregressive_handler is not None:
877
+ # Autoregressive integrations have no input encoders — the hook's first call supplies
878
+ # the sample's (step-0) model inputs, so every sample-fetching flow (graph validation,
879
+ # analysis, import checks) sees a fully-populated inputs dict.
880
+ preprocess_response = self._preprocess_result()[state]
881
+ step_zero_result = autoregressive_handler.function(sample_id, None, None, None,
882
+ preprocess_response)
883
+ if not isinstance(step_zero_result, tuple) or len(step_zero_result) != 2 or \
884
+ not isinstance(step_zero_result[0], dict):
885
+ raise Exception(
886
+ 'The autoregressive step hook must return a (next_inputs, state) tuple with a '
887
+ f'dict of initial model inputs on its first call, got {type(step_zero_result)} '
888
+ f'for sample {sample_id}.')
889
+ inputs.update(step_zero_result[0])
890
+ return inputs
878
891
 
879
892
  def _get_instances_masks(self, state: DataStateEnum, sample_id: Union[int, str], instance_id: int) -> Optional[Dict[str, ElementInstance]]:
880
893
  if instance_id is None:
@@ -897,6 +910,12 @@ class LeapLoader(LeapLoaderBase):
897
910
  def _get_gt(self, state: DataStateEnum, sample_id: Union[int, str]) -> Dict[str, npt.NDArray[np.float32]]:
898
911
  return self._get_dataset_handlers(global_leap_binder.setup_container.ground_truths, state, sample_id)
899
912
 
913
+ def get_gt(self, state: DataStateEnum, sample_id: Union[int, str]) -> Dict[str, npt.NDArray[np.float32]]:
914
+ """Ground-truth tensors only ({} when no GT encoders are declared) — lets the engine's
915
+ rollout fetch a chain's GT without re-running the autoregressive hook via get_sample."""
916
+ self.exec_script()
917
+ return self._get_gt(state, sample_id)
918
+
900
919
  @lru_cache()
901
920
  def _metadata_name_to_type(self) -> Dict[str, DatasetMetadataType]:
902
921
  global_leap_binder.check_preprocess(self._preprocess_result())
@@ -953,18 +972,12 @@ class LeapLoader(LeapLoaderBase):
953
972
  sample_id = original_local_id
954
973
  else:
955
974
  preprocess_state = preprocess_result[state]
956
- group_ids, pos = (None, None)
957
- if preprocess_state.is_grouped:
958
- group_ids, pos = self._locate_group(preprocess_state, sample_id)
959
975
  for handler in global_leap_binder.setup_container.metadata:
960
976
  if requested_metadata_names:
961
977
  if not is_metadata_name_starts_with_handler_name(handler):
962
978
  continue
963
979
 
964
- if preprocess_state.is_grouped:
965
- handler_result = handler.function(group_ids, preprocess_state)[pos]
966
- else:
967
- handler_result = handler.function(sample_id, preprocess_state)
980
+ handler_result = handler.function(sample_id, preprocess_state)
968
981
  if isinstance(handler_result, dict):
969
982
  for single_metadata_name, single_metadata_result in handler_result.items():
970
983
  handler_name = f'{handler.name}_{single_metadata_name}'
@@ -983,66 +996,6 @@ class LeapLoader(LeapLoaderBase):
983
996
 
984
997
  return result_agg, is_none
985
998
 
986
- def _grouped_metadata_for_group(self, preprocess_state: PreprocessResponse,
987
- group_ids: List[Union[int, str]],
988
- requested_metadata_names: Optional[List[str]]
989
- ) -> Tuple[Dict[str, List[Any]], Dict[str, List[bool]]]:
990
- def is_wanted(_handler):
991
- if not requested_metadata_names:
992
- return True
993
- for metadata_name in requested_metadata_names:
994
- if metadata_name.startswith(_handler.name + '_') or metadata_name == _handler.name:
995
- return True
996
- return False
997
-
998
- results: Dict[str, List[Any]] = {}
999
- is_none: Dict[str, List[bool]] = {}
1000
- for handler in global_leap_binder.setup_container.metadata:
1001
- if requested_metadata_names and not is_wanted(handler):
1002
- continue
1003
- rows = handler.function(group_ids, preprocess_state)
1004
- if rows and isinstance(rows[0], dict):
1005
- for key in self._grouped_dict_keys(rows, handler.name):
1006
- name = f'{handler.name}_{key}'
1007
- if requested_metadata_names and name not in requested_metadata_names:
1008
- continue
1009
- converted = [self._convert_metadata_to_correct_type(name, row[key]) for row in rows]
1010
- results[name] = [v for v, _ in converted]
1011
- is_none[name] = [n for _, n in converted]
1012
- else:
1013
- name = handler.name
1014
- if requested_metadata_names and name not in requested_metadata_names:
1015
- continue
1016
- converted = [self._convert_metadata_to_correct_type(name, row) for row in rows]
1017
- results[name] = [v for v, _ in converted]
1018
- is_none[name] = [n for _, n in converted]
1019
- return results, is_none
1020
-
1021
- def get_metadata_multiple_samples(self, state: DataStateEnum, sample_ids: Union[List[int], List[str]],
1022
- requested_metadata_names: Optional[List[str]] = None
1023
- ) -> Tuple[Dict[str, Union[List[str], List[int], List[bool], List[float]]],
1024
- Dict[str, List[bool]]]:
1025
- preprocess_state = self._preprocess_result().get(state)
1026
- if preprocess_state is None or not preprocess_state.is_grouped:
1027
- return super().get_metadata_multiple_samples(state, sample_ids, requested_metadata_names)
1028
-
1029
- sample_id_type = self.get_sample_id_type()
1030
- aggregated_results: Dict[str, List[Any]] = {}
1031
- aggregated_is_none: Dict[str, List[bool]] = {}
1032
- group_cache: Dict[int, Tuple[Dict[str, List[Any]], Dict[str, List[bool]]]] = {}
1033
- for sample_id in sample_ids:
1034
- sample_id = sample_id_type(sample_id)
1035
- group_ids, pos = self._locate_group(preprocess_state, sample_id)
1036
- key = id(group_ids)
1037
- if key not in group_cache:
1038
- group_cache[key] = self._grouped_metadata_for_group(
1039
- preprocess_state, group_ids, requested_metadata_names)
1040
- group_results, group_is_none = group_cache[key]
1041
- for name, values in group_results.items():
1042
- aggregated_results.setdefault(name, []).append(values[pos])
1043
- aggregated_is_none.setdefault(name, []).append(group_is_none[name][pos])
1044
- return aggregated_results, aggregated_is_none
1045
-
1046
999
  @lru_cache()
1047
1000
  def get_sample_id_type(self) -> Type:
1048
1001
  preprocess_results = list(self._preprocess_result().values())
@@ -1058,6 +1011,152 @@ class LeapLoader(LeapLoaderBase):
1058
1011
  self.exec_script()
1059
1012
  return global_leap_binder.setup_container.custom_latent_space is not None
1060
1013
 
1014
+ @lru_cache()
1015
+ def has_autoregressive_step(self) -> bool:
1016
+ self.exec_script()
1017
+ return global_leap_binder.setup_container.autoregressive_step is not None
1018
+
1019
+ @lru_cache()
1020
+ def get_autoregressive_latent_space_aggregation(self) -> str:
1021
+ self.exec_script()
1022
+ handler = global_leap_binder.setup_container.autoregressive_step
1023
+ if handler is None:
1024
+ return 'last_step'
1025
+ return handler.latent_space_aggregation
1026
+
1027
+ def run_autoregressive_step(self, sample_id: Union[int, str],
1028
+ prev_inputs: Optional[Dict[str, npt.NDArray[np.float32]]],
1029
+ prev_outputs: Optional[Dict[str, npt.NDArray[np.float32]]],
1030
+ chain_state: Any,
1031
+ state: DataStateEnum
1032
+ ) -> Tuple[Optional[Dict[str, npt.NDArray[np.float32]]], Any]:
1033
+ """Execute the user's autoregressive step hook against the in-process preprocess result.
1034
+
1035
+ This is the engine rollout's per-step entry point (invoked on the generic pod). The first
1036
+ call per chain passes prev_inputs=None, prev_outputs=None, chain_state=None and returns
1037
+ the initial model inputs plus the initial state; a None next_inputs ends the chain — the
1038
+ terminating call still returns the final state.
1039
+ """
1040
+ self.exec_script()
1041
+ handler = global_leap_binder.setup_container.autoregressive_step
1042
+ if handler is None:
1043
+ raise Exception('run_autoregressive_step was called but the integration has no '
1044
+ 'tensorleap_autoregressive_step hook.')
1045
+ preprocess_response = self._preprocess_result()[state]
1046
+ result = handler.function(sample_id, prev_inputs, prev_outputs, chain_state,
1047
+ preprocess_response)
1048
+ if not isinstance(result, tuple) or len(result) != 2:
1049
+ raise Exception('The autoregressive step hook must return a (next_inputs, state) '
1050
+ f'tuple, got {type(result)} for sample {sample_id}.')
1051
+ next_inputs, new_chain_state = result
1052
+ validate_autoregressive_state_types(new_chain_state)
1053
+ if next_inputs is None:
1054
+ return None, new_chain_state
1055
+ if not isinstance(next_inputs, dict):
1056
+ raise Exception('The autoregressive step hook must return a dict of model inputs or '
1057
+ f'None to end the chain, got {type(next_inputs)} for sample '
1058
+ f'{sample_id}.')
1059
+ # Parse-time validation only exercises step 0; enforce the fixed key-set/shape contract
1060
+ # on every step here so drift fails with an actionable message instead of a cryptic
1061
+ # engine error on a compiled graph.
1062
+ expected_shapes = self._ensure_autoregressive_input_shapes()
1063
+ if set(next_inputs) != set(expected_shapes):
1064
+ raise Exception('The autoregressive step hook must return the same model input keys '
1065
+ f'on every call. Expected {sorted(expected_shapes)}, got '
1066
+ f'{sorted(next_inputs)} for sample {sample_id}.')
1067
+ for key, value in next_inputs.items():
1068
+ if isinstance(value, np.ndarray) and list(value.shape) != expected_shapes[key]:
1069
+ raise Exception('The autoregressive step hook must return fixed tensor shapes on '
1070
+ f'every call (use fixed-length padding + mask for growing '
1071
+ f'sequences). Input "{key}" was {expected_shapes[key]} on the '
1072
+ f'first call and {list(value.shape)} now (sample {sample_id}).')
1073
+ return next_inputs, new_chain_state
1074
+
1075
+ @lru_cache()
1076
+ def autoregressive_visualizer_by_name(self) -> Dict[str, VisualizerHandlerData]:
1077
+ self.exec_script()
1078
+ return {handler.visualizer_handler_data.name: handler.visualizer_handler_data
1079
+ for handler in global_leap_binder.setup_container.autoregressive_visualizers}
1080
+
1081
+ @lru_cache()
1082
+ def get_autoregressive_decorator_names(self) -> Dict[str, List[str]]:
1083
+ self.exec_script()
1084
+ setup = global_leap_binder.setup_container
1085
+ return {
1086
+ 'metrics': [handler.metric_handler_data.name
1087
+ for handler in setup.autoregressive_metrics],
1088
+ 'losses': [handler.custom_loss_handler_data.name
1089
+ for handler in setup.autoregressive_losses],
1090
+ 'visualizers': [handler.visualizer_handler_data.name
1091
+ for handler in setup.autoregressive_visualizers],
1092
+ }
1093
+
1094
+ @staticmethod
1095
+ def _autoregressive_callable_kwargs(function: Callable[..., Any],
1096
+ inputs: Dict[str, npt.NDArray[np.float32]],
1097
+ outputs: Dict[str, npt.NDArray[np.float32]],
1098
+ chain_state: Any,
1099
+ wired_args: Dict[str, npt.NDArray[np.float32]]
1100
+ ) -> Dict[str, Any]:
1101
+ implicit_values = {'inputs': inputs, 'outputs': outputs, 'state': chain_state}
1102
+ kwargs: Dict[str, Any] = {}
1103
+ for arg_name in inspect.getfullargspec(function)[0]:
1104
+ if arg_name in implicit_values:
1105
+ kwargs[arg_name] = implicit_values[arg_name]
1106
+ elif arg_name in wired_args:
1107
+ kwargs[arg_name] = wired_args[arg_name]
1108
+ else:
1109
+ raise Exception(f'Autoregressive callable is missing wired argument '
1110
+ f'"{arg_name}" — the platform mapping supplies only '
1111
+ f'{sorted(wired_args)}.')
1112
+ return kwargs
1113
+
1114
+ def run_autoregressive_metric(self, name: str,
1115
+ inputs: Dict[str, npt.NDArray[np.float32]],
1116
+ outputs: Dict[str, npt.NDArray[np.float32]],
1117
+ chain_state: Any,
1118
+ wired_args: Dict[str, npt.NDArray[np.float32]]) -> Any:
1119
+ self.exec_script()
1120
+ for handler in global_leap_binder.setup_container.autoregressive_metrics:
1121
+ if handler.metric_handler_data.name == name:
1122
+ return handler.function(**self._autoregressive_callable_kwargs(
1123
+ handler.function, inputs, outputs, chain_state, wired_args))
1124
+ raise Exception(f'No autoregressive metric named "{name}" is registered.')
1125
+
1126
+ def run_autoregressive_loss(self, name: str,
1127
+ inputs: Dict[str, npt.NDArray[np.float32]],
1128
+ outputs: Dict[str, npt.NDArray[np.float32]],
1129
+ chain_state: Any,
1130
+ wired_args: Dict[str, npt.NDArray[np.float32]]) -> Any:
1131
+ self.exec_script()
1132
+ for handler in global_leap_binder.setup_container.autoregressive_losses:
1133
+ if handler.custom_loss_handler_data.name == name:
1134
+ return handler.function(**self._autoregressive_callable_kwargs(
1135
+ handler.function, inputs, outputs, chain_state, wired_args))
1136
+ raise Exception(f'No autoregressive loss named "{name}" is registered.')
1137
+
1138
+ def run_autoregressive_visualizer(self, name: str,
1139
+ inputs: Dict[str, npt.NDArray[np.float32]],
1140
+ outputs: Dict[str, npt.NDArray[np.float32]],
1141
+ chain_state: Any,
1142
+ wired_args: Dict[str, npt.NDArray[np.float32]]) -> Any:
1143
+ self.exec_script()
1144
+ for handler in global_leap_binder.setup_container.autoregressive_visualizers:
1145
+ if handler.visualizer_handler_data.name == name:
1146
+ return handler.function(**self._autoregressive_callable_kwargs(
1147
+ handler.function, inputs, outputs, chain_state, wired_args))
1148
+ raise Exception(f'No autoregressive visualizer named "{name}" is registered.')
1149
+
1150
+ def get_prediction_names_in_order(self) -> List[str]:
1151
+ """Model output names by declared prediction-type order, used to key prev_outputs for the
1152
+ hook. AR integrations must declare prediction types (validate_autoregressive_setup) and
1153
+ the count is checked against the model's outputs at integration-test time; the declared
1154
+ ORDER matching the model's output order is the user's responsibility — it is not
1155
+ validated."""
1156
+ self.exec_script()
1157
+ return [prediction_type.name for prediction_type in
1158
+ global_leap_binder.setup_container.prediction_types]
1159
+
1061
1160
  def get_instances_data(self, state: DataStateEnum) -> Tuple[Dict[str, List[str]], Dict[str, str]]:
1062
1161
  preprocess_result = self._preprocess_result()
1063
1162
  preprocess_state = preprocess_result.get(state)
@@ -149,6 +149,11 @@ class LeapLoaderBase:
149
149
  def has_custom_latent_space_decorator(self) -> bool:
150
150
  pass
151
151
 
152
+ def has_autoregressive_step(self) -> bool:
153
+ # Default False so existing LeapLoaderBase implementations stay valid; the concrete
154
+ # loaders that can answer (LeapLoader in-process, LeapLoaderWithRedis via RPC) override.
155
+ return False
156
+
152
157
  @abstractmethod
153
158
  def get_heatmap_visualizer_raw_vis_input_arg_name(self, visualizer_name: str) -> Optional[str]:
154
159
  pass
code_loader/utils.py CHANGED
@@ -14,11 +14,6 @@ from code_loader.contract.enums import DatasetMetadataType
14
14
  def to_numpy_return_wrapper(encoder_function: SectionCallableInterface) -> SectionCallableInterface:
15
15
  def numpy_encoder_function(idx: Union[int, str], samples: PreprocessResponse) -> npt.NDArray[np.float32]:
16
16
  result = encoder_function(idx, samples)
17
- if isinstance(result, list) and len(result) > 0 and all(isinstance(sample, np.ndarray) for sample in result):
18
- if len({sample.shape for sample in result}) > 1:
19
- # Variable per-sample shapes within a group: keep the per-sample arrays as a list
20
- # instead of forcing a stack that would raise an inhomogeneous-shape error.
21
- return result
22
17
  numpy_result: npt.NDArray[np.float32] = np.array(result)
23
18
  return numpy_result
24
19
 
@@ -106,3 +101,39 @@ def get_metadata_type_from_variable(variable: Union[Optional[str], int, bool, Op
106
101
  f"The return type should be one of [int, float, str, bool]. Got {metadata_type}")
107
102
  return dataset_metadata_type
108
103
 
104
+
105
+
106
+ AUTOREGRESSIVE_STATE_LEAF_TYPES = (np.ndarray, np.generic, int, float, str, bool, type(None))
107
+
108
+
109
+ def validate_autoregressive_state_types(value: Any, path: str = 'state') -> None:
110
+ if isinstance(value, dict):
111
+ for key, item in value.items():
112
+ if not isinstance(key, str):
113
+ raise AssertionError(
114
+ f'autoregressive state validation failed: dict key {key!r} at {path} must be '
115
+ f'a string. Got {type(key).__name__}.')
116
+ validate_autoregressive_state_types(item, f'{path}[{key!r}]')
117
+ return
118
+ if isinstance(value, (list, tuple)):
119
+ for i, item in enumerate(value):
120
+ validate_autoregressive_state_types(item, f'{path}[{i}]')
121
+ return
122
+ if not isinstance(value, AUTOREGRESSIVE_STATE_LEAF_TYPES):
123
+ raise AssertionError(
124
+ f'autoregressive state validation failed: {path} holds a {type(value).__name__}. '
125
+ f'State must be built only from numpy arrays, numbers, strings, bools and nests of '
126
+ f'dicts/lists of those — it is serialized to the platform queue on every chain step.')
127
+
128
+
129
+ def autoregressive_nests_equal(a: Any, b: Any) -> bool:
130
+ if isinstance(a, dict) or isinstance(b, dict):
131
+ return isinstance(a, dict) and isinstance(b, dict) and set(a) == set(b) and \
132
+ all(autoregressive_nests_equal(a[key], b[key]) for key in a)
133
+ if isinstance(a, (list, tuple)) or isinstance(b, (list, tuple)):
134
+ return isinstance(a, (list, tuple)) and isinstance(b, (list, tuple)) and \
135
+ len(a) == len(b) and all(autoregressive_nests_equal(x, y) for x, y in zip(a, b))
136
+ if isinstance(a, np.ndarray) or isinstance(b, np.ndarray):
137
+ return isinstance(a, np.ndarray) and isinstance(b, np.ndarray) and \
138
+ a.shape == b.shape and bool(np.array_equal(a, b))
139
+ return bool(a == b)