code-loader 1.0.201.dev0__tar.gz → 1.0.202.dev1__tar.gz

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.
Files changed (37) hide show
  1. {code_loader-1.0.201.dev0 → code_loader-1.0.202.dev1}/PKG-INFO +4 -3
  2. {code_loader-1.0.201.dev0 → code_loader-1.0.202.dev1}/code_loader/contract/datasetclasses.py +8 -2
  3. {code_loader-1.0.201.dev0 → code_loader-1.0.202.dev1}/code_loader/contract/visualizer_classes.py +2 -2
  4. {code_loader-1.0.201.dev0 → code_loader-1.0.202.dev1}/code_loader/inner_leap_binder/leapbinder.py +86 -8
  5. {code_loader-1.0.201.dev0 → code_loader-1.0.202.dev1}/code_loader/inner_leap_binder/leapbinder_decorators.py +102 -19
  6. {code_loader-1.0.201.dev0 → code_loader-1.0.202.dev1}/code_loader/leaploader.py +69 -28
  7. {code_loader-1.0.201.dev0 → code_loader-1.0.202.dev1}/code_loader/leaploaderbase.py +13 -0
  8. {code_loader-1.0.201.dev0 → code_loader-1.0.202.dev1}/code_loader/visualizers/default_visualizers.py +4 -4
  9. {code_loader-1.0.201.dev0 → code_loader-1.0.202.dev1}/pyproject.toml +1 -1
  10. {code_loader-1.0.201.dev0 → code_loader-1.0.202.dev1}/LICENSE +0 -0
  11. {code_loader-1.0.201.dev0 → code_loader-1.0.202.dev1}/README.md +0 -0
  12. {code_loader-1.0.201.dev0 → code_loader-1.0.202.dev1}/code_loader/__init__.py +0 -0
  13. {code_loader-1.0.201.dev0 → code_loader-1.0.202.dev1}/code_loader/contract/__init__.py +0 -0
  14. {code_loader-1.0.201.dev0 → code_loader-1.0.202.dev1}/code_loader/contract/enums.py +0 -0
  15. {code_loader-1.0.201.dev0 → code_loader-1.0.202.dev1}/code_loader/contract/exceptions.py +0 -0
  16. {code_loader-1.0.201.dev0 → code_loader-1.0.202.dev1}/code_loader/contract/mapping.py +0 -0
  17. {code_loader-1.0.201.dev0 → code_loader-1.0.202.dev1}/code_loader/contract/responsedataclasses.py +0 -0
  18. {code_loader-1.0.201.dev0 → code_loader-1.0.202.dev1}/code_loader/contract/sim_config.py +0 -0
  19. {code_loader-1.0.201.dev0 → code_loader-1.0.202.dev1}/code_loader/default_losses.py +0 -0
  20. {code_loader-1.0.201.dev0 → code_loader-1.0.202.dev1}/code_loader/default_metrics.py +0 -0
  21. {code_loader-1.0.201.dev0 → code_loader-1.0.202.dev1}/code_loader/experiment_api/__init__.py +0 -0
  22. {code_loader-1.0.201.dev0 → code_loader-1.0.202.dev1}/code_loader/experiment_api/api.py +0 -0
  23. {code_loader-1.0.201.dev0 → code_loader-1.0.202.dev1}/code_loader/experiment_api/cli_config_utils.py +0 -0
  24. {code_loader-1.0.201.dev0 → code_loader-1.0.202.dev1}/code_loader/experiment_api/client.py +0 -0
  25. {code_loader-1.0.201.dev0 → code_loader-1.0.202.dev1}/code_loader/experiment_api/epoch.py +0 -0
  26. {code_loader-1.0.201.dev0 → code_loader-1.0.202.dev1}/code_loader/experiment_api/experiment.py +0 -0
  27. {code_loader-1.0.201.dev0 → code_loader-1.0.202.dev1}/code_loader/experiment_api/experiment_context.py +0 -0
  28. {code_loader-1.0.201.dev0 → code_loader-1.0.202.dev1}/code_loader/experiment_api/types.py +0 -0
  29. {code_loader-1.0.201.dev0 → code_loader-1.0.202.dev1}/code_loader/experiment_api/utils.py +0 -0
  30. {code_loader-1.0.201.dev0 → code_loader-1.0.202.dev1}/code_loader/experiment_api/workingspace_config_utils.py +0 -0
  31. {code_loader-1.0.201.dev0 → code_loader-1.0.202.dev1}/code_loader/inner_leap_binder/__init__.py +0 -0
  32. {code_loader-1.0.201.dev0 → code_loader-1.0.202.dev1}/code_loader/mixpanel_tracker.py +0 -0
  33. {code_loader-1.0.201.dev0 → code_loader-1.0.202.dev1}/code_loader/plot_functions/__init__.py +0 -0
  34. {code_loader-1.0.201.dev0 → code_loader-1.0.202.dev1}/code_loader/plot_functions/plot_functions.py +0 -0
  35. {code_loader-1.0.201.dev0 → code_loader-1.0.202.dev1}/code_loader/plot_functions/visualize.py +0 -0
  36. {code_loader-1.0.201.dev0 → code_loader-1.0.202.dev1}/code_loader/utils.py +0 -0
  37. {code_loader-1.0.201.dev0 → code_loader-1.0.202.dev1}/code_loader/visualizers/__init__.py +0 -0
@@ -1,9 +1,9 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: code-loader
3
- Version: 1.0.201.dev0
3
+ Version: 1.0.202.dev1
4
4
  Summary:
5
- Home-page: https://github.com/tensorleap/code-loader
6
5
  License: MIT
6
+ License-File: LICENSE
7
7
  Author: dorhar
