code-loader 1.0.197.dev1__py3-none-any.whl → 1.0.197.dev2__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/contract/datasetclasses.py +66 -10
- code_loader/inner_leap_binder/leapbinder.py +75 -7
- code_loader/inner_leap_binder/leapbinder_decorators.py +219 -62
- code_loader/leaploader.py +289 -9
- code_loader/utils.py +10 -0
- {code_loader-1.0.197.dev1.dist-info → code_loader-1.0.197.dev2.dist-info}/METADATA +1 -1
- {code_loader-1.0.197.dev1.dist-info → code_loader-1.0.197.dev2.dist-info}/RECORD +9 -9
- {code_loader-1.0.197.dev1.dist-info → code_loader-1.0.197.dev2.dist-info}/LICENSE +0 -0
- {code_loader-1.0.197.dev1.dist-info → code_loader-1.0.197.dev2.dist-info}/WHEEL +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import warnings
|
|
2
2
|
from dataclasses import dataclass, field
|
|
3
|
-
from typing import Any, Callable, List, Optional, Dict, Union, Type, Literal, Tuple
|
|
3
|
+
from typing import Any, Callable, List, Optional, Dict, Union, Type, Literal, Tuple, cast
|
|
4
4
|
import re
|
|
5
5
|
import numpy as np
|
|
6
6
|
import numpy.typing as npt
|
|
@@ -15,6 +15,8 @@ custom_latent_space_attribute = "custom_latent_space"
|
|
|
15
15
|
|
|
16
16
|
_simulation_context: Dict[str, bool] = {"active": False}
|
|
17
17
|
|
|
18
|
+
SampleId = Union[int, str]
|
|
19
|
+
|
|
18
20
|
|
|
19
21
|
@dataclass
|
|
20
22
|
class PreprocessResponse:
|
|
@@ -39,27 +41,54 @@ class PreprocessResponse:
|
|
|
39
41
|
"""
|
|
40
42
|
length: Optional[int] = None # Deprecated. Please use sample_ids instead
|
|
41
43
|
data: Any = None
|
|
42
|
-
sample_ids: Optional[Union[List[
|
|
44
|
+
sample_ids: Optional[Union[List[SampleId], List[List[SampleId]]]] = None
|
|
43
45
|
state: Optional[DataStateType] = None
|
|
44
46
|
sample_id_type: Optional[Union[Type[str], Type[int]]] = None
|
|
45
47
|
sample_ids_to_instance_mappings: Optional[Dict[str, List[str]]] = None # in use only for element instance
|
|
46
48
|
instance_to_sample_ids_mappings: Optional[Dict[str, str]] = None # in use only for element instance
|
|
47
49
|
tl_generated: bool = False
|
|
50
|
+
_grouped: bool = field(default=False, init=False, repr=False, compare=False)
|
|
51
|
+
_group_pos_cache: Optional[Dict[SampleId, Tuple[List[SampleId], int]]] = field(
|
|
52
|
+
default=None, init=False, repr=False, compare=False)
|
|
48
53
|
|
|
49
54
|
def __post_init__(self) -> None:
|
|
50
55
|
assert self.sample_ids_to_instance_mappings is None, f"Keep sample_ids_to_instance_mappings None when initializing PreprocessResponse"
|
|
51
56
|
assert self.instance_to_sample_ids_mappings is None, f"Keep instance_to_sample_ids_mappings None when initializing PreprocessResponse"
|
|
52
57
|
|
|
53
58
|
if self.length is not None and self.sample_ids is None:
|
|
54
|
-
self.sample_ids = [i for i in range(self.length)]
|
|
59
|
+
self.sample_ids = cast(List[SampleId], [i for i in range(self.length)])
|
|
55
60
|
self.sample_id_type = int
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if self.
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
self._grouped = False
|
|
62
|
+
elif self.sample_ids is not None:
|
|
63
|
+
given_length = self.length
|
|
64
|
+
self._grouped = len(self.sample_ids) > 0 and isinstance(self.sample_ids[0], list)
|
|
65
|
+
if self._grouped:
|
|
66
|
+
groups = cast(List[List[SampleId]], self.sample_ids)
|
|
67
|
+
assert len(groups) >= 1, "Grouped PreprocessResponse must have at least one group."
|
|
68
|
+
for group in groups:
|
|
69
|
+
assert isinstance(group, list) and len(group) > 0, \
|
|
70
|
+
"Each group in a grouped PreprocessResponse must be a non-empty list."
|
|
71
|
+
self.sample_id_type = type(groups[0][0])
|
|
72
|
+
assert issubclass(self.sample_id_type, (str, int)), \
|
|
73
|
+
f"Sample id should be of type str or int. Got: {self.sample_id_type.__name__}"
|
|
74
|
+
for group in groups:
|
|
75
|
+
for sample_id in group:
|
|
76
|
+
assert isinstance(sample_id, self.sample_id_type), \
|
|
77
|
+
f"Sample id should be of type {self.sample_id_type.__name__}. Got: {type(sample_id)}"
|
|
78
|
+
self.length = sum(len(group) for group in groups)
|
|
79
|
+
else:
|
|
80
|
+
flat_ids = cast(List[SampleId], self.sample_ids)
|
|
81
|
+
self.length = len(flat_ids)
|
|
82
|
+
if self.sample_id_type is None:
|
|
83
|
+
self.sample_id_type = str
|
|
84
|
+
if self.sample_id_type == str:
|
|
85
|
+
for sample_id in flat_ids:
|
|
86
|
+
assert isinstance(sample_id, str), f"Sample id should be of type str. Got: {type(sample_id)}"
|
|
87
|
+
# dataclasses.replace() re-invokes __init__ with both fields already populated
|
|
88
|
+
# (length previously derived from sample_ids); only reject a genuine mismatch.
|
|
89
|
+
if given_length is not None:
|
|
90
|
+
assert given_length == self.length, \
|
|
91
|
+
f"Inconsistent PreprocessResponse: length={given_length} but sample_ids implies {self.length}."
|
|
63
92
|
else:
|
|
64
93
|
raise Exception("length is deprecated, please use sample_ids instead.")
|
|
65
94
|
|
|
@@ -79,9 +108,36 @@ class PreprocessResponse:
|
|
|
79
108
|
return id(self)
|
|
80
109
|
|
|
81
110
|
def __len__(self) -> int:
|
|
111
|
+
assert self.length is not None
|
|
112
|
+
return self.length
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def is_grouped(self) -> bool:
|
|
116
|
+
return self._grouped
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def num_groups(self) -> int:
|
|
120
|
+
# Grouped: the number of groups. Flat: the (flat) sample count, since sample_ids is the
|
|
121
|
+
# flat id list and there is one sample per "group".
|
|
82
122
|
assert self.sample_ids is not None
|
|
83
123
|
return len(self.sample_ids)
|
|
84
124
|
|
|
125
|
+
@property
|
|
126
|
+
def groups(self) -> Optional[List[List[SampleId]]]:
|
|
127
|
+
# The nested list of groups when grouped, otherwise None. Callers should
|
|
128
|
+
# branch on is_grouped before relying on this.
|
|
129
|
+
return cast(List[List[SampleId]], self.sample_ids) if self._grouped else None
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def flat_sample_ids(self) -> List[SampleId]:
|
|
133
|
+
# All sample ids in order: concatenation of the groups when grouped,
|
|
134
|
+
# otherwise the flat sample_ids list as-is.
|
|
135
|
+
assert self.sample_ids is not None
|
|
136
|
+
if self._grouped:
|
|
137
|
+
groups = cast(List[List[SampleId]], self.sample_ids)
|
|
138
|
+
return [sample_id for group in groups for sample_id in group]
|
|
139
|
+
return cast(List[SampleId], self.sample_ids)
|
|
140
|
+
|
|
85
141
|
@dataclass
|
|
86
142
|
class ElementInstance:
|
|
87
143
|
name: str
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
|
|
2
|
+
import builtins
|
|
2
3
|
import inspect
|
|
3
|
-
|
|
4
|
+
import os
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from typing import Callable, List, Optional, Dict, Any, Type, Union, get_args, cast, Iterator, Set
|
|
4
7
|
|
|
5
8
|
import numpy as np
|
|
6
9
|
import numpy.typing as npt
|
|
@@ -16,7 +19,8 @@ from code_loader.contract.datasetclasses import SectionCallableInterface, InputH
|
|
|
16
19
|
SimulationHandler, _simulation_context, AutoregressiveStepHandler, AutoregressiveStepCallableInterface, \
|
|
17
20
|
AUTOREGRESSIVE_LATENT_SPACE_AGGREGATIONS, AUTOREGRESSIVE_IMPLICIT_ARG_NAMES, \
|
|
18
21
|
AutoregressiveMetricHandler, AutoregressiveLossHandler, AutoregressiveVisualizerHandler
|
|
19
|
-
from code_loader.contract.enums import LeapDataType, DataStateEnum, DataStateType, MetricDirection, DatasetMetadataType
|
|
22
|
+
from code_loader.contract.enums import LeapDataType, DataStateEnum, DataStateType, MetricDirection, DatasetMetadataType, \
|
|
23
|
+
TestingSectionEnum
|
|
20
24
|
from code_loader.contract.mapping import NodeConnection, NodeMapping, NodeMappingType
|
|
21
25
|
from code_loader.contract.responsedataclasses import DatasetTestResultPayload, LeapAnalysisConfiguration
|
|
22
26
|
from code_loader.contract.visualizer_classes import map_leap_data_type_to_visualizer_class
|
|
@@ -33,6 +37,25 @@ from code_loader.visualizers.default_visualizers import DefaultVisualizer, \
|
|
|
33
37
|
mapping_runtime_mode_env_var_mame = '__MAPPING_RUNTIME_MODE__'
|
|
34
38
|
|
|
35
39
|
|
|
40
|
+
@contextmanager
|
|
41
|
+
def _track_opened_files() -> Iterator[Set[str]]:
|
|
42
|
+
opened: Set[str] = set()
|
|
43
|
+
original_open = builtins.open
|
|
44
|
+
|
|
45
|
+
def tracking_open(file: Any, *args: Any, **kwargs: Any) -> Any:
|
|
46
|
+
mode = args[0] if args else kwargs.get('mode', 'r')
|
|
47
|
+
is_read_mode = isinstance(mode, str) and not any(c in mode for c in ('a', 'w', 'x'))
|
|
48
|
+
if is_read_mode and isinstance(file, (str, os.PathLike)):
|
|
49
|
+
opened.add(os.fspath(file))
|
|
50
|
+
return original_open(file, *args, **kwargs)
|
|
51
|
+
|
|
52
|
+
builtins.open = tracking_open
|
|
53
|
+
try:
|
|
54
|
+
yield opened
|
|
55
|
+
finally:
|
|
56
|
+
builtins.open = original_open
|
|
57
|
+
|
|
58
|
+
|
|
36
59
|
def _stringized_annotation_type_name(annotation: Any) -> Optional[str]:
|
|
37
60
|
"""Bare type name if ``annotation`` is a stringized annotation (from ``from __future__
|
|
38
61
|
import annotations`` or a quoted hint), else ``None``. code_loader inspects raw
|
|
@@ -585,7 +608,9 @@ class LeapBinder:
|
|
|
585
608
|
# Builtin chain metadata, declared at parse time so it survives the reporter's
|
|
586
609
|
# metadata type mapping; the placeholder values are overwritten by the engine when a
|
|
587
610
|
# chain finalizes (realized length, truncated-by-safety-cap flag).
|
|
588
|
-
def builtin_chain_metadata(idx: Any, preprocess: PreprocessResponse) ->
|
|
611
|
+
def builtin_chain_metadata(idx: Any, preprocess: PreprocessResponse) -> Any:
|
|
612
|
+
if isinstance(idx, list):
|
|
613
|
+
return [{'length': -1, 'truncated': False} for _ in idx]
|
|
589
614
|
return {'length': -1, 'truncated': False}
|
|
590
615
|
|
|
591
616
|
self.set_metadata(builtin_chain_metadata, 'builtin_chain')
|
|
@@ -749,6 +774,15 @@ class LeapBinder:
|
|
|
749
774
|
if state_enum in preprocess_result_dict:
|
|
750
775
|
preprocess_result_dict_in_correct_order[state_enum] = preprocess_result_dict[state_enum]
|
|
751
776
|
|
|
777
|
+
# All-or-none: every state must agree on grouped (nested sample_ids) vs flat.
|
|
778
|
+
if len({r.is_grouped for r in preprocess_result_dict_in_correct_order.values()}) > 1:
|
|
779
|
+
shapes = ', '.join(
|
|
780
|
+
f"{state.name}={'grouped' if r.is_grouped else 'flat'}"
|
|
781
|
+
for state, r in preprocess_result_dict_in_correct_order.items())
|
|
782
|
+
raise Exception(
|
|
783
|
+
"All preprocess states must be either all grouped (nested sample_ids) or all flat, "
|
|
784
|
+
f"but got a mix: {shapes}.")
|
|
785
|
+
|
|
752
786
|
return preprocess_result_dict_in_correct_order
|
|
753
787
|
|
|
754
788
|
def get_preprocess_unlabeled_result(self) -> Optional[PreprocessResponse]:
|
|
@@ -769,7 +803,34 @@ class LeapBinder:
|
|
|
769
803
|
preprocess_response: PreprocessResponse, test_result: List[DatasetTestResultPayload],
|
|
770
804
|
dataset_base_handler: Union[DatasetBaseHandler, MetadataHandler], state: DataStateEnum) -> List[DatasetTestResultPayload]:
|
|
771
805
|
assert preprocess_response.sample_ids is not None
|
|
772
|
-
|
|
806
|
+
opened_files: Optional[Set[str]] = None
|
|
807
|
+
if preprocess_response.is_grouped:
|
|
808
|
+
# Grouped: probe with the first group, then reduce to a single sample's result so the
|
|
809
|
+
# recorded shape/type is per-sample (matching the flat case), not the (B, *) batch.
|
|
810
|
+
# (casts: sample_ids[0] is a group list here, and the grouped result is a per-sample list.)
|
|
811
|
+
group = preprocess_response.sample_ids[0]
|
|
812
|
+
with _track_opened_files() as opened_files:
|
|
813
|
+
raw_result = cast(Any, dataset_base_handler.function(cast(Any, group), preprocess_response))[0]
|
|
814
|
+
else:
|
|
815
|
+
raw_result = dataset_base_handler.function(
|
|
816
|
+
cast(Union[int, str], preprocess_response.sample_ids[0]), preprocess_response)
|
|
817
|
+
|
|
818
|
+
def _warn_if_group_spans_multiple_files(payloads: List[DatasetTestResultPayload]) -> None:
|
|
819
|
+
if opened_files is None or len(opened_files) <= 1:
|
|
820
|
+
return
|
|
821
|
+
shown = sorted(opened_files)[:5]
|
|
822
|
+
suffix = f" (+{len(opened_files) - 5} more)" if len(opened_files) > 5 else ""
|
|
823
|
+
warning = (
|
|
824
|
+
f"Encoding declared group 0 (size {len(cast(List[Any], group))}) for '{dataset_base_handler.name}' opened "
|
|
825
|
+
f"{len(opened_files)} distinct files: {shown}{suffix}. If this group is meant to be one "
|
|
826
|
+
f"physical source (e.g. one Parquet file), it may be grouped incorrectly - check your "
|
|
827
|
+
f"preprocess grouping logic. (Only files opened via Python's open() are tracked, so this "
|
|
828
|
+
f"can miss other I/O paths or be a false positive if grouping isn't meant to reflect file "
|
|
829
|
+
f"locality.)")
|
|
830
|
+
for payload in payloads:
|
|
831
|
+
existing = payload.display.get(TestingSectionEnum.Errors.name, '')
|
|
832
|
+
payload.display[TestingSectionEnum.Errors.name] = (existing + '\n' if existing else '') + warning
|
|
833
|
+
|
|
773
834
|
handler_type = 'metadata' if isinstance(dataset_base_handler, MetadataHandler) else None
|
|
774
835
|
if isinstance(dataset_base_handler, MetadataHandler):
|
|
775
836
|
if isinstance(raw_result, dict):
|
|
@@ -809,6 +870,7 @@ class LeapBinder:
|
|
|
809
870
|
else:
|
|
810
871
|
if raw_result is None:
|
|
811
872
|
if state != DataStateEnum.training:
|
|
873
|
+
_warn_if_group_spans_multiple_files(test_result)
|
|
812
874
|
return test_result
|
|
813
875
|
|
|
814
876
|
if dataset_base_handler.metadata_type is None:
|
|
@@ -830,6 +892,7 @@ class LeapBinder:
|
|
|
830
892
|
# setting shape in setup for all encoders
|
|
831
893
|
if isinstance(dataset_base_handler, (InputHandler, GroundTruthHandler)):
|
|
832
894
|
dataset_base_handler.shape = result_shape
|
|
895
|
+
_warn_if_group_spans_multiple_files(test_result)
|
|
833
896
|
return test_result
|
|
834
897
|
|
|
835
898
|
def check_handlers(self, preprocess_result: Dict[DataStateEnum, PreprocessResponse]) -> None:
|
|
@@ -865,10 +928,15 @@ class LeapBinder:
|
|
|
865
928
|
preprocess_response.tl_generated = True
|
|
866
929
|
if not preprocess_response.length or preprocess_response.length < 1:
|
|
867
930
|
raise Exception("Simulation '{}' returned PreprocessResponse with length < 1".format(sim.name))
|
|
868
|
-
preprocess_response.
|
|
931
|
+
if preprocess_response.is_grouped:
|
|
932
|
+
raise Exception(
|
|
933
|
+
"Simulation '{}' returned a grouped PreprocessResponse; simulations must "
|
|
934
|
+
"return a flat (non-grouped) PreprocessResponse.".format(sim.name))
|
|
935
|
+
preprocess_response.sample_ids = cast(List[Union[int, str]], [0])
|
|
936
|
+
sim_sample_ids = cast(List[Union[int, str]], preprocess_response.sample_ids)
|
|
869
937
|
for handler in self.setup_container.inputs:
|
|
870
|
-
out1 = handler.function(
|
|
871
|
-
out2 = handler.function(
|
|
938
|
+
out1 = handler.function(sim_sample_ids[0], preprocess_response)
|
|
939
|
+
out2 = handler.function(sim_sample_ids[0], preprocess_response)
|
|
872
940
|
if not np.allclose(out1, out2):
|
|
873
941
|
raise Exception(
|
|
874
942
|
"Simulation '{}': encoder '{}' is non-deterministic — consecutive calls with seed=0 returned different outputs".format(
|
|
@@ -40,6 +40,7 @@ _called_from_inside_tl_integration_test_decorator = False
|
|
|
40
40
|
# to tell a raise in the test body (report as code-flow failure) from a module-level crash
|
|
41
41
|
# before any test ran (report as a plain script crash).
|
|
42
42
|
_integration_test_started = False
|
|
43
|
+
_mapping_dataset_is_grouped = False
|
|
43
44
|
_call_from_tl_platform = os.environ.get('IS_TENSORLEAP_PLATFORM') == 'true'
|
|
44
45
|
|
|
45
46
|
# ---- warnings store (module-level) ----
|
|
@@ -213,6 +214,53 @@ def batch_warning(result, func_name):
|
|
|
213
214
|
)
|
|
214
215
|
|
|
215
216
|
|
|
217
|
+
def _validate_id_or_group(sample_id, preprocess_response, func_name):
|
|
218
|
+
"""Accept either a single sample id (flat dataset) or a group — a list of ids (grouped
|
|
219
|
+
dataset). The argument shape must match the dataset: a group requires a grouped
|
|
220
|
+
PreprocessResponse and a single id requires a flat one. Every id must match the declared
|
|
221
|
+
sample_id_type."""
|
|
222
|
+
is_group = isinstance(sample_id, list)
|
|
223
|
+
if is_group and not preprocess_response.is_grouped:
|
|
224
|
+
raise AssertionError(
|
|
225
|
+
f'{func_name}() validation failed: got a group (list of sample ids) but the '
|
|
226
|
+
f'PreprocessResponse is not grouped. Pass a single sample id for a flat dataset.')
|
|
227
|
+
if not is_group and preprocess_response.is_grouped:
|
|
228
|
+
raise AssertionError(
|
|
229
|
+
f'{func_name}() validation failed: got a single sample id but the PreprocessResponse '
|
|
230
|
+
f'is grouped. Pass a group (list of sample ids) for a grouped dataset.')
|
|
231
|
+
ids = sample_id if is_group else [sample_id]
|
|
232
|
+
for sid in ids:
|
|
233
|
+
assert type(sid) == preprocess_response.sample_id_type, \
|
|
234
|
+
(f'{func_name}() validation failed: '
|
|
235
|
+
f'Argument sample_id should be as the same type as defined in the preprocess response '
|
|
236
|
+
f'{preprocess_response.sample_id_type}. Got {type(sid)}.')
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _validate_grouped_result(result, group_size, func_name, validate_single):
|
|
240
|
+
"""A grouped (group) encoder result must be either a single ndarray with leading dim
|
|
241
|
+
== group_size, or a list of `group_size` per-sample results (each validated by
|
|
242
|
+
validate_single). Metadata validates its grouped result separately (list only).
|
|
243
|
+
An `(B, *)` array is validated per-sample slice (not as the whole batched array) so
|
|
244
|
+
per-sample checks (e.g. channel_dim <= rank) use the sample shape, not the batch shape."""
|
|
245
|
+
if isinstance(result, np.ndarray):
|
|
246
|
+
assert len(result.shape) > 0 and result.shape[0] == group_size, \
|
|
247
|
+
(f'{func_name}() validation failed: expected a grouped output for a group of '
|
|
248
|
+
f'{group_size}: an array with leading dim {group_size}, got shape {result.shape}.')
|
|
249
|
+
for single in result:
|
|
250
|
+
validate_single(single)
|
|
251
|
+
elif isinstance(result, list):
|
|
252
|
+
assert len(result) == group_size, \
|
|
253
|
+
(f'{func_name}() validation failed: expected a grouped output for a group of '
|
|
254
|
+
f'{group_size}: a list of {group_size} arrays, got {len(result)}.')
|
|
255
|
+
for r in result:
|
|
256
|
+
validate_single(r)
|
|
257
|
+
else:
|
|
258
|
+
raise AssertionError(
|
|
259
|
+
f'{func_name}() validation failed: expected a grouped output for a group of '
|
|
260
|
+
f'{group_size}: either an array with leading dim {group_size} or a list of '
|
|
261
|
+
f'{group_size} arrays, got {type(result)}.')
|
|
262
|
+
|
|
263
|
+
|
|
216
264
|
def _add_mapping_connection(user_unique_name, connection_destinations, arg_names, name, node_mapping_type):
|
|
217
265
|
connection_destinations = [connection_destination for connection_destination in connection_destinations
|
|
218
266
|
if not isinstance(connection_destination, SamplePreprocessResponse)]
|
|
@@ -290,11 +338,7 @@ def tensorleap_integration_test():
|
|
|
290
338
|
|
|
291
339
|
def _validate_input_args(*args, **kwargs):
|
|
292
340
|
sample_id, preprocess_response = args
|
|
293
|
-
|
|
294
|
-
f"tensorleap_integration_test validation failed: "
|
|
295
|
-
f"sample_id type ({type(sample_id).__name__}) does not match the expected "
|
|
296
|
-
f"type ({preprocess_response.sample_id_type}) from the PreprocessResponse."
|
|
297
|
-
)
|
|
341
|
+
_validate_id_or_group(sample_id, preprocess_response, 'tensorleap_integration_test')
|
|
298
342
|
|
|
299
343
|
def inner(*args, **kwargs):
|
|
300
344
|
if not _call_from_tl_platform:
|
|
@@ -305,7 +349,7 @@ def tensorleap_integration_test():
|
|
|
305
349
|
expected_names = inspect.getfullargspec(integration_test_function)[0][:2]
|
|
306
350
|
if len(expected_names) < 2:
|
|
307
351
|
expected_names = ['idx', 'preprocess']
|
|
308
|
-
validate_args_structure(*args, types_order=[Union[int, str], PreprocessResponse],
|
|
352
|
+
validate_args_structure(*args, types_order=[Union[int, str, list], PreprocessResponse],
|
|
309
353
|
func_name='integration_test', expected_names=expected_names,
|
|
310
354
|
**kwargs)
|
|
311
355
|
sample_id, preprocess_response = (
|
|
@@ -329,6 +373,8 @@ def tensorleap_integration_test():
|
|
|
329
373
|
_reset_model_loop_state()
|
|
330
374
|
ret = integration_test_function(*args, **kwargs)
|
|
331
375
|
|
|
376
|
+
global _mapping_dataset_is_grouped
|
|
377
|
+
_mapping_dataset_is_grouped = args[1].is_grouped
|
|
332
378
|
try:
|
|
333
379
|
os.environ[mapping_runtime_mode_env_var_mame] = 'True'
|
|
334
380
|
_reset_model_loop_state()
|
|
@@ -349,6 +395,7 @@ def tensorleap_integration_test():
|
|
|
349
395
|
f'Integration test is only allowed to call Tensorleap decorators. '
|
|
350
396
|
f'Ensure any arithmetics, external library use, Python logic is placed within Tensorleap decoders')
|
|
351
397
|
finally:
|
|
398
|
+
_mapping_dataset_is_grouped = False
|
|
352
399
|
if mapping_runtime_mode_env_var_mame in os.environ:
|
|
353
400
|
del os.environ[mapping_runtime_mode_env_var_mame]
|
|
354
401
|
finally:
|
|
@@ -639,10 +686,29 @@ def tensorleap_load_model(prediction_types: Optional[List[PredictionTypeHandler]
|
|
|
639
686
|
|
|
640
687
|
class ModelPlaceholder:
|
|
641
688
|
|
|
689
|
+
@staticmethod
|
|
690
|
+
def _reject_reused_input_source(elem, seen):
|
|
691
|
+
# A model input slot is wired by mutating elem.node_mapping.type, so the same
|
|
692
|
+
# source object at two positions silently overwrites the first slot. Each input
|
|
693
|
+
# encoder mints its own NodeMapping, so a collision means one source fed two
|
|
694
|
+
# slots — e.g. indexing a single grouped input, model([imgs[0], imgs[1]]), where
|
|
695
|
+
# imgs[i] returns the same node (a group is the batch dim of ONE input, not many
|
|
696
|
+
# inputs), or the flat model([img, img]). Reject it instead of mis-wiring.
|
|
697
|
+
if id(elem.node_mapping) in seen:
|
|
698
|
+
raise Exception(
|
|
699
|
+
"The same input source was passed to more than one model input slot. Each "
|
|
700
|
+
"model input must come from a distinct input encoder. Indexing a single "
|
|
701
|
+
"grouped input (e.g. model([imgs[0], imgs[1]])) feeds several samples of ONE "
|
|
702
|
+
"input into separate slots — a group is the batch dimension of one input, "
|
|
703
|
+
"not multiple inputs.")
|
|
704
|
+
seen.add(id(elem.node_mapping))
|
|
705
|
+
|
|
642
706
|
# keras interface
|
|
643
707
|
def __call__(self, arg):
|
|
644
708
|
if isinstance(arg, list):
|
|
709
|
+
seen: set = set()
|
|
645
710
|
for i, elem in enumerate(arg):
|
|
711
|
+
self._reject_reused_input_source(elem, seen)
|
|
646
712
|
elem.node_mapping.type = _safe_get_item(i)
|
|
647
713
|
else:
|
|
648
714
|
arg.node_mapping.type = NodeMappingType.Input0
|
|
@@ -656,7 +722,9 @@ def tensorleap_load_model(prediction_types: Optional[List[PredictionTypeHandler]
|
|
|
656
722
|
"use model.run(None, inputs) to return all outputs.")
|
|
657
723
|
assert isinstance(input_dict, dict), \
|
|
658
724
|
f'Expected input_dict to be a dict, got {type(input_dict)} instead.'
|
|
725
|
+
seen: set = set()
|
|
659
726
|
for i, (input_key, elem) in enumerate(input_dict.items()):
|
|
727
|
+
self._reject_reused_input_source(elem, seen)
|
|
660
728
|
if isinstance(input_key, NodeMappingType):
|
|
661
729
|
elem.node_mapping.type = input_key
|
|
662
730
|
else:
|
|
@@ -1561,13 +1629,10 @@ def tensorleap_metadata(
|
|
|
1561
1629
|
raise Exception(f'Metadata with name {name} already exists. '
|
|
1562
1630
|
f'Please choose another')
|
|
1563
1631
|
|
|
1564
|
-
def _validate_input_args(sample_id: Union[int, str], preprocess_response: PreprocessResponse):
|
|
1565
|
-
|
|
1566
|
-
(f'{user_function.__name__}() validation failed: '
|
|
1567
|
-
f'Argument sample_id should be as the same type as defined in the preprocess response '
|
|
1568
|
-
f'{preprocess_response.sample_id_type}. Got {type(sample_id)}.')
|
|
1632
|
+
def _validate_input_args(sample_id: Union[int, str, list], preprocess_response: PreprocessResponse):
|
|
1633
|
+
_validate_id_or_group(sample_id, preprocess_response, user_function.__name__)
|
|
1569
1634
|
|
|
1570
|
-
def
|
|
1635
|
+
def _validate_single(result):
|
|
1571
1636
|
supported_result_types = (type(None), int, str, bool, float, dict, np.floating,
|
|
1572
1637
|
np.bool_, np.unsignedinteger, np.signedinteger, np.integer)
|
|
1573
1638
|
if isinstance(result, tuple):
|
|
@@ -1585,6 +1650,20 @@ def tensorleap_metadata(
|
|
|
1585
1650
|
(f'{user_function.__name__}() validation failed: '
|
|
1586
1651
|
f'Values in the return dict should be of type {str(supported_result_types)}. Got {type(value)}.')
|
|
1587
1652
|
|
|
1653
|
+
def _validate_result(result, grouped=False, group_size=None):
|
|
1654
|
+
if not grouped:
|
|
1655
|
+
_validate_single(result)
|
|
1656
|
+
return
|
|
1657
|
+
# Grouped metadata: a list of `group_size` per-sample scalars/dicts.
|
|
1658
|
+
assert isinstance(result, list), \
|
|
1659
|
+
(f'{user_function.__name__}() validation failed: expected a grouped output for a group '
|
|
1660
|
+
f'of {group_size}: a list of {group_size} metadata values, got {type(result)}.')
|
|
1661
|
+
assert len(result) == group_size, \
|
|
1662
|
+
(f'{user_function.__name__}() validation failed: expected a grouped output for a group '
|
|
1663
|
+
f'of {group_size}: a list of {group_size} metadata values, got {len(result)}.')
|
|
1664
|
+
for value in result:
|
|
1665
|
+
_validate_single(value)
|
|
1666
|
+
|
|
1588
1667
|
def inner_without_validate(sample_id, preprocess_response):
|
|
1589
1668
|
|
|
1590
1669
|
global _called_from_inside_tl_decorator
|
|
@@ -1604,14 +1683,16 @@ def tensorleap_metadata(
|
|
|
1604
1683
|
set_current('tensorleap_metadata')
|
|
1605
1684
|
if os.environ.get(mapping_runtime_mode_env_var_mame):
|
|
1606
1685
|
return None
|
|
1607
|
-
validate_args_structure(*args, types_order=[Union[int, str], PreprocessResponse],
|
|
1686
|
+
validate_args_structure(*args, types_order=[Union[int, str, list], PreprocessResponse],
|
|
1608
1687
|
func_name=user_function.__name__, expected_names=["idx", "preprocess"], **kwargs)
|
|
1609
1688
|
sample_id, preprocess_response = args if len(args) != 0 else kwargs.values()
|
|
1610
1689
|
_validate_input_args(sample_id, preprocess_response)
|
|
1611
1690
|
|
|
1691
|
+
grouped = isinstance(sample_id, list)
|
|
1692
|
+
group_size = len(sample_id) if grouped else None
|
|
1612
1693
|
result = inner_without_validate(sample_id, preprocess_response)
|
|
1613
1694
|
|
|
1614
|
-
_validate_result(result)
|
|
1695
|
+
_validate_result(result, grouped, group_size)
|
|
1615
1696
|
if not _call_from_tl_platform:
|
|
1616
1697
|
update_env_params_func("tensorleap_metadata", "v")
|
|
1617
1698
|
return result
|
|
@@ -1628,35 +1709,51 @@ def tensorleap_custom_latent_space(name: Optional[str] = None, use_ls_for_analys
|
|
|
1628
1709
|
|
|
1629
1710
|
def decorating_function(user_function: SectionCallableInterface):
|
|
1630
1711
|
ls_name = name if name is not None else user_function.__name__
|
|
1631
|
-
def _validate_input_args(sample_id: Union[int, str], preprocess_response: PreprocessResponse):
|
|
1632
|
-
assert isinstance(sample_id, (int, str)), \
|
|
1633
|
-
(f'tensorleap_custom_latent_space validation failed: '
|
|
1634
|
-
f'Argument sample_id should be either int or str. Got {type(sample_id)}.')
|
|
1635
|
-
assert isinstance(preprocess_response, PreprocessResponse), \
|
|
1636
|
-
(f'tensorleap_custom_latent_space validation failed: '
|
|
1637
|
-
f'Argument preprocess_response should be a PreprocessResponse. Got {type(preprocess_response)}.')
|
|
1638
|
-
assert type(sample_id) == preprocess_response.sample_id_type, \
|
|
1639
|
-
(f'tensorleap_custom_latent_space validation failed: '
|
|
1640
|
-
f'Argument sample_id should be as the same type as defined in the preprocess response '
|
|
1641
|
-
f'{preprocess_response.sample_id_type}. Got {type(sample_id)}.')
|
|
1642
1712
|
|
|
1643
|
-
def
|
|
1644
|
-
|
|
1713
|
+
def _validate_input_args(sample_id: Union[int, str, list], preprocess_response: PreprocessResponse):
|
|
1714
|
+
_validate_id_or_group(sample_id, preprocess_response, 'tensorleap_custom_latent_space')
|
|
1715
|
+
|
|
1716
|
+
def _validate_single(single_result):
|
|
1717
|
+
assert isinstance(single_result, np.ndarray), \
|
|
1645
1718
|
(f'tensorleap_custom_latent_space validation failed: '
|
|
1646
|
-
f'The return type should be a numpy array. Got {type(
|
|
1647
|
-
if
|
|
1648
|
-
flat_dim = int(np.prod(
|
|
1719
|
+
f'The return type should be a numpy array. Got {type(single_result)}.')
|
|
1720
|
+
if single_result.ndim > 1:
|
|
1721
|
+
flat_dim = int(np.prod(single_result.shape))
|
|
1649
1722
|
store_general_warning(
|
|
1650
|
-
key=("tensorleap_custom_latent_space_flatten", ls_name, tuple(
|
|
1723
|
+
key=("tensorleap_custom_latent_space_flatten", ls_name, tuple(single_result.shape)),
|
|
1651
1724
|
message=(
|
|
1652
|
-
f"tensorleap_custom_latent_space '{ls_name}' returned per-sample shape {tuple(
|
|
1653
|
-
f"(ndim={
|
|
1725
|
+
f"tensorleap_custom_latent_space '{ls_name}' returned per-sample shape {tuple(single_result.shape)} "
|
|
1726
|
+
f"(ndim={single_result.ndim}). Tensorleap assumes per-sample shape (d, ...) and will "
|
|
1654
1727
|
f"flatten to ({flat_dim},) before downstream visualization and clustering. "
|
|
1655
1728
|
f"If you want a different aggregation (e.g. global average pooling), do it "
|
|
1656
1729
|
f"inside your function."
|
|
1657
1730
|
),
|
|
1658
1731
|
)
|
|
1659
1732
|
|
|
1733
|
+
def _validate_result(result, grouped=False, group_size=None):
|
|
1734
|
+
if not grouped:
|
|
1735
|
+
_validate_single(result)
|
|
1736
|
+
return
|
|
1737
|
+
# Grouped: (B, *) array or list of B per-sample arrays. Validate each per-sample slice so
|
|
1738
|
+
# the per-sample flatten check/warning uses the sample shape, not the (B, *) batch shape.
|
|
1739
|
+
if isinstance(result, np.ndarray):
|
|
1740
|
+
assert result.ndim >= 1 and result.shape[0] == group_size, \
|
|
1741
|
+
(f'tensorleap_custom_latent_space validation failed: expected a grouped output for a '
|
|
1742
|
+
f'group of {group_size}: an array with leading dim {group_size}, got shape {result.shape}.')
|
|
1743
|
+
for single in result:
|
|
1744
|
+
_validate_single(single)
|
|
1745
|
+
elif isinstance(result, list):
|
|
1746
|
+
assert len(result) == group_size, \
|
|
1747
|
+
(f'tensorleap_custom_latent_space validation failed: expected a grouped output for a '
|
|
1748
|
+
f'group of {group_size}: a list of {group_size} arrays, got {len(result)}.')
|
|
1749
|
+
for single in result:
|
|
1750
|
+
_validate_single(single)
|
|
1751
|
+
else:
|
|
1752
|
+
raise AssertionError(
|
|
1753
|
+
f'tensorleap_custom_latent_space validation failed: expected a grouped output for a '
|
|
1754
|
+
f'group of {group_size}: either an array with leading dim {group_size} or a list of '
|
|
1755
|
+
f'{group_size} arrays, got {type(result)}.')
|
|
1756
|
+
|
|
1660
1757
|
def inner_without_validate(sample_id, preprocess_response):
|
|
1661
1758
|
global _called_from_inside_tl_decorator
|
|
1662
1759
|
_called_from_inside_tl_decorator += 1
|
|
@@ -1677,9 +1774,11 @@ def tensorleap_custom_latent_space(name: Optional[str] = None, use_ls_for_analys
|
|
|
1677
1774
|
|
|
1678
1775
|
_validate_input_args(sample_id, preprocess_response)
|
|
1679
1776
|
|
|
1777
|
+
grouped = isinstance(sample_id, list)
|
|
1778
|
+
group_size = len(sample_id) if grouped else None
|
|
1680
1779
|
result = inner_without_validate(sample_id, preprocess_response)
|
|
1681
1780
|
|
|
1682
|
-
_validate_result(result)
|
|
1781
|
+
_validate_result(result, grouped, group_size)
|
|
1683
1782
|
return result
|
|
1684
1783
|
|
|
1685
1784
|
return inner
|
|
@@ -2648,6 +2747,15 @@ def tensorleap_preprocess():
|
|
|
2648
2747
|
assert len(set(result)) == len(result), \
|
|
2649
2748
|
(f'{user_function.__name__}() validation failed: '
|
|
2650
2749
|
f'The return list should not contain duplicate PreprocessResponse objects.')
|
|
2750
|
+
# All-or-none: a dataset is either all grouped (nested sample_ids) or all flat.
|
|
2751
|
+
if len({response.is_grouped for response in result}) != 1:
|
|
2752
|
+
shapes = ', '.join(
|
|
2753
|
+
f"#{i}={'grouped' if response.is_grouped else 'flat'}"
|
|
2754
|
+
for i, response in enumerate(result))
|
|
2755
|
+
raise AssertionError(
|
|
2756
|
+
f'{user_function.__name__}() validation failed: '
|
|
2757
|
+
f'All PreprocessResponses must be either all grouped (nested sample_ids) '
|
|
2758
|
+
f'or all flat, but got a mix: {shapes}.')
|
|
2651
2759
|
|
|
2652
2760
|
def inner(*args, **kwargs):
|
|
2653
2761
|
if not _call_from_tl_platform:
|
|
@@ -2765,6 +2873,13 @@ def tensorleap_element_instance_preprocess(
|
|
|
2765
2873
|
result = user_function()
|
|
2766
2874
|
found_instance_metadata = False
|
|
2767
2875
|
for preprocess_response in result:
|
|
2876
|
+
if preprocess_response.is_grouped:
|
|
2877
|
+
raise Exception(
|
|
2878
|
+
"tensorleap_element_instance_preprocess validation failed: a grouped "
|
|
2879
|
+
"(nested sample_ids) PreprocessResponse cannot also declare element "
|
|
2880
|
+
"instances -- instance ids are derived per scalar sample id, and the "
|
|
2881
|
+
"engine's grouped/batch fetch path has no per-instance id to mask by. "
|
|
2882
|
+
"Remove the instance encoders or don't group this dataset.")
|
|
2768
2883
|
sample_ids_to_instance_mappings = {}
|
|
2769
2884
|
instance_to_sample_ids_mappings = {}
|
|
2770
2885
|
all_sample_ids = preprocess_response.sample_ids.copy()
|
|
@@ -2985,14 +3100,10 @@ def tensorleap_input_encoder(name: str, channel_dim=_UNSET, model_input_index=No
|
|
|
2985
3100
|
f"channel axis in the batched tensor (batch = axis 0), so 0 is invalid. Use 1 "
|
|
2986
3101
|
f"for channel-first (NCHW) and -1 for channel-last (NHWC).")
|
|
2987
3102
|
|
|
2988
|
-
def _validate_input_args(sample_id: Union[int, str], preprocess_response: PreprocessResponse):
|
|
2989
|
-
|
|
2990
|
-
(f'{user_function.__name__}() validation failed: '
|
|
2991
|
-
f'Argument sample_id should be as the same type as defined in the preprocess response '
|
|
2992
|
-
f'{preprocess_response.sample_id_type}. Got {type(sample_id)}.')
|
|
3103
|
+
def _validate_input_args(sample_id: Union[int, str, list], preprocess_response: PreprocessResponse):
|
|
3104
|
+
_validate_id_or_group(sample_id, preprocess_response, user_function.__name__)
|
|
2993
3105
|
|
|
2994
|
-
def
|
|
2995
|
-
validate_output_structure(result, func_name=user_function.__name__, expected_type_name="np.ndarray")
|
|
3106
|
+
def _validate_single(result):
|
|
2996
3107
|
assert isinstance(result, np.ndarray), \
|
|
2997
3108
|
(f'{user_function.__name__}() validation failed: '
|
|
2998
3109
|
f'Unsupported return type. Should be a numpy array. Got {type(result)}.')
|
|
@@ -3002,6 +3113,13 @@ def tensorleap_input_encoder(name: str, channel_dim=_UNSET, model_input_index=No
|
|
|
3002
3113
|
assert channel_dim - 1 <= len(result.shape), (f'{user_function.__name__}() validation failed: '
|
|
3003
3114
|
f'The channel_dim ({channel_dim}) should be <= to the rank of the resulting input rank ({len(result.shape)}).')
|
|
3004
3115
|
|
|
3116
|
+
def _validate_result(result, grouped=False, group_size=None):
|
|
3117
|
+
if not grouped:
|
|
3118
|
+
validate_output_structure(result, func_name=user_function.__name__, expected_type_name="np.ndarray")
|
|
3119
|
+
_validate_single(result)
|
|
3120
|
+
return
|
|
3121
|
+
_validate_grouped_result(result, group_size, user_function.__name__, _validate_single)
|
|
3122
|
+
|
|
3005
3123
|
def inner_without_validate(sample_id, preprocess_response):
|
|
3006
3124
|
global _called_from_inside_tl_decorator
|
|
3007
3125
|
_called_from_inside_tl_decorator += 1
|
|
@@ -3018,18 +3136,29 @@ def tensorleap_input_encoder(name: str, channel_dim=_UNSET, model_input_index=No
|
|
|
3018
3136
|
def inner(*args, **kwargs):
|
|
3019
3137
|
if not _call_from_tl_platform:
|
|
3020
3138
|
set_current("tensorleap_input_encoder")
|
|
3021
|
-
validate_args_structure(*args, types_order=[Union[int, str], PreprocessResponse],
|
|
3139
|
+
validate_args_structure(*args, types_order=[Union[int, str, list], PreprocessResponse],
|
|
3022
3140
|
func_name=user_function.__name__, expected_names=["idx", "preprocess"], **kwargs)
|
|
3023
3141
|
sample_id, preprocess_response = args if len(args) != 0 else kwargs.values()
|
|
3024
3142
|
_validate_input_args(sample_id, preprocess_response)
|
|
3025
3143
|
|
|
3144
|
+
grouped = isinstance(sample_id, list)
|
|
3145
|
+
group_size = len(sample_id) if grouped else None
|
|
3026
3146
|
result = inner_without_validate(sample_id, preprocess_response)
|
|
3027
3147
|
|
|
3028
|
-
_validate_result(result)
|
|
3148
|
+
_validate_result(result, grouped, group_size)
|
|
3029
3149
|
|
|
3030
3150
|
if _called_from_inside_tl_decorator == 0 and _called_from_inside_tl_integration_test_decorator:
|
|
3031
|
-
|
|
3032
|
-
|
|
3151
|
+
if grouped:
|
|
3152
|
+
# A grouped result is a list of per-sample arrays (never stacked, so a
|
|
3153
|
+
# B=1-only model can be fed one sample at a time). A user may also return a
|
|
3154
|
+
# supported (B, *) array; split it back into per-sample rows first (mirrors
|
|
3155
|
+
# LeapLoader._to_grouped_list) so both forms match the runtime path. Then add
|
|
3156
|
+
# a batch dim to each sample, mirroring the flat path's single-sample expand_dims.
|
|
3157
|
+
rows = result if isinstance(result, list) else [row for row in np.asarray(result)]
|
|
3158
|
+
result = [np.expand_dims(r, axis=0) for r in rows]
|
|
3159
|
+
else:
|
|
3160
|
+
batch_warning(result, user_function.__name__)
|
|
3161
|
+
result = np.expand_dims(result, axis=0)
|
|
3033
3162
|
# Emit integration test event once per test
|
|
3034
3163
|
try:
|
|
3035
3164
|
emit_integration_event_once(AnalyticsEvent.INPUT_ENCODER_INTEGRATION_TEST, {
|
|
@@ -3050,8 +3179,15 @@ def tensorleap_input_encoder(name: str, channel_dim=_UNSET, model_input_index=No
|
|
|
3050
3179
|
inner.node_mapping = NodeMapping(name, node_mapping_type)
|
|
3051
3180
|
|
|
3052
3181
|
def mapping_inner(*args, **kwargs):
|
|
3053
|
-
|
|
3054
|
-
|
|
3182
|
+
if _mapping_dataset_is_grouped:
|
|
3183
|
+
class TempMapping:
|
|
3184
|
+
def __getitem__(self, key):
|
|
3185
|
+
# Indexing a grouped input group (input_list[i]) selects a sample from the
|
|
3186
|
+
# same input source; the model wiring is identical to the un-grouped input.
|
|
3187
|
+
return self
|
|
3188
|
+
else:
|
|
3189
|
+
class TempMapping:
|
|
3190
|
+
pass
|
|
3055
3191
|
|
|
3056
3192
|
ret = TempMapping()
|
|
3057
3193
|
ret.node_mapping = mapping_inner.node_mapping
|
|
@@ -3081,15 +3217,10 @@ def tensorleap_gt_encoder(name: str):
|
|
|
3081
3217
|
raise Exception(f'GT with name {name} already exists. '
|
|
3082
3218
|
f'Please choose another')
|
|
3083
3219
|
|
|
3084
|
-
def _validate_input_args(sample_id: Union[int, str], preprocess_response: PreprocessResponse):
|
|
3085
|
-
|
|
3086
|
-
(f'{user_function.__name__}() validation failed: '
|
|
3087
|
-
f'Argument sample_id should be as the same type as defined in the preprocess response '
|
|
3088
|
-
f'{preprocess_response.sample_id_type}. Got {type(sample_id)}.')
|
|
3220
|
+
def _validate_input_args(sample_id: Union[int, str, list], preprocess_response: PreprocessResponse):
|
|
3221
|
+
_validate_id_or_group(sample_id, preprocess_response, user_function.__name__)
|
|
3089
3222
|
|
|
3090
|
-
def
|
|
3091
|
-
validate_output_structure(result, func_name=user_function.__name__, expected_type_name="np.ndarray",
|
|
3092
|
-
gt_flag=True)
|
|
3223
|
+
def _validate_single(result):
|
|
3093
3224
|
assert isinstance(result, np.ndarray), \
|
|
3094
3225
|
(f'{user_function.__name__}() validation failed: '
|
|
3095
3226
|
f'Unsupported return type. Should be a numpy array. Got {type(result)}.')
|
|
@@ -3097,6 +3228,14 @@ def tensorleap_gt_encoder(name: str):
|
|
|
3097
3228
|
(f'{user_function.__name__}() validation failed: '
|
|
3098
3229
|
f'The return type should be a numpy array of type float32. Got {result.dtype}.')
|
|
3099
3230
|
|
|
3231
|
+
def _validate_result(result, grouped=False, group_size=None):
|
|
3232
|
+
if not grouped:
|
|
3233
|
+
validate_output_structure(result, func_name=user_function.__name__, expected_type_name="np.ndarray",
|
|
3234
|
+
gt_flag=True)
|
|
3235
|
+
_validate_single(result)
|
|
3236
|
+
return
|
|
3237
|
+
_validate_grouped_result(result, group_size, user_function.__name__, _validate_single)
|
|
3238
|
+
|
|
3100
3239
|
def inner_without_validate(sample_id, preprocess_response):
|
|
3101
3240
|
global _called_from_inside_tl_decorator
|
|
3102
3241
|
_called_from_inside_tl_decorator += 1
|
|
@@ -3113,18 +3252,29 @@ def tensorleap_gt_encoder(name: str):
|
|
|
3113
3252
|
def inner(*args, **kwargs):
|
|
3114
3253
|
if not _call_from_tl_platform:
|
|
3115
3254
|
set_current("tensorleap_gt_encoder")
|
|
3116
|
-
validate_args_structure(*args, types_order=[Union[int, str], PreprocessResponse],
|
|
3255
|
+
validate_args_structure(*args, types_order=[Union[int, str, list], PreprocessResponse],
|
|
3117
3256
|
func_name=user_function.__name__, expected_names=["idx", "preprocess"], **kwargs)
|
|
3118
|
-
sample_id, preprocess_response = args
|
|
3257
|
+
sample_id, preprocess_response = args if len(args) != 0 else kwargs.values()
|
|
3119
3258
|
_validate_input_args(sample_id, preprocess_response)
|
|
3120
3259
|
|
|
3260
|
+
grouped = isinstance(sample_id, list)
|
|
3261
|
+
group_size = len(sample_id) if grouped else None
|
|
3121
3262
|
result = inner_without_validate(sample_id, preprocess_response)
|
|
3122
3263
|
|
|
3123
|
-
_validate_result(result)
|
|
3264
|
+
_validate_result(result, grouped, group_size)
|
|
3124
3265
|
|
|
3125
3266
|
if _called_from_inside_tl_decorator == 0 and _called_from_inside_tl_integration_test_decorator:
|
|
3126
|
-
|
|
3127
|
-
|
|
3267
|
+
if grouped:
|
|
3268
|
+
# A grouped result is a list of per-sample arrays (never stacked, so a
|
|
3269
|
+
# B=1-only model can be fed one sample at a time). A user may also return a
|
|
3270
|
+
# supported (B, *) array; split it back into per-sample rows first (mirrors
|
|
3271
|
+
# LeapLoader._to_grouped_list) so both forms match the runtime path. Then add
|
|
3272
|
+
# a batch dim to each sample, mirroring the flat path's single-sample expand_dims.
|
|
3273
|
+
rows = result if isinstance(result, list) else [row for row in np.asarray(result)]
|
|
3274
|
+
result = [np.expand_dims(r, axis=0) for r in rows]
|
|
3275
|
+
else:
|
|
3276
|
+
batch_warning(result, user_function.__name__)
|
|
3277
|
+
result = np.expand_dims(result, axis=0)
|
|
3128
3278
|
_register_chain_artifact(result, 'ground_truth')
|
|
3129
3279
|
# Emit integration test event once per test
|
|
3130
3280
|
try:
|
|
@@ -3140,8 +3290,15 @@ def tensorleap_gt_encoder(name: str):
|
|
|
3140
3290
|
inner.node_mapping = NodeMapping(name, NodeMappingType.GroundTruth)
|
|
3141
3291
|
|
|
3142
3292
|
def mapping_inner(*args, **kwargs):
|
|
3143
|
-
|
|
3144
|
-
|
|
3293
|
+
if _mapping_dataset_is_grouped:
|
|
3294
|
+
class TempMapping:
|
|
3295
|
+
def __getitem__(self, key):
|
|
3296
|
+
# Indexing a grouped GT group (gt_list[i]) selects a sample from the same
|
|
3297
|
+
# ground-truth source; wiring is identical to the un-grouped GT node.
|
|
3298
|
+
return self
|
|
3299
|
+
else:
|
|
3300
|
+
class TempMapping:
|
|
3301
|
+
pass
|
|
3145
3302
|
|
|
3146
3303
|
ret = TempMapping()
|
|
3147
3304
|
ret.node_mapping = mapping_inner.node_mapping
|
code_loader/leaploader.py
CHANGED
|
@@ -41,6 +41,11 @@ def _serialize_sim_bounds(bounds) -> dict:
|
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
class LeapLoader(LeapLoaderBase):
|
|
44
|
+
# Opt-in ceiling (env var) on samples materialized per get_samples call — the engine sets it to
|
|
45
|
+
# turn a mistaken whole-group ask into a clear error instead of a silent worker OOM. Unset => no
|
|
46
|
+
# ceiling. See get_samples and grouped-fetch-oom-bug.md.
|
|
47
|
+
_grouped_fetch_max_samples_env = "LEAP_GROUPED_FETCH_MAX_SAMPLES_PER_CALL"
|
|
48
|
+
|
|
44
49
|
def __init__(self, code_path: str, code_entry_name: str):
|
|
45
50
|
super().__init__(code_path, code_entry_name)
|
|
46
51
|
|
|
@@ -60,14 +65,36 @@ class LeapLoader(LeapLoaderBase):
|
|
|
60
65
|
|
|
61
66
|
@lru_cache()
|
|
62
67
|
def exec_script(self) -> None:
|
|
68
|
+
from code_loader.inner_leap_binder import leapbinder_decorators as _leap_dec
|
|
63
69
|
try:
|
|
64
70
|
os.environ[mapping_runtime_mode_env_var_mame] = 'TRUE'
|
|
65
71
|
self.evaluate_module()
|
|
72
|
+
|
|
73
|
+
# A grouped integration test may index a grouped input/gt (image_list[i]) to wire a
|
|
74
|
+
# single sample of a group; that indexing is only valid when the mapping graph is
|
|
75
|
+
# built in grouped mode. Detect grouped-ness so the placeholder pass below matches
|
|
76
|
+
# the dataset. Preprocess is stubbed while mapping mode is on, so run it with mapping
|
|
77
|
+
# mode momentarily disabled, and cache it so _preprocess_result() doesn't re-run it.
|
|
78
|
+
is_grouped = False
|
|
79
|
+
if global_leap_binder.integration_test_func is not None:
|
|
80
|
+
del os.environ[mapping_runtime_mode_env_var_mame]
|
|
81
|
+
try:
|
|
82
|
+
preprocess_result = global_leap_binder.get_preprocess_result()
|
|
83
|
+
self._preprocess_result_cached = preprocess_result
|
|
84
|
+
is_grouped = any(r.is_grouped for r in preprocess_result.values())
|
|
85
|
+
finally:
|
|
86
|
+
os.environ[mapping_runtime_mode_env_var_mame] = 'TRUE'
|
|
87
|
+
|
|
88
|
+
_leap_dec._mapping_dataset_is_grouped = is_grouped
|
|
66
89
|
if global_leap_binder.integration_test_func is not None:
|
|
67
90
|
from code_loader.inner_leap_binder.leapbinder_decorators import \
|
|
68
91
|
_reset_model_loop_state
|
|
69
92
|
_reset_model_loop_state()
|
|
70
|
-
|
|
93
|
+
mapping_preprocess = (
|
|
94
|
+
PreprocessResponse(sample_ids=[["__mapping_placeholder__"]], state=DataStateType.training)
|
|
95
|
+
if is_grouped else
|
|
96
|
+
PreprocessResponse(state=DataStateType.training, length=0))
|
|
97
|
+
global_leap_binder.integration_test_func(None, mapping_preprocess)
|
|
71
98
|
except TypeError as e:
|
|
72
99
|
import traceback
|
|
73
100
|
global_leap_binder.setup_container = DatasetIntegrationSetup()
|
|
@@ -80,6 +107,7 @@ class LeapLoader(LeapLoaderBase):
|
|
|
80
107
|
raise DatasetScriptException(getattr(e, 'message', repr(e))) from e
|
|
81
108
|
finally:
|
|
82
109
|
# ensure that the environment variable is removed after the script execution
|
|
110
|
+
_leap_dec._mapping_dataset_is_grouped = False
|
|
83
111
|
if mapping_runtime_mode_env_var_mame in os.environ:
|
|
84
112
|
del os.environ[mapping_runtime_mode_env_var_mame]
|
|
85
113
|
|
|
@@ -209,7 +237,7 @@ class LeapLoader(LeapLoaderBase):
|
|
|
209
237
|
additional = self._preprocess_result().get(DataStateEnum.additional)
|
|
210
238
|
if additional is None:
|
|
211
239
|
return set()
|
|
212
|
-
return set(additional.
|
|
240
|
+
return set(additional.flat_sample_ids)
|
|
213
241
|
|
|
214
242
|
def _resolve_synthetic(self, sample_id: Union[int, str],
|
|
215
243
|
state: Optional[DataStateEnum] = None
|
|
@@ -250,7 +278,7 @@ class LeapLoader(LeapLoaderBase):
|
|
|
250
278
|
)
|
|
251
279
|
|
|
252
280
|
preprocess_result = self._preprocess_result()
|
|
253
|
-
if state == DataStateEnum.unlabeled and sample_id not in preprocess_result[state].
|
|
281
|
+
if state == DataStateEnum.unlabeled and sample_id not in preprocess_result[state].flat_sample_ids:
|
|
254
282
|
self._preprocess_result(update_unlabeled_preprocess=True)
|
|
255
283
|
|
|
256
284
|
metadata, metadata_is_none = self.get_metadata(state, sample_id)
|
|
@@ -379,10 +407,10 @@ class LeapLoader(LeapLoaderBase):
|
|
|
379
407
|
test_result = DatasetTestResultPayload('preprocess')
|
|
380
408
|
try:
|
|
381
409
|
preprocess_result = self._preprocess_result()
|
|
382
|
-
if self.get_sample_id_type()
|
|
410
|
+
if issubclass(self.get_sample_id_type(), str):
|
|
383
411
|
max_allowed_item_size = np.dtype('<U256').itemsize
|
|
384
412
|
for state, preprocess_response in preprocess_result.items():
|
|
385
|
-
sample_ids_array = np.array(preprocess_response.
|
|
413
|
+
sample_ids_array = np.array(preprocess_response.flat_sample_ids)
|
|
386
414
|
if sample_ids_array.dtype.itemsize > max_allowed_item_size:
|
|
387
415
|
raise Exception(f"Sample id are too long. Max allowed length is 256 charecters.")
|
|
388
416
|
|
|
@@ -443,6 +471,10 @@ class LeapLoader(LeapLoaderBase):
|
|
|
443
471
|
preprocess_response.tl_generated = True
|
|
444
472
|
if preprocess_response.length < 1:
|
|
445
473
|
raise ValueError("Simulation returned PreprocessResponse with length < 1")
|
|
474
|
+
if preprocess_response.is_grouped:
|
|
475
|
+
raise ValueError(
|
|
476
|
+
"Simulation returned a grouped PreprocessResponse; simulations must "
|
|
477
|
+
"return a flat (non-grouped) PreprocessResponse.")
|
|
446
478
|
preprocess_response.sample_ids = [0]
|
|
447
479
|
for handler in global_leap_binder.setup_container.inputs:
|
|
448
480
|
out1 = handler.function(preprocess_response.sample_ids[0], preprocess_response)
|
|
@@ -478,7 +510,10 @@ class LeapLoader(LeapLoaderBase):
|
|
|
478
510
|
if preprocess_response.sample_ids_to_instance_mappings:
|
|
479
511
|
raise Exception('Element instances are not supported together with '
|
|
480
512
|
'tensorleap_autoregressive_step.')
|
|
481
|
-
|
|
513
|
+
# Grouped preprocess is fine: grouping only amortizes the shared base reads
|
|
514
|
+
# (GT/metadata/custom_latent) — the AR hook always drives a single scalar chain
|
|
515
|
+
# and is never handed a group.
|
|
516
|
+
sample_id = preprocess_response.flat_sample_ids[0]
|
|
482
517
|
first_result = handler.function(sample_id, None, None, None, preprocess_response)
|
|
483
518
|
second_result = handler.function(sample_id, None, None, None, preprocess_response)
|
|
484
519
|
if not isinstance(first_result, tuple) or len(first_result) != 2:
|
|
@@ -559,7 +594,7 @@ class LeapLoader(LeapLoaderBase):
|
|
|
559
594
|
if handler.input_shapes is None:
|
|
560
595
|
preprocess_result = self._preprocess_result()
|
|
561
596
|
first_state_response = next(iter(preprocess_result.values()))
|
|
562
|
-
first_sample_id = first_state_response.
|
|
597
|
+
first_sample_id = first_state_response.flat_sample_ids[0]
|
|
563
598
|
first_result = handler.function(first_sample_id, None, None, None,
|
|
564
599
|
first_state_response)
|
|
565
600
|
if not isinstance(first_result, tuple) or len(first_result) != 2 or \
|
|
@@ -898,6 +933,57 @@ class LeapLoader(LeapLoaderBase):
|
|
|
898
933
|
|
|
899
934
|
return sample_ids
|
|
900
935
|
|
|
936
|
+
@staticmethod
|
|
937
|
+
def _grouped_dict_keys(rows: List[Dict[str, Any]], handler_name: str) -> List[str]:
|
|
938
|
+
"""Keys for a group of dict-metadata rows, requiring every row to share the same keys.
|
|
939
|
+
A group must expose a uniform metadata schema; otherwise the per-key lists would silently
|
|
940
|
+
drop or KeyError on rows that differ from row 0."""
|
|
941
|
+
keys = list(rows[0].keys())
|
|
942
|
+
key_set = set(keys)
|
|
943
|
+
for pos, row in enumerate(rows):
|
|
944
|
+
if set(row.keys()) != key_set:
|
|
945
|
+
raise ValueError(
|
|
946
|
+
f"Metadata handler {handler_name!r} returned inconsistent dict keys across the "
|
|
947
|
+
f"group: row 0 has {sorted(key_set)} but row {pos} has {sorted(row.keys())}. "
|
|
948
|
+
f"All rows in a group must share the same metadata keys.")
|
|
949
|
+
return keys
|
|
950
|
+
|
|
951
|
+
@staticmethod
|
|
952
|
+
def _to_grouped_list(raw: Any) -> List[npt.NDArray[np.float32]]:
|
|
953
|
+
"""Normalize a grouped encoder result to a list of per-sample arrays.
|
|
954
|
+
|
|
955
|
+
Grouped results are NEVER stacked into a (B, *) batch. Grouping is only a storage
|
|
956
|
+
read-unit (one file load per group); assembling the model batch (B) is the engine's
|
|
957
|
+
single responsibility, so it stays the only place samples get batched. Keeping
|
|
958
|
+
per-sample arrays lets the engine batch to any B — including a B=1-only model — without
|
|
959
|
+
an intermediate stack/unstack. A user encoder that already returned a stacked (B, *)
|
|
960
|
+
array is split back into its per-sample rows so the contract is a list either way."""
|
|
961
|
+
if isinstance(raw, list):
|
|
962
|
+
return raw
|
|
963
|
+
return [row for row in np.asarray(raw)]
|
|
964
|
+
|
|
965
|
+
def _locate_group(self, preprocess_state: PreprocessResponse,
|
|
966
|
+
sample_id: Union[int, str]) -> Tuple[List[Union[int, str]], int]:
|
|
967
|
+
"""Map a sample id to (its group, its position in that group). Cached on the
|
|
968
|
+
PreprocessResponse itself (not keyed by id() on this loader) so grouped single-sample
|
|
969
|
+
fetches don't rescan the groups, and the cache can never go stale: when the response is
|
|
970
|
+
replaced (e.g. unlabeled preprocess refresh), the old object and its cache are simply
|
|
971
|
+
discarded together instead of lingering under a possibly-reused id()."""
|
|
972
|
+
mapping = preprocess_state._group_pos_cache
|
|
973
|
+
if mapping is None:
|
|
974
|
+
mapping = {}
|
|
975
|
+
for group in preprocess_state.groups:
|
|
976
|
+
for pos, sid in enumerate(group):
|
|
977
|
+
if sid in mapping:
|
|
978
|
+
raise ValueError(
|
|
979
|
+
f"Duplicate sample id {sid!r} across groups in the preprocess response; "
|
|
980
|
+
f"sample ids must be unique within and across groups.")
|
|
981
|
+
mapping[sid] = (group, pos)
|
|
982
|
+
preprocess_state._group_pos_cache = mapping
|
|
983
|
+
if sample_id not in mapping:
|
|
984
|
+
raise KeyError(f"Sample id {sample_id!r} is not present in any group of this preprocess response.")
|
|
985
|
+
return mapping[sample_id]
|
|
986
|
+
|
|
901
987
|
def _get_dataset_handlers(self, handlers: Iterable[DatasetBaseHandler],
|
|
902
988
|
state: DataStateEnum, sample_id: Union[int, str]) -> Dict[str, npt.NDArray[np.float32]]:
|
|
903
989
|
result_agg = {}
|
|
@@ -909,12 +995,127 @@ class LeapLoader(LeapLoaderBase):
|
|
|
909
995
|
sample_id = original_local_id
|
|
910
996
|
else:
|
|
911
997
|
preprocess_state = preprocess_result[state]
|
|
998
|
+
if preprocess_state.is_grouped:
|
|
999
|
+
# Grouped dataset, SINGLE-sample fetch: encode only THIS sample, never the whole group.
|
|
1000
|
+
# Grouping is an I/O read-unit, not an encode-unit — the integration's per-file cache
|
|
1001
|
+
# makes the group's file decode once across consecutive per-sample calls, so a singleton
|
|
1002
|
+
# ([sample_id]) is both a valid single-group subset of the get_samples contract and
|
|
1003
|
+
# correct (samples in a group are independent; see _to_grouped_list). Handing the encoder
|
|
1004
|
+
# the whole group here just to index one sample out materialized B encoded tensors per
|
|
1005
|
+
# sample — a driver of the grouped-fetch worker OOM (see grouped-fetch-oom-bug.md).
|
|
1006
|
+
self._locate_group(preprocess_state, sample_id) # validates the id belongs to a group
|
|
1007
|
+
for handler in handlers:
|
|
1008
|
+
result_agg[handler.name] = self._to_grouped_list(
|
|
1009
|
+
handler.function([sample_id], preprocess_state))[0]
|
|
1010
|
+
return result_agg
|
|
912
1011
|
for handler in handlers:
|
|
913
1012
|
handler_result = handler.function(sample_id, preprocess_state)
|
|
914
1013
|
handler_name = handler.name
|
|
915
1014
|
result_agg[handler_name] = handler_result
|
|
916
1015
|
return result_agg
|
|
917
1016
|
|
|
1017
|
+
def get_samples(self, state: DataStateEnum, group_ids: List[Union[int, str]]) -> DatasetSample:
|
|
1018
|
+
"""Group-aware fetch: hand the whole group to each encoder in a single call and return a
|
|
1019
|
+
grouped DatasetSample (inputs/gt as per-sample lists of length B, metadata as per-key lists
|
|
1020
|
+
of length B, index = the group). Grouped results are never stacked here — batching the
|
|
1021
|
+
samples into the model's B is the engine's job. This is the path a group-aware engine calls
|
|
1022
|
+
to read a file once per group. Row order matches group_ids exactly."""
|
|
1023
|
+
self.exec_script()
|
|
1024
|
+
preprocess_result = self._preprocess_result()
|
|
1025
|
+
if state == DataStateEnum.unlabeled and any(
|
|
1026
|
+
sid not in preprocess_result[state].flat_sample_ids for sid in group_ids):
|
|
1027
|
+
# Mirrors get_sample's refresh: the unlabeled preprocess can grow between calls, so a
|
|
1028
|
+
# group_id absent from the current snapshot may just not have been generated yet.
|
|
1029
|
+
self._preprocess_result(update_unlabeled_preprocess=True)
|
|
1030
|
+
preprocess_state = preprocess_result[state]
|
|
1031
|
+
assert preprocess_state.is_grouped, (
|
|
1032
|
+
"get_samples is the group-aware fetch path and requires a grouped preprocess "
|
|
1033
|
+
"response; call get_sample for a flat (non-grouped) dataset.")
|
|
1034
|
+
# All requested ids must belong to a single group (one file). A cross-group request
|
|
1035
|
+
# would force the encoder to load multiple files, defeating the one-load-per-group
|
|
1036
|
+
# contract; partitioning a scattered request by group is the engine's responsibility.
|
|
1037
|
+
distinct_groups = {id(self._locate_group(preprocess_state, sid)[0]) for sid in group_ids}
|
|
1038
|
+
assert len(distinct_groups) == 1, (
|
|
1039
|
+
f"get_samples received sample ids spanning {len(distinct_groups)} groups; a request "
|
|
1040
|
+
f"must be confined to a single group. The engine partitions cross-group requests and "
|
|
1041
|
+
f"issues one get_samples call per group.")
|
|
1042
|
+
# Opt-in safety net for the grouped-fetch OOM (grouped-fetch-oom-bug.md). This call
|
|
1043
|
+
# materializes len(group_ids) encoded samples at once; on a large group/file that peak is
|
|
1044
|
+
# what kernel-OOM-kills the worker. A group's file is decoded once by the integration's
|
|
1045
|
+
# per-file cache regardless of how the encode is chunked, so the engine should request
|
|
1046
|
+
# memory-bounded sub-chunks of a group (each a valid single-group subset, e.g.
|
|
1047
|
+
# get_samples(state, group_ids[i:j])) rather than the whole group. When the engine sets the
|
|
1048
|
+
# ceiling below, an over-large ask fails here with a message that names the lever instead of
|
|
1049
|
+
# a silent GENERIC_MEMORY_LIMIT kill. Unset => no ceiling (preserves current whole-group callers).
|
|
1050
|
+
max_per_call = os.environ.get(self._grouped_fetch_max_samples_env)
|
|
1051
|
+
if max_per_call is not None and len(group_ids) > int(max_per_call):
|
|
1052
|
+
raise ValueError(
|
|
1053
|
+
f"get_samples asked to materialize {len(group_ids)} samples in a single call, above "
|
|
1054
|
+
f"the configured ceiling {max_per_call} ({self._grouped_fetch_max_samples_env}). The "
|
|
1055
|
+
f"group's file is decoded once via the integration's per-file cache regardless of "
|
|
1056
|
+
f"chunking, so request memory-bounded sub-chunks of the group "
|
|
1057
|
+
f"(get_samples(state, group_ids[i:j])) instead of the whole group. "
|
|
1058
|
+
f"See grouped-fetch-oom-bug.md.")
|
|
1059
|
+
inputs = {handler.name: self._to_grouped_list(handler.function(group_ids, preprocess_state))
|
|
1060
|
+
for handler in global_leap_binder.setup_container.inputs}
|
|
1061
|
+
autoregressive_handler = global_leap_binder.setup_container.autoregressive_step
|
|
1062
|
+
if autoregressive_handler is not None:
|
|
1063
|
+
# AR integrations have no input encoders — the hook's first call supplies each
|
|
1064
|
+
# chain's step-0 model inputs. The hook is strictly per-scalar-chain (never handed
|
|
1065
|
+
# a group), so it is called once per flat id in the group and the per-sample results
|
|
1066
|
+
# are collected into per-key lists of length B, matching the grouped (never-stacked)
|
|
1067
|
+
# contract used for every other encoder here.
|
|
1068
|
+
step_zero_per_sample = []
|
|
1069
|
+
for sid in group_ids:
|
|
1070
|
+
step_zero_result = autoregressive_handler.function(sid, None, None, None,
|
|
1071
|
+
preprocess_state)
|
|
1072
|
+
if not isinstance(step_zero_result, tuple) or len(step_zero_result) != 2 or \
|
|
1073
|
+
not isinstance(step_zero_result[0], dict):
|
|
1074
|
+
raise Exception(
|
|
1075
|
+
'The autoregressive step hook must return a (next_inputs, state) tuple '
|
|
1076
|
+
f'with a dict of initial model inputs on its first call, got '
|
|
1077
|
+
f'{type(step_zero_result)} for sample {sid}.')
|
|
1078
|
+
self._validate_autoregressive_step_inputs(step_zero_result[0], sid)
|
|
1079
|
+
step_zero_per_sample.append(step_zero_result[0])
|
|
1080
|
+
if step_zero_per_sample:
|
|
1081
|
+
for key in self._grouped_dict_keys(step_zero_per_sample, autoregressive_handler.name):
|
|
1082
|
+
inputs[key] = [row[key] for row in step_zero_per_sample]
|
|
1083
|
+
gt = None
|
|
1084
|
+
if state != DataStateEnum.unlabeled:
|
|
1085
|
+
gt = {handler.name: self._to_grouped_list(handler.function(group_ids, preprocess_state))
|
|
1086
|
+
for handler in global_leap_binder.setup_container.ground_truths}
|
|
1087
|
+
|
|
1088
|
+
metadata: Dict[str, Any] = {}
|
|
1089
|
+
metadata_is_none: Dict[str, Any] = {}
|
|
1090
|
+
for handler in global_leap_binder.setup_container.metadata:
|
|
1091
|
+
rows = handler.function(group_ids, preprocess_state) # length-B sequence of scalars/dicts
|
|
1092
|
+
if len(rows) > 0 and isinstance(rows[0], dict):
|
|
1093
|
+
for key in self._grouped_dict_keys(rows, handler.name):
|
|
1094
|
+
name = "{}_{}".format(handler.name, key)
|
|
1095
|
+
converted = [self._convert_metadata_to_correct_type(name, row[key]) for row in rows]
|
|
1096
|
+
metadata[name] = [v for v, _ in converted]
|
|
1097
|
+
metadata_is_none[name] = [is_none for _, is_none in converted]
|
|
1098
|
+
else:
|
|
1099
|
+
converted = [self._convert_metadata_to_correct_type(handler.name, row) for row in rows]
|
|
1100
|
+
metadata[handler.name] = [v for v, _ in converted]
|
|
1101
|
+
metadata_is_none[handler.name] = [is_none for _, is_none in converted]
|
|
1102
|
+
|
|
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
|
+
}
|
|
1114
|
+
|
|
1115
|
+
return DatasetSample(inputs=inputs, gt=gt, metadata=metadata, metadata_is_none=metadata_is_none,
|
|
1116
|
+
index=list(group_ids), state=state, custom_latent_spaces=custom_latent_spaces,
|
|
1117
|
+
instance_masks=None)
|
|
1118
|
+
|
|
918
1119
|
def _get_inputs(self, state: DataStateEnum, sample_id: Union[int, str]) -> Dict[str, npt.NDArray[np.float32]]:
|
|
919
1120
|
inputs = self._get_dataset_handlers(global_leap_binder.setup_container.inputs, state, sample_id)
|
|
920
1121
|
autoregressive_handler = global_leap_binder.setup_container.autoregressive_step
|
|
@@ -1018,12 +1219,25 @@ class LeapLoader(LeapLoaderBase):
|
|
|
1018
1219
|
sample_id = original_local_id
|
|
1019
1220
|
else:
|
|
1020
1221
|
preprocess_state = preprocess_result[state]
|
|
1222
|
+
if preprocess_state.is_grouped:
|
|
1223
|
+
self._locate_group(preprocess_state, sample_id) # validates the id belongs to a group
|
|
1021
1224
|
for handler in global_leap_binder.setup_container.metadata:
|
|
1022
1225
|
if requested_metadata_names:
|
|
1023
1226
|
if not is_metadata_name_starts_with_handler_name(handler):
|
|
1024
1227
|
continue
|
|
1025
1228
|
|
|
1026
|
-
|
|
1229
|
+
if preprocess_state.is_grouped:
|
|
1230
|
+
# Single-sample metadata fetch: encode only THIS sample, never the whole group —
|
|
1231
|
+
# same memory rationale as _get_dataset_handlers / the get_sample latent branch
|
|
1232
|
+
# (grouped-fetch-oom-bug.md). Metadata values are not guaranteed small scalars: a
|
|
1233
|
+
# metadata encoder may return an np.asarray or read the sample's heavy data to
|
|
1234
|
+
# compute a statistic, so materializing all B rows just to index one out is the same
|
|
1235
|
+
# OOM path this fix closes. The batched get_metadata_multiple_samples correctly keeps
|
|
1236
|
+
# the whole-group encode (amortized across many ids via its group_cache); a single
|
|
1237
|
+
# id does not need it. A singleton is a valid single-group subset of the contract.
|
|
1238
|
+
handler_result = handler.function([sample_id], preprocess_state)[0]
|
|
1239
|
+
else:
|
|
1240
|
+
handler_result = handler.function(sample_id, preprocess_state)
|
|
1027
1241
|
if isinstance(handler_result, dict):
|
|
1028
1242
|
for single_metadata_name, single_metadata_result in handler_result.items():
|
|
1029
1243
|
handler_name = f'{handler.name}_{single_metadata_name}'
|
|
@@ -1042,6 +1256,66 @@ class LeapLoader(LeapLoaderBase):
|
|
|
1042
1256
|
|
|
1043
1257
|
return result_agg, is_none
|
|
1044
1258
|
|
|
1259
|
+
def _grouped_metadata_for_group(self, preprocess_state: PreprocessResponse,
|
|
1260
|
+
group_ids: List[Union[int, str]],
|
|
1261
|
+
requested_metadata_names: Optional[List[str]]
|
|
1262
|
+
) -> Tuple[Dict[str, List[Any]], Dict[str, List[bool]]]:
|
|
1263
|
+
def is_wanted(_handler):
|
|
1264
|
+
if not requested_metadata_names:
|
|
1265
|
+
return True
|
|
1266
|
+
for metadata_name in requested_metadata_names:
|
|
1267
|
+
if metadata_name.startswith(_handler.name + '_') or metadata_name == _handler.name:
|
|
1268
|
+
return True
|
|
1269
|
+
return False
|
|
1270
|
+
|
|
1271
|
+
results: Dict[str, List[Any]] = {}
|
|
1272
|
+
is_none: Dict[str, List[bool]] = {}
|
|
1273
|
+
for handler in global_leap_binder.setup_container.metadata:
|
|
1274
|
+
if requested_metadata_names and not is_wanted(handler):
|
|
1275
|
+
continue
|
|
1276
|
+
rows = handler.function(group_ids, preprocess_state)
|
|
1277
|
+
if len(rows) > 0 and isinstance(rows[0], dict):
|
|
1278
|
+
for key in self._grouped_dict_keys(rows, handler.name):
|
|
1279
|
+
name = f'{handler.name}_{key}'
|
|
1280
|
+
if requested_metadata_names and name not in requested_metadata_names:
|
|
1281
|
+
continue
|
|
1282
|
+
converted = [self._convert_metadata_to_correct_type(name, row[key]) for row in rows]
|
|
1283
|
+
results[name] = [v for v, _ in converted]
|
|
1284
|
+
is_none[name] = [n for _, n in converted]
|
|
1285
|
+
else:
|
|
1286
|
+
name = handler.name
|
|
1287
|
+
if requested_metadata_names and name not in requested_metadata_names:
|
|
1288
|
+
continue
|
|
1289
|
+
converted = [self._convert_metadata_to_correct_type(name, row) for row in rows]
|
|
1290
|
+
results[name] = [v for v, _ in converted]
|
|
1291
|
+
is_none[name] = [n for _, n in converted]
|
|
1292
|
+
return results, is_none
|
|
1293
|
+
|
|
1294
|
+
def get_metadata_multiple_samples(self, state: DataStateEnum, sample_ids: Union[List[int], List[str]],
|
|
1295
|
+
requested_metadata_names: Optional[List[str]] = None
|
|
1296
|
+
) -> Tuple[Dict[str, Union[List[str], List[int], List[bool], List[float]]],
|
|
1297
|
+
Dict[str, List[bool]]]:
|
|
1298
|
+
preprocess_state = self._preprocess_result().get(state)
|
|
1299
|
+
if preprocess_state is None or not preprocess_state.is_grouped:
|
|
1300
|
+
return super().get_metadata_multiple_samples(state, sample_ids, requested_metadata_names)
|
|
1301
|
+
|
|
1302
|
+
sample_id_type = self.get_sample_id_type()
|
|
1303
|
+
aggregated_results: Dict[str, List[Any]] = {}
|
|
1304
|
+
aggregated_is_none: Dict[str, List[bool]] = {}
|
|
1305
|
+
group_cache: Dict[int, Tuple[Dict[str, List[Any]], Dict[str, List[bool]]]] = {}
|
|
1306
|
+
for sample_id in sample_ids:
|
|
1307
|
+
sample_id = sample_id_type(sample_id)
|
|
1308
|
+
group_ids, pos = self._locate_group(preprocess_state, sample_id)
|
|
1309
|
+
key = id(group_ids)
|
|
1310
|
+
if key not in group_cache:
|
|
1311
|
+
group_cache[key] = self._grouped_metadata_for_group(
|
|
1312
|
+
preprocess_state, group_ids, requested_metadata_names)
|
|
1313
|
+
group_results, group_is_none = group_cache[key]
|
|
1314
|
+
for name, values in group_results.items():
|
|
1315
|
+
aggregated_results.setdefault(name, []).append(values[pos])
|
|
1316
|
+
aggregated_is_none.setdefault(name, []).append(group_is_none[name][pos])
|
|
1317
|
+
return aggregated_results, aggregated_is_none
|
|
1318
|
+
|
|
1045
1319
|
@lru_cache()
|
|
1046
1320
|
def get_sample_id_type(self) -> Type:
|
|
1047
1321
|
preprocess_results = list(self._preprocess_result().values())
|
|
@@ -1052,13 +1326,19 @@ class LeapLoader(LeapLoaderBase):
|
|
|
1052
1326
|
|
|
1053
1327
|
return id_type
|
|
1054
1328
|
|
|
1055
|
-
@staticmethod
|
|
1056
1329
|
def _get_custom_latent_spaces(
|
|
1330
|
+
self,
|
|
1057
1331
|
sample_id: Union[int, str],
|
|
1058
1332
|
preprocess: "PreprocessResponse") -> Optional[Dict[str, npt.NDArray[np.float32]]]:
|
|
1059
1333
|
handlers = global_leap_binder.setup_container.custom_latent_spaces
|
|
1060
1334
|
if not handlers:
|
|
1061
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()}
|
|
1062
1342
|
return {name: handler.function(sample_id, preprocess) for name, handler in handlers.items()}
|
|
1063
1343
|
|
|
1064
1344
|
@lru_cache()
|
code_loader/utils.py
CHANGED
|
@@ -17,6 +17,16 @@ from code_loader.contract.enums import DatasetMetadataType
|
|
|
17
17
|
def to_numpy_return_wrapper(encoder_function: SectionCallableInterface) -> SectionCallableInterface:
|
|
18
18
|
def numpy_encoder_function(idx: Union[int, str], samples: PreprocessResponse) -> npt.NDArray[np.float32]:
|
|
19
19
|
result = encoder_function(idx, samples)
|
|
20
|
+
if isinstance(idx, list):
|
|
21
|
+
# Grouped call (idx is a list of sample ids): never stack the per-sample results into a
|
|
22
|
+
# (B, *) batch — grouping is only a storage read-unit; batching the group to the model's
|
|
23
|
+
# B is the engine's single responsibility. Gate on the call shape (idx is a list), not
|
|
24
|
+
# the result shape, so a flat encoder returning a list of arrays still stacks below.
|
|
25
|
+
# Normalize each sample independently (a user-stacked (B, *) array is split back into its
|
|
26
|
+
# rows) so a ragged group — variable per-sample shapes, e.g. token lists of differing
|
|
27
|
+
# length — is preserved instead of crashing np.array() on an inhomogeneous shape.
|
|
28
|
+
rows = result if isinstance(result, list) else np.asarray(result)
|
|
29
|
+
return [np.asarray(sample) for sample in rows]
|
|
20
30
|
numpy_result: npt.NDArray[np.float32] = np.array(result)
|
|
21
31
|
return numpy_result
|
|
22
32
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
LICENSE,sha256=qIwWjdspQeSMTtnFZBC8MuT-95L02FPvzRUdWFxrwJY,1067
|
|
2
2
|
code_loader/__init__.py,sha256=outxRQ0M-zMfV0QGVJmAed5qWfRmyD0TV6-goEGAzBw,406
|
|
3
3
|
code_loader/contract/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
-
code_loader/contract/datasetclasses.py,sha256=
|
|
4
|
+
code_loader/contract/datasetclasses.py,sha256=LrhOsNirP3MRKUDQcI6u0r-GeSGHppdSUNzVEAkADeU,16816
|
|
5
5
|
code_loader/contract/enums.py,sha256=2q-IV_5g9lLE306DIbWA1c0tn5IhDtxsKxyV1x_Lreg,1671
|
|
6
6
|
code_loader/contract/exceptions.py,sha256=jWqu5i7t-0IG0jGRsKF4DjJdrsdpJjIYpUkN1F4RiyQ,51
|
|
7
7
|
code_loader/contract/mapping.py,sha256=sWJhpng-IkOzQnWQdMT5w2ZZ3X1Z_OOzSwCLXIS7oxE,1446
|
|
@@ -21,18 +21,18 @@ code_loader/experiment_api/types.py,sha256=MY8xFARHwdVA7p4dxyhD60ShmttgTvb4qdp1o
|
|
|
21
21
|
code_loader/experiment_api/utils.py,sha256=XZHtxge12TS4H4-8PjV3sKuhp8Ud6ojAiIzTZJEqBqc,3304
|
|
22
22
|
code_loader/experiment_api/workingspace_config_utils.py,sha256=DLzXQCg4dgTV_YgaSbeTVzq-2ja_SQw4zi7LXwKL9cY,990
|
|
23
23
|
code_loader/inner_leap_binder/__init__.py,sha256=koOlJyMNYzGbEsoIbXathSmQ-L38N_pEXH_HvL7beXU,99
|
|
24
|
-
code_loader/inner_leap_binder/leapbinder.py,sha256=
|
|
25
|
-
code_loader/inner_leap_binder/leapbinder_decorators.py,sha256=
|
|
26
|
-
code_loader/leaploader.py,sha256=
|
|
24
|
+
code_loader/inner_leap_binder/leapbinder.py,sha256=J-EjPz49kHqneaFhmgzid0MRokjxYb7kX6XkErxwtlc,58909
|
|
25
|
+
code_loader/inner_leap_binder/leapbinder_decorators.py,sha256=WE6gpZP8ckJp8OQ4CgWbz8BuF9KLwAeQYwDk-ua2jsY,186309
|
|
26
|
+
code_loader/leaploader.py,sha256=8q7JnsE1jx-PJNJN0oKak7i47X9dzenbz8lk-ezbjpw,88293
|
|
27
27
|
code_loader/leaploaderbase.py,sha256=Aa8wCcxojf8EBn_14W5I1gKI4OMBdpdEcNAZ_6E2eV4,10940
|
|
28
28
|
code_loader/mixpanel_tracker.py,sha256=rNwRmFifNbdUoqLQvvhhgpKczWpWiEmd8MfyJe27sxw,9131
|
|
29
29
|
code_loader/plot_functions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
30
30
|
code_loader/plot_functions/plot_functions.py,sha256=2DC-zlVaN13P4VNx5d8csgs80C6SisaeP1-Kq2LW7iM,16075
|
|
31
31
|
code_loader/plot_functions/visualize.py,sha256=gsBAYYkwMh7jIpJeDMPS8G4CW-pxwx6LznoQIvi4vpo,657
|
|
32
|
-
code_loader/utils.py,sha256=
|
|
32
|
+
code_loader/utils.py,sha256=Yj06BbS1UIjN9Cv2eQBjDfdDbpQlABAg6KwD3sB1BZ0,7496
|
|
33
33
|
code_loader/visualizers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
34
34
|
code_loader/visualizers/default_visualizers.py,sha256=onRnLE_TXfgLN4o52hQIOOhUcFexGlqJ3xSpQDVLuZM,2604
|
|
35
|
-
code_loader-1.0.197.
|
|
36
|
-
code_loader-1.0.197.
|
|
37
|
-
code_loader-1.0.197.
|
|
38
|
-
code_loader-1.0.197.
|
|
35
|
+
code_loader-1.0.197.dev2.dist-info/LICENSE,sha256=qIwWjdspQeSMTtnFZBC8MuT-95L02FPvzRUdWFxrwJY,1067
|
|
36
|
+
code_loader-1.0.197.dev2.dist-info/METADATA,sha256=Ep3bwfV6ttp9_4BD5nL8a5zAzu0C3hauxJIkLisVuDU,1095
|
|
37
|
+
code_loader-1.0.197.dev2.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
38
|
+
code_loader-1.0.197.dev2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|