code-loader 1.0.195.dev1__py3-none-any.whl → 1.0.196.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, 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:
@@ -76,13 +77,14 @@ class LeapLoader(LeapLoaderBase):
76
77
  preprocess_result = global_leap_binder.get_preprocess_result()
77
78
  self._preprocess_result_cached = preprocess_result
78
79
  is_grouped = any(r.is_grouped for r in preprocess_result.values())
79
- except Exception:
80
- is_grouped = False
81
80
  finally:
82
81
  os.environ[mapping_runtime_mode_env_var_mame] = 'TRUE'
83
82
 
84
83
  _leap_dec._mapping_dataset_is_grouped = is_grouped
85
84
  if global_leap_binder.integration_test_func is not None:
85
+ from code_loader.inner_leap_binder.leapbinder_decorators import \
86
+ _reset_model_loop_state
87
+ _reset_model_loop_state()
86
88
  mapping_preprocess = (
87
89
  PreprocessResponse(sample_ids=[["__mapping_placeholder__"]], state=DataStateType.training)
88
90
  if is_grouped else
@@ -230,7 +232,7 @@ class LeapLoader(LeapLoaderBase):
230
232
  additional = self._preprocess_result().get(DataStateEnum.additional)
231
233
  if additional is None:
232
234
  return set()
233
- return set(additional.sample_ids)
235
+ return set(additional.flat_sample_ids)
234
236
 
235
237
  def _resolve_synthetic(self, sample_id: Union[int, str],
236
238
  state: Optional[DataStateEnum] = None
@@ -505,31 +507,52 @@ class LeapLoader(LeapLoaderBase):
505
507
  try:
506
508
  preprocess_result = self._preprocess_result()
507
509
  input_shapes: Dict[str, List[int]] = {}
510
+ expected_keys: Optional[FrozenSet[str]] = None
511
+ expected_keys_state = ''
508
512
  for state, preprocess_response in preprocess_result.items():
509
513
  if preprocess_response.sample_ids_to_instance_mappings:
510
514
  raise Exception('Element instances are not supported together with '
511
515
  'tensorleap_autoregressive_step.')
512
- 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):
516
+ # Grouped preprocess is fine: grouping only amortizes the shared base reads
517
+ # (GT/metadata/custom_latent) the AR hook always drives a single scalar chain
518
+ # and is never handed a group.
519
+ sample_id = preprocess_response.flat_sample_ids[0]
520
+ first_result = handler.function(sample_id, None, None, None, preprocess_response)
521
+ second_result = handler.function(sample_id, None, None, None, preprocess_response)
522
+ if not isinstance(first_result, tuple) or len(first_result) != 2:
523
+ raise Exception('The autoregressive step hook must return a (next_inputs, '
524
+ f'state) tuple, got {type(first_result)} '
525
+ f'(state: {state.name}).')
526
+ first_inputs, first_state = first_result
527
+ if not isinstance(first_inputs, dict) or not first_inputs:
516
528
  raise Exception('The autoregressive step hook must return a dict of initial model '
517
- f'inputs on its first call, got {type(first_result)} '
529
+ f'inputs on its first call, got {type(first_inputs)} '
518
530
  f'(state: {state.name}).')
519
- if not isinstance(second_result, dict) or \
520
- set(second_result.keys()) != set(first_result.keys()):
531
+ validate_autoregressive_state_types(first_state)
532
+ if not isinstance(second_result, tuple) or len(second_result) != 2 or \
533
+ not isinstance(second_result[0], dict) or \
534
+ set(second_result[0].keys()) != set(first_inputs.keys()) or \
535
+ not autoregressive_nests_equal(first_state, second_result[1]):
521
536
  raise Exception('The autoregressive step hook is not deterministic — two calls '
522
537
  'with identical arguments returned different results '
523
538
  f'(state: {state.name}). Seed any stochasticity from '
524
539
  'sample_id: the engine replays steps after crash recovery '
525
540
  '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():
541
+ if expected_keys is None:
542
+ expected_keys = frozenset(first_inputs)
543
+ expected_keys_state = state.name
544
+ elif frozenset(first_inputs) != expected_keys:
545
+ raise Exception(f'The autoregressive step hook returned different model input '
546
+ f'keys across states: {sorted(expected_keys)} '
547
+ f'({expected_keys_state}) vs {sorted(first_inputs)} '
548
+ f'({state.name}). The key set must be identical for every '
549
+ f'state.')
550
+ for key, value in first_inputs.items():
551
+ if key.startswith('_'):
552
+ raise Exception(f'The autoregressive step hook returned model input key '
553
+ f'"{key}" — underscore passthrough keys were replaced by '
554
+ f'the explicit state channel; move passthrough values '
555
+ f'into the returned state. State: {state.name}.')
533
556
  if not isinstance(value, np.ndarray):
534
557
  raise Exception(f'The autoregressive step hook returned a non-numpy value for '
535
558
  f'model input "{key}" ({type(value)}). State: {state.name}.')
@@ -538,7 +561,7 @@ class LeapLoader(LeapLoaderBase):
538
561
  f'model input "{key}" across states: {input_shapes[key]} vs '
539
562
  f'{list(value.shape)}. Shapes must be fixed.')
540
563
  input_shapes[key] = list(value.shape)
541
- if not np.array_equal(value, second_result[key]):
564
+ if not autoregressive_nests_equal(value, second_result[0][key]):
542
565
  raise Exception('The autoregressive step hook is not deterministic — two calls '
543
566
  'with identical arguments returned different outputs for model '
544
567
  f'input "{key}". Seed any stochasticity from sample_id: the '
@@ -552,19 +575,39 @@ class LeapLoader(LeapLoaderBase):
552
575
  test_result.is_passed = False
553
576
  return test_result
554
577
 
578
+ @staticmethod
579
+ def _validate_autoregressive_step_inputs(step_inputs: Dict[str, Any],
580
+ sample_id: Union[int, str]) -> None:
581
+ # The parse-time check only covers the first sample of each state; the engine runtime
582
+ # paths must enforce the same key/value contract for every sample they touch.
583
+ for key, value in step_inputs.items():
584
+ if key.startswith('_'):
585
+ raise Exception(f'The autoregressive step hook returned model input key "{key}" '
586
+ f'for sample {sample_id} — underscore passthrough keys were '
587
+ 'replaced by the explicit state channel; move passthrough values '
588
+ 'into the returned state.')
589
+ if not isinstance(value, np.ndarray):
590
+ raise Exception('The autoregressive step hook returned a non-numpy value for '
591
+ f'model input "{key}" ({type(value).__name__}) for sample '
592
+ f'{sample_id}.')
593
+
555
594
  def _ensure_autoregressive_input_shapes(self) -> Dict[str, List[int]]:
556
595
  handler = global_leap_binder.setup_container.autoregressive_step
557
596
  assert handler is not None
558
597
  if handler.input_shapes is None:
559
598
  preprocess_result = self._preprocess_result()
560
599
  first_state_response = next(iter(preprocess_result.values()))
561
- first_result = handler.function(first_state_response.sample_ids[0], None, None,
600
+ first_sample_id = first_state_response.flat_sample_ids[0]
601
+ first_result = handler.function(first_sample_id, None, None, None,
562
602
  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)}
603
+ if not isinstance(first_result, tuple) or len(first_result) != 2 or \
604
+ not isinstance(first_result[0], dict):
605
+ raise Exception('The autoregressive step hook must return a (next_inputs, state) '
606
+ 'tuple with a dict of initial model inputs on its first call, '
607
+ f'got {type(first_result)}.')
608
+ self._validate_autoregressive_step_inputs(first_result[0], first_sample_id)
609
+ handler.input_shapes = {key: list(value.shape)
610
+ for key, value in first_result[0].items()}
568
611
  return handler.input_shapes
569
612
 
570
613
  def run_simulation(self, sim_name, params=None, n_samples=1, seed=0,
@@ -833,6 +876,21 @@ class LeapLoader(LeapLoaderBase):
833
876
  metric_inst = MetricInstance(metric.metric_handler_data.name, metric.metric_handler_data.arg_names)
834
877
  metrics.append(metric_inst)
835
878
 
879
+ # Autoregressive decorators are reported alongside the regular ones so the platform sees
880
+ # their names; the engine tells them apart by name via get_autoregressive_decorator_names.
881
+ for autoregressive_metric in setup.autoregressive_metrics:
882
+ metrics.append(MetricInstance(autoregressive_metric.metric_handler_data.name,
883
+ autoregressive_metric.metric_handler_data.arg_names))
884
+ for autoregressive_loss in setup.autoregressive_losses:
885
+ custom_losses.append(CustomLossInstance(
886
+ autoregressive_loss.custom_loss_handler_data.name,
887
+ autoregressive_loss.custom_loss_handler_data.arg_names))
888
+ for autoregressive_visualizer in setup.autoregressive_visualizers:
889
+ visualizers.append(VisualizerInstance(
890
+ autoregressive_visualizer.visualizer_handler_data.name,
891
+ autoregressive_visualizer.visualizer_handler_data.type,
892
+ autoregressive_visualizer.visualizer_handler_data.arg_names))
893
+
836
894
  simulations = []
837
895
  for sim in setup.simulations:
838
896
  sim_config_serialized = {
@@ -909,13 +967,12 @@ class LeapLoader(LeapLoaderBase):
909
967
 
910
968
  def _locate_group(self, preprocess_state: PreprocessResponse,
911
969
  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))
970
+ """Map a sample id to (its group, its position in that group). Cached on the
971
+ PreprocessResponse itself (not keyed by id() on this loader) so grouped single-sample
972
+ fetches don't rescan the groups, and the cache can never go stale: when the response is
973
+ replaced (e.g. unlabeled preprocess refresh), the old object and its cache are simply
974
+ discarded together instead of lingering under a possibly-reused id()."""
975
+ mapping = preprocess_state._group_pos_cache
919
976
  if mapping is None:
920
977
  mapping = {}
921
978
  for group in preprocess_state.groups:
@@ -925,7 +982,7 @@ class LeapLoader(LeapLoaderBase):
925
982
  f"Duplicate sample id {sid!r} across groups in the preprocess response; "
926
983
  f"sample ids must be unique within and across groups.")
927
984
  mapping[sid] = (group, pos)
928
- cache[id(preprocess_state)] = mapping
985
+ preprocess_state._group_pos_cache = mapping
929
986
  if sample_id not in mapping:
930
987
  raise KeyError(f"Sample id {sample_id!r} is not present in any group of this preprocess response.")
931
988
  return mapping[sample_id]
@@ -961,18 +1018,47 @@ class LeapLoader(LeapLoaderBase):
961
1018
  samples into the model's B is the engine's job. This is the path a group-aware engine calls
962
1019
  to read a file once per group. Row order matches group_ids exactly."""
963
1020
  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.")
1021
+ preprocess_result = self._preprocess_result()
1022
+ if state == DataStateEnum.unlabeled and any(
1023
+ sid not in preprocess_result[state].flat_sample_ids for sid in group_ids):
1024
+ # Mirrors get_sample's refresh: the unlabeled preprocess can grow between calls, so a
1025
+ # group_id absent from the current snapshot may just not have been generated yet.
1026
+ self._preprocess_result(update_unlabeled_preprocess=True)
1027
+ preprocess_state = preprocess_result[state]
1028
+ assert preprocess_state.is_grouped, (
1029
+ "get_samples is the group-aware fetch path and requires a grouped preprocess "
1030
+ "response; call get_sample for a flat (non-grouped) dataset.")
1031
+ # All requested ids must belong to a single group (one file). A cross-group request
1032
+ # would force the encoder to load multiple files, defeating the one-load-per-group
1033
+ # contract; partitioning a scattered request by group is the engine's responsibility.
1034
+ distinct_groups = {id(self._locate_group(preprocess_state, sid)[0]) for sid in group_ids}
1035
+ assert len(distinct_groups) == 1, (
1036
+ f"get_samples received sample ids spanning {len(distinct_groups)} groups; a request "
1037
+ f"must be confined to a single group. The engine partitions cross-group requests and "
1038
+ f"issues one get_samples call per group.")
974
1039
  inputs = {handler.name: self._to_grouped_list(handler.function(group_ids, preprocess_state))
975
1040
  for handler in global_leap_binder.setup_container.inputs}
1041
+ autoregressive_handler = global_leap_binder.setup_container.autoregressive_step
1042
+ if autoregressive_handler is not None:
1043
+ # AR integrations have no input encoders — the hook's first call supplies each
1044
+ # chain's step-0 model inputs. The hook is strictly per-scalar-chain (never handed
1045
+ # a group), so it is called once per flat id in the group and the per-sample results
1046
+ # are collected into per-key lists of length B, matching the grouped (never-stacked)
1047
+ # contract used for every other encoder here.
1048
+ step_zero_per_sample = []
1049
+ for sid in group_ids:
1050
+ step_zero_result = autoregressive_handler.function(sid, None, None, None,
1051
+ preprocess_state)
1052
+ if not isinstance(step_zero_result, tuple) or len(step_zero_result) != 2 or \
1053
+ not isinstance(step_zero_result[0], dict):
1054
+ raise Exception(
1055
+ 'The autoregressive step hook must return a (next_inputs, state) tuple '
1056
+ f'with a dict of initial model inputs on its first call, got '
1057
+ f'{type(step_zero_result)} for sample {sid}.')
1058
+ self._validate_autoregressive_step_inputs(step_zero_result[0], sid)
1059
+ step_zero_per_sample.append(step_zero_result[0])
1060
+ for key in (step_zero_per_sample[0].keys() if step_zero_per_sample else []):
1061
+ inputs[key] = [row[key] for row in step_zero_per_sample]
976
1062
  gt = None
977
1063
  if state != DataStateEnum.unlabeled:
978
1064
  gt = {handler.name: self._to_grouped_list(handler.function(group_ids, preprocess_state))
@@ -1014,13 +1100,16 @@ class LeapLoader(LeapLoaderBase):
1014
1100
  # the sample's (step-0) model inputs, so every sample-fetching flow (graph validation,
1015
1101
  # analysis, import checks) sees a fully-populated inputs dict.
1016
1102
  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):
1103
+ step_zero_result = autoregressive_handler.function(sample_id, None, None, None,
1104
+ preprocess_response)
1105
+ if not isinstance(step_zero_result, tuple) or len(step_zero_result) != 2 or \
1106
+ not isinstance(step_zero_result[0], dict):
1019
1107
  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('_')})
1108
+ 'The autoregressive step hook must return a (next_inputs, state) tuple with a '
1109
+ f'dict of initial model inputs on its first call, got {type(step_zero_result)} '
1110
+ f'for sample {sample_id}.')
1111
+ self._validate_autoregressive_step_inputs(step_zero_result[0], sample_id)
1112
+ inputs.update(step_zero_result[0])
1024
1113
  return inputs
1025
1114
 
1026
1115
  def _get_instances_masks(self, state: DataStateEnum, sample_id: Union[int, str], instance_id: int) -> Optional[Dict[str, ElementInstance]]:
@@ -1227,12 +1316,15 @@ class LeapLoader(LeapLoaderBase):
1227
1316
  def run_autoregressive_step(self, sample_id: Union[int, str],
1228
1317
  prev_inputs: Optional[Dict[str, npt.NDArray[np.float32]]],
1229
1318
  prev_outputs: Optional[Dict[str, npt.NDArray[np.float32]]],
1230
- state: DataStateEnum) -> Optional[Dict[str, npt.NDArray[np.float32]]]:
1319
+ chain_state: Any,
1320
+ state: DataStateEnum
1321
+ ) -> Tuple[Optional[Dict[str, npt.NDArray[np.float32]]], Any]:
1231
1322
  """Execute the user's autoregressive step hook against the in-process preprocess result.
1232
1323
 
1233
1324
  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.
1325
+ call per chain passes prev_inputs=None, prev_outputs=None, chain_state=None and returns
1326
+ the initial model inputs plus the initial state; a None next_inputs ends the chain — the
1327
+ terminating call still returns the final state.
1236
1328
  """
1237
1329
  self.exec_script()
1238
1330
  handler = global_leap_binder.setup_container.autoregressive_step
@@ -1240,28 +1332,119 @@ class LeapLoader(LeapLoaderBase):
1240
1332
  raise Exception('run_autoregressive_step was called but the integration has no '
1241
1333
  'tensorleap_autoregressive_step hook.')
1242
1334
  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):
1335
+ result = handler.function(sample_id, prev_inputs, prev_outputs, chain_state,
1336
+ preprocess_response)
1337
+ if not isinstance(result, tuple) or len(result) != 2:
1338
+ raise Exception('The autoregressive step hook must return a (next_inputs, state) '
1339
+ f'tuple, got {type(result)} for sample {sample_id}.')
1340
+ next_inputs, new_chain_state = result
1341
+ validate_autoregressive_state_types(new_chain_state)
1342
+ if next_inputs is None:
1343
+ if prev_inputs is None:
1344
+ raise Exception('The autoregressive step hook returned None for next_inputs on '
1345
+ f'the first call of the chain (sample {sample_id}) — the first '
1346
+ 'call must return the initial model inputs; None would produce '
1347
+ 'an empty chain with no forward pass.')
1348
+ return None, new_chain_state
1349
+ if not isinstance(next_inputs, dict):
1247
1350
  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}.')
1351
+ f'None to end the chain, got {type(next_inputs)} for sample '
1352
+ f'{sample_id}.')
1249
1353
  # Parse-time validation only exercises step 0; enforce the fixed key-set/shape contract
1250
1354
  # on every step here so drift fails with an actionable message instead of a cryptic
1251
1355
  # engine error on a compiled graph.
1252
1356
  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):
1357
+ if set(next_inputs) != set(expected_shapes):
1255
1358
  raise Exception('The autoregressive step hook must return the same model input keys '
1256
1359
  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]:
1360
+ f'{sorted(next_inputs)} for sample {sample_id}.')
1361
+ for key, value in next_inputs.items():
1362
+ if not isinstance(value, np.ndarray):
1363
+ raise Exception('The autoregressive step hook must return numpy arrays for every '
1364
+ f'model input on every call — input "{key}" is a '
1365
+ f'{type(value).__name__} for sample {sample_id} (forgot .numpy() '
1366
+ 'on a fed-back model output?).')
1367
+ if list(value.shape) != expected_shapes[key]:
1260
1368
  raise Exception('The autoregressive step hook must return fixed tensor shapes on '
1261
1369
  f'every call (use fixed-length padding + mask for growing '
1262
1370
  f'sequences). Input "{key}" was {expected_shapes[key]} on the '
1263
1371
  f'first call and {list(value.shape)} now (sample {sample_id}).')
1264
- return result
1372
+ return next_inputs, new_chain_state
1373
+
1374
+ @lru_cache()
1375
+ def autoregressive_visualizer_by_name(self) -> Dict[str, VisualizerHandlerData]:
1376
+ self.exec_script()
1377
+ return {handler.visualizer_handler_data.name: handler.visualizer_handler_data
1378
+ for handler in global_leap_binder.setup_container.autoregressive_visualizers}
1379
+
1380
+ @lru_cache()
1381
+ def get_autoregressive_decorator_names(self) -> Dict[str, List[str]]:
1382
+ self.exec_script()
1383
+ setup = global_leap_binder.setup_container
1384
+ return {
1385
+ 'metrics': [handler.metric_handler_data.name
1386
+ for handler in setup.autoregressive_metrics],
1387
+ 'losses': [handler.custom_loss_handler_data.name
1388
+ for handler in setup.autoregressive_losses],
1389
+ 'visualizers': [handler.visualizer_handler_data.name
1390
+ for handler in setup.autoregressive_visualizers],
1391
+ }
1392
+
1393
+ @staticmethod
1394
+ def _autoregressive_callable_kwargs(function: Callable[..., Any],
1395
+ inputs: Dict[str, npt.NDArray[np.float32]],
1396
+ outputs: Dict[str, npt.NDArray[np.float32]],
1397
+ chain_state: Any,
1398
+ wired_args: Dict[str, npt.NDArray[np.float32]]
1399
+ ) -> Dict[str, Any]:
1400
+ implicit_values = {'inputs': inputs, 'outputs': outputs, 'state': chain_state}
1401
+ kwargs: Dict[str, Any] = {}
1402
+ for arg_name in inspect.getfullargspec(function)[0]:
1403
+ if arg_name in implicit_values:
1404
+ kwargs[arg_name] = implicit_values[arg_name]
1405
+ elif arg_name in wired_args:
1406
+ kwargs[arg_name] = wired_args[arg_name]
1407
+ else:
1408
+ raise Exception(f'Autoregressive callable is missing wired argument '
1409
+ f'"{arg_name}" — the platform mapping supplies only '
1410
+ f'{sorted(wired_args)}.')
1411
+ return kwargs
1412
+
1413
+ def run_autoregressive_metric(self, name: str,
1414
+ inputs: Dict[str, npt.NDArray[np.float32]],
1415
+ outputs: Dict[str, npt.NDArray[np.float32]],
1416
+ chain_state: Any,
1417
+ wired_args: Dict[str, npt.NDArray[np.float32]]) -> Any:
1418
+ self.exec_script()
1419
+ for handler in global_leap_binder.setup_container.autoregressive_metrics:
1420
+ if handler.metric_handler_data.name == name:
1421
+ return handler.function(**self._autoregressive_callable_kwargs(
1422
+ handler.function, inputs, outputs, chain_state, wired_args))
1423
+ raise Exception(f'No autoregressive metric named "{name}" is registered.')
1424
+
1425
+ def run_autoregressive_loss(self, name: str,
1426
+ inputs: Dict[str, npt.NDArray[np.float32]],
1427
+ outputs: Dict[str, npt.NDArray[np.float32]],
1428
+ chain_state: Any,
1429
+ wired_args: Dict[str, npt.NDArray[np.float32]]) -> Any:
1430
+ self.exec_script()
1431
+ for handler in global_leap_binder.setup_container.autoregressive_losses:
1432
+ if handler.custom_loss_handler_data.name == name:
1433
+ return handler.function(**self._autoregressive_callable_kwargs(
1434
+ handler.function, inputs, outputs, chain_state, wired_args))
1435
+ raise Exception(f'No autoregressive loss named "{name}" is registered.')
1436
+
1437
+ def run_autoregressive_visualizer(self, name: str,
1438
+ inputs: Dict[str, npt.NDArray[np.float32]],
1439
+ outputs: Dict[str, npt.NDArray[np.float32]],
1440
+ chain_state: Any,
1441
+ wired_args: Dict[str, npt.NDArray[np.float32]]) -> Any:
1442
+ self.exec_script()
1443
+ for handler in global_leap_binder.setup_container.autoregressive_visualizers:
1444
+ if handler.visualizer_handler_data.name == name:
1445
+ return handler.function(**self._autoregressive_callable_kwargs(
1446
+ handler.function, inputs, outputs, chain_state, wired_args))
1447
+ raise Exception(f'No autoregressive visualizer named "{name}" is registered.')
1265
1448
 
1266
1449
  def get_prediction_names_in_order(self) -> List[str]:
1267
1450
  """Model output names by declared prediction-type order, used to key prev_outputs for the
@@ -149,10 +149,74 @@ class LeapLoaderBase:
149
149
  def has_custom_latent_space_decorator(self) -> bool:
150
150
  pass
151
151
 
152
+ # The autoregressive entry points raise instead of `pass`-ing like the abstract methods
153
+ # above: this class has no ABCMeta, so @abstractmethod is not enforced at runtime and an
154
+ # un-overridden `pass` body would return None — has_autoregressive_step would silently
155
+ # report False for an AR integration and the engine would skip the chain rollout with no
156
+ # error. A stale loader subclass keeps working for non-AR flows and fails loudly here the
157
+ # moment an AR flow reaches it.
158
+ @abstractmethod
152
159
  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
160
+ raise NotImplementedError(f'{type(self).__name__} does not implement '
161
+ 'has_autoregressive_step it must be overridden '
162
+ '(LeapLoader in-process, LeapLoaderWithRedis via RPC).')
163
+
164
+ @abstractmethod
165
+ def get_gt(self, state: DataStateEnum, sample_id: Union[int, str]
166
+ ) -> Dict[str, npt.NDArray[np.float32]]:
167
+ raise NotImplementedError(f'{type(self).__name__} does not implement get_gt.')
168
+
169
+ @abstractmethod
170
+ def get_prediction_names_in_order(self) -> List[str]:
171
+ raise NotImplementedError(f'{type(self).__name__} does not implement '
172
+ 'get_prediction_names_in_order.')
173
+
174
+ @abstractmethod
175
+ def get_autoregressive_latent_space_aggregation(self) -> str:
176
+ raise NotImplementedError(f'{type(self).__name__} does not implement '
177
+ 'get_autoregressive_latent_space_aggregation.')
178
+
179
+ @abstractmethod
180
+ def get_autoregressive_decorator_names(self) -> Dict[str, List[str]]:
181
+ raise NotImplementedError(f'{type(self).__name__} does not implement '
182
+ 'get_autoregressive_decorator_names.')
183
+
184
+ @abstractmethod
185
+ def run_autoregressive_step(self, sample_id: Union[int, str],
186
+ prev_inputs: Optional[Dict[str, npt.NDArray[np.float32]]],
187
+ prev_outputs: Optional[Dict[str, npt.NDArray[np.float32]]],
188
+ chain_state: Any,
189
+ state: DataStateEnum
190
+ ) -> Tuple[Optional[Dict[str, npt.NDArray[np.float32]]], Any]:
191
+ raise NotImplementedError(f'{type(self).__name__} does not implement '
192
+ 'run_autoregressive_step.')
193
+
194
+ @abstractmethod
195
+ def run_autoregressive_metric(self, name: str, inputs: Dict[str, npt.NDArray[np.float32]],
196
+ outputs: Dict[str, npt.NDArray[np.float32]], chain_state: Any,
197
+ wired_args: Dict[str, npt.NDArray[np.float32]]) -> Any:
198
+ raise NotImplementedError(f'{type(self).__name__} does not implement '
199
+ 'run_autoregressive_metric.')
200
+
201
+ @abstractmethod
202
+ def run_autoregressive_loss(self, name: str, inputs: Dict[str, npt.NDArray[np.float32]],
203
+ outputs: Dict[str, npt.NDArray[np.float32]], chain_state: Any,
204
+ wired_args: Dict[str, npt.NDArray[np.float32]]) -> Any:
205
+ raise NotImplementedError(f'{type(self).__name__} does not implement '
206
+ 'run_autoregressive_loss.')
207
+
208
+ @abstractmethod
209
+ def run_autoregressive_visualizer(self, name: str, inputs: Dict[str, npt.NDArray[np.float32]],
210
+ outputs: Dict[str, npt.NDArray[np.float32]],
211
+ chain_state: Any,
212
+ wired_args: Dict[str, npt.NDArray[np.float32]]) -> Any:
213
+ raise NotImplementedError(f'{type(self).__name__} does not implement '
214
+ 'run_autoregressive_visualizer.')
215
+
216
+ @abstractmethod
217
+ def autoregressive_visualizer_by_name(self) -> Dict[str, VisualizerHandlerData]:
218
+ raise NotImplementedError(f'{type(self).__name__} does not implement '
219
+ 'autoregressive_visualizer_by_name.')
156
220
 
157
221
  @abstractmethod
158
222
  def get_heatmap_visualizer_raw_vis_input_arg_name(self, visualizer_name: str) -> Optional[str]:
code_loader/utils.py CHANGED
@@ -1,3 +1,6 @@
1
+ import io
2
+ import math
3
+ import pickle
1
4
  import sys
2
5
  from pathlib import Path
3
6
  from types import TracebackType
@@ -14,10 +17,12 @@ from code_loader.contract.enums import DatasetMetadataType
14
17
  def to_numpy_return_wrapper(encoder_function: SectionCallableInterface) -> SectionCallableInterface:
15
18
  def numpy_encoder_function(idx: Union[int, str], samples: PreprocessResponse) -> npt.NDArray[np.float32]:
16
19
  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
- # A list of per-sample arrays only comes from a grouped encoder (a flat encoder returns
19
- # a single array). Never stack it into a (B, *) batch grouping is only a storage
20
- # read-unit; batching the group to the model's B is the engine's single responsibility.
20
+ if isinstance(idx, list) and isinstance(result, list) and len(result) > 0 \
21
+ and all(isinstance(sample, np.ndarray) for sample in result):
22
+ # Grouped call (idx is a list of sample ids): keep the per-sample list, never stack
23
+ # it into a (B, *) batch grouping is only a storage read-unit; batching the group to
24
+ # the model's B is the engine's single responsibility. Gate on the call shape (idx is a
25
+ # list), not the result shape, so a flat encoder returning a list of arrays still stacks.
21
26
  return result
22
27
  numpy_result: npt.NDArray[np.float32] = np.array(result)
23
28
  return numpy_result
@@ -106,3 +111,51 @@ def get_metadata_type_from_variable(variable: Union[Optional[str], int, bool, Op
106
111
  f"The return type should be one of [int, float, str, bool]. Got {metadata_type}")
107
112
  return dataset_metadata_type
108
113
 
114
+
115
+
116
+ # The platform queue transports state as pickled payloads which the engine deserializes
117
+ # WITHOUT the integration code loaded, so a class defined in the user's modules round-trips
118
+ # locally but crashes on the engine. Only modules importable on the engine may resolve.
119
+ _STATE_SAFE_PICKLE_MODULES = ('builtins', 'copyreg', 'collections', 'numpy', 'datetime')
120
+
121
+
122
+ class _EngineSafeUnpickler(pickle.Unpickler):
123
+ def find_class(self, module: str, name: str) -> Any:
124
+ if module.split('.')[0] in _STATE_SAFE_PICKLE_MODULES:
125
+ return super().find_class(module, name)
126
+ raise pickle.UnpicklingError(
127
+ f'{module}.{name} is defined in integration code, which is not importable on the '
128
+ f'platform engine')
129
+
130
+
131
+ def validate_autoregressive_state_types(value: Any, path: str = 'state') -> None:
132
+ try:
133
+ _EngineSafeUnpickler(io.BytesIO(pickle.dumps(value))).load()
134
+ except Exception as e:
135
+ raise AssertionError(
136
+ f'autoregressive state validation failed: {path} is not serializable ({e}). State '
137
+ 'is serialized to the platform queue on every chain step and deserialized without '
138
+ 'the integration code — any picklable nest of builtin/numpy values works (dicts, '
139
+ 'lists, tuples, sets, numbers, strings, arrays), but user-defined classes, lambdas '
140
+ 'and open resources do not.') from e
141
+
142
+
143
+ def autoregressive_nests_equal(a: Any, b: Any) -> bool:
144
+ if isinstance(a, dict) or isinstance(b, dict):
145
+ return isinstance(a, dict) and isinstance(b, dict) and set(a) == set(b) and \
146
+ all(autoregressive_nests_equal(a[key], b[key]) for key in a)
147
+ if isinstance(a, (list, tuple)) or isinstance(b, (list, tuple)):
148
+ return isinstance(a, (list, tuple)) and isinstance(b, (list, tuple)) and \
149
+ len(a) == len(b) and all(autoregressive_nests_equal(x, y) for x, y in zip(a, b))
150
+ if isinstance(a, np.ndarray) or isinstance(b, np.ndarray):
151
+ if not (isinstance(a, np.ndarray) and isinstance(b, np.ndarray) and a.shape == b.shape):
152
+ return False
153
+ # NaN leaves must equal themselves: this comparison backs the determinism checks, where
154
+ # NaN padding / running stats in a perfectly deterministic hook are legitimate.
155
+ if a.dtype.kind in 'fc' and b.dtype.kind in 'fc':
156
+ return bool(np.array_equal(a, b, equal_nan=True))
157
+ return bool(np.array_equal(a, b))
158
+ if isinstance(a, (float, np.floating)) and isinstance(b, (float, np.floating)) and \
159
+ math.isnan(a) and math.isnan(b):
160
+ return True
161
+ return bool(a == b)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: code-loader
3
- Version: 1.0.195.dev1
3
+ Version: 1.0.196.dev1
4
4
  Summary:
5
5
  Home-page: https://github.com/tensorleap/code-loader
6
6
  License: MIT