optimaxx 0.1.0__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.
- optimax/__init__.py +21 -0
- optimax/analyzer/__init__.py +24 -0
- optimax/analyzer/analyzer.py +255 -0
- optimax/analyzer/models.py +85 -0
- optimax/analyzer/recommendation.py +298 -0
- optimax/benchmark/__init__.py +11 -0
- optimax/benchmark/benchmark.py +151 -0
- optimax/benchmark/models.py +46 -0
- optimax/cli/__init__.py +7 -0
- optimax/cli/main.py +186 -0
- optimax/config/__init__.py +8 -0
- optimax/config/config.py +214 -0
- optimax/graph/__init__.py +9 -0
- optimax/graph/graph.py +94 -0
- optimax/hardware/__init__.py +20 -0
- optimax/hardware/detector.py +204 -0
- optimax/hardware/models.py +121 -0
- optimax/logging/__init__.py +8 -0
- optimax/logging/logger.py +79 -0
- optimax/memory/__init__.py +10 -0
- optimax/memory/tracker.py +152 -0
- optimax/optimizer/__init__.py +20 -0
- optimax/optimizer/base.py +209 -0
- optimax/optimizer/optimizer.py +194 -0
- optimax/optimizer/pass_registry.py +65 -0
- optimax/optimizer/passes/__init__.py +23 -0
- optimax/optimizer/passes/channels_last.py +132 -0
- optimax/optimizer/passes/flash_attention.py +153 -0
- optimax/optimizer/passes/gradient_checkpoint.py +125 -0
- optimax/optimizer/passes/mixed_precision.py +136 -0
- optimax/optimizer/passes/onnx_export.py +125 -0
- optimax/optimizer/passes/operator_fusion.py +126 -0
- optimax/optimizer/passes/quantization.py +142 -0
- optimax/optimizer/passes/tensorrt.py +150 -0
- optimax/optimizer/passes/torch_compile.py +124 -0
- optimax/optimizer/pipeline.py +662 -0
- optimax/optimizer/result.py +61 -0
- optimax/profiler/__init__.py +12 -0
- optimax/profiler/models.py +75 -0
- optimax/profiler/profiler.py +195 -0
- optimax/report/__init__.py +13 -0
- optimax/report/formatters.py +196 -0
- optimax/report/report.py +117 -0
- optimax/utils/__init__.py +12 -0
- optimax/utils/timing.py +55 -0
- optimax/utils/torch_utils.py +55 -0
- optimax/version.py +3 -0
- optimaxx-0.1.0.dist-info/METADATA +154 -0
- optimaxx-0.1.0.dist-info/RECORD +52 -0
- optimaxx-0.1.0.dist-info/WHEEL +4 -0
- optimaxx-0.1.0.dist-info/entry_points.txt +2 -0
- optimaxx-0.1.0.dist-info/licenses/LICENSE +176 -0
optimax/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Optimax: An intelligent AI model optimization framework for PyTorch."""
|
|
2
|
+
|
|
3
|
+
from optimax.analyzer import analyze
|
|
4
|
+
from optimax.optimizer import optimize, OptimizationResult
|
|
5
|
+
from optimax.benchmark import benchmark
|
|
6
|
+
from optimax.hardware import detect_hardware
|
|
7
|
+
from optimax.report import Report, generate_report
|
|
8
|
+
from optimax.config import load_config
|
|
9
|
+
from optimax.version import __version__
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"analyze",
|
|
13
|
+
"optimize",
|
|
14
|
+
"benchmark",
|
|
15
|
+
"detect_hardware",
|
|
16
|
+
"generate_report",
|
|
17
|
+
"load_config",
|
|
18
|
+
"OptimizationResult",
|
|
19
|
+
"Report",
|
|
20
|
+
"__version__",
|
|
21
|
+
]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Analyzer module for Optimax."""
|
|
2
|
+
|
|
3
|
+
from optimax.analyzer.analyzer import analyze, ModelAnalyzer
|
|
4
|
+
from optimax.analyzer.models import ModelProfile, LayerInfo, TensorProfile
|
|
5
|
+
from optimax.analyzer.recommendation import (
|
|
6
|
+
generate_recommendations,
|
|
7
|
+
Recommendation,
|
|
8
|
+
RecommendationEngine,
|
|
9
|
+
RiskLevel,
|
|
10
|
+
Compatibility,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"analyze",
|
|
15
|
+
"ModelAnalyzer",
|
|
16
|
+
"ModelProfile",
|
|
17
|
+
"LayerInfo",
|
|
18
|
+
"TensorProfile",
|
|
19
|
+
"generate_recommendations",
|
|
20
|
+
"Recommendation",
|
|
21
|
+
"RecommendationEngine",
|
|
22
|
+
"RiskLevel",
|
|
23
|
+
"Compatibility",
|
|
24
|
+
]
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""Model analysis for Optimax."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
import torch
|
|
6
|
+
|
|
7
|
+
from optimax.analyzer.models import LayerInfo, ModelProfile, TensorProfile
|
|
8
|
+
from optimax.logging import get_logger
|
|
9
|
+
|
|
10
|
+
logger = get_logger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ModelAnalyzer:
|
|
14
|
+
"""Analyzes PyTorch models and collects comprehensive statistics."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, model: torch.nn.Module) -> None:
|
|
17
|
+
"""Initialize the analyzer with a model.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
model: PyTorch model to analyze.
|
|
21
|
+
"""
|
|
22
|
+
self.model = model
|
|
23
|
+
self._hooks: list[Any] = []
|
|
24
|
+
self._activation_shapes: dict[str, list[tuple[int, ...]]] = {}
|
|
25
|
+
|
|
26
|
+
def _register_forward_hooks(self) -> None:
|
|
27
|
+
"""Register forward hooks to capture activation shapes."""
|
|
28
|
+
self._activation_shapes.clear()
|
|
29
|
+
|
|
30
|
+
def hook(name: str) -> Any:
|
|
31
|
+
def fn(_module: torch.nn.Module, input: Any, output: Any) -> None:
|
|
32
|
+
shapes: list[tuple[int, ...]] = []
|
|
33
|
+
if isinstance(output, torch.Tensor):
|
|
34
|
+
shapes.append(tuple(output.shape))
|
|
35
|
+
elif isinstance(output, (list, tuple)):
|
|
36
|
+
for o in output:
|
|
37
|
+
if isinstance(o, torch.Tensor):
|
|
38
|
+
shapes.append(tuple(o.shape))
|
|
39
|
+
self._activation_shapes[name] = shapes
|
|
40
|
+
|
|
41
|
+
return fn
|
|
42
|
+
|
|
43
|
+
for name, module in self.model.named_modules():
|
|
44
|
+
if name:
|
|
45
|
+
h = module.register_forward_hook(hook(name))
|
|
46
|
+
self._hooks.append(h)
|
|
47
|
+
|
|
48
|
+
def _remove_hooks(self) -> None:
|
|
49
|
+
"""Remove all registered forward hooks."""
|
|
50
|
+
for h in self._hooks:
|
|
51
|
+
h.remove()
|
|
52
|
+
self._hooks.clear()
|
|
53
|
+
|
|
54
|
+
@staticmethod
|
|
55
|
+
def _count_flops_linear(module: torch.nn.Linear, input_shape: tuple[int, ...]) -> int:
|
|
56
|
+
"""Estimate FLOPs for a linear layer.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
module: Linear module.
|
|
60
|
+
input_shape: Input tensor shape.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Estimated FLOPs.
|
|
64
|
+
"""
|
|
65
|
+
# FLOPs = 2 * in_features * out_features * batch_size (assuming matmul)
|
|
66
|
+
in_features = module.in_features
|
|
67
|
+
out_features = module.out_features
|
|
68
|
+
batch_size = int(torch.prod(torch.tensor(input_shape[:-1])))
|
|
69
|
+
return 2 * in_features * out_features * batch_size
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def _count_flops_conv(module: torch.nn.modules.conv._ConvNd, input_shape: tuple[int, ...]) -> int:
|
|
73
|
+
"""Estimate FLOPs for a convolution layer.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
module: Conv module.
|
|
77
|
+
input_shape: Input tensor shape (N, C_in, ...).
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Estimated FLOPs.
|
|
81
|
+
"""
|
|
82
|
+
# FLOPs = 2 * output_elements * kernel_elements * C_in / groups
|
|
83
|
+
output_shape = module(output_shape=input_shape) # type: ignore[arg-type]
|
|
84
|
+
# Simpler estimation: use formula
|
|
85
|
+
kernel_dims = list(module.kernel_size)
|
|
86
|
+
kernel_elements = int(torch.prod(torch.tensor(kernel_dims)))
|
|
87
|
+
in_channels = module.in_channels
|
|
88
|
+
out_channels = module.out_channels
|
|
89
|
+
groups = module.groups
|
|
90
|
+
# Output spatial size
|
|
91
|
+
if len(input_shape) == 4: # 2D conv
|
|
92
|
+
batch_size, _, h, w = input_shape
|
|
93
|
+
out_h = (h + 2 * module.padding[0] - module.dilation[0] * (kernel_dims[0] - 1) - 1) // module.stride[0] + 1
|
|
94
|
+
out_w = (w + 2 * module.padding[1] - module.dilation[1] * (kernel_dims[1] - 1) - 1) // module.stride[1] + 1
|
|
95
|
+
output_elements = batch_size * out_h * out_w
|
|
96
|
+
elif len(input_shape) == 5: # 3D conv
|
|
97
|
+
batch_size, _, d, h, w = input_shape
|
|
98
|
+
out_d = (d + 2 * module.padding[0] - module.dilation[0] * (kernel_dims[0] - 1) - 1) // module.stride[0] + 1
|
|
99
|
+
out_h = (h + 2 * module.padding[1] - module.dilation[1] * (kernel_dims[1] - 1) - 1) // module.stride[1] + 1
|
|
100
|
+
out_w = (w + 2 * module.padding[2] - module.dilation[2] * (kernel_dims[2] - 1) - 1) // module.stride[2] + 1
|
|
101
|
+
output_elements = batch_size * out_d * out_h * out_w
|
|
102
|
+
elif len(input_shape) == 3: # 1D conv
|
|
103
|
+
batch_size, _, l = input_shape
|
|
104
|
+
out_l = (l + 2 * module.padding[0] - module.dilation[0] * (kernel_dims[0] - 1) - 1) // module.stride[0] + 1
|
|
105
|
+
output_elements = batch_size * out_l
|
|
106
|
+
else:
|
|
107
|
+
return 0
|
|
108
|
+
return 2 * output_elements * kernel_elements * in_channels * out_channels // groups
|
|
109
|
+
|
|
110
|
+
def _estimate_module_flops(
|
|
111
|
+
self, module: torch.nn.Module, input_shapes: list[tuple[int, ...]]
|
|
112
|
+
) -> Optional[int]:
|
|
113
|
+
"""Estimate FLOPs for a single module.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
module: A PyTorch module.
|
|
117
|
+
input_shapes: List of input tensor shapes.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Estimated FLOPs or None if not supported.
|
|
121
|
+
"""
|
|
122
|
+
if not input_shapes:
|
|
123
|
+
return None
|
|
124
|
+
input_shape = input_shapes[0]
|
|
125
|
+
if isinstance(module, torch.nn.Linear):
|
|
126
|
+
return self._count_flops_linear(module, input_shape)
|
|
127
|
+
if isinstance(module, (torch.nn.Conv1d, torch.nn.Conv2d, torch.nn.Conv3d)):
|
|
128
|
+
return self._count_flops_conv(module, input_shape)
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
def _analyze_module(self, name: str, module: torch.nn.Module) -> LayerInfo:
|
|
132
|
+
"""Recursively analyze a module and its children.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
name: Module name.
|
|
136
|
+
module: PyTorch module.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
LayerInfo with statistics.
|
|
140
|
+
"""
|
|
141
|
+
params = sum(p.numel() for p in module.parameters())
|
|
142
|
+
trainable = sum(p.numel() for p in module.parameters() if p.requires_grad)
|
|
143
|
+
frozen = params - trainable
|
|
144
|
+
memory_mb = sum(p.numel() * p.element_size() for p in module.parameters()) / (1024 * 1024)
|
|
145
|
+
input_shapes = self._activation_shapes.get(name, [])
|
|
146
|
+
flops = self._estimate_module_flops(module, input_shapes)
|
|
147
|
+
children: list[LayerInfo] = []
|
|
148
|
+
for child_name, child_module in module.named_children():
|
|
149
|
+
full_name = f"{name}.{child_name}" if name else child_name
|
|
150
|
+
children.append(self._analyze_module(full_name, child_module))
|
|
151
|
+
return LayerInfo(
|
|
152
|
+
name=name,
|
|
153
|
+
type=module.__class__.__name__,
|
|
154
|
+
parameters=params,
|
|
155
|
+
trainable_parameters=trainable,
|
|
156
|
+
frozen_parameters=frozen,
|
|
157
|
+
input_shapes=[tuple(s) for s in input_shapes],
|
|
158
|
+
memory_mb=memory_mb,
|
|
159
|
+
flops=flops,
|
|
160
|
+
children=children,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
def analyze(
|
|
164
|
+
self,
|
|
165
|
+
example_input: Optional[torch.Tensor] = None,
|
|
166
|
+
) -> ModelProfile:
|
|
167
|
+
"""Analyze the model and return a comprehensive profile.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
example_input: Optional example input tensor for activation tracing.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
A ModelProfile instance.
|
|
174
|
+
"""
|
|
175
|
+
total_params = sum(p.numel() for p in self.model.parameters())
|
|
176
|
+
trainable_params = sum(p.numel() for p in self.model.parameters() if p.requires_grad)
|
|
177
|
+
frozen_params = total_params - trainable_params
|
|
178
|
+
model_size = sum(p.numel() * p.element_size() for p in self.model.parameters()) / (1024 * 1024)
|
|
179
|
+
estimated_memory = model_size # Base estimate
|
|
180
|
+
|
|
181
|
+
if example_input is not None:
|
|
182
|
+
self._register_forward_hooks()
|
|
183
|
+
try:
|
|
184
|
+
self.model(example_input)
|
|
185
|
+
except Exception as e:
|
|
186
|
+
logger.warning("Forward pass with example_input failed: %s", e)
|
|
187
|
+
self._remove_hooks()
|
|
188
|
+
|
|
189
|
+
layer_hierarchy = []
|
|
190
|
+
for name, module in self.model.named_children():
|
|
191
|
+
layer_hierarchy.append(self._analyze_module(name, module))
|
|
192
|
+
|
|
193
|
+
layer_count = sum(1 for _ in self.model.modules()) - 1 # exclude root
|
|
194
|
+
|
|
195
|
+
# Estimate total FLOPs from layer hierarchy
|
|
196
|
+
estimated_flops: Optional[int] = None
|
|
197
|
+
try:
|
|
198
|
+
total_flops = 0
|
|
199
|
+
def sum_flops(layers: list[LayerInfo]) -> None:
|
|
200
|
+
nonlocal total_flops
|
|
201
|
+
for layer in layers:
|
|
202
|
+
if layer.flops is not None:
|
|
203
|
+
total_flops += layer.flops
|
|
204
|
+
sum_flops(layer.children)
|
|
205
|
+
sum_flops(layer_hierarchy)
|
|
206
|
+
estimated_flops = total_flops
|
|
207
|
+
except Exception:
|
|
208
|
+
pass
|
|
209
|
+
|
|
210
|
+
tensor_profiles: list[TensorProfile] = []
|
|
211
|
+
for shapes in self._activation_shapes.values():
|
|
212
|
+
for shape in shapes:
|
|
213
|
+
elements = int(torch.prod(torch.tensor(shape)))
|
|
214
|
+
memory_bytes = elements * 4 # assume float32
|
|
215
|
+
tensor_profiles.append(
|
|
216
|
+
TensorProfile(
|
|
217
|
+
shape=shape,
|
|
218
|
+
dtype="float32",
|
|
219
|
+
elements=elements,
|
|
220
|
+
memory_bytes=memory_bytes,
|
|
221
|
+
)
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# Add activation memory to estimated memory
|
|
225
|
+
activation_memory = sum(t.memory_bytes for t in tensor_profiles) / (1024 * 1024)
|
|
226
|
+
estimated_memory += activation_memory
|
|
227
|
+
|
|
228
|
+
return ModelProfile(
|
|
229
|
+
total_parameters=total_params,
|
|
230
|
+
trainable_parameters=trainable_params,
|
|
231
|
+
frozen_parameters=frozen_params,
|
|
232
|
+
model_size_mb=model_size,
|
|
233
|
+
estimated_memory_mb=estimated_memory,
|
|
234
|
+
estimated_flops=estimated_flops,
|
|
235
|
+
layer_hierarchy=layer_hierarchy,
|
|
236
|
+
layer_count=layer_count,
|
|
237
|
+
tensor_profiles=tensor_profiles,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def analyze(
|
|
242
|
+
model: torch.nn.Module,
|
|
243
|
+
example_input: Optional[torch.Tensor] = None,
|
|
244
|
+
) -> ModelProfile:
|
|
245
|
+
"""Convenience function to analyze a model.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
model: PyTorch model to analyze.
|
|
249
|
+
example_input: Optional example input for activation tracing.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
A ModelProfile instance.
|
|
253
|
+
"""
|
|
254
|
+
analyzer = ModelAnalyzer(model)
|
|
255
|
+
return analyzer.analyze(example_input)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Data models for the analyzer."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
import torch
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class LayerInfo:
|
|
11
|
+
"""Information about a single layer/module."""
|
|
12
|
+
|
|
13
|
+
name: str
|
|
14
|
+
type: str
|
|
15
|
+
parameters: int
|
|
16
|
+
trainable_parameters: int
|
|
17
|
+
frozen_parameters: int
|
|
18
|
+
input_shapes: list[Optional[tuple[int, ...]]] = field(default_factory=list)
|
|
19
|
+
output_shapes: list[Optional[tuple[int, ...]]] = field(default_factory=list)
|
|
20
|
+
memory_mb: float = 0.0
|
|
21
|
+
flops: Optional[int] = None
|
|
22
|
+
children: list["LayerInfo"] = field(default_factory=list)
|
|
23
|
+
|
|
24
|
+
def to_dict(self) -> dict[str, Any]:
|
|
25
|
+
"""Return dictionary representation."""
|
|
26
|
+
return {
|
|
27
|
+
"name": self.name,
|
|
28
|
+
"type": self.type,
|
|
29
|
+
"parameters": self.parameters,
|
|
30
|
+
"trainable_parameters": self.trainable_parameters,
|
|
31
|
+
"frozen_parameters": self.frozen_parameters,
|
|
32
|
+
"input_shapes": [list(s) if s else None for s in self.input_shapes],
|
|
33
|
+
"output_shapes": [list(s) if s else None for s in self.output_shapes],
|
|
34
|
+
"memory_mb": self.memory_mb,
|
|
35
|
+
"flops": self.flops,
|
|
36
|
+
"children": [c.to_dict() for c in self.children],
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class TensorProfile:
|
|
42
|
+
"""Profile of tensor activations."""
|
|
43
|
+
|
|
44
|
+
shape: tuple[int, ...]
|
|
45
|
+
dtype: str
|
|
46
|
+
elements: int
|
|
47
|
+
memory_bytes: int
|
|
48
|
+
|
|
49
|
+
def to_dict(self) -> dict[str, Any]:
|
|
50
|
+
"""Return dictionary representation."""
|
|
51
|
+
return {
|
|
52
|
+
"shape": list(self.shape),
|
|
53
|
+
"dtype": self.dtype,
|
|
54
|
+
"elements": self.elements,
|
|
55
|
+
"memory_bytes": self.memory_bytes,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class ModelProfile:
|
|
61
|
+
"""Complete profile of a PyTorch model."""
|
|
62
|
+
|
|
63
|
+
total_parameters: int
|
|
64
|
+
trainable_parameters: int
|
|
65
|
+
frozen_parameters: int
|
|
66
|
+
model_size_mb: float
|
|
67
|
+
estimated_memory_mb: float
|
|
68
|
+
estimated_flops: Optional[int] = None
|
|
69
|
+
layer_hierarchy: list[LayerInfo] = field(default_factory=list)
|
|
70
|
+
layer_count: int = 0
|
|
71
|
+
tensor_profiles: list[TensorProfile] = field(default_factory=list)
|
|
72
|
+
|
|
73
|
+
def to_dict(self) -> dict[str, Any]:
|
|
74
|
+
"""Return dictionary representation."""
|
|
75
|
+
return {
|
|
76
|
+
"total_parameters": self.total_parameters,
|
|
77
|
+
"trainable_parameters": self.trainable_parameters,
|
|
78
|
+
"frozen_parameters": self.frozen_parameters,
|
|
79
|
+
"model_size_mb": self.model_size_mb,
|
|
80
|
+
"estimated_memory_mb": self.estimated_memory_mb,
|
|
81
|
+
"estimated_flops": self.estimated_flops,
|
|
82
|
+
"layer_hierarchy": [l.to_dict() for l in self.layer_hierarchy],
|
|
83
|
+
"layer_count": self.layer_count,
|
|
84
|
+
"tensor_profiles": [t.to_dict() for t in self.tensor_profiles],
|
|
85
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"""Recommendation engine for Optimax."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
import torch
|
|
8
|
+
|
|
9
|
+
from optimax.analyzer.models import ModelProfile
|
|
10
|
+
from optimax.hardware.models import HardwareProfile
|
|
11
|
+
from optimax.logging import get_logger
|
|
12
|
+
|
|
13
|
+
logger = get_logger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class RiskLevel(str, Enum):
|
|
17
|
+
"""Risk level for an optimization recommendation."""
|
|
18
|
+
|
|
19
|
+
LOW = "low"
|
|
20
|
+
MEDIUM = "medium"
|
|
21
|
+
HIGH = "high"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Compatibility(str, Enum):
|
|
25
|
+
"""Compatibility assessment for a recommendation."""
|
|
26
|
+
|
|
27
|
+
FULL = "full"
|
|
28
|
+
PARTIAL = "partial"
|
|
29
|
+
LIMITED = "limited"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class Recommendation:
|
|
34
|
+
"""A single optimization recommendation."""
|
|
35
|
+
|
|
36
|
+
name: str
|
|
37
|
+
description: str
|
|
38
|
+
reason: str
|
|
39
|
+
expected_speedup: Optional[float] = None
|
|
40
|
+
expected_memory_savings: Optional[float] = None
|
|
41
|
+
risk_level: RiskLevel = RiskLevel.LOW
|
|
42
|
+
compatibility: Compatibility = Compatibility.FULL
|
|
43
|
+
pass_name: Optional[str] = None
|
|
44
|
+
confidence: float = 0.5
|
|
45
|
+
supported_hardware: list[str] = field(default_factory=list)
|
|
46
|
+
required_dependencies: list[str] = field(default_factory=list)
|
|
47
|
+
|
|
48
|
+
def to_dict(self) -> dict[str, Any]:
|
|
49
|
+
"""Return dictionary representation."""
|
|
50
|
+
return {
|
|
51
|
+
"name": self.name,
|
|
52
|
+
"description": self.description,
|
|
53
|
+
"reason": self.reason,
|
|
54
|
+
"expected_speedup": self.expected_speedup,
|
|
55
|
+
"expected_memory_savings": self.expected_memory_savings,
|
|
56
|
+
"risk_level": self.risk_level.value,
|
|
57
|
+
"compatibility": self.compatibility.value,
|
|
58
|
+
"pass_name": self.pass_name,
|
|
59
|
+
"confidence": self.confidence,
|
|
60
|
+
"supported_hardware": self.supported_hardware,
|
|
61
|
+
"required_dependencies": self.required_dependencies,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class RecommendationEngine:
|
|
66
|
+
"""Generates optimization recommendations based on model and hardware analysis."""
|
|
67
|
+
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
model: torch.nn.Module,
|
|
71
|
+
profile: ModelProfile,
|
|
72
|
+
hardware: HardwareProfile,
|
|
73
|
+
) -> None:
|
|
74
|
+
"""Initialize the recommendation engine.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
model: The PyTorch model.
|
|
78
|
+
profile: The model profile from analysis.
|
|
79
|
+
hardware: The detected hardware profile.
|
|
80
|
+
"""
|
|
81
|
+
self.model = model
|
|
82
|
+
self.profile = profile
|
|
83
|
+
self.hardware = hardware
|
|
84
|
+
self._recommendations: list[Recommendation] = []
|
|
85
|
+
|
|
86
|
+
def _recommend_torch_compile(self) -> Optional[Recommendation]:
|
|
87
|
+
"""Recommend torch.compile if compatible."""
|
|
88
|
+
if self.hardware.software.pytorch_version.startswith("2."):
|
|
89
|
+
reason = "PyTorch 2.x supports torch.compile() which can significantly reduce Python overhead and fuse kernels."
|
|
90
|
+
confidence = 0.8
|
|
91
|
+
if self.hardware.has_cuda:
|
|
92
|
+
confidence = 0.9
|
|
93
|
+
return Recommendation(
|
|
94
|
+
name="Enable torch.compile",
|
|
95
|
+
description="Use torch.compile() to optimize model execution.",
|
|
96
|
+
reason=reason,
|
|
97
|
+
expected_speedup=1.2,
|
|
98
|
+
expected_memory_savings=None,
|
|
99
|
+
risk_level=RiskLevel.LOW,
|
|
100
|
+
compatibility=Compatibility.FULL,
|
|
101
|
+
pass_name="torch_compile",
|
|
102
|
+
confidence=confidence,
|
|
103
|
+
supported_hardware=["cpu", "cuda", "mps"],
|
|
104
|
+
required_dependencies=["torch>=2.0"],
|
|
105
|
+
)
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
def _recommend_mixed_precision(self) -> Optional[Recommendation]:
|
|
109
|
+
"""Recommend mixed precision if GPU supports it."""
|
|
110
|
+
if self.hardware.has_cuda or self.hardware.has_metal:
|
|
111
|
+
reason = "Modern GPUs support Tensor Cores/Metal performance shaders that can accelerate fp16/bf16 operations significantly."
|
|
112
|
+
speedup = 1.5 if self.hardware.has_cuda else 1.2
|
|
113
|
+
memory = 0.5
|
|
114
|
+
confidence = 0.85 if self.hardware.has_cuda else 0.7
|
|
115
|
+
return Recommendation(
|
|
116
|
+
name="Enable Mixed Precision",
|
|
117
|
+
description="Use torch.autocast with fp16 or bf16.",
|
|
118
|
+
reason=reason,
|
|
119
|
+
expected_speedup=speedup,
|
|
120
|
+
expected_memory_savings=memory,
|
|
121
|
+
risk_level=RiskLevel.LOW,
|
|
122
|
+
compatibility=Compatibility.FULL,
|
|
123
|
+
pass_name="mixed_precision",
|
|
124
|
+
confidence=confidence,
|
|
125
|
+
supported_hardware=["cuda", "mps"],
|
|
126
|
+
required_dependencies=["torch>=2.0"],
|
|
127
|
+
)
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
def _recommend_flash_attention(self) -> Optional[Recommendation]:
|
|
131
|
+
"""Recommend FlashAttention if model has attention patterns."""
|
|
132
|
+
has_attention = any(
|
|
133
|
+
"attention" in layer.name.lower() or "attn" in layer.type.lower()
|
|
134
|
+
for layer in self.profile.layer_hierarchy
|
|
135
|
+
)
|
|
136
|
+
if has_attention and (self.hardware.has_cuda or self.hardware.has_metal):
|
|
137
|
+
reason = "FlashAttention reduces attention memory usage from O(N^2) to O(N) and is compute-bound on modern GPUs."
|
|
138
|
+
return Recommendation(
|
|
139
|
+
name="Enable FlashAttention",
|
|
140
|
+
description="Replace standard attention with FlashAttention kernels.",
|
|
141
|
+
reason=reason,
|
|
142
|
+
expected_speedup=1.3,
|
|
143
|
+
expected_memory_savings=0.6,
|
|
144
|
+
risk_level=RiskLevel.MEDIUM,
|
|
145
|
+
compatibility=Compatibility.PARTIAL,
|
|
146
|
+
pass_name="flash_attention",
|
|
147
|
+
confidence=0.7,
|
|
148
|
+
supported_hardware=["cuda", "mps"],
|
|
149
|
+
required_dependencies=["torch>=2.0"],
|
|
150
|
+
)
|
|
151
|
+
return None
|
|
152
|
+
|
|
153
|
+
def _recommend_quantization(self) -> Optional[Recommendation]:
|
|
154
|
+
"""Recommend quantization for large models."""
|
|
155
|
+
if self.profile.total_parameters > 50_000_000: # 50M params
|
|
156
|
+
reason = "Large models benefit from INT8/INT4 quantization for inference, reducing memory footprint and improving throughput."
|
|
157
|
+
return Recommendation(
|
|
158
|
+
name="Use Quantization",
|
|
159
|
+
description="Apply post-training quantization or QAT.",
|
|
160
|
+
reason=reason,
|
|
161
|
+
expected_speedup=1.5,
|
|
162
|
+
expected_memory_savings=0.75,
|
|
163
|
+
risk_level=RiskLevel.MEDIUM,
|
|
164
|
+
compatibility=Compatibility.PARTIAL,
|
|
165
|
+
pass_name="quantization",
|
|
166
|
+
confidence=0.6,
|
|
167
|
+
supported_hardware=["cpu", "cuda"],
|
|
168
|
+
required_dependencies=["torch"],
|
|
169
|
+
)
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
def _recommend_operator_fusion(self) -> Optional[Recommendation]:
|
|
173
|
+
"""Recommend operator fusion if there are many small ops."""
|
|
174
|
+
if self.profile.layer_count > 50:
|
|
175
|
+
reason = "Models with many small layers benefit from operator fusion, reducing kernel launch overhead and improving memory locality."
|
|
176
|
+
return Recommendation(
|
|
177
|
+
name="Fuse Operators",
|
|
178
|
+
description="Fuse adjacent compatible operations.",
|
|
179
|
+
reason=reason,
|
|
180
|
+
expected_speedup=1.1,
|
|
181
|
+
expected_memory_savings=0.1,
|
|
182
|
+
risk_level=RiskLevel.LOW,
|
|
183
|
+
compatibility=Compatibility.FULL,
|
|
184
|
+
pass_name="operator_fusion",
|
|
185
|
+
confidence=0.75,
|
|
186
|
+
supported_hardware=["cpu", "cuda", "mps"],
|
|
187
|
+
required_dependencies=["torch>=2.0"],
|
|
188
|
+
)
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
def _recommend_channels_last(self) -> Optional[Recommendation]:
|
|
192
|
+
"""Recommend channels-last memory layout for CNNs."""
|
|
193
|
+
has_conv = any(
|
|
194
|
+
"conv" in layer.type.lower()
|
|
195
|
+
for layer in self.profile.layer_hierarchy
|
|
196
|
+
)
|
|
197
|
+
if has_conv and (self.hardware.has_cuda or self.hardware.has_metal):
|
|
198
|
+
reason = "Channels-last (NHWC) memory layout can improve performance on GPUs with Tensor Cores for convolutional networks."
|
|
199
|
+
return Recommendation(
|
|
200
|
+
name="Use Channels-Last Layout",
|
|
201
|
+
description="Convert conv inputs to channels-last format.",
|
|
202
|
+
reason=reason,
|
|
203
|
+
expected_speedup=1.1,
|
|
204
|
+
expected_memory_savings=None,
|
|
205
|
+
risk_level=RiskLevel.LOW,
|
|
206
|
+
compatibility=Compatibility.PARTIAL,
|
|
207
|
+
pass_name="channels_last",
|
|
208
|
+
confidence=0.7,
|
|
209
|
+
supported_hardware=["cuda", "mps"],
|
|
210
|
+
required_dependencies=["torch"],
|
|
211
|
+
)
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
def _recommend_gradient_checkpointing(self) -> Optional[Recommendation]:
|
|
215
|
+
"""Recommend gradient checkpointing for large models with limited memory."""
|
|
216
|
+
memory_pressure = self.hardware.memory.percent_used > 80.0
|
|
217
|
+
large_model = self.profile.estimated_memory_mb > self.hardware.memory.available_mb * 0.5
|
|
218
|
+
if memory_pressure and large_model:
|
|
219
|
+
reason = "Gradient checkpointing trades compute for memory by recomputing activations during backward pass instead of storing them."
|
|
220
|
+
return Recommendation(
|
|
221
|
+
name="Use Gradient Checkpointing",
|
|
222
|
+
description="Enable torch.utils.checkpoint for memory-constrained training.",
|
|
223
|
+
reason=reason,
|
|
224
|
+
expected_speedup=None,
|
|
225
|
+
expected_memory_savings=0.5,
|
|
226
|
+
risk_level=RiskLevel.MEDIUM,
|
|
227
|
+
compatibility=Compatibility.PARTIAL,
|
|
228
|
+
pass_name="gradient_checkpointing",
|
|
229
|
+
confidence=0.8,
|
|
230
|
+
supported_hardware=["cpu", "cuda", "mps"],
|
|
231
|
+
required_dependencies=["torch"],
|
|
232
|
+
)
|
|
233
|
+
return None
|
|
234
|
+
|
|
235
|
+
def _recommend_reduce_batch_size(self) -> Optional[Recommendation]:
|
|
236
|
+
"""Recommend reducing batch size if memory is constrained."""
|
|
237
|
+
if self.hardware.memory.percent_used > 90.0:
|
|
238
|
+
reason = "System memory is under high pressure. Reducing batch size is a straightforward way to decrease peak memory usage."
|
|
239
|
+
return Recommendation(
|
|
240
|
+
name="Reduce Batch Size",
|
|
241
|
+
description="Lower batch size to fit within available memory.",
|
|
242
|
+
reason=reason,
|
|
243
|
+
expected_speedup=None,
|
|
244
|
+
expected_memory_savings=0.3,
|
|
245
|
+
risk_level=RiskLevel.LOW,
|
|
246
|
+
compatibility=Compatibility.FULL,
|
|
247
|
+
pass_name=None,
|
|
248
|
+
confidence=0.9,
|
|
249
|
+
supported_hardware=["cpu", "cuda", "mps"],
|
|
250
|
+
required_dependencies=[],
|
|
251
|
+
)
|
|
252
|
+
return None
|
|
253
|
+
|
|
254
|
+
def generate(self) -> list[Recommendation]:
|
|
255
|
+
"""Generate all applicable recommendations.
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
A list of Recommendation instances sorted by confidence descending.
|
|
259
|
+
"""
|
|
260
|
+
generators = [
|
|
261
|
+
self._recommend_torch_compile,
|
|
262
|
+
self._recommend_mixed_precision,
|
|
263
|
+
self._recommend_flash_attention,
|
|
264
|
+
self._recommend_quantization,
|
|
265
|
+
self._recommend_operator_fusion,
|
|
266
|
+
self._recommend_channels_last,
|
|
267
|
+
self._recommend_gradient_checkpointing,
|
|
268
|
+
self._recommend_reduce_batch_size,
|
|
269
|
+
]
|
|
270
|
+
recommendations = []
|
|
271
|
+
for gen in generators:
|
|
272
|
+
try:
|
|
273
|
+
rec = gen()
|
|
274
|
+
if rec is not None:
|
|
275
|
+
recommendations.append(rec)
|
|
276
|
+
except Exception as e:
|
|
277
|
+
logger.warning("Recommendation generator %s failed: %s", gen.__name__, e)
|
|
278
|
+
recommendations.sort(key=lambda r: r.confidence, reverse=True)
|
|
279
|
+
return recommendations
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def generate_recommendations(
|
|
283
|
+
model: torch.nn.Module,
|
|
284
|
+
profile: ModelProfile,
|
|
285
|
+
hardware: HardwareProfile,
|
|
286
|
+
) -> list[Recommendation]:
|
|
287
|
+
"""Convenience function to generate recommendations.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
model: PyTorch model.
|
|
291
|
+
profile: Model profile.
|
|
292
|
+
hardware: Hardware profile.
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
List of recommendations.
|
|
296
|
+
"""
|
|
297
|
+
engine = RecommendationEngine(model=model, profile=profile, hardware=hardware)
|
|
298
|
+
return engine.generate()
|