code-loader 1.0.191.dev1__py3-none-any.whl → 1.0.193.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- code_loader/contract/datasetclasses.py +70 -55
- code_loader/inner_leap_binder/leapbinder.py +164 -17
- code_loader/inner_leap_binder/leapbinder_decorators.py +1063 -192
- code_loader/leaploader.py +286 -187
- code_loader/leaploaderbase.py +5 -0
- code_loader/utils.py +36 -5
- {code_loader-1.0.191.dev1.dist-info → code_loader-1.0.193.dev0.dist-info}/METADATA +1 -1
- {code_loader-1.0.191.dev1.dist-info → code_loader-1.0.193.dev0.dist-info}/RECORD +10 -10
- {code_loader-1.0.191.dev1.dist-info → code_loader-1.0.193.dev0.dist-info}/LICENSE +0 -0
- {code_loader-1.0.191.dev1.dist-info → code_loader-1.0.193.dev0.dist-info}/WHEEL +0 -0
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
# mypy: ignore-errors
|
|
2
|
+
import copy
|
|
3
|
+
import hashlib
|
|
2
4
|
import inspect
|
|
3
5
|
import os
|
|
4
6
|
import warnings
|
|
@@ -13,7 +15,8 @@ from typing import Optional, Union, Callable, List, Dict, get_args, get_origin,
|
|
|
13
15
|
import numpy as np
|
|
14
16
|
import numpy.typing as npt
|
|
15
17
|
|
|
16
|
-
from code_loader.utils import get_metadata_type_from_variable, map_dict_to_metadata_types
|
|
18
|
+
from code_loader.utils import get_metadata_type_from_variable, map_dict_to_metadata_types, \
|
|
19
|
+
validate_autoregressive_state_types, autoregressive_nests_equal
|
|
17
20
|
|
|
18
21
|
logger = logging.getLogger(__name__)
|
|
19
22
|
|
|
@@ -21,18 +24,18 @@ from code_loader.contract.datasetclasses import CustomCallableInterfaceMultiArgs
|
|
|
21
24
|
CustomMultipleReturnCallableInterfaceMultiArgs, ConfusionMatrixCallableInterfaceMultiArgs, CustomCallableInterface, \
|
|
22
25
|
VisualizerCallableInterface, MetadataSectionCallableInterface, PreprocessResponse, SectionCallableInterface, \
|
|
23
26
|
ConfusionMatrixElement, SamplePreprocessResponse, PredictionTypeHandler, InstanceCallableInterface, ElementInstance, \
|
|
24
|
-
InstanceLengthCallableInterface
|
|
27
|
+
InstanceLengthCallableInterface, AutoregressiveStepCallableInterface
|
|
25
28
|
from code_loader.contract.enums import MetricDirection, LeapDataType, DatasetMetadataType, DataStateType
|
|
26
29
|
from code_loader import leap_binder, LeapLoader
|
|
27
30
|
from code_loader.contract.mapping import NodeMapping, NodeMappingType, NodeConnection
|
|
28
31
|
from code_loader.contract.visualizer_classes import LeapImage, LeapImageMask, LeapTextMask, LeapText, LeapGraph, \
|
|
29
|
-
LeapHorizontalBar, LeapImageWithBBox, LeapImageWithHeatmap, LeapVideo
|
|
32
|
+
LeapHorizontalBar, LeapImageWithBBox, LeapImageWithHeatmap, LeapVideo, LeapValidationError, \
|
|
33
|
+
map_leap_data_type_to_visualizer_class
|
|
30
34
|
from code_loader.inner_leap_binder.leapbinder import mapping_runtime_mode_env_var_mame
|
|
31
35
|
from code_loader.mixpanel_tracker import clear_integration_events, AnalyticsEvent, emit_integration_event_once
|
|
32
36
|
|
|
33
37
|
_called_from_inside_tl_decorator = 0
|
|
34
38
|
_called_from_inside_tl_integration_test_decorator = False
|
|
35
|
-
_mapping_dataset_is_grouped = False
|
|
36
39
|
_call_from_tl_platform = os.environ.get('IS_TENSORLEAP_PLATFORM') == 'true'
|
|
37
40
|
|
|
38
41
|
# ---- warnings store (module-level) ----
|
|
@@ -198,58 +201,14 @@ def validate_output_structure(result, func_name: str, expected_type_name="np.nda
|
|
|
198
201
|
def batch_warning(result, func_name):
|
|
199
202
|
if len(result.shape) > 0 and result.shape[0] == 1:
|
|
200
203
|
warnings.warn(
|
|
201
|
-
f"{func_name} warning: Tensorleap will add a batch dimension at axis 0
|
|
202
|
-
f"
|
|
203
|
-
f"
|
|
204
|
-
f"
|
|
205
|
-
f"
|
|
204
|
+
f"{func_name} warning: Tensorleap will add a batch dimension at axis 0 of "
|
|
205
|
+
f"{func_name}'s output, but axis 0 is already size 1 — this can produce a "
|
|
206
|
+
f"doubly-batched shape (e.g. (1, 1, ...)). If axis 0 is a real dimension "
|
|
207
|
+
f"(e.g. a single channel) and not an accidental batch, you can ignore this; "
|
|
208
|
+
f"only fix it if you pre-batched the output."
|
|
206
209
|
)
|
|
207
210
|
|
|
208
211
|
|
|
209
|
-
def _validate_id_or_group(sample_id, preprocess_response, func_name):
|
|
210
|
-
"""Accept either a single sample id (flat dataset) or a group — a list of ids (grouped
|
|
211
|
-
dataset). The argument shape must match the dataset: a group requires a grouped
|
|
212
|
-
PreprocessResponse and a single id requires a flat one. Every id must match the declared
|
|
213
|
-
sample_id_type."""
|
|
214
|
-
is_group = isinstance(sample_id, list)
|
|
215
|
-
if is_group and not preprocess_response.is_grouped:
|
|
216
|
-
raise AssertionError(
|
|
217
|
-
f'{func_name}() validation failed: got a group (list of sample ids) but the '
|
|
218
|
-
f'PreprocessResponse is not grouped. Pass a single sample id for a flat dataset.')
|
|
219
|
-
if not is_group and preprocess_response.is_grouped:
|
|
220
|
-
raise AssertionError(
|
|
221
|
-
f'{func_name}() validation failed: got a single sample id but the PreprocessResponse '
|
|
222
|
-
f'is grouped. Pass a group (list of sample ids) for a grouped dataset.')
|
|
223
|
-
ids = sample_id if is_group else [sample_id]
|
|
224
|
-
for sid in ids:
|
|
225
|
-
assert type(sid) == preprocess_response.sample_id_type, \
|
|
226
|
-
(f'{func_name}() validation failed: '
|
|
227
|
-
f'Argument sample_id should be as the same type as defined in the preprocess response '
|
|
228
|
-
f'{preprocess_response.sample_id_type}. Got {type(sid)}.')
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
def _validate_grouped_result(result, group_size, func_name, validate_single):
|
|
232
|
-
"""A grouped (group) encoder result must be either a single ndarray with leading dim
|
|
233
|
-
== group_size, or a list of `group_size` per-sample results (each validated by
|
|
234
|
-
validate_single). Metadata validates its grouped result separately (list only)."""
|
|
235
|
-
if isinstance(result, np.ndarray):
|
|
236
|
-
assert len(result.shape) > 0 and result.shape[0] == group_size, \
|
|
237
|
-
(f'{func_name}() validation failed: expected a grouped output for a group of '
|
|
238
|
-
f'{group_size}: an array with leading dim {group_size}, got shape {result.shape}.')
|
|
239
|
-
validate_single(result)
|
|
240
|
-
elif isinstance(result, list):
|
|
241
|
-
assert len(result) == group_size, \
|
|
242
|
-
(f'{func_name}() validation failed: expected a grouped output for a group of '
|
|
243
|
-
f'{group_size}: a list of {group_size} arrays, got {len(result)}.')
|
|
244
|
-
for r in result:
|
|
245
|
-
validate_single(r)
|
|
246
|
-
else:
|
|
247
|
-
raise AssertionError(
|
|
248
|
-
f'{func_name}() validation failed: expected a grouped output for a group of '
|
|
249
|
-
f'{group_size}: either an array with leading dim {group_size} or a list of '
|
|
250
|
-
f'{group_size} arrays, got {type(result)}.')
|
|
251
|
-
|
|
252
|
-
|
|
253
212
|
def _add_mapping_connection(user_unique_name, connection_destinations, arg_names, name, node_mapping_type):
|
|
254
213
|
connection_destinations = [connection_destination for connection_destination in connection_destinations
|
|
255
214
|
if not isinstance(connection_destination, SamplePreprocessResponse)]
|
|
@@ -327,12 +286,16 @@ def tensorleap_integration_test():
|
|
|
327
286
|
|
|
328
287
|
def _validate_input_args(*args, **kwargs):
|
|
329
288
|
sample_id, preprocess_response = args
|
|
330
|
-
|
|
289
|
+
assert type(sample_id) == preprocess_response.sample_id_type, (
|
|
290
|
+
f"tensorleap_integration_test validation failed: "
|
|
291
|
+
f"sample_id type ({type(sample_id).__name__}) does not match the expected "
|
|
292
|
+
f"type ({preprocess_response.sample_id_type}) from the PreprocessResponse."
|
|
293
|
+
)
|
|
331
294
|
|
|
332
295
|
def inner(*args, **kwargs):
|
|
333
296
|
if not _call_from_tl_platform:
|
|
334
297
|
set_current('tensorleap_integration_test')
|
|
335
|
-
validate_args_structure(*args, types_order=[Union[int, str
|
|
298
|
+
validate_args_structure(*args, types_order=[Union[int, str], PreprocessResponse],
|
|
336
299
|
func_name='integration_test', expected_names=["idx", "preprocess"], **kwargs)
|
|
337
300
|
_validate_input_args(*args, **kwargs)
|
|
338
301
|
|
|
@@ -347,30 +310,29 @@ def tensorleap_integration_test():
|
|
|
347
310
|
if not _call_from_tl_platform:
|
|
348
311
|
update_env_params_func("tensorleap_integration_test",
|
|
349
312
|
"v") # put here because otherwise it will become v only if it finishes all the script
|
|
313
|
+
_reset_model_loop_state()
|
|
350
314
|
ret = integration_test_function(*args, **kwargs)
|
|
351
315
|
|
|
352
|
-
global _mapping_dataset_is_grouped
|
|
353
|
-
_mapping_dataset_is_grouped = args[1].is_grouped
|
|
354
316
|
try:
|
|
355
317
|
os.environ[mapping_runtime_mode_env_var_mame] = 'True'
|
|
318
|
+
_reset_model_loop_state()
|
|
356
319
|
integration_test_function(None, PreprocessResponse(state=DataStateType.training, length=0))
|
|
357
320
|
except Exception as e:
|
|
358
321
|
import traceback
|
|
359
322
|
first_tb = traceback.extract_tb(e.__traceback__)[-1]
|
|
360
323
|
file_name = Path(first_tb.filename).name
|
|
361
324
|
line_number = first_tb.lineno
|
|
362
|
-
|
|
363
|
-
|
|
325
|
+
update_env_params_func("code_mapping", "x")
|
|
326
|
+
if isinstance(e, LeapValidationError):
|
|
327
|
+
raise Exception(f'Invalid integration code. File {file_name}, line {line_number}: {e}')
|
|
328
|
+
elif isinstance(e, TypeError) and 'is not subscriptable' in str(e):
|
|
364
329
|
raise Exception(f'Invalid integration code. File {file_name}, line {line_number}: '
|
|
365
|
-
|
|
330
|
+
f"indexing is supported only on the model's predictions inside the integration test. Please remove this indexing operation usage from the integration test code.")
|
|
366
331
|
else:
|
|
367
|
-
update_env_params_func("code_mapping", "x")
|
|
368
|
-
|
|
369
332
|
raise Exception(f'Invalid integration code. File {file_name}, line {line_number}: '
|
|
370
|
-
|
|
371
|
-
|
|
333
|
+
f'Integration test is only allowed to call Tensorleap decorators. '
|
|
334
|
+
f'Ensure any arithmetics, external library use, Python logic is placed within Tensorleap decoders')
|
|
372
335
|
finally:
|
|
373
|
-
_mapping_dataset_is_grouped = False
|
|
374
336
|
if mapping_runtime_mode_env_var_mame in os.environ:
|
|
375
337
|
del os.environ[mapping_runtime_mode_env_var_mame]
|
|
376
338
|
finally:
|
|
@@ -393,6 +355,14 @@ def _safe_get_item(key):
|
|
|
393
355
|
|
|
394
356
|
|
|
395
357
|
def tensorleap_load_model(prediction_types: Optional[List[PredictionTypeHandler]] = _UNSET):
|
|
358
|
+
"""Register the model-loading function.
|
|
359
|
+
|
|
360
|
+
``prediction_types``: declare one ``PredictionTypeHandler`` per model output, in
|
|
361
|
+
output order. Multi-output models must declare an entry for EVERY output — the
|
|
362
|
+
count is validated against the model's outputs — so for an output you don't consume
|
|
363
|
+
as a prediction, pass a throwaway handler to satisfy the count. Omit the argument
|
|
364
|
+
only when the model has a single prediction output.
|
|
365
|
+
"""
|
|
396
366
|
prediction_types_was_provided = prediction_types is not _UNSET
|
|
397
367
|
|
|
398
368
|
if not prediction_types_was_provided:
|
|
@@ -612,6 +582,8 @@ def tensorleap_load_model(prediction_types: Optional[List[PredictionTypeHandler]
|
|
|
612
582
|
return self.model.get_inputs()
|
|
613
583
|
|
|
614
584
|
model_placeholder = ModelPlaceholder(prediction_types)
|
|
585
|
+
global _last_loaded_model
|
|
586
|
+
_last_loaded_model = model_placeholder
|
|
615
587
|
if not _call_from_tl_platform:
|
|
616
588
|
update_env_params_func("tensorleap_load_model", "v")
|
|
617
589
|
return model_placeholder
|
|
@@ -622,8 +594,16 @@ def tensorleap_load_model(prediction_types: Optional[List[PredictionTypeHandler]
|
|
|
622
594
|
self.node_mapping = NodeMapping('', NodeMappingType.Prediction0)
|
|
623
595
|
|
|
624
596
|
def __getitem__(self, key):
|
|
625
|
-
assert isinstance(key, int), \
|
|
626
|
-
f'Expected key to be an int, got {type(key)} instead.'
|
|
597
|
+
assert isinstance(key, (int, str)), \
|
|
598
|
+
f'Expected key to be an int or a prediction-type name, got {type(key)} instead.'
|
|
599
|
+
if isinstance(key, str):
|
|
600
|
+
declared_names = [prediction_type.name for prediction_type
|
|
601
|
+
in leap_binder.setup_container.prediction_types]
|
|
602
|
+
if key not in declared_names:
|
|
603
|
+
raise Exception(
|
|
604
|
+
f'Unknown prediction name "{key}" — declared prediction types: '
|
|
605
|
+
f'{declared_names}.')
|
|
606
|
+
key = declared_names.index(key)
|
|
627
607
|
|
|
628
608
|
ret = TempMapping()
|
|
629
609
|
try:
|
|
@@ -647,7 +627,9 @@ def tensorleap_load_model(prediction_types: Optional[List[PredictionTypeHandler]
|
|
|
647
627
|
|
|
648
628
|
# onnx runtime interface
|
|
649
629
|
def run(self, output_names, input_dict):
|
|
650
|
-
|
|
630
|
+
if output_names is not None:
|
|
631
|
+
raise LeapValidationError("Tensorleap doesn't support selecting outputs by name — "
|
|
632
|
+
"use model.run(None, inputs) to return all outputs.")
|
|
651
633
|
assert isinstance(input_dict, dict), \
|
|
652
634
|
f'Expected input_dict to be a dict, got {type(input_dict)} instead.'
|
|
653
635
|
for i, (input_key, elem) in enumerate(input_dict.items()):
|
|
@@ -675,7 +657,10 @@ def tensorleap_load_model(prediction_types: Optional[List[PredictionTypeHandler]
|
|
|
675
657
|
|
|
676
658
|
return FollowInputIndex()
|
|
677
659
|
|
|
678
|
-
|
|
660
|
+
model_placeholder = ModelPlaceholder()
|
|
661
|
+
global _last_loaded_model
|
|
662
|
+
_last_loaded_model = model_placeholder
|
|
663
|
+
return model_placeholder
|
|
679
664
|
|
|
680
665
|
def final_inner(*args, **kwargs):
|
|
681
666
|
if os.environ.get(mapping_runtime_mode_env_var_mame):
|
|
@@ -1552,10 +1537,13 @@ def tensorleap_metadata(
|
|
|
1552
1537
|
raise Exception(f'Metadata with name {name} already exists. '
|
|
1553
1538
|
f'Please choose another')
|
|
1554
1539
|
|
|
1555
|
-
def _validate_input_args(sample_id: Union[int, str
|
|
1556
|
-
|
|
1540
|
+
def _validate_input_args(sample_id: Union[int, str], preprocess_response: PreprocessResponse):
|
|
1541
|
+
assert type(sample_id) == preprocess_response.sample_id_type, \
|
|
1542
|
+
(f'{user_function.__name__}() validation failed: '
|
|
1543
|
+
f'Argument sample_id should be as the same type as defined in the preprocess response '
|
|
1544
|
+
f'{preprocess_response.sample_id_type}. Got {type(sample_id)}.')
|
|
1557
1545
|
|
|
1558
|
-
def
|
|
1546
|
+
def _validate_result(result):
|
|
1559
1547
|
supported_result_types = (type(None), int, str, bool, float, dict, np.floating,
|
|
1560
1548
|
np.bool_, np.unsignedinteger, np.signedinteger, np.integer)
|
|
1561
1549
|
if isinstance(result, tuple):
|
|
@@ -1573,20 +1561,6 @@ def tensorleap_metadata(
|
|
|
1573
1561
|
(f'{user_function.__name__}() validation failed: '
|
|
1574
1562
|
f'Values in the return dict should be of type {str(supported_result_types)}. Got {type(value)}.')
|
|
1575
1563
|
|
|
1576
|
-
def _validate_result(result, grouped=False, group_size=None):
|
|
1577
|
-
if not grouped:
|
|
1578
|
-
_validate_single(result)
|
|
1579
|
-
return
|
|
1580
|
-
# Grouped metadata: a list of `group_size` per-sample scalars/dicts.
|
|
1581
|
-
assert isinstance(result, list), \
|
|
1582
|
-
(f'{user_function.__name__}() validation failed: expected a grouped output for a group '
|
|
1583
|
-
f'of {group_size}: a list of {group_size} metadata values, got {type(result)}.')
|
|
1584
|
-
assert len(result) == group_size, \
|
|
1585
|
-
(f'{user_function.__name__}() validation failed: expected a grouped output for a group '
|
|
1586
|
-
f'of {group_size}: a list of {group_size} metadata values, got {len(result)}.')
|
|
1587
|
-
for value in result:
|
|
1588
|
-
_validate_single(value)
|
|
1589
|
-
|
|
1590
1564
|
def inner_without_validate(sample_id, preprocess_response):
|
|
1591
1565
|
|
|
1592
1566
|
global _called_from_inside_tl_decorator
|
|
@@ -1606,16 +1580,14 @@ def tensorleap_metadata(
|
|
|
1606
1580
|
set_current('tensorleap_metadata')
|
|
1607
1581
|
if os.environ.get(mapping_runtime_mode_env_var_mame):
|
|
1608
1582
|
return None
|
|
1609
|
-
validate_args_structure(*args, types_order=[Union[int, str
|
|
1583
|
+
validate_args_structure(*args, types_order=[Union[int, str], PreprocessResponse],
|
|
1610
1584
|
func_name=user_function.__name__, expected_names=["idx", "preprocess"], **kwargs)
|
|
1611
1585
|
sample_id, preprocess_response = args if len(args) != 0 else kwargs.values()
|
|
1612
1586
|
_validate_input_args(sample_id, preprocess_response)
|
|
1613
1587
|
|
|
1614
|
-
grouped = isinstance(sample_id, list)
|
|
1615
|
-
group_size = len(sample_id) if grouped else None
|
|
1616
1588
|
result = inner_without_validate(sample_id, preprocess_response)
|
|
1617
1589
|
|
|
1618
|
-
_validate_result(result
|
|
1590
|
+
_validate_result(result)
|
|
1619
1591
|
if not _call_from_tl_platform:
|
|
1620
1592
|
update_env_params_func("tensorleap_metadata", "v")
|
|
1621
1593
|
return result
|
|
@@ -1627,50 +1599,35 @@ def tensorleap_metadata(
|
|
|
1627
1599
|
|
|
1628
1600
|
def tensorleap_custom_latent_space():
|
|
1629
1601
|
def decorating_function(user_function: SectionCallableInterface):
|
|
1630
|
-
def _validate_input_args(sample_id: Union[int, str
|
|
1631
|
-
|
|
1602
|
+
def _validate_input_args(sample_id: Union[int, str], preprocess_response: PreprocessResponse):
|
|
1603
|
+
assert isinstance(sample_id, (int, str)), \
|
|
1604
|
+
(f'tensorleap_custom_latent_space validation failed: '
|
|
1605
|
+
f'Argument sample_id should be either int or str. Got {type(sample_id)}.')
|
|
1606
|
+
assert isinstance(preprocess_response, PreprocessResponse), \
|
|
1607
|
+
(f'tensorleap_custom_latent_space validation failed: '
|
|
1608
|
+
f'Argument preprocess_response should be a PreprocessResponse. Got {type(preprocess_response)}.')
|
|
1609
|
+
assert type(sample_id) == preprocess_response.sample_id_type, \
|
|
1610
|
+
(f'tensorleap_custom_latent_space validation failed: '
|
|
1611
|
+
f'Argument sample_id should be as the same type as defined in the preprocess response '
|
|
1612
|
+
f'{preprocess_response.sample_id_type}. Got {type(sample_id)}.')
|
|
1632
1613
|
|
|
1633
|
-
def
|
|
1634
|
-
assert isinstance(
|
|
1614
|
+
def _validate_result(result):
|
|
1615
|
+
assert isinstance(result, np.ndarray), \
|
|
1635
1616
|
(f'tensorleap_custom_latent_space validation failed: '
|
|
1636
|
-
f'The return type should be a numpy array. Got {type(
|
|
1637
|
-
if
|
|
1638
|
-
flat_dim = int(np.prod(
|
|
1617
|
+
f'The return type should be a numpy array. Got {type(result)}.')
|
|
1618
|
+
if result.ndim > 1:
|
|
1619
|
+
flat_dim = int(np.prod(result.shape))
|
|
1639
1620
|
store_general_warning(
|
|
1640
|
-
key=("tensorleap_custom_latent_space_flatten", tuple(
|
|
1621
|
+
key=("tensorleap_custom_latent_space_flatten", tuple(result.shape)),
|
|
1641
1622
|
message=(
|
|
1642
|
-
f"tensorleap_custom_latent_space returned per-sample shape {tuple(
|
|
1643
|
-
f"(ndim={
|
|
1623
|
+
f"tensorleap_custom_latent_space returned per-sample shape {tuple(result.shape)} "
|
|
1624
|
+
f"(ndim={result.ndim}). Tensorleap assumes per-sample shape (d, ...) and will "
|
|
1644
1625
|
f"flatten to ({flat_dim},) before downstream visualization and clustering. "
|
|
1645
1626
|
f"If you want a different aggregation (e.g. global average pooling), do it "
|
|
1646
1627
|
f"inside your function."
|
|
1647
1628
|
),
|
|
1648
1629
|
)
|
|
1649
1630
|
|
|
1650
|
-
def _validate_result(result, grouped=False, group_size=None):
|
|
1651
|
-
if not grouped:
|
|
1652
|
-
_validate_single(result)
|
|
1653
|
-
return
|
|
1654
|
-
# Grouped: (B, *) array or list of B per-sample arrays. Validate each per-sample slice so
|
|
1655
|
-
# the per-sample flatten check/warning uses the sample shape, not the (B, *) batch shape.
|
|
1656
|
-
if isinstance(result, np.ndarray):
|
|
1657
|
-
assert result.ndim >= 1 and result.shape[0] == group_size, \
|
|
1658
|
-
(f'tensorleap_custom_latent_space validation failed: expected a grouped output for a '
|
|
1659
|
-
f'group of {group_size}: an array with leading dim {group_size}, got shape {result.shape}.')
|
|
1660
|
-
for single in result:
|
|
1661
|
-
_validate_single(single)
|
|
1662
|
-
elif isinstance(result, list):
|
|
1663
|
-
assert len(result) == group_size, \
|
|
1664
|
-
(f'tensorleap_custom_latent_space validation failed: expected a grouped output for a '
|
|
1665
|
-
f'group of {group_size}: a list of {group_size} arrays, got {len(result)}.')
|
|
1666
|
-
for single in result:
|
|
1667
|
-
_validate_single(single)
|
|
1668
|
-
else:
|
|
1669
|
-
raise AssertionError(
|
|
1670
|
-
f'tensorleap_custom_latent_space validation failed: expected a grouped output for a '
|
|
1671
|
-
f'group of {group_size}: either an array with leading dim {group_size} or a list of '
|
|
1672
|
-
f'{group_size} arrays, got {type(result)}.')
|
|
1673
|
-
|
|
1674
1631
|
def inner_without_validate(sample_id, preprocess_response):
|
|
1675
1632
|
global _called_from_inside_tl_decorator
|
|
1676
1633
|
_called_from_inside_tl_decorator += 1
|
|
@@ -1690,11 +1647,9 @@ def tensorleap_custom_latent_space():
|
|
|
1690
1647
|
|
|
1691
1648
|
_validate_input_args(sample_id, preprocess_response)
|
|
1692
1649
|
|
|
1693
|
-
grouped = isinstance(sample_id, list)
|
|
1694
|
-
group_size = len(sample_id) if grouped else None
|
|
1695
1650
|
result = inner_without_validate(sample_id, preprocess_response)
|
|
1696
1651
|
|
|
1697
|
-
_validate_result(result
|
|
1652
|
+
_validate_result(result)
|
|
1698
1653
|
return result
|
|
1699
1654
|
|
|
1700
1655
|
return inner
|
|
@@ -1702,6 +1657,939 @@ def tensorleap_custom_latent_space():
|
|
|
1702
1657
|
return decorating_function
|
|
1703
1658
|
|
|
1704
1659
|
|
|
1660
|
+
_MODEL_LOOP_MAX_STEPS = 1000
|
|
1661
|
+
|
|
1662
|
+
_active_model_loop = None
|
|
1663
|
+
_model_loop_run_in_current_test = False
|
|
1664
|
+
_last_loaded_model = None
|
|
1665
|
+
_chain_artifact_provenance: Dict[int, tuple] = {}
|
|
1666
|
+
|
|
1667
|
+
_validate_state_types = validate_autoregressive_state_types
|
|
1668
|
+
_nest_equal = autoregressive_nests_equal
|
|
1669
|
+
|
|
1670
|
+
|
|
1671
|
+
def _nest_fingerprint(value):
|
|
1672
|
+
digest = hashlib.sha1()
|
|
1673
|
+
|
|
1674
|
+
def _feed(item):
|
|
1675
|
+
if isinstance(item, dict):
|
|
1676
|
+
digest.update(b'{')
|
|
1677
|
+
for key in sorted(item):
|
|
1678
|
+
digest.update(str(key).encode())
|
|
1679
|
+
digest.update(b':')
|
|
1680
|
+
_feed(item[key])
|
|
1681
|
+
digest.update(b'}')
|
|
1682
|
+
elif isinstance(item, (list, tuple)):
|
|
1683
|
+
digest.update(b'[')
|
|
1684
|
+
for element in item:
|
|
1685
|
+
_feed(element)
|
|
1686
|
+
digest.update(b']')
|
|
1687
|
+
elif isinstance(item, np.ndarray):
|
|
1688
|
+
digest.update(str(item.dtype).encode())
|
|
1689
|
+
digest.update(str(item.shape).encode())
|
|
1690
|
+
digest.update(np.ascontiguousarray(item).tobytes())
|
|
1691
|
+
else:
|
|
1692
|
+
digest.update(repr(item).encode())
|
|
1693
|
+
|
|
1694
|
+
_feed(value)
|
|
1695
|
+
return digest.hexdigest()
|
|
1696
|
+
|
|
1697
|
+
|
|
1698
|
+
def _ndarray_leaves(value):
|
|
1699
|
+
if isinstance(value, np.ndarray):
|
|
1700
|
+
return [value]
|
|
1701
|
+
if isinstance(value, dict):
|
|
1702
|
+
return [leaf for item in value.values() for leaf in _ndarray_leaves(item)]
|
|
1703
|
+
if isinstance(value, (list, tuple)):
|
|
1704
|
+
return [leaf for item in value for leaf in _ndarray_leaves(item)]
|
|
1705
|
+
return []
|
|
1706
|
+
|
|
1707
|
+
|
|
1708
|
+
def _squeeze_leading_batch_axis(tensors_dict):
|
|
1709
|
+
squeezed = {}
|
|
1710
|
+
for key, value in tensors_dict.items():
|
|
1711
|
+
if isinstance(value, np.ndarray) and value.ndim > 0 and value.shape[0] == 1:
|
|
1712
|
+
squeezed[key] = value[0]
|
|
1713
|
+
else:
|
|
1714
|
+
squeezed[key] = value
|
|
1715
|
+
return squeezed
|
|
1716
|
+
|
|
1717
|
+
|
|
1718
|
+
def _register_chain_artifact(obj, kind):
|
|
1719
|
+
# The object reference is kept in the registry entry so its id() cannot be recycled by the GC.
|
|
1720
|
+
_chain_artifact_provenance[id(obj)] = (kind, _nest_fingerprint(obj), obj)
|
|
1721
|
+
|
|
1722
|
+
|
|
1723
|
+
def _lookup_chain_artifact(obj):
|
|
1724
|
+
entry = _chain_artifact_provenance.get(id(obj))
|
|
1725
|
+
if entry is None or entry[2] is not obj:
|
|
1726
|
+
return None
|
|
1727
|
+
return entry
|
|
1728
|
+
|
|
1729
|
+
|
|
1730
|
+
def _reset_model_loop_state():
|
|
1731
|
+
global _active_model_loop, _model_loop_run_in_current_test
|
|
1732
|
+
_active_model_loop = None
|
|
1733
|
+
_model_loop_run_in_current_test = False
|
|
1734
|
+
_chain_artifact_provenance.clear()
|
|
1735
|
+
|
|
1736
|
+
|
|
1737
|
+
class _MappingStatePlaceholder:
|
|
1738
|
+
def _unsupported(self):
|
|
1739
|
+
raise LeapValidationError(
|
|
1740
|
+
'The autoregressive state is opaque inside the integration test — pass it whole '
|
|
1741
|
+
'between the hook, the model loop and the autoregressive metrics. Read or derive '
|
|
1742
|
+
'state content inside the hook or inside the metric/visualizer body.')
|
|
1743
|
+
|
|
1744
|
+
def __getitem__(self, key):
|
|
1745
|
+
self._unsupported()
|
|
1746
|
+
|
|
1747
|
+
def __getattr__(self, name):
|
|
1748
|
+
raise AttributeError(name)
|
|
1749
|
+
|
|
1750
|
+
def __iter__(self):
|
|
1751
|
+
self._unsupported()
|
|
1752
|
+
|
|
1753
|
+
|
|
1754
|
+
class _ModelLoopContext:
|
|
1755
|
+
def __init__(self, sample_id, preprocess_response, prediction_names, is_mapping):
|
|
1756
|
+
self.sample_id = sample_id
|
|
1757
|
+
self.preprocess_response = preprocess_response
|
|
1758
|
+
self.prediction_names = prediction_names
|
|
1759
|
+
self.is_mapping = is_mapping
|
|
1760
|
+
self.phase = 'awaiting_first_hook'
|
|
1761
|
+
self.steps = 0
|
|
1762
|
+
self.fed_inputs = None
|
|
1763
|
+
self.fed_inputs_fingerprint = None
|
|
1764
|
+
self.last_outputs = None
|
|
1765
|
+
self.last_outputs_fingerprint = None
|
|
1766
|
+
self.last_state = None
|
|
1767
|
+
self.last_state_fingerprint = None
|
|
1768
|
+
|
|
1769
|
+
def _fail(self, message):
|
|
1770
|
+
raise LeapValidationError(f'tensorleap_model_loop validation failed: {message}')
|
|
1771
|
+
|
|
1772
|
+
def on_hook_call(self, sample_id, prev_inputs, prev_outputs, state, preprocess_response):
|
|
1773
|
+
if self.phase == 'ended':
|
|
1774
|
+
self._fail('the chain already ended — once the autoregressive step returns None for '
|
|
1775
|
+
'next_inputs the loop must exit without further hook or model calls.')
|
|
1776
|
+
if self.phase == 'awaiting_model':
|
|
1777
|
+
self._fail('two autoregressive step calls in a row — the model must run exactly once '
|
|
1778
|
+
'between hook calls.')
|
|
1779
|
+
if sample_id != self.sample_id:
|
|
1780
|
+
self._fail('the hook must be called with the sample_id the model loop received.')
|
|
1781
|
+
if preprocess_response is not self.preprocess_response:
|
|
1782
|
+
self._fail('the hook must be called with the preprocess response the model loop '
|
|
1783
|
+
'received.')
|
|
1784
|
+
if self.phase == 'awaiting_first_hook':
|
|
1785
|
+
if prev_inputs is not None or prev_outputs is not None or state is not None:
|
|
1786
|
+
self._fail('the first hook call inside the model loop must be the initial call: '
|
|
1787
|
+
'prev_inputs=None, prev_outputs=None, state=None.')
|
|
1788
|
+
return
|
|
1789
|
+
if prev_inputs is not self.fed_inputs:
|
|
1790
|
+
self._fail('prev_inputs must be exactly the inputs dict the previous hook call '
|
|
1791
|
+
'returned (the one fed to the model) — do not copy, rebuild or slice it.')
|
|
1792
|
+
if prev_outputs is not self.last_outputs:
|
|
1793
|
+
self._fail('prev_outputs must be exactly the outputs dict the model call returned — '
|
|
1794
|
+
'do not copy, rebuild or slice it.')
|
|
1795
|
+
if state is not self.last_state:
|
|
1796
|
+
self._fail('state must be exactly the state object the previous hook call returned.')
|
|
1797
|
+
if _nest_fingerprint(prev_inputs) != self.fed_inputs_fingerprint:
|
|
1798
|
+
self._fail('the model inputs were mutated in place after being fed to the model — '
|
|
1799
|
+
'the platform re-reads the stored step tensors, so local mutations diverge '
|
|
1800
|
+
'from platform results. Compute new tensors inside the hook instead.')
|
|
1801
|
+
if _nest_fingerprint(prev_outputs) != self.last_outputs_fingerprint:
|
|
1802
|
+
self._fail('the model outputs were mutated in place after the model call — the '
|
|
1803
|
+
'platform re-reads the stored step tensors, so local mutations diverge '
|
|
1804
|
+
'from platform results. Compute derived tensors inside the hook instead.')
|
|
1805
|
+
if _nest_fingerprint(state) != self.last_state_fingerprint:
|
|
1806
|
+
self._fail('the state object was mutated between hook calls — state may only change '
|
|
1807
|
+
'inside the hook body.')
|
|
1808
|
+
|
|
1809
|
+
def on_hook_return(self, next_inputs, state):
|
|
1810
|
+
self.steps += 1
|
|
1811
|
+
if self.steps > _MODEL_LOOP_MAX_STEPS:
|
|
1812
|
+
self._fail(f'the chain exceeded {_MODEL_LOOP_MAX_STEPS} steps without the '
|
|
1813
|
+
f'autoregressive step returning None — the loop would never terminate on '
|
|
1814
|
+
f'the platform.')
|
|
1815
|
+
self.last_state = state
|
|
1816
|
+
self.last_state_fingerprint = _nest_fingerprint(state)
|
|
1817
|
+
if next_inputs is None:
|
|
1818
|
+
self.phase = 'ended'
|
|
1819
|
+
else:
|
|
1820
|
+
self.fed_inputs = next_inputs
|
|
1821
|
+
self.fed_inputs_fingerprint = _nest_fingerprint(next_inputs)
|
|
1822
|
+
self.phase = 'awaiting_model'
|
|
1823
|
+
|
|
1824
|
+
def mapping_hook_call(self, inputs_placeholder, state_placeholder):
|
|
1825
|
+
if self.phase == 'ended':
|
|
1826
|
+
self._fail('the chain already ended — once the autoregressive step returns None for '
|
|
1827
|
+
'next_inputs the loop must exit without further hook or model calls.')
|
|
1828
|
+
if self.phase == 'awaiting_model':
|
|
1829
|
+
self._fail('two autoregressive step calls in a row — the model must run exactly once '
|
|
1830
|
+
'between hook calls.')
|
|
1831
|
+
self.last_state = state_placeholder
|
|
1832
|
+
if self.phase == 'awaiting_first_hook':
|
|
1833
|
+
self.fed_inputs = inputs_placeholder
|
|
1834
|
+
self.phase = 'awaiting_model'
|
|
1835
|
+
return inputs_placeholder, state_placeholder
|
|
1836
|
+
self.phase = 'ended'
|
|
1837
|
+
return None, state_placeholder
|
|
1838
|
+
|
|
1839
|
+
def on_model_call(self, fed):
|
|
1840
|
+
if self.phase == 'awaiting_first_hook':
|
|
1841
|
+
self._fail('the model was called before the autoregressive step produced the initial '
|
|
1842
|
+
'inputs — the loop must start with the initial hook call '
|
|
1843
|
+
'(prev_inputs=None, prev_outputs=None, state=None).')
|
|
1844
|
+
if self.phase == 'awaiting_hook':
|
|
1845
|
+
self._fail('the model was called twice in a row — the autoregressive step must run '
|
|
1846
|
+
'between model calls.')
|
|
1847
|
+
if self.phase == 'ended':
|
|
1848
|
+
self._fail('the chain already ended — once the autoregressive step returns None for '
|
|
1849
|
+
'next_inputs the loop must exit without further hook or model calls.')
|
|
1850
|
+
if self.is_mapping:
|
|
1851
|
+
return
|
|
1852
|
+
fed_ids = {id(leaf) for leaf in _ndarray_leaves(fed)}
|
|
1853
|
+
hook_ids = {id(value) for value in self.fed_inputs.values()}
|
|
1854
|
+
if fed_ids != hook_ids:
|
|
1855
|
+
self._fail('the model must be fed exactly the tensors the last hook call returned — '
|
|
1856
|
+
'any computation between the hook and the model is invisible to the '
|
|
1857
|
+
'platform. Move it into the hook.')
|
|
1858
|
+
if _nest_fingerprint(self.fed_inputs) != self.fed_inputs_fingerprint:
|
|
1859
|
+
self._fail('the model inputs were mutated in place after the hook returned them — '
|
|
1860
|
+
'the platform feeds the model the tensors exactly as the hook returned '
|
|
1861
|
+
'them. Compute new tensors inside the hook instead.')
|
|
1862
|
+
|
|
1863
|
+
def on_model_return(self, raw_outputs):
|
|
1864
|
+
self.phase = 'awaiting_hook'
|
|
1865
|
+
if self.is_mapping:
|
|
1866
|
+
self.last_outputs = raw_outputs
|
|
1867
|
+
return raw_outputs
|
|
1868
|
+
outputs_list = raw_outputs if isinstance(raw_outputs, list) else [raw_outputs]
|
|
1869
|
+
if len(outputs_list) != len(self.prediction_names):
|
|
1870
|
+
self._fail(f'the model returned {len(outputs_list)} outputs but '
|
|
1871
|
+
f'{len(self.prediction_names)} prediction types are declared on '
|
|
1872
|
+
f'tensorleap_load_model — declare one prediction type per model output.')
|
|
1873
|
+
named_outputs = {name: np.asarray(output)
|
|
1874
|
+
for name, output in zip(self.prediction_names, outputs_list)}
|
|
1875
|
+
self.last_outputs = named_outputs
|
|
1876
|
+
self.last_outputs_fingerprint = _nest_fingerprint(named_outputs)
|
|
1877
|
+
return named_outputs
|
|
1878
|
+
|
|
1879
|
+
|
|
1880
|
+
class _ModelLoopModelProxy:
|
|
1881
|
+
def __init__(self, model, context):
|
|
1882
|
+
self._model = model
|
|
1883
|
+
self._context = context
|
|
1884
|
+
|
|
1885
|
+
def __call__(self, inputs):
|
|
1886
|
+
self._context.on_model_call(inputs)
|
|
1887
|
+
return self._context.on_model_return(self._model(inputs))
|
|
1888
|
+
|
|
1889
|
+
def run(self, output_names, input_dict):
|
|
1890
|
+
self._context.on_model_call(input_dict)
|
|
1891
|
+
return self._context.on_model_return(self._model.run(output_names, input_dict))
|
|
1892
|
+
|
|
1893
|
+
def get_inputs(self):
|
|
1894
|
+
return self._model.get_inputs()
|
|
1895
|
+
|
|
1896
|
+
|
|
1897
|
+
def tensorleap_autoregressive_step(latent_space_aggregation: str = 'last_step'):
|
|
1898
|
+
"""The feedback hook that drives an autoregressive chain.
|
|
1899
|
+
|
|
1900
|
+
Signature of the decorated function:
|
|
1901
|
+
(sample_id, prev_inputs, prev_outputs, state, preprocess)
|
|
1902
|
+
-> (Optional[Dict[str, np.ndarray]], state)
|
|
1903
|
+
|
|
1904
|
+
First call per chain: prev_inputs=None, prev_outputs=None, state=None — returns the initial
|
|
1905
|
+
model inputs and the initial state. Later calls receive the previous step's model inputs and
|
|
1906
|
+
outputs (unbatched, per-sample; outputs keyed by the declared prediction-type names) plus the
|
|
1907
|
+
state returned by the previous call, and return the next step's full input dict together with
|
|
1908
|
+
the updated state; returning (None, state) ends the chain — the terminating call still
|
|
1909
|
+
returns state, so the final step participates in state aggregation. State is never fed to
|
|
1910
|
+
the model; it may be any nest of dicts/lists holding numpy arrays, numbers, strings or bools,
|
|
1911
|
+
and it rides the platform queue on every step — accumulate reductions, not raw per-step
|
|
1912
|
+
tensors. The hook must be stateless and deterministically seeded (seed stochasticity from
|
|
1913
|
+
sample_id) — the engine replays steps after crash recovery and relies on identical results.
|
|
1914
|
+
|
|
1915
|
+
latent_space_aggregation ('last_step' | 'mean') declares how the chain's latent-space
|
|
1916
|
+
vectors are derived from its per-step forward passes. 'last_step' (default): every latent
|
|
1917
|
+
space comes from the final step's forward pass, except input-kind latent spaces which come
|
|
1918
|
+
from the first step — the original sample, before generated content dominates the model
|
|
1919
|
+
inputs. 'mean': every latent space is the elementwise mean over all steps of the chain.
|
|
1920
|
+
"""
|
|
1921
|
+
assert isinstance(latent_space_aggregation, str), \
|
|
1922
|
+
('tensorleap_autoregressive_step must be called with parentheses: '
|
|
1923
|
+
'@tensorleap_autoregressive_step() or '
|
|
1924
|
+
"@tensorleap_autoregressive_step(latent_space_aggregation='mean').")
|
|
1925
|
+
|
|
1926
|
+
def decorating_function(user_function: AutoregressiveStepCallableInterface):
|
|
1927
|
+
# Contract: shapes and key set are fixed across all calls AND all chains (compiled graphs
|
|
1928
|
+
# need static shapes), so one record shared across samples is the correct scope.
|
|
1929
|
+
first_call_record: Dict[str, Any] = {'keys': None, 'shapes': None}
|
|
1930
|
+
|
|
1931
|
+
def _normalize_args(args, kwargs):
|
|
1932
|
+
expected_names = ["sample_id", "prev_inputs", "prev_outputs", "state", "preprocess"]
|
|
1933
|
+
normalized = []
|
|
1934
|
+
for i, arg_name in enumerate(expected_names):
|
|
1935
|
+
if i < len(args):
|
|
1936
|
+
normalized.append(args[i])
|
|
1937
|
+
elif arg_name in kwargs:
|
|
1938
|
+
normalized.append(kwargs[arg_name])
|
|
1939
|
+
else:
|
|
1940
|
+
raise AssertionError(
|
|
1941
|
+
f'{user_function.__name__} validation failed: missing required argument '
|
|
1942
|
+
f"'{arg_name}'. Expected arguments: {expected_names}.")
|
|
1943
|
+
if len(args) + len(kwargs) != len(expected_names):
|
|
1944
|
+
raise AssertionError(
|
|
1945
|
+
f'{user_function.__name__} validation failed: expected exactly '
|
|
1946
|
+
f'{len(expected_names)} arguments {expected_names}, got '
|
|
1947
|
+
f'{len(args) + len(kwargs)}.')
|
|
1948
|
+
sample_id, prev_inputs, prev_outputs, state, preprocess_response = normalized
|
|
1949
|
+
assert isinstance(sample_id, (int, str)), \
|
|
1950
|
+
(f'{user_function.__name__}() validation failed: '
|
|
1951
|
+
f'sample_id must be an int or str. Got {type(sample_id).__name__}.')
|
|
1952
|
+
assert prev_inputs is None or isinstance(prev_inputs, dict), \
|
|
1953
|
+
(f'{user_function.__name__}() validation failed: '
|
|
1954
|
+
f'prev_inputs must be a dict or None. Got {type(prev_inputs).__name__}.')
|
|
1955
|
+
assert prev_outputs is None or isinstance(prev_outputs, dict), \
|
|
1956
|
+
(f'{user_function.__name__}() validation failed: '
|
|
1957
|
+
f'prev_outputs must be a dict or None. Got {type(prev_outputs).__name__}.')
|
|
1958
|
+
assert isinstance(preprocess_response, PreprocessResponse), \
|
|
1959
|
+
(f'{user_function.__name__}() validation failed: '
|
|
1960
|
+
f'preprocess must be a PreprocessResponse. Got '
|
|
1961
|
+
f'{type(preprocess_response).__name__}.')
|
|
1962
|
+
return normalized
|
|
1963
|
+
|
|
1964
|
+
def _validate_input_args(sample_id, prev_inputs, prev_outputs, state, preprocess_response):
|
|
1965
|
+
assert (prev_inputs is None) == (prev_outputs is None), \
|
|
1966
|
+
(f'{user_function.__name__}() validation failed: '
|
|
1967
|
+
f'prev_inputs and prev_outputs must both be None (first call) or both be dicts '
|
|
1968
|
+
f'(subsequent calls). Got prev_inputs={type(prev_inputs).__name__}, '
|
|
1969
|
+
f'prev_outputs={type(prev_outputs).__name__}.')
|
|
1970
|
+
assert not (prev_inputs is None and state is not None), \
|
|
1971
|
+
(f'{user_function.__name__}() validation failed: '
|
|
1972
|
+
f'the first call (prev_inputs=None, prev_outputs=None) must pass state=None. '
|
|
1973
|
+
f'Got state of type {type(state).__name__}.')
|
|
1974
|
+
assert type(sample_id) == preprocess_response.sample_id_type, \
|
|
1975
|
+
(f'{user_function.__name__}() validation failed: '
|
|
1976
|
+
f'Argument sample_id should be as the same type as defined in the preprocess response '
|
|
1977
|
+
f'{preprocess_response.sample_id_type}. Got {type(sample_id)}.')
|
|
1978
|
+
|
|
1979
|
+
def _validate_result(result, is_first_call):
|
|
1980
|
+
assert isinstance(result, tuple) and len(result) == 2, \
|
|
1981
|
+
(f'{user_function.__name__}() validation failed: '
|
|
1982
|
+
f'the hook must return a (next_inputs, state) tuple — next_inputs is a dict of '
|
|
1983
|
+
f'named model input tensors, or None to end the chain; state rides to the next '
|
|
1984
|
+
f'call. Got {type(result).__name__}.')
|
|
1985
|
+
next_inputs, state = result
|
|
1986
|
+
if next_inputs is None:
|
|
1987
|
+
assert not is_first_call, \
|
|
1988
|
+
(f'{user_function.__name__}() validation failed: '
|
|
1989
|
+
f'the first call (prev_inputs=None, prev_outputs=None, state=None) must return '
|
|
1990
|
+
f'the initial model inputs dict — returning None on the first call would '
|
|
1991
|
+
f'produce an empty chain.')
|
|
1992
|
+
return
|
|
1993
|
+
assert isinstance(next_inputs, dict), \
|
|
1994
|
+
(f'{user_function.__name__}() validation failed: '
|
|
1995
|
+
f'next_inputs must be a dict of named model input tensors, or None to end the '
|
|
1996
|
+
f'chain. Got {type(next_inputs)}.')
|
|
1997
|
+
assert next_inputs, \
|
|
1998
|
+
(f'{user_function.__name__}() validation failed: '
|
|
1999
|
+
f'next_inputs is an empty dict — return the full model input dict, or None to '
|
|
2000
|
+
f'end the chain.')
|
|
2001
|
+
for key, value in next_inputs.items():
|
|
2002
|
+
assert isinstance(key, str), \
|
|
2003
|
+
(f'{user_function.__name__}() validation failed: '
|
|
2004
|
+
f'all keys in next_inputs must be strings. Got {type(key)}.')
|
|
2005
|
+
assert not key.startswith('_'), \
|
|
2006
|
+
(f'{user_function.__name__}() validation failed: '
|
|
2007
|
+
f'next_inputs key "{key}" starts with "_" — underscore passthrough keys were '
|
|
2008
|
+
f'replaced by the explicit state channel. Move passthrough values into the '
|
|
2009
|
+
f'returned state.')
|
|
2010
|
+
assert isinstance(value, np.ndarray), \
|
|
2011
|
+
(f'{user_function.__name__}() validation failed: '
|
|
2012
|
+
f'value for model input "{key}" must be a numpy array. Got {type(value)}.')
|
|
2013
|
+
|
|
2014
|
+
def _validate_consistency(next_inputs):
|
|
2015
|
+
# Rules 2-3 of the autoregressive contract: identical key set and identical shapes on
|
|
2016
|
+
# every call — compiled graphs need static shapes.
|
|
2017
|
+
if next_inputs is None:
|
|
2018
|
+
return
|
|
2019
|
+
keys = set(next_inputs.keys())
|
|
2020
|
+
shapes = {key: tuple(value.shape) for key, value in next_inputs.items()}
|
|
2021
|
+
if first_call_record['keys'] is None:
|
|
2022
|
+
first_call_record['keys'] = keys
|
|
2023
|
+
first_call_record['shapes'] = shapes
|
|
2024
|
+
return
|
|
2025
|
+
assert keys == first_call_record['keys'], \
|
|
2026
|
+
(f'{user_function.__name__}() validation failed: '
|
|
2027
|
+
f'every call must return the same key set as the first call. '
|
|
2028
|
+
f'First call keys: {sorted(first_call_record["keys"])}, this call: {sorted(keys)}.')
|
|
2029
|
+
for key, shape in shapes.items():
|
|
2030
|
+
assert shape == first_call_record['shapes'][key], \
|
|
2031
|
+
(f'{user_function.__name__}() validation failed: '
|
|
2032
|
+
f'tensor shapes must be identical on every call (fixed-length padding + mask for '
|
|
2033
|
+
f'growing sequences). Input "{key}" was {first_call_record["shapes"][key]} on the '
|
|
2034
|
+
f'first call and {shape} now.')
|
|
2035
|
+
|
|
2036
|
+
def _assert_deterministic(first_result, second_result):
|
|
2037
|
+
error_message = (
|
|
2038
|
+
f'{user_function.__name__}() validation failed: '
|
|
2039
|
+
f'the hook is not deterministic — two calls with identical arguments returned different '
|
|
2040
|
+
f'outputs. Seed any stochasticity (e.g. initial noise) from sample_id: the engine '
|
|
2041
|
+
f'replays steps after crash recovery and relies on identical results.')
|
|
2042
|
+
assert isinstance(first_result, tuple) and isinstance(second_result, tuple) and \
|
|
2043
|
+
len(first_result) == 2 and len(second_result) == 2, error_message
|
|
2044
|
+
first_inputs, first_state = first_result
|
|
2045
|
+
second_inputs, second_state = second_result
|
|
2046
|
+
if first_inputs is None or second_inputs is None:
|
|
2047
|
+
assert first_inputs is None and second_inputs is None, error_message
|
|
2048
|
+
else:
|
|
2049
|
+
assert set(first_inputs.keys()) == set(second_inputs.keys()), error_message
|
|
2050
|
+
for key, value in first_inputs.items():
|
|
2051
|
+
if not isinstance(value, np.ndarray):
|
|
2052
|
+
continue
|
|
2053
|
+
assert np.array_equal(value, second_inputs[key]), error_message
|
|
2054
|
+
assert _nest_equal(first_state, second_state), error_message
|
|
2055
|
+
|
|
2056
|
+
def _squeeze_test_batch_dim(tensors_dict):
|
|
2057
|
+
# Inside the integration test the model works on batch-1 tensors (this wrapper adds the
|
|
2058
|
+
# batch axis to returned inputs, and model outputs come back batched). The user's hook
|
|
2059
|
+
# body must see the same unbatched per-sample tensors it will see at engine runtime, so
|
|
2060
|
+
# strip the leading batch-1 axis before calling it.
|
|
2061
|
+
if tensors_dict is None:
|
|
2062
|
+
return None
|
|
2063
|
+
squeezed = {}
|
|
2064
|
+
for key, value in tensors_dict.items():
|
|
2065
|
+
if isinstance(value, np.ndarray) and value.ndim > 0 and value.shape[0] == 1:
|
|
2066
|
+
squeezed[key] = value[0]
|
|
2067
|
+
else:
|
|
2068
|
+
squeezed[key] = value
|
|
2069
|
+
return squeezed
|
|
2070
|
+
|
|
2071
|
+
def inner_without_validate(sample_id, prev_inputs, prev_outputs, state, preprocess_response):
|
|
2072
|
+
global _called_from_inside_tl_decorator
|
|
2073
|
+
_called_from_inside_tl_decorator += 1
|
|
2074
|
+
try:
|
|
2075
|
+
result = user_function(sample_id, prev_inputs, prev_outputs, state,
|
|
2076
|
+
preprocess_response)
|
|
2077
|
+
finally:
|
|
2078
|
+
_called_from_inside_tl_decorator -= 1
|
|
2079
|
+
return result
|
|
2080
|
+
|
|
2081
|
+
leap_binder.set_autoregressive_step(inner_without_validate,
|
|
2082
|
+
latent_space_aggregation=latent_space_aggregation)
|
|
2083
|
+
|
|
2084
|
+
def inner(*args, **kwargs):
|
|
2085
|
+
if not _call_from_tl_platform:
|
|
2086
|
+
set_current('tensorleap_autoregressive_step')
|
|
2087
|
+
sample_id, prev_inputs, prev_outputs, state, preprocess_response = \
|
|
2088
|
+
_normalize_args(args, kwargs)
|
|
2089
|
+
|
|
2090
|
+
is_top_level_in_test = (_called_from_inside_tl_decorator == 0
|
|
2091
|
+
and _called_from_inside_tl_integration_test_decorator)
|
|
2092
|
+
loop_context = _active_model_loop
|
|
2093
|
+
if is_top_level_in_test:
|
|
2094
|
+
if loop_context is None:
|
|
2095
|
+
raise LeapValidationError(
|
|
2096
|
+
f'{user_function.__name__}() validation failed: inside the integration '
|
|
2097
|
+
f'test the autoregressive step may only be called from within the '
|
|
2098
|
+
f'tensorleap_model_loop function — including the initial call '
|
|
2099
|
+
f'(prev_inputs=None). The platform drives the chain through the model '
|
|
2100
|
+
f'loop only, so a hook call outside it would not run on the platform.')
|
|
2101
|
+
loop_context.on_hook_call(sample_id, prev_inputs, prev_outputs, state,
|
|
2102
|
+
preprocess_response)
|
|
2103
|
+
prev_inputs = _squeeze_test_batch_dim(prev_inputs)
|
|
2104
|
+
prev_outputs = _squeeze_test_batch_dim(prev_outputs)
|
|
2105
|
+
|
|
2106
|
+
_validate_input_args(sample_id, prev_inputs, prev_outputs, state, preprocess_response)
|
|
2107
|
+
|
|
2108
|
+
# The deep copy keeps the determinism double-call independent: a hook that
|
|
2109
|
+
# accumulates into state in place would otherwise see its own first-call mutations.
|
|
2110
|
+
state_for_second_call = copy.deepcopy(state) if is_top_level_in_test else None
|
|
2111
|
+
result = inner_without_validate(sample_id, prev_inputs, prev_outputs, state,
|
|
2112
|
+
preprocess_response)
|
|
2113
|
+
|
|
2114
|
+
if is_top_level_in_test:
|
|
2115
|
+
# Rule 7: determinism double-call. An unseeded sampler fails here, on the user's
|
|
2116
|
+
# machine, instead of silently forking a chain on engine crash-replay.
|
|
2117
|
+
second_result = inner_without_validate(sample_id, prev_inputs, prev_outputs,
|
|
2118
|
+
state_for_second_call, preprocess_response)
|
|
2119
|
+
_assert_deterministic(result, second_result)
|
|
2120
|
+
|
|
2121
|
+
_validate_result(result, is_first_call=prev_inputs is None)
|
|
2122
|
+
next_inputs, new_state = result
|
|
2123
|
+
_validate_consistency(next_inputs)
|
|
2124
|
+
_validate_state_types(new_state)
|
|
2125
|
+
|
|
2126
|
+
if is_top_level_in_test:
|
|
2127
|
+
if next_inputs is not None:
|
|
2128
|
+
next_inputs = {key: np.expand_dims(value, axis=0)
|
|
2129
|
+
for key, value in next_inputs.items()}
|
|
2130
|
+
loop_context.on_hook_return(next_inputs, new_state)
|
|
2131
|
+
|
|
2132
|
+
if not _call_from_tl_platform:
|
|
2133
|
+
update_env_params_func("tensorleap_autoregressive_step", "v")
|
|
2134
|
+
return next_inputs, new_state
|
|
2135
|
+
|
|
2136
|
+
class _MappingInputsPlaceholder:
|
|
2137
|
+
"""Mapping-mode stand-in for the hook's returned inputs dict.
|
|
2138
|
+
|
|
2139
|
+
Indexing it by a model-input name yields a placeholder whose node_mapping the model
|
|
2140
|
+
placeholder tags Input0/Input1/... — exactly how input encoders wire the graph. One
|
|
2141
|
+
NodeConnection is recorded per key, once.
|
|
2142
|
+
"""
|
|
2143
|
+
def __init__(self):
|
|
2144
|
+
self._items: Dict[str, Any] = {}
|
|
2145
|
+
|
|
2146
|
+
def __getitem__(self, key):
|
|
2147
|
+
assert isinstance(key, str), \
|
|
2148
|
+
f'Expected a string model input name, got {type(key)} instead.'
|
|
2149
|
+
if key not in self._items:
|
|
2150
|
+
class TempMapping:
|
|
2151
|
+
pass
|
|
2152
|
+
ret = TempMapping()
|
|
2153
|
+
ret.node_mapping = NodeMapping(key, NodeMappingType.Input)
|
|
2154
|
+
leap_binder.mapping_connections.append(NodeConnection(ret.node_mapping, None))
|
|
2155
|
+
self._items[key] = ret
|
|
2156
|
+
return self._items[key]
|
|
2157
|
+
|
|
2158
|
+
def get(self, key, default=None):
|
|
2159
|
+
# Deliberate alias of [] — the fixed-key-set contract makes a defaulted lookup
|
|
2160
|
+
# meaningless in mapping mode, and every accessed key must wire into the graph.
|
|
2161
|
+
return self[key]
|
|
2162
|
+
|
|
2163
|
+
def _unsupported_iteration(self):
|
|
2164
|
+
raise Exception(
|
|
2165
|
+
'Iterating over the autoregressive hook result is not supported inside the '
|
|
2166
|
+
'integration test. Pass the model its inputs with explicit keys, e.g. '
|
|
2167
|
+
"model.run(None, {'x': inputs['x'], 't': inputs['t']}).")
|
|
2168
|
+
|
|
2169
|
+
def items(self):
|
|
2170
|
+
self._unsupported_iteration()
|
|
2171
|
+
|
|
2172
|
+
def keys(self):
|
|
2173
|
+
self._unsupported_iteration()
|
|
2174
|
+
|
|
2175
|
+
def values(self):
|
|
2176
|
+
self._unsupported_iteration()
|
|
2177
|
+
|
|
2178
|
+
mapping_placeholder = _MappingInputsPlaceholder()
|
|
2179
|
+
mapping_state_placeholder = _MappingStatePlaceholder()
|
|
2180
|
+
|
|
2181
|
+
def mapping_inner(*args, **kwargs):
|
|
2182
|
+
loop_context = _active_model_loop
|
|
2183
|
+
if loop_context is None:
|
|
2184
|
+
raise LeapValidationError(
|
|
2185
|
+
f'{user_function.__name__}() validation failed: inside the integration test '
|
|
2186
|
+
f'the autoregressive step may only be called from within the '
|
|
2187
|
+
f'tensorleap_model_loop function — including the initial call '
|
|
2188
|
+
f'(prev_inputs=None).')
|
|
2189
|
+
return loop_context.mapping_hook_call(mapping_placeholder, mapping_state_placeholder)
|
|
2190
|
+
|
|
2191
|
+
def final_inner(*args, **kwargs):
|
|
2192
|
+
if os.environ.get(mapping_runtime_mode_env_var_mame):
|
|
2193
|
+
return mapping_inner(*args, **kwargs)
|
|
2194
|
+
else:
|
|
2195
|
+
return inner(*args, **kwargs)
|
|
2196
|
+
|
|
2197
|
+
if not _call_from_tl_platform:
|
|
2198
|
+
register_decorator_row("tensorleap_autoregressive_step")
|
|
2199
|
+
|
|
2200
|
+
return final_inner
|
|
2201
|
+
|
|
2202
|
+
return decorating_function
|
|
2203
|
+
|
|
2204
|
+
|
|
2205
|
+
def tensorleap_model_loop():
|
|
2206
|
+
"""The user-authored autoregressive rollout loop.
|
|
2207
|
+
|
|
2208
|
+
Signature of the decorated function:
|
|
2209
|
+
(model, sample_id, preprocess) -> (final_inputs, final_outputs, final_state)
|
|
2210
|
+
|
|
2211
|
+
The function drives the chain locally, exactly as the platform will: it makes the initial
|
|
2212
|
+
hook call (prev_inputs=None, prev_outputs=None, state=None) inside the loop, alternates
|
|
2213
|
+
model forwards with hook calls, feeds the model exactly the tensors the hook returned, and
|
|
2214
|
+
exits when the hook returns None for next_inputs — returning the last inputs dict fed to the
|
|
2215
|
+
model, the last model outputs (a dict keyed by the declared prediction-type names) and the
|
|
2216
|
+
final state. The loop body itself never runs on the platform — the engine drives the chain —
|
|
2217
|
+
so the decorator validates, while the loop runs, that its behavior matches the engine loop;
|
|
2218
|
+
any computation in the loop body outside the hook and the model is invisible to the platform
|
|
2219
|
+
and fails validation. Canonical shape:
|
|
2220
|
+
|
|
2221
|
+
@tensorleap_model_loop()
|
|
2222
|
+
def model_loop(model, sample_id, preprocess):
|
|
2223
|
+
inputs, state = generate_next_inputs(sample_id, None, None, None, preprocess)
|
|
2224
|
+
while True:
|
|
2225
|
+
outputs = model(inputs)
|
|
2226
|
+
next_inputs, state = generate_next_inputs(sample_id, inputs, outputs, state,
|
|
2227
|
+
preprocess)
|
|
2228
|
+
if next_inputs is None:
|
|
2229
|
+
return inputs, outputs, state
|
|
2230
|
+
inputs = next_inputs
|
|
2231
|
+
"""
|
|
2232
|
+
|
|
2233
|
+
def decorating_function(user_function):
|
|
2234
|
+
|
|
2235
|
+
def _normalize_args(args, kwargs):
|
|
2236
|
+
expected_names = ["model", "sample_id", "preprocess"]
|
|
2237
|
+
normalized = []
|
|
2238
|
+
for i, arg_name in enumerate(expected_names):
|
|
2239
|
+
if i < len(args):
|
|
2240
|
+
normalized.append(args[i])
|
|
2241
|
+
elif arg_name in kwargs:
|
|
2242
|
+
normalized.append(kwargs[arg_name])
|
|
2243
|
+
else:
|
|
2244
|
+
raise AssertionError(
|
|
2245
|
+
f'{user_function.__name__} validation failed: missing required argument '
|
|
2246
|
+
f"'{arg_name}'. Expected arguments: {expected_names}.")
|
|
2247
|
+
if len(args) + len(kwargs) != len(expected_names):
|
|
2248
|
+
raise AssertionError(
|
|
2249
|
+
f'{user_function.__name__} validation failed: expected exactly '
|
|
2250
|
+
f'{len(expected_names)} arguments {expected_names}, got '
|
|
2251
|
+
f'{len(args) + len(kwargs)}.')
|
|
2252
|
+
return normalized
|
|
2253
|
+
|
|
2254
|
+
def _fail(message):
|
|
2255
|
+
raise LeapValidationError(f'tensorleap_model_loop validation failed: {message}')
|
|
2256
|
+
|
|
2257
|
+
def _run_with_context(model, sample_id, preprocess_response, is_mapping):
|
|
2258
|
+
global _active_model_loop, _model_loop_run_in_current_test
|
|
2259
|
+
if _active_model_loop is not None:
|
|
2260
|
+
_fail('model loops cannot nest or run concurrently.')
|
|
2261
|
+
if _model_loop_run_in_current_test:
|
|
2262
|
+
_fail('only one model loop run is allowed per integration test.')
|
|
2263
|
+
if model is None or model is not _last_loaded_model:
|
|
2264
|
+
_fail('the model argument must be the object returned by the '
|
|
2265
|
+
'tensorleap_load_model function, unchanged.')
|
|
2266
|
+
if leap_binder.setup_container.autoregressive_step is None:
|
|
2267
|
+
_fail('tensorleap_model_loop requires a tensorleap_autoregressive_step hook.')
|
|
2268
|
+
prediction_names = [prediction_type.name for prediction_type
|
|
2269
|
+
in leap_binder.setup_container.prediction_types]
|
|
2270
|
+
if not prediction_names:
|
|
2271
|
+
_fail('tensorleap_model_loop requires prediction types declared on '
|
|
2272
|
+
'tensorleap_load_model — the loop keys the model outputs by their names.')
|
|
2273
|
+
context = _ModelLoopContext(sample_id, preprocess_response, prediction_names,
|
|
2274
|
+
is_mapping)
|
|
2275
|
+
_active_model_loop = context
|
|
2276
|
+
try:
|
|
2277
|
+
result = user_function(_ModelLoopModelProxy(model, context), sample_id,
|
|
2278
|
+
preprocess_response)
|
|
2279
|
+
finally:
|
|
2280
|
+
_active_model_loop = None
|
|
2281
|
+
_model_loop_run_in_current_test = True
|
|
2282
|
+
if context.phase == 'awaiting_first_hook':
|
|
2283
|
+
_fail('the loop never called the autoregressive step — it must start with the '
|
|
2284
|
+
'initial hook call (prev_inputs=None, prev_outputs=None, state=None).')
|
|
2285
|
+
if context.phase != 'ended':
|
|
2286
|
+
_fail('the loop returned before the autoregressive step signaled the chain end — '
|
|
2287
|
+
'it must run until the hook returns None for next_inputs and then return '
|
|
2288
|
+
'(final_inputs, final_outputs, final_state).')
|
|
2289
|
+
if not isinstance(result, tuple) or len(result) != 3:
|
|
2290
|
+
_fail('the loop must return (final_inputs, final_outputs, final_state). Got '
|
|
2291
|
+
f'{type(result).__name__}.')
|
|
2292
|
+
final_inputs, final_outputs, final_state = result
|
|
2293
|
+
if final_inputs is not context.fed_inputs:
|
|
2294
|
+
_fail('final_inputs must be exactly the last inputs dict fed to the model — the '
|
|
2295
|
+
'one the hook returned before the terminating call.')
|
|
2296
|
+
if final_outputs is not context.last_outputs:
|
|
2297
|
+
_fail('final_outputs must be exactly the outputs dict of the last model call.')
|
|
2298
|
+
if final_state is not context.last_state:
|
|
2299
|
+
_fail('final_state must be exactly the state returned by the terminating hook '
|
|
2300
|
+
'call.')
|
|
2301
|
+
return context, final_inputs, final_outputs, final_state
|
|
2302
|
+
|
|
2303
|
+
def inner(*args, **kwargs):
|
|
2304
|
+
if not _call_from_tl_platform:
|
|
2305
|
+
set_current('tensorleap_model_loop')
|
|
2306
|
+
model, sample_id, preprocess_response = _normalize_args(args, kwargs)
|
|
2307
|
+
in_test = _called_from_inside_tl_integration_test_decorator
|
|
2308
|
+
context, final_inputs, final_outputs, final_state = _run_with_context(
|
|
2309
|
+
model, sample_id, preprocess_response, is_mapping=False)
|
|
2310
|
+
if _nest_fingerprint(final_inputs) != context.fed_inputs_fingerprint:
|
|
2311
|
+
_fail('the final inputs were mutated in place after the last model call.')
|
|
2312
|
+
if _nest_fingerprint(final_outputs) != context.last_outputs_fingerprint:
|
|
2313
|
+
_fail('the final outputs were mutated in place after the last model call.')
|
|
2314
|
+
if _nest_fingerprint(final_state) != context.last_state_fingerprint:
|
|
2315
|
+
_fail('the final state was mutated in place after the terminating hook call.')
|
|
2316
|
+
if not in_test:
|
|
2317
|
+
if not _call_from_tl_platform:
|
|
2318
|
+
update_env_params_func("tensorleap_model_loop", "v")
|
|
2319
|
+
return final_inputs, final_outputs, final_state
|
|
2320
|
+
# Inside the integration test the chain tensors carry a batch-1 axis; the platform
|
|
2321
|
+
# feeds autoregressive metrics per-chain unbatched dicts, so strip it here.
|
|
2322
|
+
squeezed_inputs = _squeeze_leading_batch_axis(final_inputs)
|
|
2323
|
+
squeezed_outputs = _squeeze_leading_batch_axis(final_outputs)
|
|
2324
|
+
_register_chain_artifact(squeezed_inputs, 'final_inputs')
|
|
2325
|
+
_register_chain_artifact(squeezed_outputs, 'final_outputs')
|
|
2326
|
+
_register_chain_artifact(final_state, 'final_state')
|
|
2327
|
+
if not _call_from_tl_platform:
|
|
2328
|
+
update_env_params_func("tensorleap_model_loop", "v")
|
|
2329
|
+
return squeezed_inputs, squeezed_outputs, final_state
|
|
2330
|
+
|
|
2331
|
+
def mapping_inner(*args, **kwargs):
|
|
2332
|
+
model, sample_id, preprocess_response = _normalize_args(args, kwargs)
|
|
2333
|
+
context, final_inputs, final_outputs, final_state = _run_with_context(
|
|
2334
|
+
model, sample_id, preprocess_response, is_mapping=True)
|
|
2335
|
+
_register_chain_artifact(final_inputs, 'final_inputs')
|
|
2336
|
+
_register_chain_artifact(final_outputs, 'final_outputs')
|
|
2337
|
+
_register_chain_artifact(final_state, 'final_state')
|
|
2338
|
+
return final_inputs, final_outputs, final_state
|
|
2339
|
+
|
|
2340
|
+
def final_inner(*args, **kwargs):
|
|
2341
|
+
if os.environ.get(mapping_runtime_mode_env_var_mame):
|
|
2342
|
+
return mapping_inner(*args, **kwargs)
|
|
2343
|
+
else:
|
|
2344
|
+
return inner(*args, **kwargs)
|
|
2345
|
+
|
|
2346
|
+
if not _call_from_tl_platform:
|
|
2347
|
+
register_decorator_row("tensorleap_model_loop")
|
|
2348
|
+
|
|
2349
|
+
return final_inner
|
|
2350
|
+
|
|
2351
|
+
return decorating_function
|
|
2352
|
+
|
|
2353
|
+
|
|
2354
|
+
_IMPLICIT_ARG_TO_ARTIFACT_KIND = {'inputs': 'final_inputs', 'outputs': 'final_outputs',
|
|
2355
|
+
'state': 'final_state'}
|
|
2356
|
+
_ARTIFACT_KIND_DESCRIPTIONS = {
|
|
2357
|
+
'final_inputs': 'final inputs dict returned by the model loop',
|
|
2358
|
+
'final_outputs': 'final outputs dict returned by the model loop',
|
|
2359
|
+
'final_state': 'final state returned by the model loop',
|
|
2360
|
+
'ground_truth': 'direct return value of a ground-truth encoder',
|
|
2361
|
+
}
|
|
2362
|
+
_AUTOREGRESSIVE_SCALAR_TYPES = (int, float, np.floating, np.integer, np.bool_)
|
|
2363
|
+
|
|
2364
|
+
|
|
2365
|
+
def _normalize_autoregressive_call_args(func_name, full_arg_names, args, kwargs):
|
|
2366
|
+
if len(args) + len(kwargs) != len(full_arg_names):
|
|
2367
|
+
raise AssertionError(
|
|
2368
|
+
f'{func_name} validation failed: expected exactly {len(full_arg_names)} arguments '
|
|
2369
|
+
f'{full_arg_names}, got {len(args) + len(kwargs)}.')
|
|
2370
|
+
normalized = {}
|
|
2371
|
+
for i, arg_name in enumerate(full_arg_names):
|
|
2372
|
+
if i < len(args):
|
|
2373
|
+
normalized[arg_name] = args[i]
|
|
2374
|
+
elif arg_name in kwargs:
|
|
2375
|
+
normalized[arg_name] = kwargs[arg_name]
|
|
2376
|
+
else:
|
|
2377
|
+
raise AssertionError(
|
|
2378
|
+
f'{func_name} validation failed: missing required argument \'{arg_name}\'. '
|
|
2379
|
+
f'Expected arguments: {full_arg_names}.')
|
|
2380
|
+
return normalized
|
|
2381
|
+
|
|
2382
|
+
|
|
2383
|
+
def _validate_autoregressive_arg_provenance(func_name, normalized):
|
|
2384
|
+
for arg_name, value in normalized.items():
|
|
2385
|
+
expected_kind = _IMPLICIT_ARG_TO_ARTIFACT_KIND.get(arg_name, 'ground_truth')
|
|
2386
|
+
entry = _lookup_chain_artifact(value)
|
|
2387
|
+
if entry is None:
|
|
2388
|
+
raise LeapValidationError(
|
|
2389
|
+
f'{func_name} validation failed: argument "{arg_name}" must be the '
|
|
2390
|
+
f'{_ARTIFACT_KIND_DESCRIPTIONS[expected_kind]}, passed whole and unmodified — '
|
|
2391
|
+
f'the platform feeds it from the finished chain, so anything else would not '
|
|
2392
|
+
f'exist at platform runtime. Slice or derive inside the function body instead.')
|
|
2393
|
+
kind, fingerprint, _ = entry
|
|
2394
|
+
if kind != expected_kind:
|
|
2395
|
+
raise LeapValidationError(
|
|
2396
|
+
f'{func_name} validation failed: argument "{arg_name}" received the '
|
|
2397
|
+
f'{_ARTIFACT_KIND_DESCRIPTIONS[kind]} but must be the '
|
|
2398
|
+
f'{_ARTIFACT_KIND_DESCRIPTIONS[expected_kind]}.')
|
|
2399
|
+
if fingerprint != _nest_fingerprint(value):
|
|
2400
|
+
raise LeapValidationError(
|
|
2401
|
+
f'{func_name} validation failed: argument "{arg_name}" was mutated in place '
|
|
2402
|
+
f'after it was produced — the platform feeds the original values, so local '
|
|
2403
|
+
f'mutations diverge from platform results.')
|
|
2404
|
+
|
|
2405
|
+
|
|
2406
|
+
def _squeeze_wired_autoregressive_args(normalized):
|
|
2407
|
+
squeezed = {}
|
|
2408
|
+
for arg_name, value in normalized.items():
|
|
2409
|
+
if arg_name not in _IMPLICIT_ARG_TO_ARTIFACT_KIND and isinstance(value, np.ndarray) \
|
|
2410
|
+
and value.ndim > 0 and value.shape[0] == 1:
|
|
2411
|
+
squeezed[arg_name] = value[0]
|
|
2412
|
+
else:
|
|
2413
|
+
squeezed[arg_name] = value
|
|
2414
|
+
return squeezed
|
|
2415
|
+
|
|
2416
|
+
|
|
2417
|
+
def _validate_autoregressive_scalar_result(func_name, result, allow_dict):
|
|
2418
|
+
def _is_scalar(value):
|
|
2419
|
+
return isinstance(value, _AUTOREGRESSIVE_SCALAR_TYPES) or \
|
|
2420
|
+
(isinstance(value, np.ndarray) and value.ndim == 0)
|
|
2421
|
+
|
|
2422
|
+
if _is_scalar(result):
|
|
2423
|
+
return
|
|
2424
|
+
if allow_dict and isinstance(result, dict) and result and \
|
|
2425
|
+
all(isinstance(key, str) for key in result) and \
|
|
2426
|
+
all(_is_scalar(value) for value in result.values()):
|
|
2427
|
+
return
|
|
2428
|
+
expected = 'a scalar number' + (' or a dict of named scalar numbers' if allow_dict else '')
|
|
2429
|
+
raise AssertionError(
|
|
2430
|
+
f'{func_name} validation failed: autoregressive results are per-chain — the function '
|
|
2431
|
+
f'must return {expected} for the single chain it received. Got {type(result).__name__}.')
|
|
2432
|
+
|
|
2433
|
+
|
|
2434
|
+
def _autoregressive_mapping_call(mapping_inner_func, args, kwargs, node_mapping_type):
|
|
2435
|
+
user_unique_name = mapping_inner_func.name
|
|
2436
|
+
if 'user_unique_name' in kwargs:
|
|
2437
|
+
kwargs = dict(kwargs)
|
|
2438
|
+
user_unique_name = kwargs.pop('user_unique_name')
|
|
2439
|
+
if user_unique_name in mapping_inner_func.name_to_unique_name[mapping_inner_func.name]:
|
|
2440
|
+
user_unique_name = \
|
|
2441
|
+
f'{user_unique_name}_{len(mapping_inner_func.name_to_unique_name[mapping_inner_func.name])}'
|
|
2442
|
+
mapping_inner_func.name_to_unique_name[mapping_inner_func.name].add(user_unique_name)
|
|
2443
|
+
|
|
2444
|
+
normalized = _normalize_autoregressive_call_args(mapping_inner_func.name,
|
|
2445
|
+
mapping_inner_func.full_arg_names, args,
|
|
2446
|
+
kwargs)
|
|
2447
|
+
for arg_name, value in normalized.items():
|
|
2448
|
+
if arg_name in _IMPLICIT_ARG_TO_ARTIFACT_KIND:
|
|
2449
|
+
entry = _lookup_chain_artifact(value)
|
|
2450
|
+
expected_kind = _IMPLICIT_ARG_TO_ARTIFACT_KIND[arg_name]
|
|
2451
|
+
if entry is None or entry[0] != expected_kind:
|
|
2452
|
+
raise LeapValidationError(
|
|
2453
|
+
f'{mapping_inner_func.name} validation failed: argument "{arg_name}" must '
|
|
2454
|
+
f'be the {_ARTIFACT_KIND_DESCRIPTIONS[expected_kind]}, passed whole.')
|
|
2455
|
+
elif not hasattr(value, 'node_mapping'):
|
|
2456
|
+
raise LeapValidationError(
|
|
2457
|
+
f'{mapping_inner_func.name} validation failed: argument "{arg_name}" must be '
|
|
2458
|
+
f'the direct return value of a ground-truth encoder.')
|
|
2459
|
+
ordered_connections = [normalized[arg_name] for arg_name in mapping_inner_func.arg_names]
|
|
2460
|
+
_add_mapping_connection(user_unique_name, ordered_connections, mapping_inner_func.arg_names,
|
|
2461
|
+
mapping_inner_func.name, node_mapping_type)
|
|
2462
|
+
return None
|
|
2463
|
+
|
|
2464
|
+
|
|
2465
|
+
def _make_autoregressive_decorator_inner(user_function, name, decorator_env_name, handler_data,
|
|
2466
|
+
node_mapping_type, validate_result):
|
|
2467
|
+
full_arg_names = inspect.getfullargspec(user_function)[0]
|
|
2468
|
+
|
|
2469
|
+
def inner_without_validate(*args, **kwargs):
|
|
2470
|
+
global _called_from_inside_tl_decorator
|
|
2471
|
+
_called_from_inside_tl_decorator += 1
|
|
2472
|
+
try:
|
|
2473
|
+
result = user_function(*args, **kwargs)
|
|
2474
|
+
finally:
|
|
2475
|
+
_called_from_inside_tl_decorator -= 1
|
|
2476
|
+
return result
|
|
2477
|
+
|
|
2478
|
+
def inner(*args, **kwargs):
|
|
2479
|
+
if not _call_from_tl_platform:
|
|
2480
|
+
set_current(decorator_env_name)
|
|
2481
|
+
normalized = _normalize_autoregressive_call_args(user_function.__name__, full_arg_names,
|
|
2482
|
+
args, kwargs)
|
|
2483
|
+
is_top_level_in_test = (_called_from_inside_tl_decorator == 0
|
|
2484
|
+
and _called_from_inside_tl_integration_test_decorator)
|
|
2485
|
+
if is_top_level_in_test:
|
|
2486
|
+
_validate_autoregressive_arg_provenance(user_function.__name__, normalized)
|
|
2487
|
+
normalized = _squeeze_wired_autoregressive_args(normalized)
|
|
2488
|
+
result = inner_without_validate(**normalized)
|
|
2489
|
+
validate_result(user_function.__name__, result)
|
|
2490
|
+
if not _call_from_tl_platform:
|
|
2491
|
+
update_env_params_func(decorator_env_name, "v")
|
|
2492
|
+
return result
|
|
2493
|
+
|
|
2494
|
+
def mapping_inner(*args, **kwargs):
|
|
2495
|
+
return _autoregressive_mapping_call(mapping_inner, args, kwargs, node_mapping_type)
|
|
2496
|
+
|
|
2497
|
+
mapping_inner.name = name
|
|
2498
|
+
mapping_inner.full_arg_names = full_arg_names
|
|
2499
|
+
mapping_inner.arg_names = handler_data.arg_names
|
|
2500
|
+
mapping_inner.name_to_unique_name = defaultdict(set)
|
|
2501
|
+
|
|
2502
|
+
def final_inner(*args, **kwargs):
|
|
2503
|
+
if os.environ.get(mapping_runtime_mode_env_var_mame):
|
|
2504
|
+
return mapping_inner(*args, **kwargs)
|
|
2505
|
+
else:
|
|
2506
|
+
return inner(*args, **kwargs)
|
|
2507
|
+
|
|
2508
|
+
if not _call_from_tl_platform:
|
|
2509
|
+
register_decorator_row(decorator_env_name)
|
|
2510
|
+
|
|
2511
|
+
return final_inner
|
|
2512
|
+
|
|
2513
|
+
|
|
2514
|
+
def tensorleap_autoregressive_metric(name: str,
|
|
2515
|
+
direction: Union[MetricDirection, Dict[str, MetricDirection]]
|
|
2516
|
+
= MetricDirection.Downward,
|
|
2517
|
+
compute_insights: Optional[Union[bool, Dict[str, bool]]] = None):
|
|
2518
|
+
"""A per-chain metric over a finished autoregressive chain.
|
|
2519
|
+
|
|
2520
|
+
The reserved arguments inputs / outputs / state are fed implicitly by the platform from the
|
|
2521
|
+
chain's final step (unbatched dicts; outputs keyed by the declared prediction-type names;
|
|
2522
|
+
state as the terminating hook call returned it). Declare whichever of them the metric needs.
|
|
2523
|
+
Every other argument is wired to a ground-truth encoder through the integration test. Runs
|
|
2524
|
+
once per chain and must return a scalar number (or a dict of named scalar numbers).
|
|
2525
|
+
"""
|
|
2526
|
+
|
|
2527
|
+
def decorating_function(user_function):
|
|
2528
|
+
leap_binder.add_autoregressive_metric(user_function, name, direction, compute_insights)
|
|
2529
|
+
handler_data = \
|
|
2530
|
+
leap_binder.setup_container.autoregressive_metrics[-1].metric_handler_data
|
|
2531
|
+
|
|
2532
|
+
def validate_result(func_name, result):
|
|
2533
|
+
_validate_autoregressive_scalar_result(func_name, result, allow_dict=True)
|
|
2534
|
+
|
|
2535
|
+
return _make_autoregressive_decorator_inner(
|
|
2536
|
+
user_function, name, 'tensorleap_autoregressive_metric', handler_data,
|
|
2537
|
+
NodeMappingType.Metric, validate_result)
|
|
2538
|
+
|
|
2539
|
+
return decorating_function
|
|
2540
|
+
|
|
2541
|
+
|
|
2542
|
+
def tensorleap_autoregressive_loss(name: str):
|
|
2543
|
+
"""A per-chain loss over a finished autoregressive chain.
|
|
2544
|
+
|
|
2545
|
+
Same argument contract as tensorleap_autoregressive_metric; must return a single scalar
|
|
2546
|
+
number for the chain it received.
|
|
2547
|
+
"""
|
|
2548
|
+
|
|
2549
|
+
def decorating_function(user_function):
|
|
2550
|
+
leap_binder.add_autoregressive_loss(user_function, name)
|
|
2551
|
+
handler_data = \
|
|
2552
|
+
leap_binder.setup_container.autoregressive_losses[-1].custom_loss_handler_data
|
|
2553
|
+
|
|
2554
|
+
def validate_result(func_name, result):
|
|
2555
|
+
_validate_autoregressive_scalar_result(func_name, result, allow_dict=False)
|
|
2556
|
+
|
|
2557
|
+
return _make_autoregressive_decorator_inner(
|
|
2558
|
+
user_function, name, 'tensorleap_autoregressive_loss', handler_data,
|
|
2559
|
+
NodeMappingType.CustomLoss, validate_result)
|
|
2560
|
+
|
|
2561
|
+
return decorating_function
|
|
2562
|
+
|
|
2563
|
+
|
|
2564
|
+
def tensorleap_autoregressive_visualizer(name: str, visualizer_type: LeapDataType):
|
|
2565
|
+
"""A per-chain visualizer over a finished autoregressive chain.
|
|
2566
|
+
|
|
2567
|
+
Same argument contract as tensorleap_autoregressive_metric; must return a LeapData object
|
|
2568
|
+
matching visualizer_type.
|
|
2569
|
+
"""
|
|
2570
|
+
|
|
2571
|
+
def decorating_function(user_function):
|
|
2572
|
+
assert isinstance(visualizer_type, LeapDataType), \
|
|
2573
|
+
(f'{user_function.__name__} validation failed: visualizer_type should be of type '
|
|
2574
|
+
f'{LeapDataType.__name__} but got {type(visualizer_type)}')
|
|
2575
|
+
leap_binder.add_autoregressive_visualizer(user_function, name, visualizer_type)
|
|
2576
|
+
handler_data = \
|
|
2577
|
+
leap_binder.setup_container.autoregressive_visualizers[-1].visualizer_handler_data
|
|
2578
|
+
|
|
2579
|
+
def validate_result(func_name, result):
|
|
2580
|
+
expected_class = map_leap_data_type_to_visualizer_class[visualizer_type.value]
|
|
2581
|
+
assert isinstance(result, expected_class), \
|
|
2582
|
+
(f'{func_name} validation failed: the return type should be '
|
|
2583
|
+
f'{expected_class.__name__} (visualizer_type={visualizer_type}). '
|
|
2584
|
+
f'Got {type(result).__name__}.')
|
|
2585
|
+
|
|
2586
|
+
return _make_autoregressive_decorator_inner(
|
|
2587
|
+
user_function, name, 'tensorleap_autoregressive_visualizer', handler_data,
|
|
2588
|
+
NodeMappingType.Visualizer, validate_result)
|
|
2589
|
+
|
|
2590
|
+
return decorating_function
|
|
2591
|
+
|
|
2592
|
+
|
|
1705
2593
|
def tensorleap_preprocess():
|
|
1706
2594
|
def decorating_function(user_function: Callable[[], List[PreprocessResponse]]):
|
|
1707
2595
|
leap_binder.set_preprocess(user_function)
|
|
@@ -1732,15 +2620,6 @@ def tensorleap_preprocess():
|
|
|
1732
2620
|
assert len(set(result)) == len(result), \
|
|
1733
2621
|
(f'{user_function.__name__}() validation failed: '
|
|
1734
2622
|
f'The return list should not contain duplicate PreprocessResponse objects.')
|
|
1735
|
-
# All-or-none: a dataset is either all grouped (nested sample_ids) or all flat.
|
|
1736
|
-
if len({response.is_grouped for response in result}) != 1:
|
|
1737
|
-
shapes = ', '.join(
|
|
1738
|
-
f"#{i}={'grouped' if response.is_grouped else 'flat'}"
|
|
1739
|
-
for i, response in enumerate(result))
|
|
1740
|
-
raise AssertionError(
|
|
1741
|
-
f'{user_function.__name__}() validation failed: '
|
|
1742
|
-
f'All PreprocessResponses must be either all grouped (nested sample_ids) '
|
|
1743
|
-
f'or all flat, but got a mix: {shapes}.')
|
|
1744
2623
|
|
|
1745
2624
|
def inner(*args, **kwargs):
|
|
1746
2625
|
if not _call_from_tl_platform:
|
|
@@ -2078,10 +2957,14 @@ def tensorleap_input_encoder(name: str, channel_dim=_UNSET, model_input_index=No
|
|
|
2078
2957
|
f"channel axis in the batched tensor (batch = axis 0), so 0 is invalid. Use 1 "
|
|
2079
2958
|
f"for channel-first (NCHW) and -1 for channel-last (NHWC).")
|
|
2080
2959
|
|
|
2081
|
-
def _validate_input_args(sample_id: Union[int, str
|
|
2082
|
-
|
|
2960
|
+
def _validate_input_args(sample_id: Union[int, str], preprocess_response: PreprocessResponse):
|
|
2961
|
+
assert type(sample_id) == preprocess_response.sample_id_type, \
|
|
2962
|
+
(f'{user_function.__name__}() validation failed: '
|
|
2963
|
+
f'Argument sample_id should be as the same type as defined in the preprocess response '
|
|
2964
|
+
f'{preprocess_response.sample_id_type}. Got {type(sample_id)}.')
|
|
2083
2965
|
|
|
2084
|
-
def
|
|
2966
|
+
def _validate_result(result):
|
|
2967
|
+
validate_output_structure(result, func_name=user_function.__name__, expected_type_name="np.ndarray")
|
|
2085
2968
|
assert isinstance(result, np.ndarray), \
|
|
2086
2969
|
(f'{user_function.__name__}() validation failed: '
|
|
2087
2970
|
f'Unsupported return type. Should be a numpy array. Got {type(result)}.')
|
|
@@ -2091,13 +2974,6 @@ def tensorleap_input_encoder(name: str, channel_dim=_UNSET, model_input_index=No
|
|
|
2091
2974
|
assert channel_dim - 1 <= len(result.shape), (f'{user_function.__name__}() validation failed: '
|
|
2092
2975
|
f'The channel_dim ({channel_dim}) should be <= to the rank of the resulting input rank ({len(result.shape)}).')
|
|
2093
2976
|
|
|
2094
|
-
def _validate_result(result, grouped=False, group_size=None):
|
|
2095
|
-
if not grouped:
|
|
2096
|
-
validate_output_structure(result, func_name=user_function.__name__, expected_type_name="np.ndarray")
|
|
2097
|
-
_validate_single(result)
|
|
2098
|
-
return
|
|
2099
|
-
_validate_grouped_result(result, group_size, user_function.__name__, _validate_single)
|
|
2100
|
-
|
|
2101
2977
|
def inner_without_validate(sample_id, preprocess_response):
|
|
2102
2978
|
global _called_from_inside_tl_decorator
|
|
2103
2979
|
_called_from_inside_tl_decorator += 1
|
|
@@ -2114,18 +2990,16 @@ def tensorleap_input_encoder(name: str, channel_dim=_UNSET, model_input_index=No
|
|
|
2114
2990
|
def inner(*args, **kwargs):
|
|
2115
2991
|
if not _call_from_tl_platform:
|
|
2116
2992
|
set_current("tensorleap_input_encoder")
|
|
2117
|
-
validate_args_structure(*args, types_order=[Union[int, str
|
|
2993
|
+
validate_args_structure(*args, types_order=[Union[int, str], PreprocessResponse],
|
|
2118
2994
|
func_name=user_function.__name__, expected_names=["idx", "preprocess"], **kwargs)
|
|
2119
2995
|
sample_id, preprocess_response = args if len(args) != 0 else kwargs.values()
|
|
2120
2996
|
_validate_input_args(sample_id, preprocess_response)
|
|
2121
2997
|
|
|
2122
|
-
grouped = isinstance(sample_id, list)
|
|
2123
|
-
group_size = len(sample_id) if grouped else None
|
|
2124
2998
|
result = inner_without_validate(sample_id, preprocess_response)
|
|
2125
2999
|
|
|
2126
|
-
_validate_result(result
|
|
3000
|
+
_validate_result(result)
|
|
2127
3001
|
|
|
2128
|
-
if
|
|
3002
|
+
if _called_from_inside_tl_decorator == 0 and _called_from_inside_tl_integration_test_decorator:
|
|
2129
3003
|
batch_warning(result, user_function.__name__)
|
|
2130
3004
|
result = np.expand_dims(result, axis=0)
|
|
2131
3005
|
# Emit integration test event once per test
|
|
@@ -2148,15 +3022,8 @@ def tensorleap_input_encoder(name: str, channel_dim=_UNSET, model_input_index=No
|
|
|
2148
3022
|
inner.node_mapping = NodeMapping(name, node_mapping_type)
|
|
2149
3023
|
|
|
2150
3024
|
def mapping_inner(*args, **kwargs):
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
def __getitem__(self, key):
|
|
2154
|
-
# Indexing a grouped input group (input_list[i]) selects a sample from the
|
|
2155
|
-
# same input source; the model wiring is identical to the un-grouped input.
|
|
2156
|
-
return self
|
|
2157
|
-
else:
|
|
2158
|
-
class TempMapping:
|
|
2159
|
-
pass
|
|
3025
|
+
class TempMapping:
|
|
3026
|
+
pass
|
|
2160
3027
|
|
|
2161
3028
|
ret = TempMapping()
|
|
2162
3029
|
ret.node_mapping = mapping_inner.node_mapping
|
|
@@ -2186,10 +3053,15 @@ def tensorleap_gt_encoder(name: str):
|
|
|
2186
3053
|
raise Exception(f'GT with name {name} already exists. '
|
|
2187
3054
|
f'Please choose another')
|
|
2188
3055
|
|
|
2189
|
-
def _validate_input_args(sample_id: Union[int, str
|
|
2190
|
-
|
|
3056
|
+
def _validate_input_args(sample_id: Union[int, str], preprocess_response: PreprocessResponse):
|
|
3057
|
+
assert type(sample_id) == preprocess_response.sample_id_type, \
|
|
3058
|
+
(f'{user_function.__name__}() validation failed: '
|
|
3059
|
+
f'Argument sample_id should be as the same type as defined in the preprocess response '
|
|
3060
|
+
f'{preprocess_response.sample_id_type}. Got {type(sample_id)}.')
|
|
2191
3061
|
|
|
2192
|
-
def
|
|
3062
|
+
def _validate_result(result):
|
|
3063
|
+
validate_output_structure(result, func_name=user_function.__name__, expected_type_name="np.ndarray",
|
|
3064
|
+
gt_flag=True)
|
|
2193
3065
|
assert isinstance(result, np.ndarray), \
|
|
2194
3066
|
(f'{user_function.__name__}() validation failed: '
|
|
2195
3067
|
f'Unsupported return type. Should be a numpy array. Got {type(result)}.')
|
|
@@ -2197,14 +3069,6 @@ def tensorleap_gt_encoder(name: str):
|
|
|
2197
3069
|
(f'{user_function.__name__}() validation failed: '
|
|
2198
3070
|
f'The return type should be a numpy array of type float32. Got {result.dtype}.')
|
|
2199
3071
|
|
|
2200
|
-
def _validate_result(result, grouped=False, group_size=None):
|
|
2201
|
-
if not grouped:
|
|
2202
|
-
validate_output_structure(result, func_name=user_function.__name__, expected_type_name="np.ndarray",
|
|
2203
|
-
gt_flag=True)
|
|
2204
|
-
_validate_single(result)
|
|
2205
|
-
return
|
|
2206
|
-
_validate_grouped_result(result, group_size, user_function.__name__, _validate_single)
|
|
2207
|
-
|
|
2208
3072
|
def inner_without_validate(sample_id, preprocess_response):
|
|
2209
3073
|
global _called_from_inside_tl_decorator
|
|
2210
3074
|
_called_from_inside_tl_decorator += 1
|
|
@@ -2221,20 +3085,19 @@ def tensorleap_gt_encoder(name: str):
|
|
|
2221
3085
|
def inner(*args, **kwargs):
|
|
2222
3086
|
if not _call_from_tl_platform:
|
|
2223
3087
|
set_current("tensorleap_gt_encoder")
|
|
2224
|
-
validate_args_structure(*args, types_order=[Union[int, str
|
|
3088
|
+
validate_args_structure(*args, types_order=[Union[int, str], PreprocessResponse],
|
|
2225
3089
|
func_name=user_function.__name__, expected_names=["idx", "preprocess"], **kwargs)
|
|
2226
3090
|
sample_id, preprocess_response = args
|
|
2227
3091
|
_validate_input_args(sample_id, preprocess_response)
|
|
2228
3092
|
|
|
2229
|
-
grouped = isinstance(sample_id, list)
|
|
2230
|
-
group_size = len(sample_id) if grouped else None
|
|
2231
3093
|
result = inner_without_validate(sample_id, preprocess_response)
|
|
2232
3094
|
|
|
2233
|
-
_validate_result(result
|
|
3095
|
+
_validate_result(result)
|
|
2234
3096
|
|
|
2235
|
-
if
|
|
3097
|
+
if _called_from_inside_tl_decorator == 0 and _called_from_inside_tl_integration_test_decorator:
|
|
2236
3098
|
batch_warning(result, user_function.__name__)
|
|
2237
3099
|
result = np.expand_dims(result, axis=0)
|
|
3100
|
+
_register_chain_artifact(result, 'ground_truth')
|
|
2238
3101
|
# Emit integration test event once per test
|
|
2239
3102
|
try:
|
|
2240
3103
|
emit_integration_event_once(AnalyticsEvent.GT_ENCODER_INTEGRATION_TEST, {
|
|
@@ -2249,15 +3112,8 @@ def tensorleap_gt_encoder(name: str):
|
|
|
2249
3112
|
inner.node_mapping = NodeMapping(name, NodeMappingType.GroundTruth)
|
|
2250
3113
|
|
|
2251
3114
|
def mapping_inner(*args, **kwargs):
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
def __getitem__(self, key):
|
|
2255
|
-
# Indexing a grouped GT group (gt_list[i]) selects a sample from the same
|
|
2256
|
-
# ground-truth source; wiring is identical to the un-grouped GT node.
|
|
2257
|
-
return self
|
|
2258
|
-
else:
|
|
2259
|
-
class TempMapping:
|
|
2260
|
-
pass
|
|
3115
|
+
class TempMapping:
|
|
3116
|
+
pass
|
|
2261
3117
|
|
|
2262
3118
|
ret = TempMapping()
|
|
2263
3119
|
ret.node_mapping = mapping_inner.node_mapping
|
|
@@ -2534,12 +3390,12 @@ def tensorleap_status_table():
|
|
|
2534
3390
|
ready = False
|
|
2535
3391
|
next_step = row["name"]
|
|
2536
3392
|
|
|
2537
|
-
if
|
|
2538
|
-
print(f"\
|
|
3393
|
+
if code_mapping_failure[0]:
|
|
3394
|
+
print(f"\n{CROSS} {code_mapping_failure_mes}")
|
|
2539
3395
|
return
|
|
2540
3396
|
|
|
2541
|
-
if
|
|
2542
|
-
print(f"\
|
|
3397
|
+
if _crashed["value"]:
|
|
3398
|
+
print(f"\nScript crashed before completing all steps. crashed at function '{_current_func['name']}'.")
|
|
2543
3399
|
return
|
|
2544
3400
|
|
|
2545
3401
|
print(ready_mess) if ready else print(
|
|
@@ -2554,6 +3410,10 @@ def tensorleap_status_table():
|
|
|
2554
3410
|
code_mapping_failure[0] = 1
|
|
2555
3411
|
if status == "v":
|
|
2556
3412
|
_set_status(name, CHECK)
|
|
3413
|
+
# A decorator that reports success is no longer the "currently running"
|
|
3414
|
+
# one, so clear the pointer. This keeps a later body-level raise from
|
|
3415
|
+
# being blamed on the last decorator that actually succeeded.
|
|
3416
|
+
_current_func["name"] = None
|
|
2557
3417
|
else:
|
|
2558
3418
|
_set_status(name, CROSS)
|
|
2559
3419
|
|
|
@@ -2565,6 +3425,12 @@ def tensorleap_status_table():
|
|
|
2565
3425
|
table.append({"name": "tensorleap simulation (optional)", "Added to integration": UNKNOWN})
|
|
2566
3426
|
_sim_tracking[sim_name] = "registered"
|
|
2567
3427
|
|
|
3428
|
+
def register_decorator_row(name: str):
|
|
3429
|
+
# Rows for decorators that only apply to some integration kinds (e.g. the autoregressive
|
|
3430
|
+
# step hook) are appended on registration, so unrelated integrations never show them.
|
|
3431
|
+
if _find_row(_remove_suffix(name, " (optional)")) is None:
|
|
3432
|
+
table.append({"name": name, "Added to integration": UNKNOWN})
|
|
3433
|
+
|
|
2568
3434
|
def mark_sim_result(sim_name: str, passed: bool):
|
|
2569
3435
|
_sim_tracking[sim_name] = "passed" if passed else "failed"
|
|
2570
3436
|
|
|
@@ -2600,10 +3466,14 @@ def tensorleap_status_table():
|
|
|
2600
3466
|
def handle_exception(exc_type, exc_value, exc_traceback):
|
|
2601
3467
|
_crashed["value"] = True
|
|
2602
3468
|
crashed_name = _current_func["name"]
|
|
2603
|
-
if crashed_name:
|
|
3469
|
+
if crashed_name and not code_mapping_failure[0]:
|
|
2604
3470
|
row = _find_row(crashed_name)
|
|
2605
3471
|
if row:
|
|
2606
3472
|
row["Added to integration"] = CROSS
|
|
3473
|
+
elif not crashed_name:
|
|
3474
|
+
# Crash with no decorator currently running = the integration-test body /
|
|
3475
|
+
# code flow itself failed; report that rather than blaming a decorator.
|
|
3476
|
+
code_mapping_failure[0] = 1
|
|
2607
3477
|
|
|
2608
3478
|
traceback.print_exception(exc_type, exc_value, exc_traceback)
|
|
2609
3479
|
run_on_exit()
|
|
@@ -2611,11 +3481,12 @@ def tensorleap_status_table():
|
|
|
2611
3481
|
atexit.register(run_on_exit)
|
|
2612
3482
|
sys.excepthook = handle_exception
|
|
2613
3483
|
|
|
2614
|
-
return set_current, update_env_params, register_sim, mark_sim_result
|
|
3484
|
+
return set_current, update_env_params, register_sim, mark_sim_result, register_decorator_row
|
|
2615
3485
|
|
|
2616
3486
|
|
|
2617
3487
|
if not _call_from_tl_platform:
|
|
2618
|
-
set_current, update_env_params_func, register_sim, mark_sim_result =
|
|
3488
|
+
set_current, update_env_params_func, register_sim, mark_sim_result, register_decorator_row = \
|
|
3489
|
+
tensorleap_status_table()
|
|
2619
3490
|
|
|
2620
3491
|
|
|
2621
3492
|
|