8
8
  Author-email: doron.harnoy@tensorleap.ai
9
9
  Requires-Python: >=3.8,<3.13
@@ -20,6 +20,7 @@ Requires-Dist: numpy (>=2.3.2,<3.0.0) ; python_version >= "3.11" and python_vers
20
20
  Requires-Dist: psutil (>=5.9.5,<6.0.0)
21
21
  Requires-Dist: pyyaml (>=6.0.2,<7.0.0)
22
22
  Requires-Dist: requests (>=2.32.3,<3.0.0)
23
+ Project-URL: Homepage, https://github.com/tensorleap/code-loader
23
24
  Project-URL: Repository, https://github.com/tensorleap/code-loader
24
25
  Description-Content-Type: text/markdown
25
26
 
@@ -309,6 +309,7 @@ class MetadataHandler:
309
309
  class CustomLatentSpaceHandler:
310
310
  function: SectionCallableInterface
311
311
  name: str = 'custom_latent_space'
312
+ use_ls_for_analysis: bool = False
312
313
 
313
314
 
314
315
  # How a chain's latent-space vectors are derived from its per-step forward passes.
@@ -331,6 +332,11 @@ class AutoregressiveStepHandler:
331
332
  # time. Fills the role InputHandler.shape plays for input encoders.
332
333
  input_shapes: Optional[Dict[str, List[int]]] = None
333
334
  latent_space_aggregation: str = 'last_step'
335
+ # {model_input_name: model_output_name} for a KV-cache (or any threaded state): inputs whose
336
+ # value is the named output from the previous step. The platform threads these itself — they
337
+ # are never passed to the hook and never reported — so the hook must NOT return them. Empty
338
+ # when the model has no cache, in which case the chain re-feeds its full prompt each step.
339
+ cache_tensors: Optional[Dict[str, str]] = None
334
340
 
335
341
 
336
342
  # Per-chain, unbatched callables: called once per finished chain with the final step's tensors —
@@ -391,7 +397,7 @@ class DatasetIntegrationSetup:
391
397
  metrics: List[MetricHandler] = field(default_factory=list)
392
398
  instance_metrics: List[InstanceMetricHandler] = field(default_factory=list)
393
399
  custom_layers: Dict[str, CustomLayerHandler] = field(default_factory=dict)
394
- custom_latent_space: Optional[CustomLatentSpaceHandler] = None
400
+ custom_latent_spaces: Dict[str, CustomLatentSpaceHandler] = field(default_factory=dict)
395
401
  simulations: List[SimulationHandler] = field(default_factory=list)
396
402
  autoregressive_step: Optional[AutoregressiveStepHandler] = None
397
403
  autoregressive_metrics: List[AutoregressiveMetricHandler] = field(default_factory=list)
@@ -407,6 +413,6 @@ class DatasetSample:
407
413
  metadata_is_none: Dict[str, bool]
408
414
  index: Union[int, str]
409
415
  state: DataStateEnum
410
- custom_latent_space: Optional[npt.NDArray[np.float32]] = None
416
+ custom_latent_spaces: Optional[Dict[str, npt.NDArray[np.float32]]] = None
411
417
  instance_masks: Optional[Dict[str, ElementInstance]] = None
412
418
 
@@ -353,7 +353,7 @@ class LeapAudio:
353
353
  waveform (a LeapGraph). Build one with `default_audio_visualizer`.
354
354
 
355
355
  Attributes:
356
- audio (npt.NDArray): Mono waveform samples, shaped [N]. Multi-channel input
356
+ audio (npt.NDArray[np.float32]): Mono waveform samples, shaped [N]. Multi-channel input
357
357
  should be mixed down before constructing LeapAudio.
358
358
  sample_rate (int): Samples per second — needed to encode the clip and to map
359
359
  the playhead position to time.
@@ -362,7 +362,7 @@ class LeapAudio:
362
362
  driven by the audio element's currentTime, so no time range is needed here.
363
363
  type (LeapDataType): The data type, default is LeapDataType.Audio.
364
364
  """
365
- audio: npt.NDArray
365
+ audio: npt.NDArray[np.float32]
366
366
  sample_rate: int
367
367
  visual: Union[LeapImage, LeapGraph]
368
368
  type: LeapDataType = LeapDataType.Audio
@@ -534,23 +534,97 @@ class LeapBinder:
534
534
  leap_binder.set_metadata(metadata_handler_index, name='metadata_index')
535
535
  leap_binder.set_metadata(metadata_handler_image_mean, name='metadata_image_mean')
536
536
  """
537
+ if not isinstance(name, str):
538
+ raise TypeError(f"set_metadata validation failed.\n`name` must be a string, got type "
539
+ f"{type(name).__name__}. When using @tensorleap_metadata, the metadata "
540
+ f"types go in the second argument: "
541
+ f"@tensorleap_metadata('my_metadata', {{'key': DatasetMetadataType.int}}).")
537
542
  self.setup_container.metadata.append(MetadataHandler(name, function, metadata_type))
538
543
 
