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.
Files changed (52) hide show
  1. optimax/__init__.py +21 -0
  2. optimax/analyzer/__init__.py +24 -0
  3. optimax/analyzer/analyzer.py +255 -0
  4. optimax/analyzer/models.py +85 -0
  5. optimax/analyzer/recommendation.py +298 -0
  6. optimax/benchmark/__init__.py +11 -0
  7. optimax/benchmark/benchmark.py +151 -0
  8. optimax/benchmark/models.py +46 -0
  9. optimax/cli/__init__.py +7 -0
  10. optimax/cli/main.py +186 -0
  11. optimax/config/__init__.py +8 -0
  12. optimax/config/config.py +214 -0
  13. optimax/graph/__init__.py +9 -0
  14. optimax/graph/graph.py +94 -0
  15. optimax/hardware/__init__.py +20 -0
  16. optimax/hardware/detector.py +204 -0
  17. optimax/hardware/models.py +121 -0
  18. optimax/logging/__init__.py +8 -0
  19. optimax/logging/logger.py +79 -0
  20. optimax/memory/__init__.py +10 -0
  21. optimax/memory/tracker.py +152 -0
  22. optimax/optimizer/__init__.py +20 -0
  23. optimax/optimizer/base.py +209 -0
  24. optimax/optimizer/optimizer.py +194 -0
  25. optimax/optimizer/pass_registry.py +65 -0
  26. optimax/optimizer/passes/__init__.py +23 -0
  27. optimax/optimizer/passes/channels_last.py +132 -0
  28. optimax/optimizer/passes/flash_attention.py +153 -0
  29. optimax/optimizer/passes/gradient_checkpoint.py +125 -0
  30. optimax/optimizer/passes/mixed_precision.py +136 -0
  31. optimax/optimizer/passes/onnx_export.py +125 -0
  32. optimax/optimizer/passes/operator_fusion.py +126 -0
  33. optimax/optimizer/passes/quantization.py +142 -0
  34. optimax/optimizer/passes/tensorrt.py +150 -0
  35. optimax/optimizer/passes/torch_compile.py +124 -0
  36. optimax/optimizer/pipeline.py +662 -0
  37. optimax/optimizer/result.py +61 -0
  38. optimax/profiler/__init__.py +12 -0
  39. optimax/profiler/models.py +75 -0
  40. optimax/profiler/profiler.py +195 -0
  41. optimax/report/__init__.py +13 -0
  42. optimax/report/formatters.py +196 -0
  43. optimax/report/report.py +117 -0
  44. optimax/utils/__init__.py +12 -0
  45. optimax/utils/timing.py +55 -0
  46. optimax/utils/torch_utils.py +55 -0
  47. optimax/version.py +3 -0
  48. optimaxx-0.1.0.dist-info/METADATA +154 -0
  49. optimaxx-0.1.0.dist-info/RECORD +52 -0
  50. optimaxx-0.1.0.dist-info/WHEEL +4 -0
  51. optimaxx-0.1.0.dist-info/entry_points.txt +2 -0
  52. 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()