diffract-core 0.2.1__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.
- diffract/__init__.py +52 -0
- diffract/configs/fast_speed_without_disk.ini +48 -0
- diffract/configs/hybrid.ini +74 -0
- diffract/configs/sqlite.ini +62 -0
- diffract/containers.py +405 -0
- diffract/core/__init__.py +23 -0
- diffract/core/cache/__init__.py +24 -0
- diffract/core/cache/containers.py +93 -0
- diffract/core/cache/interface.py +117 -0
- diffract/core/cache/redis_manager.py +464 -0
- diffract/core/cache/simple_manager.py +478 -0
- diffract/core/compute/__init__.py +46 -0
- diffract/core/compute/config.py +48 -0
- diffract/core/compute/containers.py +81 -0
- diffract/core/compute/decorator.py +125 -0
- diffract/core/compute/exceptions.py +31 -0
- diffract/core/compute/execution/__init__.py +14 -0
- diffract/core/compute/execution/_types.py +70 -0
- diffract/core/compute/execution/aggregation.py +74 -0
- diffract/core/compute/execution/aggregation_runner.py +362 -0
- diffract/core/compute/execution/enums.py +38 -0
- diffract/core/compute/execution/executor.py +125 -0
- diffract/core/compute/execution/parameter_runner.py +125 -0
- diffract/core/compute/execution/restrictions.py +61 -0
- diffract/core/compute/execution/strategy.py +118 -0
- diffract/core/compute/execution/utils.py +112 -0
- diffract/core/compute/extensions/__init__.py +0 -0
- diffract/core/compute/extensions/power_law.py +1051 -0
- diffract/core/compute/extensions/rmt.py +150 -0
- diffract/core/compute/extensions/utils.py +36 -0
- diffract/core/compute/field_signature.py +183 -0
- diffract/core/compute/kernels/__init__.py +48 -0
- diffract/core/compute/kernels/alignment.py +106 -0
- diffract/core/compute/kernels/heavy_tailed.py +394 -0
- diffract/core/compute/kernels/marchenko_pastur.py +99 -0
- diffract/core/compute/kernels/mat_decomposition.py +101 -0
- diffract/core/compute/kernels/mat_properties.py +53 -0
- diffract/core/compute/kernels/model_quality.py +27 -0
- diffract/core/compute/kernels/norms.py +88 -0
- diffract/core/compute/kernels/ranks.py +35 -0
- diffract/core/compute/kernels/tracy_widom.py +36 -0
- diffract/core/compute/metadata.py +67 -0
- diffract/core/compute/registry.py +485 -0
- diffract/core/constants.py +147 -0
- diffract/core/data/__init__.py +32 -0
- diffract/core/data/interface.py +304 -0
- diffract/core/data/metadata/__init__.py +15 -0
- diffract/core/data/metadata/containers.py +60 -0
- diffract/core/data/metadata/interface.py +208 -0
- diffract/core/data/metadata/sqlite_index.py +604 -0
- diffract/core/data/nn/__init__.py +43 -0
- diffract/core/data/nn/aggregates/__init__.py +35 -0
- diffract/core/data/nn/aggregates/metadata.py +96 -0
- diffract/core/data/nn/aggregates/proxy.py +57 -0
- diffract/core/data/nn/aggregates/repository.py +87 -0
- diffract/core/data/nn/aggregates/view.py +307 -0
- diffract/core/data/nn/containers.py +86 -0
- diffract/core/data/nn/extractors/__init__.py +49 -0
- diffract/core/data/nn/extractors/base.py +300 -0
- diffract/core/data/nn/extractors/factory.py +234 -0
- diffract/core/data/nn/extractors/flax.py +112 -0
- diffract/core/data/nn/extractors/handlers/__init__.py +39 -0
- diffract/core/data/nn/extractors/handlers/base.py +110 -0
- diffract/core/data/nn/extractors/handlers/flax_handlers.py +71 -0
- diffract/core/data/nn/extractors/handlers/numpy_handlers.py +63 -0
- diffract/core/data/nn/extractors/handlers/onnx_handlers.py +67 -0
- diffract/core/data/nn/extractors/handlers/tensorflow_handlers.py +82 -0
- diffract/core/data/nn/extractors/handlers/torch_handlers.py +124 -0
- diffract/core/data/nn/extractors/interface.py +56 -0
- diffract/core/data/nn/extractors/numpy.py +94 -0
- diffract/core/data/nn/extractors/onnx.py +108 -0
- diffract/core/data/nn/extractors/tensorflow.py +99 -0
- diffract/core/data/nn/extractors/torch.py +204 -0
- diffract/core/data/nn/params/__init__.py +27 -0
- diffract/core/data/nn/params/interface.py +402 -0
- diffract/core/data/nn/params/metadata.py +110 -0
- diffract/core/data/nn/params/proxy.py +93 -0
- diffract/core/data/nn/params/repository.py +45 -0
- diffract/core/data/nn/params/schema.py +82 -0
- diffract/core/data/nn/params/view.py +393 -0
- diffract/core/data/proxy.py +246 -0
- diffract/core/data/repository.py +227 -0
- diffract/core/data/utils.py +33 -0
- diffract/core/data/view.py +389 -0
- diffract/core/export/__init__.py +27 -0
- diffract/core/export/containers.py +47 -0
- diffract/core/export/exporters.py +191 -0
- diffract/core/export/formatters/__init__.py +23 -0
- diffract/core/export/formatters/base.py +68 -0
- diffract/core/export/formatters/dict_formatter.py +38 -0
- diffract/core/export/formatters/json_formatter.py +58 -0
- diffract/core/export/formatters/list_formatter.py +41 -0
- diffract/core/export/formatters/pandas_formatter.py +86 -0
- diffract/core/export/formatters/polars_formatter.py +86 -0
- diffract/core/export/formatters/registry.py +84 -0
- diffract/core/export/interface.py +105 -0
- diffract/core/parallel/__init__.py +21 -0
- diffract/core/parallel/containers.py +57 -0
- diffract/core/parallel/execution.py +91 -0
- diffract/core/parallel/runtime.py +91 -0
- diffract/core/storage/__init__.py +35 -0
- diffract/core/storage/base_manager.py +330 -0
- diffract/core/storage/containers.py +122 -0
- diffract/core/storage/hdf5_manager.py +856 -0
- diffract/core/storage/hybrid_manager.py +218 -0
- diffract/core/storage/interface.py +222 -0
- diffract/core/storage/metadata.py +89 -0
- diffract/core/storage/ram_manager.py +159 -0
- diffract/core/storage/sqlite_manager.py +1062 -0
- diffract/core/storage/zarr_manager.py +992 -0
- diffract/core/utils/__init__.py +45 -0
- diffract/core/utils/build.py +46 -0
- diffract/core/utils/exceptions.py +11 -0
- diffract/core/utils/hashing.py +90 -0
- diffract/core/utils/imports.py +292 -0
- diffract/core/utils/math.py +15 -0
- diffract/py.typed +0 -0
- diffract/session/__init__.py +24 -0
- diffract/session/errors.py +17 -0
- diffract/session/field_cache.py +216 -0
- diffract/session/namespaces/__init__.py +15 -0
- diffract/session/namespaces/compute/__init__.py +219 -0
- diffract/session/namespaces/models/__init__.py +282 -0
- diffract/session/namespaces/models/parameters/__init__.py +133 -0
- diffract/session/namespaces/models/parameters/meta_patcher.py +167 -0
- diffract/session/namespaces/results/__init__.py +378 -0
- diffract/session/namespaces/results/eraser.py +129 -0
- diffract/session/namespaces/results/injester.py +249 -0
- diffract/session/namespaces/utils/__init__.py +141 -0
- diffract/session/namespaces/utils/merger.py +295 -0
- diffract/session/namespaces/viz/__init__.py +91 -0
- diffract/session/namespaces/viz/_utils.py +14 -0
- diffract/session/namespaces/viz/box.py +207 -0
- diffract/session/namespaces/viz/grid.py +160 -0
- diffract/session/namespaces/viz/heatmap.py +164 -0
- diffract/session/namespaces/viz/scatter.py +202 -0
- diffract/session/namespaces/viz/sparkline.py +270 -0
- diffract/session/namespaces/viz/violin.py +212 -0
- diffract/session/session.py +260 -0
- diffract/session/utils.py +81 -0
- diffract/viz/__init__.py +42 -0
- diffract/viz/data/__init__.py +34 -0
- diffract/viz/data/detection.py +57 -0
- diffract/viz/data/extraction.py +117 -0
- diffract/viz/data/filtering.py +57 -0
- diffract/viz/data/ordering.py +129 -0
- diffract/viz/data/provider.py +99 -0
- diffract/viz/data/types.py +98 -0
- diffract/viz/plots/__init__.py +37 -0
- diffract/viz/plots/base/__init__.py +20 -0
- diffract/viz/plots/base/axis.py +219 -0
- diffract/viz/plots/base/coloraxis.py +133 -0
- diffract/viz/plots/base/configurator.py +18 -0
- diffract/viz/plots/base/jitter.py +368 -0
- diffract/viz/plots/base/line.py +197 -0
- diffract/viz/plots/base/marker.py +278 -0
- diffract/viz/plots/base/overlay.py +14 -0
- diffract/viz/plots/base/plot.py +110 -0
- diffract/viz/plots/base/update.py +50 -0
- diffract/viz/plots/boxplot.py +283 -0
- diffract/viz/plots/cluster.py +374 -0
- diffract/viz/plots/heatmap.py +168 -0
- diffract/viz/plots/scatter.py +233 -0
- diffract/viz/plots/sparkline.py +405 -0
- diffract/viz/plots/subplots/__init__.py +32 -0
- diffract/viz/plots/subplots/coloraxis.py +339 -0
- diffract/viz/plots/subplots/factory.py +713 -0
- diffract/viz/plots/subplots/grid.py +214 -0
- diffract/viz/plots/subplots/layout.py +370 -0
- diffract/viz/plots/subplots/spec.py +39 -0
- diffract/viz/plots/violin.py +298 -0
- diffract/viz/renderer.py +513 -0
- diffract/viz/styling/__init__.py +78 -0
- diffract/viz/styling/palettes/__init__.py +20 -0
- diffract/viz/styling/palettes/bundle.py +16 -0
- diffract/viz/styling/palettes/color.py +83 -0
- diffract/viz/styling/palettes/dashes.py +27 -0
- diffract/viz/styling/palettes/symbols.py +42 -0
- diffract/viz/styling/resolvers/__init__.py +11 -0
- diffract/viz/styling/resolvers/categorical.py +68 -0
- diffract/viz/styling/resolvers/color.py +77 -0
- diffract/viz/styling/resolvers/numeric.py +108 -0
- diffract/viz/styling/sources.py +27 -0
- diffract/viz/styling/theme/__init__.py +29 -0
- diffract/viz/styling/theme/applier.py +114 -0
- diffract/viz/styling/theme/components.py +66 -0
- diffract/viz/styling/theme/presets.py +58 -0
- diffract/viz/styling/theme/theme.py +36 -0
- diffract_core-0.2.1.dist-info/METADATA +530 -0
- diffract_core-0.2.1.dist-info/RECORD +193 -0
- diffract_core-0.2.1.dist-info/WHEEL +4 -0
- diffract_core-0.2.1.dist-info/licenses/LICENSE +201 -0
- diffract_core-0.2.1.dist-info/licenses/NOTICE +2 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Kernel decorator and registration utilities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
import sys
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
|
+
|
|
9
|
+
from dependency_injector.wiring import Provide, inject
|
|
10
|
+
|
|
11
|
+
from .exceptions import InconsistentWiring
|
|
12
|
+
from .execution import KernelApplyLevel, KernelExecutionProtocol, KernelRestrictions
|
|
13
|
+
from .metadata import KernelInfo
|
|
14
|
+
from .registry import KernelRegistry
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from collections.abc import Callable
|
|
18
|
+
|
|
19
|
+
# Registration manifest for the built-in kernels. Import side effects only
|
|
20
|
+
# run once per process, but each fresh registry still needs the built-ins;
|
|
21
|
+
# register_default_kernels replays this manifest into the target registry.
|
|
22
|
+
_DEFAULT_KERNEL_SPECS: list[dict[str, Any]] = []
|
|
23
|
+
_COLLECTING_DEFAULTS = False
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@inject
|
|
27
|
+
def kernel(
|
|
28
|
+
_func: Callable[..., Any] | None = None,
|
|
29
|
+
*,
|
|
30
|
+
registry: KernelRegistry = Provide["kernel_registry"],
|
|
31
|
+
name: str | None = None,
|
|
32
|
+
require_fields: tuple[str, ...] | None = None,
|
|
33
|
+
produce_fields: tuple[str, ...] | None = None,
|
|
34
|
+
apply_level: KernelApplyLevel | None = KernelApplyLevel.PARAMETER,
|
|
35
|
+
execution_protocol: KernelExecutionProtocol
|
|
36
|
+
| None = KernelExecutionProtocol.SEQUENTIAL,
|
|
37
|
+
restrictions: KernelRestrictions | None = None,
|
|
38
|
+
info: KernelInfo | None = None,
|
|
39
|
+
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
40
|
+
"""Decorator to register a function as a compute kernel.
|
|
41
|
+
|
|
42
|
+
Registers a function with the kernel registry, enabling it to be used
|
|
43
|
+
in the compute pipeline. The decorator infers required fields and default
|
|
44
|
+
configuration from the function signature unless explicitly provided.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
_func: Function to decorate (used internally).
|
|
48
|
+
registry: Kernel registry for registration (injected).
|
|
49
|
+
name: Custom kernel name (defaults to function name).
|
|
50
|
+
require_fields: Input field names (inferred from signature if None).
|
|
51
|
+
produce_fields: Output field names (defaults to kernel name).
|
|
52
|
+
apply_level: Level at which kernel operates.
|
|
53
|
+
execution_protocol: Execution strategy for the kernel.
|
|
54
|
+
restrictions: Optional argument restrictions.
|
|
55
|
+
info: Optional documentation metadata.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Decorator function or decorated function.
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
InconsistentWiring: If registry injection is not properly configured.
|
|
62
|
+
|
|
63
|
+
Example:
|
|
64
|
+
>>> @kernel(name="custom_field", produce_fields=("result",))
|
|
65
|
+
... def my_kernel(input_field: float, param: float = 1.0) -> float:
|
|
66
|
+
... return input_field * param
|
|
67
|
+
"""
|
|
68
|
+
if not isinstance(registry, KernelRegistry):
|
|
69
|
+
raise InconsistentWiring
|
|
70
|
+
|
|
71
|
+
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
72
|
+
req_auto, cfg = registry._split_signature(func) # noqa: SLF001
|
|
73
|
+
final_name = name or func.__name__
|
|
74
|
+
final_require = require_fields or req_auto
|
|
75
|
+
final_produce = produce_fields or (final_name,)
|
|
76
|
+
|
|
77
|
+
spec: dict[str, Any] = {
|
|
78
|
+
"name": final_name,
|
|
79
|
+
"require_fields": final_require,
|
|
80
|
+
"produce_fields": final_produce,
|
|
81
|
+
"implementation": func,
|
|
82
|
+
"apply_level": apply_level,
|
|
83
|
+
"execution_protocol": execution_protocol,
|
|
84
|
+
"restrictions": restrictions,
|
|
85
|
+
"config": cfg,
|
|
86
|
+
"info": info or KernelInfo(),
|
|
87
|
+
}
|
|
88
|
+
registry.register_kernel(**spec)
|
|
89
|
+
if _COLLECTING_DEFAULTS:
|
|
90
|
+
_DEFAULT_KERNEL_SPECS.append(spec)
|
|
91
|
+
|
|
92
|
+
return func
|
|
93
|
+
|
|
94
|
+
if _func is None:
|
|
95
|
+
return decorator
|
|
96
|
+
return decorator(_func)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def register_default_kernels(registry: KernelRegistry | None = None) -> None:
|
|
100
|
+
"""Register the built-in kernels, replaying them into ``registry``.
|
|
101
|
+
|
|
102
|
+
The first call imports the kernels subpackage, which registers every
|
|
103
|
+
``@kernel``-decorated function into the currently wired registry and
|
|
104
|
+
records a manifest of the built-ins. Because module imports are cached
|
|
105
|
+
per process, later calls replay that manifest into ``registry`` so a
|
|
106
|
+
freshly created registry also receives the built-in kernels.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
registry: Target registry for the manifest replay. When None, only
|
|
110
|
+
the import side effects apply (first call in the process).
|
|
111
|
+
"""
|
|
112
|
+
global _COLLECTING_DEFAULTS # noqa: PLW0603
|
|
113
|
+
|
|
114
|
+
module_name = f"{__package__}.kernels"
|
|
115
|
+
if module_name not in sys.modules:
|
|
116
|
+
_COLLECTING_DEFAULTS = True
|
|
117
|
+
try:
|
|
118
|
+
importlib.import_module(".kernels", package=__package__)
|
|
119
|
+
finally:
|
|
120
|
+
_COLLECTING_DEFAULTS = False
|
|
121
|
+
|
|
122
|
+
if registry is not None:
|
|
123
|
+
for spec in _DEFAULT_KERNEL_SPECS:
|
|
124
|
+
if not registry.has_kernel(spec["name"]):
|
|
125
|
+
registry.register_kernel(**spec)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Kernel-related exception classes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class KernelError(Exception):
|
|
7
|
+
"""Base class for kernel-related errors."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DependencyNotFoundError(KernelError):
|
|
11
|
+
"""Raised when a dependency is not found in the registry."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CircularDependencyError(KernelError):
|
|
15
|
+
"""Raised when a circular dependency is detected among kernels."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class InvalidConfigurationError(KernelError):
|
|
19
|
+
"""Raised when kernel configuration is invalid."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class InconsistentWiringError(KernelError):
|
|
23
|
+
"""Raised when dependency injection wiring is inconsistent."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class KernelExecutionError(KernelError):
|
|
27
|
+
"""Raised when a kernel implementation raises during execution."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
InvalidConfiguration = InvalidConfigurationError
|
|
31
|
+
InconsistentWiring = InconsistentWiringError
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Re-export for convenience
|
|
2
|
+
from .enums import KernelApplyLevel, KernelExecutionProtocol, KernelRestrictions
|
|
3
|
+
from .executor import KernelExecutor
|
|
4
|
+
from .strategy import ExecutionStrategy, ParallelStrategy, SequentialStrategy
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"ExecutionStrategy",
|
|
8
|
+
"KernelApplyLevel",
|
|
9
|
+
"KernelExecutionProtocol",
|
|
10
|
+
"KernelExecutor",
|
|
11
|
+
"KernelRestrictions",
|
|
12
|
+
"ParallelStrategy",
|
|
13
|
+
"SequentialStrategy",
|
|
14
|
+
]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Internal data types for kernel execution.
|
|
2
|
+
|
|
3
|
+
This module defines the data structures used internally by kernel runners
|
|
4
|
+
for tracking execution state, required inputs, and pending work.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from diffract.core.data.nn.params.interface import IParameterView
|
|
14
|
+
|
|
15
|
+
from .aggregation import AggregationContext
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True, slots=True)
|
|
19
|
+
class AggregateInput:
|
|
20
|
+
"""Description of an aggregate input required by an aggregation kernel.
|
|
21
|
+
|
|
22
|
+
Used to track which aggregate values need to be read from the
|
|
23
|
+
AggregateRepository when executing aggregation-level kernels.
|
|
24
|
+
|
|
25
|
+
Attributes:
|
|
26
|
+
field_name: Name of the aggregate field to read.
|
|
27
|
+
context_models: Model IDs participating in the aggregation.
|
|
28
|
+
context_params: Parameter names participating in the aggregation.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
field_name: str
|
|
32
|
+
context_models: tuple[str, ...]
|
|
33
|
+
context_params: tuple[str, ...]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True, slots=True)
|
|
37
|
+
class RequiredInputs:
|
|
38
|
+
"""Required inputs for kernel execution, categorized by source.
|
|
39
|
+
|
|
40
|
+
Separates inputs into two categories:
|
|
41
|
+
- parameter_fields: Read directly from parameter proxies
|
|
42
|
+
- aggregate_inputs: Read from the aggregate repository
|
|
43
|
+
|
|
44
|
+
Attributes:
|
|
45
|
+
parameter_fields: Field names to read from parameters.
|
|
46
|
+
aggregate_inputs: Aggregate field descriptors for repository lookups.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
parameter_fields: tuple[str, ...]
|
|
50
|
+
aggregate_inputs: tuple[AggregateInput, ...]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True, slots=True)
|
|
54
|
+
class PendingGroup:
|
|
55
|
+
"""A group of parameters pending aggregation kernel execution.
|
|
56
|
+
|
|
57
|
+
Represents a unit of work for aggregation kernels, containing all
|
|
58
|
+
information needed to execute the kernel for a specific group.
|
|
59
|
+
|
|
60
|
+
Attributes:
|
|
61
|
+
group_id: Unique identifier for this group.
|
|
62
|
+
context: Aggregation context with model/parameter scope.
|
|
63
|
+
view: Parameter view containing the group's parameters.
|
|
64
|
+
required_inputs: Inputs needed to execute the kernel.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
group_id: str
|
|
68
|
+
context: AggregationContext
|
|
69
|
+
view: IParameterView
|
|
70
|
+
required_inputs: RequiredInputs
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Aggregation and grouping helpers extracted from executor."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable, Iterable
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
from diffract.core.constants import (
|
|
9
|
+
format_contextual_field_name,
|
|
10
|
+
format_field_suffix,
|
|
11
|
+
)
|
|
12
|
+
from diffract.core.data.nn.params.interface import IParameterView
|
|
13
|
+
|
|
14
|
+
type GroupsIterator = Iterable[tuple[str, IParameterView]]
|
|
15
|
+
type ContextBuilder = Callable[[str, IParameterView], AggregationContext]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class AggregationContext:
|
|
20
|
+
"""Context for aggregated computations with contextual field naming.
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
models: Model identifiers participating in aggregation.
|
|
24
|
+
parameters: Parameter names participating in aggregation.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
models: tuple[str, ...] | None = None
|
|
28
|
+
parameters: tuple[str, ...] | None = None
|
|
29
|
+
|
|
30
|
+
def to_field_suffix(self) -> str:
|
|
31
|
+
"""Build a deterministic suffix from context identifiers."""
|
|
32
|
+
return format_field_suffix(models=self.models, params=self.parameters)
|
|
33
|
+
|
|
34
|
+
def create_field_name(self, field_name: str) -> str:
|
|
35
|
+
"""Compose contextual field name using the context suffix."""
|
|
36
|
+
return format_contextual_field_name(
|
|
37
|
+
field_name, models=self.models, params=self.parameters
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def aggregate_parameters(
|
|
42
|
+
collection: IParameterView,
|
|
43
|
+
) -> tuple[GroupsIterator, ContextBuilder]:
|
|
44
|
+
"""Group parameters by model ID."""
|
|
45
|
+
model_ids = sorted({p.meta.model_id for p in collection})
|
|
46
|
+
|
|
47
|
+
def iter_groups() -> GroupsIterator:
|
|
48
|
+
for model_id in model_ids:
|
|
49
|
+
group = collection.filter_by_model_id(model_id)
|
|
50
|
+
yield model_id, group
|
|
51
|
+
|
|
52
|
+
def build_context(model_id: str, group: IParameterView) -> AggregationContext:
|
|
53
|
+
names = tuple(sorted(p.meta.name for p in group))
|
|
54
|
+
return AggregationContext(models=(model_id,), parameters=names)
|
|
55
|
+
|
|
56
|
+
return iter_groups(), build_context
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def aggregate_models(
|
|
60
|
+
collection: IParameterView,
|
|
61
|
+
) -> tuple[GroupsIterator, ContextBuilder]:
|
|
62
|
+
"""Group parameters by parameter name."""
|
|
63
|
+
param_names = sorted({p.meta.name for p in collection})
|
|
64
|
+
|
|
65
|
+
def iter_groups() -> GroupsIterator:
|
|
66
|
+
for param_name in param_names:
|
|
67
|
+
grp = collection.filter_by_name(param_name)
|
|
68
|
+
yield param_name, grp
|
|
69
|
+
|
|
70
|
+
def build_context(param_name: str, group: IParameterView) -> AggregationContext:
|
|
71
|
+
model_ids = tuple(sorted(p.meta.model_id for p in group))
|
|
72
|
+
return AggregationContext(models=model_ids, parameters=(param_name,))
|
|
73
|
+
|
|
74
|
+
return iter_groups(), build_context
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
"""Aggregation-level kernel execution logic."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
from diffract.core.data.nn.aggregates.metadata import AggregateMetadata
|
|
9
|
+
|
|
10
|
+
from ._types import AggregateInput, PendingGroup, RequiredInputs
|
|
11
|
+
from .aggregation import AggregationContext, aggregate_models, aggregate_parameters
|
|
12
|
+
from .enums import KernelApplyLevel
|
|
13
|
+
from .restrictions import apply_restrictions_filter
|
|
14
|
+
from .strategy import create_execution_strategy
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from collections.abc import Iterator
|
|
18
|
+
from concurrent.futures import ProcessPoolExecutor
|
|
19
|
+
|
|
20
|
+
from diffract.core.compute.registry import KernelRegistry
|
|
21
|
+
from diffract.core.data.nn.aggregates import AggregateRepository
|
|
22
|
+
from diffract.core.data.nn.params.interface import IParameterView
|
|
23
|
+
from diffract.core.parallel import ParallelContext
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AggregationKernelRunner:
|
|
29
|
+
"""Executes aggregation-level kernels (IN_MODEL and CROSS_MODEL).
|
|
30
|
+
|
|
31
|
+
Handles execution of kernels that aggregate across parameters within
|
|
32
|
+
a model or across models, with support for memory-aware batching.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
registry: KernelRegistry,
|
|
38
|
+
process_pool: ProcessPoolExecutor | None,
|
|
39
|
+
parallel: ParallelContext | None,
|
|
40
|
+
aggregate_repository: AggregateRepository | None,
|
|
41
|
+
) -> None:
|
|
42
|
+
"""Initialize the aggregation kernel runner.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
registry: Kernel registry for metadata lookup.
|
|
46
|
+
process_pool: Optional process pool for parallel execution.
|
|
47
|
+
parallel: Optional parallel context for view operations.
|
|
48
|
+
aggregate_repository: Repository for storing aggregation results.
|
|
49
|
+
"""
|
|
50
|
+
self._registry = registry
|
|
51
|
+
self._process_pool = process_pool
|
|
52
|
+
self._parallel = parallel
|
|
53
|
+
self._aggregate_repository = aggregate_repository
|
|
54
|
+
|
|
55
|
+
def run(self, kernel_name: str, parameters: IParameterView) -> None:
|
|
56
|
+
"""Execute kernel with aggregation across models or parameters.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
kernel_name: Name of the registered kernel.
|
|
60
|
+
parameters: Parameter collection to process.
|
|
61
|
+
"""
|
|
62
|
+
pending_groups = self._collect_pending_groups(kernel_name, parameters)
|
|
63
|
+
if not pending_groups:
|
|
64
|
+
logger.debug(
|
|
65
|
+
"Skip execution of kernel '%s': no pending groups", kernel_name
|
|
66
|
+
)
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
logger.info(
|
|
70
|
+
"Executing kernel '%s' (%d groups)", kernel_name, len(pending_groups)
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
batches = self._batch_groups_by_budget(pending_groups, parameters)
|
|
74
|
+
for batch in batches:
|
|
75
|
+
self._prefetch_batch(batch, parameters)
|
|
76
|
+
self._execute_batch(kernel_name, batch)
|
|
77
|
+
|
|
78
|
+
def _collect_pending_groups(
|
|
79
|
+
self, kernel_name: str, parameters: IParameterView
|
|
80
|
+
) -> list[PendingGroup]:
|
|
81
|
+
"""Collect groups that need kernel execution."""
|
|
82
|
+
apply_level = self._registry.get_kernel_apply_level(kernel_name)
|
|
83
|
+
|
|
84
|
+
if apply_level == KernelApplyLevel.IN_MODEL:
|
|
85
|
+
group_iter, build_context = aggregate_parameters(parameters)
|
|
86
|
+
elif apply_level == KernelApplyLevel.CROSS_MODEL:
|
|
87
|
+
group_iter, build_context = aggregate_models(parameters)
|
|
88
|
+
else:
|
|
89
|
+
msg = f"Unsupported level for grouping: {apply_level}"
|
|
90
|
+
raise ValueError(msg)
|
|
91
|
+
|
|
92
|
+
target_fields = self._registry.get_fields_kernel_produce(kernel_name)
|
|
93
|
+
pending: list[PendingGroup] = []
|
|
94
|
+
|
|
95
|
+
for group_id, group in group_iter:
|
|
96
|
+
context = build_context(group_id, group)
|
|
97
|
+
|
|
98
|
+
if self._all_fields_computed(target_fields, context):
|
|
99
|
+
logger.debug(
|
|
100
|
+
"Skip execution of kernel '%s' for group '%s'",
|
|
101
|
+
kernel_name,
|
|
102
|
+
group_id,
|
|
103
|
+
)
|
|
104
|
+
continue
|
|
105
|
+
|
|
106
|
+
required_inputs = self._get_required_inputs(kernel_name, context)
|
|
107
|
+
pending.append(
|
|
108
|
+
PendingGroup(
|
|
109
|
+
group_id=group_id,
|
|
110
|
+
context=context,
|
|
111
|
+
view=group,
|
|
112
|
+
required_inputs=required_inputs,
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
return pending
|
|
117
|
+
|
|
118
|
+
def _all_fields_computed(
|
|
119
|
+
self, field_names: tuple[str, ...], context: AggregationContext
|
|
120
|
+
) -> bool:
|
|
121
|
+
"""Check if all target fields are already computed for the context."""
|
|
122
|
+
if self._aggregate_repository is None:
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
for field in field_names:
|
|
126
|
+
uid = AggregateMetadata.create_uid_from_context(
|
|
127
|
+
field_name=field,
|
|
128
|
+
context_models=context.models or (),
|
|
129
|
+
context_params=context.parameters or (),
|
|
130
|
+
)
|
|
131
|
+
try:
|
|
132
|
+
proxy = self._aggregate_repository.get_proxy(uid)
|
|
133
|
+
except KeyError:
|
|
134
|
+
return False
|
|
135
|
+
|
|
136
|
+
if not proxy.has_field("value"):
|
|
137
|
+
return False
|
|
138
|
+
|
|
139
|
+
return True
|
|
140
|
+
|
|
141
|
+
def _get_required_inputs(
|
|
142
|
+
self, kernel_name: str, context: AggregationContext
|
|
143
|
+
) -> RequiredInputs:
|
|
144
|
+
"""Determine required inputs for executing an aggregation kernel."""
|
|
145
|
+
parameter_fields: list[str] = []
|
|
146
|
+
aggregate_inputs: list[AggregateInput] = []
|
|
147
|
+
|
|
148
|
+
for field_name in self._registry.get_fields_kernel_require(kernel_name):
|
|
149
|
+
if not self._registry.can_produce_field(field_name):
|
|
150
|
+
parameter_fields.append(field_name)
|
|
151
|
+
continue
|
|
152
|
+
|
|
153
|
+
producer = self._registry.get_kernel_producing_field(field_name)
|
|
154
|
+
apply_level = self._registry.get_kernel_apply_level(producer)
|
|
155
|
+
|
|
156
|
+
if apply_level == KernelApplyLevel.PARAMETER:
|
|
157
|
+
parameter_fields.append(field_name)
|
|
158
|
+
else:
|
|
159
|
+
aggregate_inputs.append(
|
|
160
|
+
AggregateInput(
|
|
161
|
+
field_name=field_name,
|
|
162
|
+
context_models=context.models or (),
|
|
163
|
+
context_params=context.parameters or (),
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
return RequiredInputs(
|
|
168
|
+
parameter_fields=tuple(parameter_fields),
|
|
169
|
+
aggregate_inputs=tuple(aggregate_inputs),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def _batch_groups_by_budget(
|
|
173
|
+
self, groups: list[PendingGroup], base_view: IParameterView
|
|
174
|
+
) -> list[list[PendingGroup]]:
|
|
175
|
+
"""Batch groups by memory budget using iter_chunks_by_read_budget."""
|
|
176
|
+
if not groups:
|
|
177
|
+
return []
|
|
178
|
+
|
|
179
|
+
uid_to_group: dict[str, PendingGroup] = {}
|
|
180
|
+
required_fields_by_uid: dict[str, list[str]] = {}
|
|
181
|
+
|
|
182
|
+
for group in groups:
|
|
183
|
+
for uid in group.view.list_uids():
|
|
184
|
+
uid_to_group[uid] = group
|
|
185
|
+
fields = list(group.required_inputs.parameter_fields)
|
|
186
|
+
required_fields_by_uid[uid] = fields
|
|
187
|
+
|
|
188
|
+
all_uids = list(uid_to_group.keys())
|
|
189
|
+
if not all_uids:
|
|
190
|
+
return []
|
|
191
|
+
|
|
192
|
+
combined_view = base_view[all_uids]
|
|
193
|
+
chunks = list(
|
|
194
|
+
combined_view.iter_chunks_by_read_budget(
|
|
195
|
+
required_fields_by_uid=required_fields_by_uid,
|
|
196
|
+
parallel=self._parallel,
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
batches: list[list[PendingGroup]] = []
|
|
201
|
+
for chunk in chunks:
|
|
202
|
+
seen_groups: set[str] = set()
|
|
203
|
+
batch: list[PendingGroup] = []
|
|
204
|
+
for uid in chunk.list_uids():
|
|
205
|
+
group = uid_to_group[uid]
|
|
206
|
+
if group.group_id not in seen_groups:
|
|
207
|
+
seen_groups.add(group.group_id)
|
|
208
|
+
batch.append(group)
|
|
209
|
+
if batch:
|
|
210
|
+
batches.append(batch)
|
|
211
|
+
|
|
212
|
+
return batches
|
|
213
|
+
|
|
214
|
+
def _prefetch_batch(
|
|
215
|
+
self, batch: list[PendingGroup], base_view: IParameterView
|
|
216
|
+
) -> None:
|
|
217
|
+
"""Prefetch fields for a batch of groups."""
|
|
218
|
+
# Prefetch parameter fields
|
|
219
|
+
param_fields_by_uid: dict[str, list[str]] = {}
|
|
220
|
+
for group in batch:
|
|
221
|
+
for uid in group.view.list_uids():
|
|
222
|
+
param_fields_by_uid[uid] = list(group.required_inputs.parameter_fields)
|
|
223
|
+
|
|
224
|
+
if param_fields_by_uid:
|
|
225
|
+
batch_uids = list(param_fields_by_uid.keys())
|
|
226
|
+
base_view[batch_uids].prefetch_fields(
|
|
227
|
+
fields_by_uid=param_fields_by_uid,
|
|
228
|
+
parallel=self._parallel,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
# Prefetch aggregate fields
|
|
232
|
+
aggregate_uids = self._collect_aggregate_uids(batch)
|
|
233
|
+
if aggregate_uids and self._aggregate_repository is not None:
|
|
234
|
+
aggregate_view = self._aggregate_repository.create_view()
|
|
235
|
+
aggregate_view[aggregate_uids].prefetch_fields(
|
|
236
|
+
fields=["value"],
|
|
237
|
+
parallel=self._parallel,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
def _collect_aggregate_uids(self, batch: list[PendingGroup]) -> list[str]:
|
|
241
|
+
"""Collect all aggregate UIDs needed for a batch."""
|
|
242
|
+
aggregate_uids: list[str] = []
|
|
243
|
+
for group in batch:
|
|
244
|
+
for agg_input in group.required_inputs.aggregate_inputs:
|
|
245
|
+
uid = AggregateMetadata.create_uid_from_context(
|
|
246
|
+
field_name=agg_input.field_name,
|
|
247
|
+
context_models=agg_input.context_models,
|
|
248
|
+
context_params=agg_input.context_params,
|
|
249
|
+
)
|
|
250
|
+
aggregate_uids.append(uid)
|
|
251
|
+
return aggregate_uids
|
|
252
|
+
|
|
253
|
+
def _execute_batch(self, kernel_name: str, batch: list[PendingGroup]) -> None:
|
|
254
|
+
"""Execute aggregation-level kernel on a batch with streaming writes."""
|
|
255
|
+
required_args = self._registry.get_fields_kernel_require(kernel_name)
|
|
256
|
+
tasks: dict[tuple[str, AggregationContext], tuple[Any, ...]] = {}
|
|
257
|
+
|
|
258
|
+
for group in batch:
|
|
259
|
+
task_args = self._build_task_args(required_args, group)
|
|
260
|
+
tasks[(group.group_id, group.context)] = tuple(task_args)
|
|
261
|
+
|
|
262
|
+
apply_restrictions_filter(
|
|
263
|
+
kernel_name,
|
|
264
|
+
tasks,
|
|
265
|
+
self._registry.get_kernel_restrictions(kernel_name),
|
|
266
|
+
)
|
|
267
|
+
if not tasks:
|
|
268
|
+
return
|
|
269
|
+
|
|
270
|
+
if self._aggregate_repository is None:
|
|
271
|
+
msg = "AggregateRepository required for aggregation kernels"
|
|
272
|
+
raise RuntimeError(msg)
|
|
273
|
+
|
|
274
|
+
with self._aggregate_repository:
|
|
275
|
+
for (group_id, context), result in self._stream_results(kernel_name, tasks):
|
|
276
|
+
logger.debug(
|
|
277
|
+
"Set results of kernel '%s' for group '%s'",
|
|
278
|
+
kernel_name,
|
|
279
|
+
group_id,
|
|
280
|
+
)
|
|
281
|
+
self._store_result(kernel_name, context, result)
|
|
282
|
+
|
|
283
|
+
def _build_task_args(
|
|
284
|
+
self, required_args: tuple[str, ...], group: PendingGroup
|
|
285
|
+
) -> list[Any]:
|
|
286
|
+
"""Build argument list for a single aggregation task."""
|
|
287
|
+
task_args: list[Any] = []
|
|
288
|
+
|
|
289
|
+
for required_arg in required_args:
|
|
290
|
+
if not self._registry.can_produce_field(required_arg):
|
|
291
|
+
# Raw field from parameters
|
|
292
|
+
values = [p.get_field(required_arg) for p in group.view]
|
|
293
|
+
task_args.append(values)
|
|
294
|
+
continue
|
|
295
|
+
|
|
296
|
+
dependency = self._registry.get_kernel_producing_field(required_arg)
|
|
297
|
+
dependency_level = self._registry.get_kernel_apply_level(dependency)
|
|
298
|
+
|
|
299
|
+
if dependency_level == KernelApplyLevel.PARAMETER:
|
|
300
|
+
# Parameter-level computed field
|
|
301
|
+
values = [p.get_field(required_arg) for p in group.view]
|
|
302
|
+
task_args.append(values)
|
|
303
|
+
else:
|
|
304
|
+
# Aggregate-level computed field
|
|
305
|
+
value = self._read_aggregate_value(required_arg, group.context)
|
|
306
|
+
task_args.append(value)
|
|
307
|
+
|
|
308
|
+
return task_args
|
|
309
|
+
|
|
310
|
+
def _read_aggregate_value(
|
|
311
|
+
self, field_name: str, context: AggregationContext
|
|
312
|
+
) -> Any:
|
|
313
|
+
"""Read an aggregate value from AggregateRepository."""
|
|
314
|
+
if self._aggregate_repository is None:
|
|
315
|
+
contextual_name = context.create_field_name(field_name)
|
|
316
|
+
msg = f"AggregateRepository not configured, cannot read '{contextual_name}'"
|
|
317
|
+
raise KeyError(msg)
|
|
318
|
+
|
|
319
|
+
uid = AggregateMetadata.create_uid_from_context(
|
|
320
|
+
field_name=field_name,
|
|
321
|
+
context_models=context.models or (),
|
|
322
|
+
context_params=context.parameters or (),
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
try:
|
|
326
|
+
aggregate = self._aggregate_repository.get_proxy(uid)
|
|
327
|
+
except KeyError:
|
|
328
|
+
contextual_name = context.create_field_name(field_name)
|
|
329
|
+
msg = f"Missing contextual dependency '{contextual_name}'"
|
|
330
|
+
raise KeyError(msg) from None
|
|
331
|
+
|
|
332
|
+
if not aggregate.has_field("value"):
|
|
333
|
+
contextual_name = context.create_field_name(field_name)
|
|
334
|
+
msg = f"Aggregate '{contextual_name}' exists but has no value"
|
|
335
|
+
raise KeyError(msg)
|
|
336
|
+
|
|
337
|
+
return aggregate.get_field("value")
|
|
338
|
+
|
|
339
|
+
def _store_result(
|
|
340
|
+
self, kernel_name: str, context: AggregationContext, result: Any
|
|
341
|
+
) -> None:
|
|
342
|
+
"""Store kernel result in aggregate repository."""
|
|
343
|
+
normalized = self._registry.normalize_kernel_result(kernel_name, result)
|
|
344
|
+
for field_name, value in normalized.items():
|
|
345
|
+
aggregate = self._aggregate_repository.get_or_create(
|
|
346
|
+
field_name=field_name,
|
|
347
|
+
context_models=context.models or (),
|
|
348
|
+
context_params=context.parameters or (),
|
|
349
|
+
)
|
|
350
|
+
aggregate.set_field("value", value)
|
|
351
|
+
|
|
352
|
+
def _stream_results(
|
|
353
|
+
self, kernel_name: str, tasks: dict[Any, tuple[Any, ...]]
|
|
354
|
+
) -> Iterator[tuple[Any, Any]]:
|
|
355
|
+
"""Execute kernel tasks and yield results as they become available."""
|
|
356
|
+
strategy = create_execution_strategy(
|
|
357
|
+
kernel_name,
|
|
358
|
+
self._registry,
|
|
359
|
+
self._process_pool,
|
|
360
|
+
)
|
|
361
|
+
implementation = self._registry.get_kernel_implementation(kernel_name)
|
|
362
|
+
yield from strategy.execute_tasks(kernel_name, tasks, implementation)
|