539
- def set_custom_latent_space(self, function: SectionCallableInterface) -> None:
544
+ def set_custom_latent_space(self, function: SectionCallableInterface,
545
+ name: Optional[str] = None,
546
+ use_ls_for_analysis: bool = False) -> None:
540
547
  """
541
- Set a custom latent space function.
548
+ Register a custom latent space function.
549
+
550
+ Multiple custom latent spaces may be registered as long as each has a
551
+ unique name. They are stored name-keyed (insertion order preserved), so a
552
+ later registration no longer overrides an earlier one.
542
553
 
543
554
  Args:
544
- function (SectionCallableInterface): The metadata handler function.
555
+ function (SectionCallableInterface): The latent-space handler function.
545
556
  This function receives:
546
557
  subset (PreprocessResponse): The subset of the data.
547
558
  index (int): The index of the sample within the subset.
548
- This function should numpy float32 array contains the latent space vec of the sample.
559
+ This function should return a numpy float32 array containing the latent
560
+ space vec of the sample.
561
+ name (Optional[str]): Unique name for this custom latent space. Defaults to
562
+ the reserved single-LS name for backward compatibility.
563
+ use_ls_for_analysis (bool): When True, the engine uses this custom latent
564
+ space for the Out-Of-Distribution and Domain-Gap insights instead of the
565
+ built-in defaults. At most one registered custom latent space may set this;
566
+ registering a second one with the flag raises.
549
567
  """
550
- self.setup_container.custom_latent_space = CustomLatentSpaceHandler(function)
568
+ if name is None:
569
+ name = custom_latent_space_attribute
570
+ if name in self.setup_container.custom_latent_spaces:
571
+ raise Exception(
572
+ f"A custom latent space named '{name}' is already registered. Each "
573
+ f"@tensorleap_custom_latent_space must have a unique name "
574
+ f"(pass name='...' to distinguish them)."
575
+ )
576
+ if use_ls_for_analysis:
577
+ already_flagged = [
578
+ existing_name
579
+ for existing_name, handler in self.setup_container.custom_latent_spaces.items()
580
+ if handler.use_ls_for_analysis
581
+ ]
582
+ if already_flagged:
583
+ raise Exception(
584
+ f"use_ls_for_analysis=True is already set on custom latent space "
585
+ f"'{already_flagged[0]}'. Only one custom latent space may set "
586
+ f"use_ls_for_analysis=True (it selects the latent space used for the "
587
+ f"Out-Of-Distribution and Domain-Gap insights). Set it on '{name}' "
588
+ f"or '{already_flagged[0]}', not both."
589
+ )
590
+ self.setup_container.custom_latent_spaces[name] = CustomLatentSpaceHandler(
591
+ function, name, use_ls_for_analysis)
592
+
593
+ @staticmethod
594
+ def _validated_cache_tensors(cache_tensors: Optional[Dict[str, str]]
595
+ ) -> Optional[Dict[str, str]]:
596
+ """Structural checks only — shape/dtype/existence are validated by the platform, which
597
+ is the side that has the model graph. This catches the typos that are cheap to catch
598
+ here, at integration-test time, instead of at push."""
599
+ if not cache_tensors:
600
+ return None
601
+ if not isinstance(cache_tensors, dict):
602
+ raise Exception(f'tensorleap_autoregressive_step: cache_tensors must be a dict of '
603
+ f'{{model_input_name: model_output_name}}, got '
604
+ f'{type(cache_tensors).__name__}.')
605
+ seen_outputs: Dict[str, str] = {}
606
+ for input_name, output_name in cache_tensors.items():
607
+ for role, name in (('key', input_name), ('value', output_name)):
608
+ if not isinstance(name, str) or not name:
609
+ raise Exception(
610
+ f'tensorleap_autoregressive_step: cache_tensors {role}s must be '
611
+ f'non-empty model tensor names, got {name!r}.')
612
+ if input_name == output_name:
613
+ raise Exception(
614
+ f'tensorleap_autoregressive_step: cache_tensors maps "{input_name}" to '
615
+ f'itself. A model input and output cannot share a name — the mapping is '
616
+ f'{{input_name: output_name}}.')
617
+ if output_name in seen_outputs:
618
+ raise Exception(
619
+ f'tensorleap_autoregressive_step: model output "{output_name}" is declared '
620
+ f'as the cache source for both "{seen_outputs[output_name]}" and '
621
+ f'"{input_name}". Each cache output feeds exactly one input.')
622
+ seen_outputs[output_name] = input_name
623
+ return dict(cache_tensors)
551
624
 
552
625
  def set_autoregressive_step(self, function: AutoregressiveStepCallableInterface,
553
- latent_space_aggregation: str = 'last_step') -> None:
626
+ latent_space_aggregation: str = 'last_step',
627
+ cache_tensors: Optional[Dict[str, str]] = None) -> None:
554
628
  """
555
629
  Set the autoregressive step hook — the feedback function that drives a chain:
556
630
  it supplies the model's initial inputs on its first call (prev_inputs=None, prev_outputs=None)
@@ -558,6 +632,8 @@ class LeapBinder:
558
632
  An autoregressive integration has no input encoders; the hook is the sole input source.
559
633
  latent_space_aggregation declares how the chain's latent-space vectors are derived from
560
634
  its steps (see AUTOREGRESSIVE_LATENT_SPACE_AGGREGATIONS).
635
+ cache_tensors declares a KV-cache as {model_input_name: model_output_name}; the platform
636
+ threads those tensors itself and the hook neither receives nor returns them.
561
637
  """
562
638
  if self.setup_container.autoregressive_step is not None:
563
639
  raise Exception('tensorleap_autoregressive_step is already defined. '
@@ -566,8 +642,10 @@ class LeapBinder:
566
642
  raise Exception(f'tensorleap_autoregressive_step: unknown latent_space_aggregation '
567
643
  f'{latent_space_aggregation!r}. Supported values: '
568
644
  f'{", ".join(AUTOREGRESSIVE_LATENT_SPACE_AGGREGATIONS)}.')
645
+ cache_tensors = self._validated_cache_tensors(cache_tensors)
569
646
  self.setup_container.autoregressive_step = AutoregressiveStepHandler(
570
- function, latent_space_aggregation=latent_space_aggregation)
647
+ function, latent_space_aggregation=latent_space_aggregation,
648
+ cache_tensors=cache_tensors)
571
649
 
572
650
  # Builtin chain metadata, declared at parse time so it survives the reporter's
573
651
  # metadata type mapping; the placeholder values are overwritten by the engine when a
@@ -958,7 +1036,7 @@ class LeapBinder:
958
1036
  "Element instances are not supported together with tensorleap_autoregressive_step: "
959
1037
  "instance generation masks the sample's encoded inputs, which do not exist in an "
960
1038
  "autoregressive integration. Remove the instance encoders or the autoregressive hook.")
961
- if self.setup_container.custom_latent_space is not None:
1039
+ if self.setup_container.custom_latent_spaces:
962
1040
  raise Exception(
963
1041
  "tensorleap_custom_latent_space is not supported together with "
964
1042
  "tensorleap_autoregressive_step: the custom latent space function only sees "
@@ -560,12 +560,22 @@ def tensorleap_load_model(prediction_types: Optional[List[PredictionTypeHandler]
560
560
  return result
561
561
 
562
562
  def validate_declared_prediction_types(self, ret):
563
- if not (len(self.prediction_types) == len(ret) if isinstance(ret, list) else 1) and len(
564
- self.prediction_types) != 0:
563
+ n_outputs = len(ret) if isinstance(ret, list) else 1
564
+ # A declared KV-cache is plumbing: those outputs are fed straight back into
565
+ # the model and never reported, so they carry no prediction type. Count only
566
+ # the rest, or every cached model would demand 60 meaningless declarations.
567
+ step = leap_binder.setup_container.autoregressive_step
568
+ n_cache = len((step.cache_tensors if step else None) or {})
569
+ if len(self.prediction_types) != 0 and \
570
+ n_outputs - n_cache != len(self.prediction_types):
565
571
  if not _call_from_tl_platform:
566
572
  update_env_params_func("tensorleap_load_model", "x")
573
+ cache_note = (f' ({n_cache} of them are the declared KV-cache, which '
574
+ f'needs none)' if n_cache else '')
567
575
  raise Exception(
568
- f"tensorleap_load_model validation failed: number of declared prediction types({len(prediction_types)}) != number of model outputs({len(ret) if isinstance(ret, list) else 1})")
576
+ f"tensorleap_load_model validation failed: number of declared "
577
+ f"prediction types({len(prediction_types)}) != number of model "
578
+ f"outputs({n_outputs}){cache_note}")
569
579
 
570
580
  def _convert_onnx_inputs_to_correct_type(
571
581
  self, float_arrays_inputs: Dict[str, np.ndarray]
@@ -723,12 +733,22 @@ def tensorleap_load_model(prediction_types: Optional[List[PredictionTypeHandler]
723
733
  assert isinstance(input_dict, dict), \
724
734
  f'Expected input_dict to be a dict, got {type(input_dict)} instead.'
725
735
  seen: set = set()
726
- for i, (input_key, elem) in enumerate(input_dict.items()):
736
+ # A declared KV-cache is threaded by the platform, not wired from an input
737
+ # encoder, so those slots have no mapping node to assign — the loop feeds
738
+ # them real arrays even in mapping mode. Skip them, and count only the wired
739
+ # inputs so the remaining slot indices stay contiguous.
740
+ step_handler = leap_binder.setup_container.autoregressive_step
741
+ cache_tensors = (step_handler.cache_tensors if step_handler else None) or {}
742
+ slot = 0
743
+ for input_key, elem in input_dict.items():
744
+ if input_key in cache_tensors:
745
+ continue
727
746
  self._reject_reused_input_source(elem, seen)
728
747
  if isinstance(input_key, NodeMappingType):
729
748
  elem.node_mapping.type = input_key
730
749
  else:
731
- elem.node_mapping.type = _safe_get_item(i)
750
+ elem.node_mapping.type = _safe_get_item(slot)
751
+ slot += 1
732
752
 
733
753
  return ModelOutputPlaceholder()
734
754
 
@@ -1703,8 +1723,14 @@ def tensorleap_metadata(
1703
1723
  return decorating_function
1704
1724
 
1705
1725
 
1706
- def tensorleap_custom_latent_space():
1726
+ def tensorleap_custom_latent_space(name: Optional[str] = None, use_ls_for_analysis: bool = False):
1727
+ assert isinstance(use_ls_for_analysis, bool), \
1728
+ ("tensorleap_custom_latent_space validation failed: use_ls_for_analysis must be a bool. "
1729
+ f"Got {type(use_ls_for_analysis)}.")
1730
+
1707
1731
  def decorating_function(user_function: SectionCallableInterface):
1732
+ ls_name = name if name is not None else user_function.__name__
1733
+
1708
1734
  def _validate_input_args(sample_id: Union[int, str, list], preprocess_response: PreprocessResponse):
1709
1735
  _validate_id_or_group(sample_id, preprocess_response, 'tensorleap_custom_latent_space')
1710
1736
 
@@ -1715,9 +1741,9 @@ def tensorleap_custom_latent_space():
1715
1741
  if single_result.ndim > 1:
1716
1742
  flat_dim = int(np.prod(single_result.shape))
1717
1743
  store_general_warning(
1718
- key=("tensorleap_custom_latent_space_flatten", tuple(single_result.shape)),
1744
+ key=("tensorleap_custom_latent_space_flatten", ls_name, tuple(single_result.shape)),
1719
1745
  message=(
1720
- f"tensorleap_custom_latent_space returned per-sample shape {tuple(single_result.shape)} "
1746
+ f"tensorleap_custom_latent_space '{ls_name}' returned per-sample shape {tuple(single_result.shape)} "
1721
1747
  f"(ndim={single_result.ndim}). Tensorleap assumes per-sample shape (d, ...) and will "
1722
1748
  f"flatten to ({flat_dim},) before downstream visualization and clustering. "
1723
1749
  f"If you want a different aggregation (e.g. global average pooling), do it "
@@ -1760,7 +1786,8 @@ def tensorleap_custom_latent_space():
1760
1786
 
1761
1787
  return result
1762
1788
 
1763
- leap_binder.set_custom_latent_space(inner_without_validate)
1789
+ leap_binder.set_custom_latent_space(inner_without_validate, ls_name,
1790
+ use_ls_for_analysis=use_ls_for_analysis)
1764
1791
 
1765
1792
  def inner(sample_id, preprocess_response):
1766
1793
  if os.environ.get(mapping_runtime_mode_env_var_mame):
@@ -1877,11 +1904,16 @@ class _MappingStatePlaceholder:
1877
1904
 
1878
1905
 
1879
1906
  class _ModelLoopContext:
1880
- def __init__(self, sample_id, preprocess_response, prediction_names, is_mapping):
1907
+ def __init__(self, sample_id, preprocess_response, prediction_names, is_mapping,
1908
+ cache_tensors=None):
1881
1909
  self.sample_id = sample_id
1882
1910
  self.preprocess_response = preprocess_response
1883
1911
  self.prediction_names = prediction_names
1884
1912
  self.is_mapping = is_mapping
1913
+ # Declared KV-cache, {model_input_name: model_output_name}. These tensors are supplied
1914
+ # by the loop (and by the platform at runtime), NOT by the hook, so both the fed-inputs
1915
+ # check and the output-count check have to account for them.
1916
+ self.cache_tensors = dict(cache_tensors or {})
1885
1917
  self.phase = 'awaiting_first_hook'
1886
1918
  self.steps = 0
1887
1919
  self.fed_inputs = None
@@ -1890,6 +1922,9 @@ class _ModelLoopContext:
1890
1922
  self.last_outputs_fingerprint = None
1891
1923
  self.last_state = None
1892
1924
  self.last_state_fingerprint = None
1925
+ # Declared-cache outputs of the last model call, in graph order (predictions first, so
1926
+ # this is the tail). Read by the loop through the model proxy.
1927
+ self.last_cache_outputs = []
1893
1928
 
1894
1929
  def _fail(self, message):
1895
1930
  raise LeapValidationError(f'tensorleap_model_loop validation failed: {message}')
@@ -1974,12 +2009,19 @@ class _ModelLoopContext:
1974
2009
  'next_inputs the loop must exit without further hook or model calls.')
1975
2010
  if self.is_mapping:
1976
2011
  return
2012
+ # A declared KV-cache is threaded by the platform, so the hook neither receives nor
2013
+ # returns it and the loop is expected to supply it. Exclude those keys from the
2014
+ # identity check rather than demanding the hook produce tensors it cannot see.
2015
+ if isinstance(fed, dict) and self.cache_tensors:
2016
+ fed = {key: value for key, value in fed.items() if key not in self.cache_tensors}
1977
2017
  fed_ids = {id(leaf) for leaf in _ndarray_leaves(fed)}
1978
2018
  hook_ids = {id(value) for value in self.fed_inputs.values()}
1979
2019
  if fed_ids != hook_ids:
2020
+ extra = (' (the declared KV-cache inputs are exempt — those are threaded for you)'
2021
+ if self.cache_tensors else '')
1980
2022
  self._fail('the model must be fed exactly the tensors the last hook call returned — '
1981
2023
  'any computation between the hook and the model is invisible to the '
1982
- 'platform. Move it into the hook.')
2024
+ f'platform. Move it into the hook.{extra}')
1983
2025
  if _nest_fingerprint(self.fed_inputs) != self.fed_inputs_fingerprint:
1984
2026
  self._fail('the model inputs were mutated in place after the hook returned them — '
1985
2027
  'the platform feeds the model the tensors exactly as the hook returned '
@@ -1991,12 +2033,24 @@ class _ModelLoopContext:
1991
2033
  self.last_outputs = raw_outputs
1992
2034
  return raw_outputs
1993
2035
  outputs_list = raw_outputs if isinstance(raw_outputs, list) else [raw_outputs]
1994
- if len(outputs_list) != len(self.prediction_names):
2036
+ # Cache outputs are plumbing, not predictions: they are fed straight back into the model
2037
+ # and never reported, so they need no prediction type. Only the remainder is keyed.
2038
+ n_cache_outputs = len(self.cache_tensors)
2039
+ if len(outputs_list) - n_cache_outputs != len(self.prediction_names):
2040
+ cache_note = (f' ({n_cache_outputs} of them are the declared KV-cache, which needs '
2041
+ f'no prediction types)' if n_cache_outputs else '')
1995
2042
  self._fail(f'the model returned {len(outputs_list)} outputs but '
1996
2043
  f'{len(self.prediction_names)} prediction types are declared on '
1997
- f'tensorleap_load_model — declare one prediction type per model output.')
1998
- named_outputs = {name: np.asarray(output)
1999
- for name, output in zip(self.prediction_names, outputs_list)}
2044
+ f'tensorleap_load_model{cache_note} — declare one prediction type per '
2045
+ f'non-cache model output.')
2046
+ # Predictions come first in graph order, as the export declares them; the cache follows.
2047
+ named_outputs = {name: np.asarray(output) for name, output
2048
+ in zip(self.prediction_names, outputs_list)}
2049
+ # The hook must receive predictions only, exactly as on the platform, and must receive
2050
+ # this very dict (on_hook_call checks identity). The loop still needs the cache, because
2051
+ # locally it stands in for the platform and threads it by hand — so that goes out on the
2052
+ # proxy rather than being mixed into the hook's view.
2053
+ self.last_cache_outputs = list(outputs_list[len(self.prediction_names):])
2000
2054
  self.last_outputs = named_outputs
2001
2055
  self.last_outputs_fingerprint = _nest_fingerprint(named_outputs)
2002
2056
  return named_outputs
@@ -2018,8 +2072,19 @@ class _ModelLoopModelProxy:
2018
2072
  def get_inputs(self):
2019
2073
  return self._model.get_inputs()
2020
2074
 
2075
+ @property
2076
+ def cache_outputs(self):
2077
+ """Declared-cache outputs of the last model call, in graph order.
2021
2078
 
2022
- def tensorleap_autoregressive_step(latent_space_aggregation: str = 'last_step'):
2079
+ Only the local loop needs these: it stands in for the platform, which threads the cache
2080
+ for you at runtime. They are kept off the hook's `prev_outputs` deliberately — the hook
2081
+ must not see the cache in either place, or the local run would diverge from the real one.
2082
+ """
2083
+ return self._context.last_cache_outputs
2084
+
2085
+
2086
+ def tensorleap_autoregressive_step(latent_space_aggregation: str = 'last_step',
2087
+ cache_tensors: Optional[Dict[str, str]] = None):
2023
2088
  """The feedback hook that drives an autoregressive chain.
2024
2089
 
2025
2090
  Signature of the decorated function:
@@ -2043,6 +2108,22 @@ def tensorleap_autoregressive_step(latent_space_aggregation: str = 'last_step'):
2043
2108
  space comes from the final step's forward pass, except input-kind latent spaces which come
2044
2109
  from the first step — the original sample, before generated content dominates the model
2045
2110
  inputs. 'mean': every latent space is the elementwise mean over all steps of the chain.
2111
+
2112
+ cache_tensors declares a KV-cache (or any threaded state) as
2113
+ {model_input_name: model_output_name} — model inputs whose value is the named output from
2114
+ the previous step. The platform threads those tensors itself and holds them on the GPU, so
2115
+ the hook neither receives them in prev_inputs/prev_outputs nor returns them in next_inputs;
2116
+ return only the inputs you actually compute (e.g. input_ids, position_ids, attention_mask).
2117
+ The cache must be fixed-capacity: the declared input and output must have the same, fully
2118
+ concrete shape, since chains at different steps are batched together and the cache is
2119
+ preallocated. Exports with a growing cache (past length P in, P+1 out) are rejected at push.
2120
+
2121
+ @tensorleap_autoregressive_step(cache_tensors={
2122
+ f'past_key_values.{i}.{kv}': f'present.{i}.{kv}'
2123
+ for i in range(30) for kv in ('key', 'value')
2124
+ })
2125
+
2126
+ Omit it for a model with no cache — the chain then re-feeds its whole prompt every step.
2046
2127
  """
2047
2128
  assert isinstance(latent_space_aggregation, str), \
2048
2129
  ('tensorleap_autoregressive_step must be called with parentheses: '
@@ -2190,7 +2271,8 @@ def tensorleap_autoregressive_step(latent_space_aggregation: str = 'last_step'):
2190
2271
  return result
2191
2272
 
2192
2273
  leap_binder.set_autoregressive_step(inner_without_validate,
2193
- latent_space_aggregation=latent_space_aggregation)
2274
+ latent_space_aggregation=latent_space_aggregation,
2275
+ cache_tensors=cache_tensors)
2194
2276
 
2195
2277
  def inner(*args, **kwargs):
2196
2278
  if not _call_from_tl_platform:
@@ -2390,8 +2472,9 @@ def tensorleap_model_loop():
2390
2472
  if not prediction_names:
2391
2473
  _fail('tensorleap_model_loop requires prediction types declared on '
2392
2474
  'tensorleap_load_model — the loop keys the model outputs by their names.')
2393
- context = _ModelLoopContext(sample_id, preprocess_response, prediction_names,
2394
- is_mapping)
2475
+ context = _ModelLoopContext(
2476
+ sample_id, preprocess_response, prediction_names, is_mapping,
2477
+ cache_tensors=leap_binder.setup_container.autoregressive_step.cache_tensors)
2395
2478
  _active_model_loop = context
2396
2479
  try:
2397
2480
  result = user_function(_ModelLoopModelProxy(model, context), sample_id,
@@ -283,17 +283,7 @@ class LeapLoader(LeapLoaderBase):
283
283
 
284
284
  metadata, metadata_is_none = self.get_metadata(state, sample_id)
285
285
 
286
- custom_latent_space = None
287
- if global_leap_binder.setup_container.custom_latent_space is not None:
288
- latent_fn = global_leap_binder.setup_container.custom_latent_space.function
289
- preprocess_state = preprocess_result[state]
290
- if preprocess_state.is_grouped:
291
- # Single-sample fetch: encode this sample only, not the whole group (same
292
- # memory rationale as _get_dataset_handlers; see grouped-fetch-oom-bug.md).
293
- self._locate_group(preprocess_state, sample_id) # validates group membership
294
- custom_latent_space = self._to_grouped_list(latent_fn([sample_id], preprocess_state))[0]
295
- else:
296
- custom_latent_space = latent_fn(sample_id, preprocess_state)
286
+ custom_latent_spaces = self._get_custom_latent_spaces(sample_id, preprocess_result[state])
297
287
  instance_mask = self._get_instances_masks(state, sample_id, instance_id)
298
288
  sample = DatasetSample(inputs=self._get_inputs(state, sample_id),
299
289
  gt=None if state == DataStateEnum.unlabeled else self._get_gt(state, sample_id),
@@ -301,7 +291,7 @@ class LeapLoader(LeapLoaderBase):
301
291
  metadata_is_none=metadata_is_none,
302
292
  index=sample_id,
303
293
  state=state,
304
- custom_latent_space=custom_latent_space,
294
+ custom_latent_spaces=custom_latent_spaces,
305
295
  instance_masks=instance_mask)
306
296
  return sample
307
297
 
@@ -332,11 +322,7 @@ class LeapLoader(LeapLoaderBase):
332
322
  handler.name, handler_result
333
323
  )
334
324
 
335
- custom_latent_space = None
336
- if global_leap_binder.setup_container.custom_latent_space is not None:
337
- custom_latent_space = global_leap_binder.setup_container.custom_latent_space.function(
338
- original_sample_id, preprocess
339
- )
325
+ custom_latent_spaces = self._get_custom_latent_spaces(original_sample_id, preprocess)
340
326
 
341
327
  return DatasetSample(
342
328
  inputs=inputs,
@@ -345,7 +331,7 @@ class LeapLoader(LeapLoaderBase):
345
331
  metadata_is_none=metadata_is_none,
346
332
  index=synthetic_index,
347
333
  state=DataStateEnum.additional,
348
- custom_latent_space=custom_latent_space,
334
+ custom_latent_spaces=custom_latent_spaces,
349
335
  instance_masks=None,
350
336
  )
351
337
 
@@ -1114,17 +1100,20 @@ class LeapLoader(LeapLoaderBase):
1114
1100
  metadata[handler.name] = [v for v, _ in converted]
1115
1101
  metadata_is_none[handler.name] = [is_none for _, is_none in converted]
1116
1102
 
1117
- # custom_latent_space is group-aware like the input/GT encoders: hand it the whole group in a
1118
- # single call so a file-backed latent fn loads the file once, and normalize to (B, d). This
1119
- # keeps the mandatory group path non-lossy. instance_masks stay None here: they are a
1120
- # per-(sample, instance) concern that needs an instance_id the group fetch does not carry.
1121
- custom_latent_space = None
1122
- if global_leap_binder.setup_container.custom_latent_space is not None:
1123
- latent_fn = global_leap_binder.setup_container.custom_latent_space.function
1124
- custom_latent_space = self._to_grouped_list(latent_fn(group_ids, preprocess_state))
1103
+ # custom_latent_spaces are group-aware like the input/GT encoders: hand each the whole group
1104
+ # in a single call so a file-backed latent fn loads the file once, and normalize to per-sample
1105
+ # lists. This keeps the mandatory group path non-lossy. instance_masks stay None here: they are
1106
+ # a per-(sample, instance) concern that needs an instance_id the group fetch does not carry.
1107
+ latent_handlers = global_leap_binder.setup_container.custom_latent_spaces
1108
+ custom_latent_spaces = None
1109
+ if latent_handlers:
1110
+ custom_latent_spaces = {
1111
+ name: self._to_grouped_list(handler.function(group_ids, preprocess_state))
1112
+ for name, handler in latent_handlers.items()
1113
+ }
1125
1114
 
1126
1115
  return DatasetSample(inputs=inputs, gt=gt, metadata=metadata, metadata_is_none=metadata_is_none,
1127
- index=list(group_ids), state=state, custom_latent_space=custom_latent_space,
1116
+ index=list(group_ids), state=state, custom_latent_spaces=custom_latent_spaces,
1128
1117
  instance_masks=None)
1129
1118
 
1130
1119
  def _get_inputs(self, state: DataStateEnum, sample_id: Union[int, str]) -> Dict[str, npt.NDArray[np.float32]]:
@@ -1337,10 +1326,48 @@ class LeapLoader(LeapLoaderBase):
1337
1326
 
1338
1327
  return id_type
1339
1328
 
1329
+ def _get_custom_latent_spaces(
1330
+ self,
1331
+ sample_id: Union[int, str],
1332
+ preprocess: "PreprocessResponse") -> Optional[Dict[str, npt.NDArray[np.float32]]]:
1333
+ handlers = global_leap_binder.setup_container.custom_latent_spaces
1334
+ if not handlers:
1335
+ return None
1336
+ if preprocess.is_grouped:
1337
+ # Single-sample fetch: encode this sample only, not the whole group (same
1338
+ # memory rationale as _get_dataset_handlers; see grouped-fetch-oom-bug.md).
1339
+ self._locate_group(preprocess, sample_id) # validates group membership
1340
+ return {name: self._to_grouped_list(handler.function([sample_id], preprocess))[0]
1341
+ for name, handler in handlers.items()}
1342
+ return {name: handler.function(sample_id, preprocess) for name, handler in handlers.items()}
1343
+
1340
1344
  @lru_cache()
1341
1345
  def has_custom_latent_space_decorator(self) -> bool:
1342
1346
  self.exec_script()
1343
- return global_leap_binder.setup_container.custom_latent_space is not None
1347
+ return len(global_leap_binder.setup_container.custom_latent_spaces) > 0
1348
+
1349
+ @lru_cache()
1350
+ def get_custom_latent_space_names(self) -> Tuple[str, ...]:
1351
+ """Ordered names of all registered custom latent spaces.
1352
+
1353
+ Registration order is the canonical index mapping consumed by the engine
1354
+ (name i -> user_custom_i). Returns a tuple so the lru_cache value is hashable.
1355
+ """
1356
+ self.exec_script()
1357
+ return tuple(global_leap_binder.setup_container.custom_latent_spaces.keys())
1358
+
1359
+ @lru_cache()
1360
+ def get_custom_latent_space_for_analysis(self) -> Optional[str]:
1361
+ """Name of the custom latent space flagged with use_ls_for_analysis=True, if any.
1362
+
1363
+ The engine uses this latent space for the Out-Of-Distribution and Domain-Gap
1364
+ insights instead of the built-in defaults. Returns None when no custom latent
1365
+ space set the flag. At most one is flagged (enforced at registration time)."""
1366
+ self.exec_script()
1367
+ for name, handler in global_leap_binder.setup_container.custom_latent_spaces.items():
1368
+ if handler.use_ls_for_analysis:
1369
+ return name
1370
+ return None
1344
1371
 
1345
1372
  @lru_cache()
1346
1373
  def has_autoregressive_step(self) -> bool:
@@ -1355,6 +1382,20 @@ class LeapLoader(LeapLoaderBase):
1355
1382
  return 'last_step'
1356
1383
  return handler.latent_space_aggregation
1357
1384
 
1385
+ @lru_cache()
1386
+ def get_autoregressive_cache_tensors(self) -> Dict[str, str]:
1387
+ """Declared KV-cache pairing, {model_input_name: model_output_name}.
1388
+
1389
+ Empty when nothing is declared — the platform then runs the chain uncached. The platform
1390
+ validates these names against the real model graph at push time; only structural checks
1391
+ happen here.
1392
+ """
1393
+ self.exec_script()
1394
+ handler = global_leap_binder.setup_container.autoregressive_step
1395
+ if handler is None or not handler.cache_tensors:
1396
+ return {}
1397
+ return dict(handler.cache_tensors)
1398
+
1358
1399
  def run_autoregressive_step(self, sample_id: Union[int, str],
1359
1400
  prev_inputs: Optional[Dict[str, npt.NDArray[np.float32]]],
1360
1401
  prev_outputs: Optional[Dict[str, npt.NDArray[np.float32]]],
@@ -176,6 +176,11 @@ class LeapLoaderBase:
176
176
  raise NotImplementedError(f'{type(self).__name__} does not implement '
177
177
  'get_autoregressive_latent_space_aggregation.')
178
178
 
179
+ @abstractmethod
180
+ def get_autoregressive_cache_tensors(self) -> Dict[str, str]:
181
+ raise NotImplementedError(f'{type(self).__name__} does not implement '
182
+ 'get_autoregressive_cache_tensors.')
183
+
179
184
  @abstractmethod
180
185
  def get_autoregressive_decorator_names(self) -> Dict[str, List[str]]:
181
186
  raise NotImplementedError(f'{type(self).__name__} does not implement '
@@ -218,6 +223,14 @@ class LeapLoaderBase:
218
223
  raise NotImplementedError(f'{type(self).__name__} does not implement '
219
224
  'autoregressive_visualizer_by_name.')
220
225
 
226
+ @abstractmethod
227
+ def get_custom_latent_space_names(self) -> Tuple[str, ...]:
228
+ pass
229
+
230
+ @abstractmethod
231
+ def get_custom_latent_space_for_analysis(self) -> Optional[str]:
232
+ pass
233
+
221
234
  @abstractmethod
222
235
  def get_heatmap_visualizer_raw_vis_input_arg_name(self, visualizer_name: str) -> Optional[str]:
223
236
  pass
@@ -33,7 +33,7 @@ def default_video_visualizer(data: npt.NDArray[np.float32]) -> LeapVideo:
33
33
  _MAX_WAVEFORM_POINTS = 2000
34
34
 
35
35
 
36
- def _to_mono(audio: npt.NDArray) -> npt.NDArray:
36
+ def _to_mono(audio: npt.NDArray[np.float32]) -> npt.NDArray[np.float32]:
37
37
  a = np.squeeze(np.asarray(audio))
38
38
  if a.ndim == 2: # mix down: channels are the shorter axis
39
39
  a = a.mean(axis=int(np.argmin(a.shape)))
@@ -42,7 +42,7 @@ def _to_mono(audio: npt.NDArray) -> npt.NDArray:
42
42
  return a.astype(np.float32)
43
43
 
44
44
 
45
- def _waveform_graph(series: npt.NDArray, duration: float) -> LeapGraph:
45
+ def _waveform_graph(series: npt.NDArray[np.float32], duration: float) -> LeapGraph:
46
46
  s = np.squeeze(np.asarray(series)).astype(np.float32)
47
47
  # ponytail: recharts chokes on 100k+ points; stride down for the default view. x_range keeps full time span.
48
48
  step = max(1, s.shape[0] // _MAX_WAVEFORM_POINTS)
@@ -50,9 +50,9 @@ def _waveform_graph(series: npt.NDArray, duration: float) -> LeapGraph:
50
50
  return LeapGraph(s, x_label='Time [s]', y_label='Amplitude', x_range=(0.0, duration))
51
51
 
52
52
 
53
- def default_audio_visualizer(audio: npt.NDArray,
53
+ def default_audio_visualizer(audio: npt.NDArray[np.float32],
54
54
  sample_rate: int,
55
- visual: Optional[npt.NDArray] = None,
55
+ visual: Optional[npt.NDArray[np.float32]] = None,
56
56
  time_axis_index: int = -1) -> LeapAudio:
57
57
  """Package a playable audio sample plus the visual a playhead runs through.
58
58
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "code-loader"
3
- version = "1.0.201.dev0"
3
+ version = "1.0.202.dev1"
4
4
  description = ""
5
5
  authors = ["dorhar <doron.harnoy@tensorleap.ai>"]
6
6
  license = "MIT"