| @@ -0,0 +1,189 @@ | |||
| 1 | +"""Unit tests for config_optimizer.py — FASTA config pruning pipeline. | ||
| 2 | + | ||
| 3 | +Tests the filtering, deduplication, sampling, and expert preservation | ||
| 4 | +logic using static configs in the FastAConfig format (kwargs, from_expert, | ||
| 5 | +circle_num). No NPU hardware or torch.compile required. | ||
| 6 | +""" | ||
| 7 | +import importlib.util | ||
| 8 | +import os | ||
| 9 | +import sys | ||
| 10 | +import types | ||
| 11 | + | ||
| 12 | +import torch | ||
| 13 | +from torch.testing._internal.common_utils import ( | ||
| 14 | + run_tests, parametrize, instantiate_parametrized_tests, | ||
| 15 | +) | ||
| 16 | +from testutils import TestUtils | ||
| 17 | + | ||
| 18 | +# The optimizer on/off flag (config.fasta_config_optimizer) is provided by the | ||
| 19 | +# stub below, so the FASTA_CONFIG_OPTIMIZER env var is not needed here. The | ||
| 20 | +# pruning bounds (MAX_CIRCLE_NUM, MIN_SUB_NUMEL, MAX_CONFIGS) are private | ||
| 21 | +# constants in config_optimizer and cannot be set by the user. | ||
| 22 | + | ||
| 23 | +# config_optimizer imports `from .config import fasta_config_optimizer` and | ||
| 24 | +# `from .fasta_autotune import log`. Importing the real modules pulls in the | ||
| 25 | +# native torch_npu extension. Stub both so we can import config_optimizer by | ||
| 26 | +# path without that dependency. | ||
| 27 | +_cfg_stub = types.ModuleType("torch_npu._inductor.experimental.dynamic_filter.dynamic_filter_config") | ||
| 28 | +_cfg_stub.fasta_config_optimizer = True | ||
| 29 | +sys.modules["torch_npu._inductor.experimental.dynamic_filter.dynamic_filter_config"] = _cfg_stub | ||
| 30 | + | ||
| 31 | +_fa_stub = types.ModuleType("torch_npu._inductor.fasta_autotune") | ||
| 32 | +import logging | ||
| 33 | +_fa_stub.log = logging.getLogger("config_optimizer_test") | ||
| 34 | +sys.modules["torch_npu._inductor.fasta_autotune"] = _fa_stub | ||
| 35 | + | ||
| 36 | +# Provide the parent package so relative imports resolve. | ||
| 37 | +if "torch_npu._inductor" not in sys.modules: | ||
| 38 | + _pkg = types.ModuleType("torch_npu._inductor") | ||
| 39 | + _pkg.__path__ = [] | ||
| 40 | + sys.modules["torch_npu._inductor"] = _pkg | ||
| 41 | + | ||
| 42 | +_CO_PATH = os.path.join( | ||
| 43 | + os.path.dirname(__file__), "..", "..", | ||
| 44 | + "torch_npu", "_inductor", "experimental", "dynamic_filter", "config_optimizer.py", | ||
| 45 | +) | ||
| 46 | +_spec = importlib.util.spec_from_file_location( | ||
| 47 | + "torch_npu._inductor.experimental.dynamic_filter.config_optimizer", os.path.abspath(_CO_PATH)) | ||
| 48 | +co = importlib.util.module_from_spec(_spec) | ||
| 49 | +sys.modules["torch_npu._inductor.experimental.dynamic_filter.config_optimizer"] = co | ||
| 50 | +_spec.loader.exec_module(co) | ||
| 51 | + | ||
| 52 | + | ||
| 53 | +class MockConfig: | ||
| 54 | + """Static config in the FastAConfig format: kwargs, from_expert, circle_num.""" | ||
| 55 | + | ||
| 56 | + def __init__(self, kwargs, from_expert=False, circle_num=-1): | ||
| 57 | + self.kwargs = kwargs | ||
| 58 | + self.from_expert = from_expert | ||
| 59 | + self.circle_num = circle_num | ||
| 60 | + | ||
| 61 | + def __repr__(self): | ||
| 62 | + return f"MockConfig({self.kwargs}, expert={self.from_expert}, cn={self.circle_num})" | ||
| 63 | + | ||
| 64 | + | ||
| 65 | +def _make_config(block, sub, from_expert=False, circle_num=-1, axis="X0"): | ||
| 66 | + """Create a 1-axis config with given BLOCK and BLOCK_SUB values.""" | ||
| 67 | + return MockConfig( | ||
| 68 | + {f"{axis}BLOCK": block, f"{axis}BLOCK_SUB": sub}, | ||
| 69 | + from_expert=from_expert, circle_num=circle_num, | ||
| 70 | + ) | ||
| 71 | + | ||
| 72 | + | ||
| 73 | +def _make_2d_config(xblock, xsub, yblock, ysub, from_expert=False): | ||
| 74 | + """Create a 2-axis config.""" | ||
| 75 | + return MockConfig( | ||
| 76 | + {"X0BLOCK": xblock, "X0BLOCK_SUB": xsub, | ||
| 77 | + "Y0BLOCK": yblock, "Y0BLOCK_SUB": ysub}, | ||
| 78 | + from_expert=from_expert, | ||
| 79 | + ) | ||
| 80 | + | ||
| 81 | + | ||
| 82 | +class TestConfigOptimizer(TestUtils): | ||
| 83 | + | ||
| 84 | + def test_expert_configs_preserved(self): | ||
| 85 | + """Expert configs must survive all pruning unconditionally.""" | ||
| 86 | + expert = _make_config(128, 64, from_expert=True) | ||
| 87 | + fasta = _make_config(256, 128, from_expert=False) | ||
| 88 | + result = co.optimize_configs([expert, fasta]) | ||
| 89 | + experts_out = [c for c in result if c.from_expert] | ||
| 90 | + self.assertEqual(len(experts_out), 1) | ||
| 91 | + self.assertIs(experts_out[0], expert) | ||
| 92 | + | ||
| 93 | + def test_expert_with_bad_circle_num_still_preserved(self): | ||
| 94 | + """Expert configs bypass the circle_num filter.""" | ||
| 95 | + expert = _make_config(128, 8, from_expert=True, circle_num=10) | ||
| 96 | + result = co.optimize_configs([expert]) | ||
| 97 | + self.assertEqual(len(result), 1) | ||
| 98 | + self.assertIs(result[0], expert) | ||
| 99 | + | ||
| 100 | + | ||
| 101 | + def test_circle_num_filter(self, circle_num, kept): | ||
| 102 | + """Native configs are kept iff circle_num <= MAX_CIRCLE_NUM (4).""" | ||
| 103 | + cfg = _make_config(128, 64, circle_num=circle_num) | ||
| 104 | + # Anchor expert config so the result is never empty (avoids the safety | ||
| 105 | + # net that returns the original list when all configs are filtered out). | ||
| 106 | + anchor = _make_config(256, 128, from_expert=True) | ||
| 107 | + result = co.optimize_configs([cfg, anchor]) | ||
| 108 | + fasta_out = [c for c in result if not c.from_expert] | ||
| 109 | + self.assertEqual(cfg in fasta_out, kept) | ||
| 110 | + | ||
| 111 | + def test_circle_num_computed_from_kwargs(self): | ||
| 112 | + """When circle_num=-1, it is computed from BLOCK/BLOCK_SUB.""" | ||
| 113 | + good = _make_config(256, 64) # ceil(256/64) = 4 <= 4 | ||
| 114 | + bad = _make_config(256, 16) # ceil(256/16) = 16 > 4 | ||
| 115 | + result = co.optimize_configs([good, bad]) | ||
| 116 | + fasta_out = [c for c in result if not c.from_expert] | ||
| 117 | + self.assertIn(good, fasta_out) | ||
| 118 | + self.assertNotIn(bad, fasta_out) | ||
| 119 | + | ||
| 120 | + def test_min_sub_numel_filter(self): | ||
| 121 | + """Native configs with sub_numel < MIN_SUB_NUMEL (32) are removed.""" | ||
| 122 | + good = _make_config(128, 64) # sub_numel=64 >= 32 | ||
| 123 | + bad = _make_config(64, 24) # cn=ceil(64/24)=3, sub_numel=24 < 32 | ||
| 124 | + result = co.optimize_configs([good, bad]) | ||
| 125 | + fasta_out = [c for c in result if not c.from_expert] | ||
| 126 | + self.assertIn(good, fasta_out) | ||
| 127 | + self.assertNotIn(bad, fasta_out) | ||
| 128 | + | ||
| 129 | + def test_min_sub_numel_2d(self): | ||
| 130 | + """For 2D configs, sub_numel is the product of both BLOCK_SUB values.""" | ||
| 131 | + good = _make_2d_config(64, 32, 64, 32) # sub=1024, cn=4 | ||
| 132 | + bad = _make_2d_config(16, 4, 16, 4) # sub=16, cn=16 | ||
| 133 | + result = co.optimize_configs([good, bad]) | ||
| 134 | + fasta_out = [c for c in result if not c.from_expert] | ||
| 135 | + self.assertIn(good, fasta_out) | ||
| 136 | + self.assertNotIn(bad, fasta_out) | ||
| 137 | + | ||
| 138 | + def test_dedup(self): | ||
| 139 | + """Duplicate configs (same circle_num + sub_block pattern) are removed.""" | ||
| 140 | + cfg1 = _make_config(128, 64) | ||
| 141 | + cfg2 = _make_config(128, 64) | ||
| 142 | + result = co.optimize_configs([cfg1, cfg2]) | ||
| 143 | + fasta_out = [c for c in result if not c.from_expert] | ||
| 144 | + self.assertEqual(len(fasta_out), 1) | ||
| 145 | + | ||
| 146 | + def test_sample_diverse_caps(self): | ||
| 147 | + """When more than MAX_CONFIGS native configs, sampling caps the count.""" | ||
| 148 | + configs = [_make_config(sub, sub) for sub in range(32, 132)] | ||
| 149 | + result = co.optimize_configs(configs) | ||
| 150 | + fasta_out = [c for c in result if not c.from_expert] | ||
| 151 | + self.assertLessEqual(len(fasta_out), 50) | ||
| 152 | + self.assertGreater(len(fasta_out), 0) | ||
| 153 | + | ||
| 154 | + def test_sample_preserves_diversity(self): | ||
| 155 | + """Sampling picks from across the sub_numel range, not just one end.""" | ||
| 156 | + configs = [_make_config(sub, sub) for sub in range(32, 1032, 10)] | ||
| 157 | + result = co.optimize_configs(configs) | ||
| 158 | + fasta_out = [c for c in result if not c.from_expert] | ||
| 159 | + sub_numels = sorted(co._get_sub_numel(c) for c in fasta_out) | ||
| 160 | + self.assertLessEqual(sub_numels[0], 42) | ||
| 161 | + self.assertGreaterEqual(sub_numels[-1], 1012) | ||
| 162 | + | ||
| 163 | + def test_empty_returns_empty(self): | ||
| 164 | + """Empty input returns empty output.""" | ||
| 165 | + self.assertEqual(co.optimize_configs([]), []) | ||
| 166 | + | ||
| 167 | + def test_optimizer_disabled(self): | ||
| 168 | + """When the optimizer flag is off, returns the original list unchanged.""" | ||
| 169 | + original = co.fasta_config_optimizer | ||
| 170 | + try: | ||
| 171 | + co.fasta_config_optimizer = False | ||
| 172 | + configs = [_make_config(128, 64), _make_config(256, 16)] | ||
| 173 | + result = co.optimize_configs(configs) | ||
| 174 | + self.assertEqual(len(result), len(configs)) | ||
| 175 | + self.assertIs(result[0], configs[0]) | ||
| 176 | + finally: | ||
| 177 | + co.fasta_config_optimizer = original | ||
| 178 | + | ||
| 179 | + def test_all_filtered_returns_original(self): | ||
| 180 | + """If all configs would be filtered, return original as a safety net.""" | ||
| 181 | + configs = [_make_config(256, 8) for _ in range(5)] | ||
| 182 | + result = co.optimize_configs(configs) | ||
| 183 | + self.assertEqual(len(result), 5) | ||
| 184 | + | ||
| 185 | + | ||
| 186 | +instantiate_parametrized_tests(TestConfigOptimizer) | ||
| 187 | + | ||
| 188 | +if __name__ == "__main__": | ||
| 189 | + run_tests() | ||
| @@ -0,0 +1,336 @@ | |||||||||
| 1 | +import importlib.util | ||||||||
| 2 | +import os | ||||||||
| 3 | +import sys | ||||||||
| 4 | +import types | ||||||||
| 5 | + | ||||||||
| 6 | +import numpy as np | ||||||||
| 7 | +from torch.testing._internal.common_utils import ( | ||||||||
| 8 | + run_tests, parametrize, instantiate_parametrized_tests, | ||||||||
| 9 | +) | ||||||||
| 10 | +from testutils import TestUtils | ||||||||
| 11 | + | ||||||||
| 12 | +# dynamic_filter_algo imports `from .dynamic_filter_config import fasta_dynamic_filter as df_cfg` | ||||||||
| 13 | +# which may not be available when the native torch_npu extension isn't loaded. | ||||||||
| 14 | +# Stub the config module so we can import dynamic_filter_algo by path without | ||||||||
| 15 | +# that dependency. | ||||||||
| 16 | + | ||||||||
| 17 | +# Create a stub for fasta_dynamic_filter with the same attributes as the dataclass | ||||||||
| 18 | +class _FastaDynamicFilterStub: | ||||||||
| 19 | + r1_pct = 0.3 | ||||||||
| 20 | + base_budget = 0.35 | ||||||||
| 21 | + high_budget = 0.4 | ||||||||
| 22 | + low_budget = 0.25 | ||||||||
| 23 | + max_rounds = 2 | ||||||||
| 24 | + | ||||||||
| 25 | +_cfg_stub = types.ModuleType("torch_npu._inductor.experimental.dynamic_filter.dynamic_filter_config") | ||||||||
| 26 | +import logging | ||||||||
| 27 | +_cfg_stub.log = logging.getLogger("dynamic_filter_test") | ||||||||
| 28 | +_cfg_stub.fasta_dynamic_filter = _FastaDynamicFilterStub() | ||||||||
| 29 | +sys.modules["torch_npu._inductor.experimental.dynamic_filter.dynamic_filter_config"] = _cfg_stub | ||||||||
| 30 | + | ||||||||
| 31 | +cfg_mod = types.ModuleType("torch_npu._inductor.config") | ||||||||
| 32 | +cfg_mod.log = logging.getLogger("dynamic_filter_test") | ||||||||
| 33 | +sys.modules["torch_npu._inductor.config"] = cfg_mod | ||||||||
| 34 | + | ||||||||
| 35 | +# Provide the parent package so relative imports resolve. | ||||||||
| 36 | +for name in [ | ||||||||
| 37 | + "torch_npu._inductor", | ||||||||
| 38 | + "torch_npu._inductor.experimental", | ||||||||
| 39 | + "torch_npu._inductor.experimental.dynamic_filter", | ||||||||
| 40 | +]: | ||||||||
| 41 | + if name not in sys.modules: | ||||||||
| 42 | + pkg = types.ModuleType(name) | ||||||||
| 43 | + pkg.__path__ = [] | ||||||||
| 44 | + sys.modules[name] = pkg | ||||||||
| 45 | + | ||||||||
| 46 | +# Load dynamic_filter_algo by path using importlib.util | ||||||||
| 47 | +_DFA_PATH = os.path.join( | ||||||||
| 48 | + os.path.dirname(__file__), "..", "..", | ||||||||
| 49 | + "torch_npu", "_inductor", "experimental", | ||||||||
| 50 | + "dynamic_filter", "dynamic_filter_algo.py", | ||||||||
| 51 | +) | ||||||||
| 52 | +_spec = importlib.util.spec_from_file_location( | ||||||||
| 53 | + "torch_npu._inductor.experimental.dynamic_filter.dynamic_filter_algo", | ||||||||
| 54 | + os.path.abspath(_DFA_PATH) | ||||||||
| 55 | +) | ||||||||
| 56 | + | ||||||||
| 57 | +algo = importlib.util.module_from_spec(_spec) | ||||||||
| 58 | +sys.modules["torch_npu._inductor.experimental.dynamic_filter.dynamic_filter_algo"] = algo | ||||||||
| 59 | +_spec.loader.exec_module(algo) | ||||||||
| 60 | + | ||||||||
| 61 | +# Load dynamic_filter_if by path using importlib.util | ||||||||
| 62 | +_DFI_PATH = os.path.join( | ||||||||
| 63 | + os.path.dirname(__file__), "..", "..", | ||||||||
| 64 | + "torch_npu", "_inductor", "experimental", | ||||||||
| 65 | + "dynamic_filter", "dynamic_filter_if.py", | ||||||||
| 66 | +) | ||||||||
| 67 | +_spec = importlib.util.spec_from_file_location( | ||||||||
| 68 | + "torch_npu._inductor.experimental.dynamic_filter.dynamic_filter_if", | ||||||||
| 69 | + os.path.abspath(_DFI_PATH) | ||||||||
| 70 | +) | ||||||||
| 71 | + | ||||||||
| 72 | +_dfi = importlib.util.module_from_spec(_spec) | ||||||||
| 73 | +sys.modules["torch_npu._inductor.experimental.dynamic_filter.dynamic_filter_if"] = _dfi | ||||||||
| 74 | +_spec.loader.exec_module(_dfi) | ||||||||
| 75 | + | ||||||||
| 76 | +DynamicFilter = _dfi.DynamicFilter | ||||||||
| 77 | +FastaCheckError = _dfi.FastaCheckError | ||||||||
| 78 | + | ||||||||
| 79 | + | ||||||||
| 80 | +class _FakeConfig: | ||||||||
| 81 | + def __init__(self, kwargs): | ||||||||
| 82 | + self.kwargs = dict(kwargs) | ||||||||
| 83 | + | ||||||||
| 84 | + | ||||||||
| 85 | +def _unique_grid(n, rng, d): | ||||||||
| 86 | + side = int(np.ceil(n ** (1.0 / d))) + 2 | ||||||||
| 87 | + seen = {} | ||||||||
| 88 | + while len(seen) < n: | ||||||||
| 89 | + seen[tuple(int(rng.randint(1, side + 1)) for _ in range(d))] = True | ||||||||
| 90 | + return list(seen.keys())[:n], side | ||||||||
| 91 | + | ||||||||
| 92 | + | ||||||||
| 93 | +def _make_pool(n, rng, d=2, difficulty="easy"): | ||||||||
| 94 | + grid, side = _unique_grid(n, rng, d) | ||||||||
| 95 | + center = np.array([(side + 1) / 2.0] * d) | ||||||||
| 96 | + | ||||||||
| 97 | + if difficulty == "easy": | ||||||||
| 98 | + amp, noise_sd = 0.20, 0.01 | ||||||||
| 99 | + elif difficulty == "medium": | ||||||||
| 100 | + amp, noise_sd = 0.08, 0.05 | ||||||||
| 101 | + elif difficulty == "worst": | ||||||||
| 102 | + amp, noise_sd = 0.015, 0.12 | ||||||||
| 103 | + else: | ||||||||
| 104 | + raise ValueError(f"unknown difficulty {difficulty!r}") | ||||||||
| 105 | + | ||||||||
| 106 | + needle = grid[rng.randint(0, len(grid))] # only used in worst | ||||||||
| 107 | + norm = d * (side ** 2) / 4.0 # keep bowl term O(amp) regardless of grid size | ||||||||
| 108 | + configs, durs = [], [] | ||||||||
| 109 | + for gp in grid: | ||||||||
| 110 | + arr = np.array(gp, dtype=float) | ||||||||
| 111 | + if difficulty == "worst": | ||||||||
| 112 | + base = 1.0 + amp * float(rng.rand()) # flat plateau with small jitter | ||||||||
| 113 | + if gp == needle: | ||||||||
| 114 | + base = 0.90 # the single real winner (~10% better) | ||||||||
| 115 | + else: | ||||||||
| 116 | + base = 1.0 + amp * float(np.sum((arr - center) ** 2)) / norm | ||||||||
| 117 | + dur = base * (1.0 + noise_sd * float(rng.randn())) | ||||||||
| 118 | + kw = {f"BLOCK_{i}": gp[i] for i in range(d)} | ||||||||
| 119 | + kw["X1BLOCK_SUB"] = 512 # constant -> must be dropped as feature | ||||||||
| 120 | + kw["multibuffer"] = bool(rng.randint(0, 2)) # skip-listed -> dropped | ||||||||
| 121 | + configs.append(_FakeConfig(kw)) | ||||||||
| 122 | + durs.append(max(dur, 1e-3)) | ||||||||
| 123 | + return configs, np.array(durs) | ||||||||
| 124 | + | ||||||||
| 125 | + | ||||||||
| 126 | +def _drive(flt, durs_by_idx): | ||||||||
| 127 | + id_to_idx = {id(c): i for i, c in enumerate(flt._all_items)} | ||||||||
| 128 | + measured_order = [] | ||||||||
| 129 | + | ||||||||
| 130 | + def bench(batch): | ||||||||
| 131 | + out = [] | ||||||||
| 132 | + for c in batch: | ||||||||
| 133 | + i = id_to_idx[id(c)] | ||||||||
| 134 | + measured_order.append(i) | ||||||||
| 135 | + out.append(float(durs_by_idx[i])) | ||||||||
| 136 | + return out | ||||||||
| 137 | + | ||||||||
| 138 | + batch = flt.r1_configs | ||||||||
| 139 | + durations = bench(batch) | ||||||||
| 140 | + batch = flt.refine(durations) | ||||||||
| 141 | + while batch: | ||||||||
| 142 | + durations = bench(batch) | ||||||||
| 143 | + batch = flt.refine(durations) | ||||||||
| 144 | + return measured_order | ||||||||
| 145 | + | ||||||||
| 146 | + | ||||||||
| 147 | +def _regret_pct(chosen, true_best): | ||||||||
| 148 | + return 100.0 * (chosen - true_best) / chosen | ||||||||
🔵 Low Priority 变更行: test_dynamic_filter.py 第 147-148 行, 函数实现为 影响:使用 建议:将分母从 改动建议
![]() ![]() | |||||||||
| 149 | + | ||||||||
| 150 | + | ||||||||
| 151 | +def _percentile_in_pool(value, durs): | ||||||||
| 152 | + vals = np.sort(durs) | ||||||||
| 153 | + return 100.0 * np.searchsorted(vals, value, side="left") / len(vals) | ||||||||
| 154 | + | ||||||||
| 155 | + | ||||||||
| 156 | +class TestDynamicFilter(TestUtils): | ||||||||
| 157 | + def setUp(self): | ||||||||
| 158 | + self.original_fastautotune = os.environ.get("FASTAUTOTUNE") | ||||||||
| 159 | + os.environ["FASTAUTOTUNE"] = "1" | ||||||||
| 160 | + | ||||||||
| 161 | + def tearDown(self): | ||||||||
| 162 | + if self.original_fastautotune is not None: | ||||||||
| 163 | + os.environ["FASTAUTOTUNE"] = self.original_fastautotune | ||||||||
| 164 | + else: | ||||||||
| 165 | + os.environ.pop("FASTAUTOTUNE", None) | ||||||||
| 166 | + | ||||||||
| 167 | + # ---- algo: feature extraction (D=2 and D=3) --------------------- | ||||||||
| 168 | + | ||||||||
| 169 | + def test_extract_features_drops_constant_and_skip_keys(self, d): | ||||||||
| 170 | + rng = np.random.RandomState(0) | ||||||||
| 171 | + configs, _ = _make_pool(60, rng, d=d) | ||||||||
| 172 | + X, names = algo.extract_features(configs) | ||||||||
| 173 | + self.assertEqual(X.shape[0], len(configs)) | ||||||||
| 174 | + self.assertEqual(sorted(names), [f"BLOCK_{i}" for i in range(d)]) | ||||||||
| 175 | + self.assertEqual(X.shape[1], d) | ||||||||
| 176 | + self.assertNotIn("X1BLOCK_SUB", names) | ||||||||
| 177 | + self.assertNotIn("multibuffer", names) | ||||||||
| 178 | + | ||||||||
| 179 | + def test_extract_features_no_numeric_keys_gives_zero_D(self): | ||||||||
| 180 | + configs = [_FakeConfig({"multibuffer": True, "compile_mode": "x"}) | ||||||||
| 181 | + for _ in range(8)] | ||||||||
| 182 | + X, names = algo.extract_features(configs) | ||||||||
| 183 | + self.assertEqual(names, []) | ||||||||
| 184 | + self.assertEqual(X.shape[1], 0) | ||||||||
| 185 | + | ||||||||
| 186 | + # ---- algo: viability -------------------------------------------- | ||||||||
| 187 | + | ||||||||
| 188 | + (0, 50, "coverage"), | ||||||||
| 189 | + (2, 4, "coverage"), | ||||||||
| 190 | + (2, 60, "model"), | ||||||||
| 191 | + (3, 60, "model"), | ||||||||
| 192 | + ]) | ||||||||
| 193 | + def test_assess_viability(self, D, n, expect_path): | ||||||||
| 194 | + path, mode = algo.assess_viability(n, D) | ||||||||
| 195 | + self.assertEqual(path, expect_path) | ||||||||
| 196 | + self.assertIn(mode, ("coverage", "linear", "quad", "full")) | ||||||||
| 197 | + | ||||||||
| 198 | + # ---- algo: surrogate predicts on a learnable pool (D=2 and D=3) - | ||||||||
| 199 | + | ||||||||
| 200 | + def test_select_mode_and_predict_fits_quadratic_bowl(self, d): | ||||||||
| 201 | + rng = np.random.RandomState(1) | ||||||||
| 202 | + configs, durs = _make_pool(90, rng, d=d) | ||||||||
| 203 | + X, _ = algo.extract_features(configs) | ||||||||
| 204 | + D = X.shape[1] | ||||||||
| 205 | + self.assertEqual(D, d) | ||||||||
| 206 | + xmin = X.min(axis=0); xr = X.max(axis=0) - xmin; xr[xr == 0] = 1.0 | ||||||||
| 207 | + Xn = (X - xmin) / xr | ||||||||
| 208 | + m = sorted(rng.choice(len(configs), size=45, replace=False).tolist()) | ||||||||
| 209 | + u = [i for i in range(len(configs)) if i not in m] | ||||||||
| 210 | + d_hat, r_sq, mode = algo.select_mode_and_predict(Xn[m], durs[m], Xn[u], D) | ||||||||
| 211 | + self.assertEqual(d_hat.shape, (len(u),)) | ||||||||
| 212 | + self.assertFalse(np.any(np.isnan(d_hat))) | ||||||||
| 213 | + self.assertGreaterEqual(r_sq, 0.0) | ||||||||
| 214 | + self.assertLessEqual(r_sq, 1.0) | ||||||||
| 215 | + rho = algo.numpy_spearman(durs[u], d_hat) | ||||||||
| 216 | + self.assertGreater(rho, 0.5) | ||||||||
| 217 | + | ||||||||
| 218 | + # ---- if: budget routing by N-class ------------------------------ | ||||||||
| 219 | + | ||||||||
| 220 | + (60, 2, "high"), | ||||||||
| 221 | + (250, 2, "med"), | ||||||||
| 222 | + (500, 2, "med"), | ||||||||
| 223 | + (200, 4, "low"), | ||||||||
| 224 | + ]) | ||||||||
| 225 | + def test_budget_routing_by_pool_size(self, n, d, expect_class): | ||||||||
| 226 | + rng = np.random.RandomState(2) | ||||||||
| 227 | + configs, _ = _make_pool(n, rng, d=d) | ||||||||
| 228 | + flt = DynamicFilter(configs, kernel_name=f"k_{n}") | ||||||||
| 229 | + self.assertEqual(flt._budget_class, expect_class) | ||||||||
| 230 | + self.assertGreaterEqual(flt._total_budget, flt._r1_size) | ||||||||
| 231 | + self.assertLessEqual(flt._total_budget, flt._N) | ||||||||
| 232 | + | ||||||||
| 233 | + # ---- if: full loop, invariants + budget cap (regimes x D) ------- | ||||||||
| 234 | + | ||||||||
| 235 | + | ||||||||
| 236 | + | ||||||||
| 237 | + def test_full_loop_invariants_and_budget(self, n, difficulty, d): | ||||||||
| 238 | + rng = np.random.RandomState(3) | ||||||||
| 239 | + configs, durs = _make_pool(n, rng, d=d, difficulty=difficulty) | ||||||||
| 240 | + durs_by_idx = {i: durs[i] for i in range(len(configs))} | ||||||||
| 241 | + flt = DynamicFilter(configs, kernel_name=f"loop_{difficulty}_{n}_d{d}") | ||||||||
| 242 | + planned = flt._total_budget | ||||||||
| 243 | + measured = _drive(flt, durs_by_idx) | ||||||||
| 244 | + | ||||||||
| 245 | + self.assertEqual(len(measured), len(set(measured)), | ||||||||
| 246 | + "a config was benchmarked more than once") | ||||||||
| 247 | + self.assertLessEqual(flt._used, planned + 1) | ||||||||
| 248 | + self.assertEqual(len(flt._r1_indices), len(set(flt._r1_indices))) | ||||||||
| 249 | + st = flt.stats | ||||||||
| 250 | + self.assertEqual(st["N"], n) | ||||||||
| 251 | + self.assertEqual(st["evals"], flt._used) | ||||||||
| 252 | + self.assertGreater(st["savings_pct"], 0.0) | ||||||||
| 253 | + self.assertTrue(np.isfinite(st["best_dur"])) | ||||||||
| 254 | + self.assertIn(st["confidence"], ("low", "med", "high")) | ||||||||
| 255 | + | ||||||||
| 256 | + # ---- if: selection quality by regime (D=2 and D=3) -------------- | ||||||||
| 257 | + # thresholds grounded in a 20-seed sweep of the real DynamicFilter: | ||||||||
| 258 | + # easy regret max ~1.9% medium median ~0.15% (p90 ~2.7%) | ||||||||
| 259 | + # worst regret noisy (mean ~10%) but placement always top ~3 percentile | ||||||||
| 260 | + | ||||||||
| 261 | + | ||||||||
| 262 | + ("easy", 3.5), # smooth bowl: tight bound on a single seed | ||||||||
| 263 | + ("medium", 6.0), # moderate noise: still within a few percent | ||||||||
| 264 | + ]) | ||||||||
| 265 | + def test_selection_quality_easy_medium(self, difficulty, regret_bound, d): | ||||||||
| 266 | + rng = np.random.RandomState(4) | ||||||||
| 267 | + n = 150 | ||||||||
| 268 | + configs, durs = _make_pool(n, rng, d=d, difficulty=difficulty) | ||||||||
| 269 | + durs_by_idx = {i: durs[i] for i in range(len(configs))} | ||||||||
| 270 | + flt = DynamicFilter(configs, kernel_name=f"q_{difficulty}_d{d}") | ||||||||
| 271 | + _drive(flt, durs_by_idx) | ||||||||
| 272 | + regret = _regret_pct(flt._best_dur, float(np.min(durs))) | ||||||||
| 273 | + self.assertLess( | ||||||||
| 274 | + regret, regret_bound, | ||||||||
| 275 | + f"{difficulty} d={d}: regret {regret:.1f}% exceeded {regret_bound}% " | ||||||||
| 276 | + f"(chose {flt._best_dur:.3f} vs best {float(np.min(durs)):.3f})") | ||||||||
| 277 | + | ||||||||
| 278 | + | ||||||||
| 279 | + def test_selection_quality_worst_case_placement(self, d): | ||||||||
| 280 | + n = 150 | ||||||||
| 281 | + pcts = [] | ||||||||
| 282 | + for seed in range(5): | ||||||||
| 283 | + rng = np.random.RandomState(40 + seed) | ||||||||
| 284 | + configs, durs = _make_pool(n, rng, d=d, difficulty="worst") | ||||||||
| 285 | + durs_by_idx = {i: durs[i] for i in range(len(configs))} | ||||||||
| 286 | + flt = DynamicFilter(configs, kernel_name=f"worst_{seed}_d{d}") | ||||||||
| 287 | + _drive(flt, durs_by_idx) | ||||||||
| 288 | + self.assertTrue(np.isfinite(flt._best_dur)) | ||||||||
| 289 | + pcts.append(_percentile_in_pool(flt._best_dur, durs)) | ||||||||
| 290 | + self.assertLessEqual(np.median(pcts), 25.0, | ||||||||
| 291 | + f"worst-case d={d} median placement " | ||||||||
| 292 | + f"{np.median(pcts):.1f} pctile worse than top-25%") | ||||||||
| 293 | + self.assertLessEqual(max(pcts), 50.0, | ||||||||
| 294 | + f"worst-case d={d} picked below pool median " | ||||||||
| 295 | + f"(pctiles={pcts})") | ||||||||
| 296 | + | ||||||||
| 297 | + # ---- if: input-contract guards ---------------------------------- | ||||||||
| 298 | + def test_empty_configs_raises(self): | ||||||||
| 299 | + with self.assertRaises(FastaCheckError): | ||||||||
| 300 | + DynamicFilter([], kernel_name="empty") | ||||||||
| 301 | + | ||||||||
| 302 | + def test_refine_wrong_length_raises(self): | ||||||||
| 303 | + rng = np.random.RandomState(5) | ||||||||
| 304 | + configs, _ = _make_pool(60, rng, d=2) | ||||||||
| 305 | + flt = DynamicFilter(configs, kernel_name="badlen") | ||||||||
| 306 | + _ = flt.r1_configs | ||||||||
| 307 | + with self.assertRaises(FastaCheckError): | ||||||||
| 308 | + flt.refine([1.0, 2.0]) | ||||||||
| 309 | + | ||||||||
| 310 | + def test_refine_rejects_negative_and_nan(self): | ||||||||
| 311 | + rng = np.random.RandomState(6) | ||||||||
| 312 | + configs, _ = _make_pool(60, rng, d=2) | ||||||||
| 313 | + flt = DynamicFilter(configs, kernel_name="badval") | ||||||||
| 314 | + batch = flt.r1_configs | ||||||||
| 315 | + bad = [1.0] * len(batch) | ||||||||
| 316 | + bad[0] = -1.0 | ||||||||
| 317 | + with self.assertRaises(FastaCheckError): | ||||||||
| 318 | + flt.refine(bad) | ||||||||
🔵 Low Priority 变更行: test_dynamic_filter.py 第 310-318 行, 测试名称声明同时验证"negative"和"nan"两种非法输入被拒绝,但测试体(第 315-318 行)仅构造了包含 影响:NaN 输入的防御代码缺少测试覆盖,如果将来有人修改了 NaN 检查逻辑,该回归不会被捕获。测试名与实现不匹配,也造成代码审查时的困惑。 建议:添加 NaN 测试用例,或重命名测试以准确反映所覆盖的行为。 ![]() ![]() | |||||||||
| 319 | + | ||||||||
| 320 | + # ---- if: D==0 coverage path ------------------------------------- | ||||||||
| 321 | + def test_zero_feature_pool_uses_coverage(self): | ||||||||
| 322 | + configs = [_FakeConfig({"BLOCK_0": 8, "multibuffer": (i % 2 == 0)}) | ||||||||
| 323 | + for i in range(40)] | ||||||||
| 324 | + durs_by_idx = {i: 1.0 + 0.01 * i for i in range(len(configs))} | ||||||||
| 325 | + flt = DynamicFilter(configs, kernel_name="cover") | ||||||||
| 326 | + self.assertEqual(flt._D, 0) | ||||||||
| 327 | + self.assertEqual(flt._path, "coverage") | ||||||||
| 328 | + measured = _drive(flt, durs_by_idx) | ||||||||
| 329 | + self.assertEqual(len(measured), len(set(measured))) | ||||||||
| 330 | + self.assertTrue(np.isfinite(flt._best_dur)) | ||||||||
| 331 | + | ||||||||
| 332 | + | ||||||||
| 333 | +instantiate_parametrized_tests(TestDynamicFilter) | ||||||||
| 334 | + | ||||||||
| 335 | +if __name__ == "__main__": | ||||||||
| 336 | + run_tests() | ||||||||
| @@ -672,6 +672,7 @@ class TestPublicBindings(TestCase): | |||
| 672 | "torch_npu._inductor.dvm.graph_fusion", | 672 | "torch_npu._inductor.dvm.graph_fusion", |
| 673 | "torch_npu._inductor.dvm.mlir_fusion", | 673 | "torch_npu._inductor.dvm.mlir_fusion", |
| 674 | "torch_npu._inductor.dvm.op_emitter", | 674 | "torch_npu._inductor.dvm.op_emitter", |
| 675 | + "torch_npu._inductor.dvm.util", | ||
| 675 | # MFusion FX<->Torch-MLIR: optional torch_mlir + AKG mfusion; gate CI omits them. | 676 | # MFusion FX<->Torch-MLIR: optional torch_mlir + AKG mfusion; gate CI omits them. |
| 676 | "torch_npu._inductor.mfusion", | 677 | "torch_npu._inductor.mfusion", |
| 677 | "torch_npu._inductor.mfusion._mfusion_log", | 678 | "torch_npu._inductor.mfusion._mfusion_log", |
| @@ -707,6 +708,13 @@ class TestPublicBindings(TestCase): | |||
| 707 | "torch_npu._inductor.fx_passes.parallelism_strategy_default", | 708 | "torch_npu._inductor.fx_passes.parallelism_strategy_default", |
| 708 | "torch_npu._inductor.fx_passes.parallelism_strategy_framework", | 709 | "torch_npu._inductor.fx_passes.parallelism_strategy_framework", |
| 709 | "torch_npu._inductor.fx_passes.utils.schedule_node_utils", | 710 | "torch_npu._inductor.fx_passes.utils.schedule_node_utils", |
| 711 | + "torch_npu._inductor.experimental", | ||
| 712 | + "torch_npu._inductor.experimental.dynamic_filter.autotune_stats", | ||
| 713 | + "torch_npu._inductor.experimental.dynamic_filter.config_optimizer", | ||
| 714 | + "torch_npu._inductor.experimental.dynamic_filter.dynamic_filter_algo", | ||
| 715 | + "torch_npu._inductor.experimental.dynamic_filter.dynamic_filter_if", | ||
| 716 | + "torch_npu._inductor.experimental.dynamic_filter.dynamic_filter_scheduler", | ||
| 717 | + "torch_npu._inductor.experimental.dynamic_filter.dynamic_filter_config", | ||
| 710 | } | 718 | } |
🔵 Low Priority
无需要报告的文档安全问题——三份文档( 建议:无需修改。新模块的排除列表正确,文档无安全问题。此为补充说明。 ![]() ![]() | |||
| 711 | 719 | ||
| 712 | # No new entries should be added to this list. | 720 | # No new entries should be added to this list. |
| @@ -1,8 +1,7 @@ | |||
| 1 | import copy | 1 | import copy |
| 2 | import functools | 2 | import functools |
| 3 | -import math | ||
| 4 | -import os | ||
| 5 | import sys | 3 | import sys |
| 4 | + | ||
| 6 | from torch._inductor.runtime.runtime_utils import next_power_of_2 | 5 | from torch._inductor.runtime.runtime_utils import next_power_of_2 |
| 7 | from torch._inductor.runtime.triton_heuristics import Config | 6 | from torch._inductor.runtime.triton_heuristics import Config |
| 8 | 7 | ||
| @@ -65,7 +64,7 @@ class TileGenerator: | |||
| 65 | continue | 64 | continue |
| 66 | if self.axis_name[tiling_axis][0] == "r" and self.persistent_reduction: | 65 | if self.axis_name[tiling_axis][0] == "r" and self.persistent_reduction: |
| 67 | continue | 66 | continue |
| 68 | - self.real_tiling_axis.append(tiling_axis) | 67 | + self.real_tiling_axis.append(tiling_axis) |
| 69 | self.split_axis_num = len(self.split_axis) | 68 | self.split_axis_num = len(self.split_axis) |
| 70 | 69 | ||
| 71 | def reset_configs(self): | 70 | def reset_configs(self): |
| @@ -83,7 +82,7 @@ class TileGenerator: | |||
| 83 | continue | 82 | continue |
| 84 | if self.axis_name[tiling_axis][0] == "r" and self.persistent_reduction: | 83 | if self.axis_name[tiling_axis][0] == "r" and self.persistent_reduction: |
| 85 | continue | 84 | continue |
| 86 | - self.real_tiling_axis.append(tiling_axis) | 85 | + self.real_tiling_axis.append(tiling_axis) |
| 87 | self.split_axis_num = len(self.split_axis) | 86 | self.split_axis_num = len(self.split_axis) |
| 88 | 87 | ||
| 89 | def calcu_last_split_blocks(self, axis): | 88 | def calcu_last_split_blocks(self, axis): |
| @@ -98,9 +97,6 @@ class TileGenerator: | |||
| 98 | last_blocks = (self.numels[axis] + last_splits - 1) // last_splits | 97 | last_blocks = (self.numels[axis] + last_splits - 1) // last_splits |
| 99 | return last_blocks | 98 | return last_blocks |
| 100 | 99 | ||
| 101 | - | ||
| 102 | - | ||
| 103 | - | ||
| 104 | def valid_tile_numel(self, total_numel): | 100 | def valid_tile_numel(self, total_numel): |
| 105 | max_numel = self.max_numel_threshold | 101 | max_numel = self.max_numel_threshold |
| 106 | return total_numel <= max_numel | 102 | return total_numel <= max_numel |
| @@ -327,7 +323,6 @@ class TileGenerator: | |||
| 327 | 323 | ||
| 328 | return reached_stop_numel | 324 | return reached_stop_numel |
| 329 | 325 | ||
| 330 | - | ||
| 331 | def descend_all_low_dims(self): | 326 | def descend_all_low_dims(self): |
| 332 | low_dim_numels = [self.sub_blocks[x] for x in self.low_dims] | 327 | low_dim_numels = [self.sub_blocks[x] for x in self.low_dims] |
| 333 | if not low_dim_numels: | 328 | if not low_dim_numels: |
| @@ -393,7 +388,7 @@ class TileGenerator: | |||
| 393 | self.tune_multibuffer() | 388 | self.tune_multibuffer() |
| 394 | if self.npu_kernel_type == NPUKernelType.SIMT_ONLY: | 389 | if self.npu_kernel_type == NPUKernelType.SIMT_ONLY: |
| 395 | self.tune_simt_num_warps() | 390 | self.tune_simt_num_warps() |
| 396 | - | 391 | + |
| 397 | def set_kernel_type(self, npu_kernel_type): | 392 | def set_kernel_type(self, npu_kernel_type): |
| 398 | self.npu_kernel_type = npu_kernel_type | 393 | self.npu_kernel_type = npu_kernel_type |
| 399 | 394 | ||
| @@ -402,7 +397,6 @@ class TileGenerator: | |||
| 402 | tiling_not_low_dims = [x for x in self.tiling_axis if x not in self.low_dims] | 397 | tiling_not_low_dims = [x for x in self.tiling_axis if x not in self.low_dims] |
| 403 | 398 | ||
| 404 | def descend_split_axis(): | 399 | def descend_split_axis(): |
| 405 | - | ||
| 406 | for axis_index in range(len(self.split_axis)): | 400 | for axis_index in range(len(self.split_axis)): |
| 407 | if self.descend_split_axis_inner(axis_index): | 401 | if self.descend_split_axis_inner(axis_index): |
| 408 | return True | 402 | return True |
| @@ -437,7 +431,7 @@ class TileGenerator: | |||
| 437 | self.add_extra_options() | 431 | self.add_extra_options() |
| 438 | # do config filter for simt kernel, if there're configs with 0 remain programs others can be dropped because: | 432 | # do config filter for simt kernel, if there're configs with 0 remain programs others can be dropped because: |
| 439 | # 1. for simt kernel, numels do not need to too detailed tiling | 433 | # 1. for simt kernel, numels do not need to too detailed tiling |
| 440 | - # 2. for kernel, loop with no mask which means numel can be divisible by tiling blocksub is preferrable | 434 | + # 2. for kernel, loop with no mask which means numel can be divisible by tiling blocksub is preferable |
| 441 | if self.npu_kernel_type == NPUKernelType.SIMT_ONLY and len(self.configs) > 0: | 435 | if self.npu_kernel_type == NPUKernelType.SIMT_ONLY and len(self.configs) > 0: |
| 442 | ranked_configs = sorted( | 436 | ranked_configs = sorted( |
| 443 | ( | 437 | ( |
| @@ -0,0 +1,84 @@ | |||
| 1 | +# Config Optimizer Parameters | ||
| 2 | + | ||
| 3 | +This document describes the configuration parameters used in the FASTA config optimizer, which performs NPU-aware pruning and diversity filtering on generated tiling configs. | ||
| 4 | + | ||
| 5 | +> **Note:** All FASTA_ parameters are environment-overridable using the `FASTA_<NAME>=value` syntax (e.g., `export FASTA_CONFIG_OPTIMIZER=1`). | ||
🔵 Low Priority 变更行: config_optimizer_params.md 第 5 行。 文档声明 "All FASTA_ parameters are environment-overridable using the 影响:用户阅读文档后可能尝试设置 建议:修改文档第 5 行的注释,明确指出 MAX_CIRCLE_NUM/MIN_SUB_NUMEL/MAX_CONFIGS 为硬编码常量,不可通过环境变量覆盖。 ![]() ![]() | |||
| 6 | + | ||
| 7 | +--- | ||
| 8 | + | ||
| 9 | +## Activation Parameter | ||
| 10 | + | ||
| 11 | +### `FASTA_CONFIG_OPTIMIZER` | ||
| 12 | + | ||
| 13 | +- **Type:** bool (string comparison) | ||
| 14 | +- **Default:** `"0"` (disabled) | ||
| 15 | +- **Description:** Master switch to enable/disable the config optimizer. Set to `"1"` to activate the optimization pipeline. | ||
| 16 | + | ||
| 17 | +--- | ||
| 18 | + | ||
| 19 | +## Stage 2: NPU-Aware Pruning Parameters | ||
| 20 | + | ||
| 21 | +### `MAX_CIRCLE_NUM` | ||
| 22 | + | ||
| 23 | +- **Type:** int | ||
| 24 | +- **Default:** `4` | ||
| 25 | +- **Description:** Maximum allowed `circle_num` value. Configs with `circle_num > MAX_CIRCLE_NUM` are filtered out. This controls the maximum number of loop iterations required, helping avoid configs with excessive computational overhead. | ||
| 26 | + | ||
| 27 | +### `MIN_SUB_NUMEL` | ||
| 28 | + | ||
| 29 | +- **Type:** int | ||
| 30 | +- **Default:** `32` | ||
| 31 | +- **Description:** Minimum tile size in elements. Configs with `sub_numel < MIN_SUB_NUMEL` are filtered out. This ensures tiles are large enough to provide efficient computation on the NPU. | ||
| 32 | + | ||
| 33 | +--- | ||
| 34 | + | ||
| 35 | +## Stage 3: Diversity Filter Parameters | ||
| 36 | + | ||
| 37 | +### `MAX_CONFIGS` | ||
| 38 | + | ||
| 39 | +- **Type:** int | ||
| 40 | +- **Default:** `50` | ||
| 41 | +- **Description:** Maximum number of configs to retain after diversity filtering. If more configs pass the pruning stages, they are capped at this value via uniform `sub_numel` sampling to maintain diversity. | ||
| 42 | + | ||
| 43 | +--- | ||
| 44 | + | ||
| 45 | +## Processing Pipeline | ||
| 46 | + | ||
| 47 | +The config optimizer runs in three stages: | ||
| 48 | + | ||
| 49 | +```text | ||
| 50 | +Generated Configs | ||
| 51 | + │ | ||
| 52 | + ▼ | ||
| 53 | +┌─────────────────────────┐ | ||
| 54 | +│ Stage 2: NPU-Aware │ | ||
| 55 | +│ Pruning │ | ||
| 56 | +│ - circle_num <= 4 │ | ||
| 57 | +│ - sub_numel >= 32 │ | ||
| 58 | +└───────────┬─────────────┘ | ||
| 59 | + │ | ||
| 60 | + ▼ | ||
| 61 | +┌─────────────────────────┐ | ||
| 62 | +│ Stage 3: Diversity │ | ||
| 63 | +│ Filter │ | ||
| 64 | +│ - Deduplicate by │ | ||
| 65 | +│ (circle_num, │ | ||
| 66 | +│ sub_block_pattern) │ | ||
| 67 | +│ - Cap at MAX_CONFIGS │ | ||
| 68 | +└───────────┬─────────────┘ | ||
| 69 | + │ | ||
| 70 | + ▼ | ||
| 71 | + Optimized Configs | ||
| 72 | +``` | ||
| 73 | + | ||
| 74 | +--- | ||
| 75 | + | ||
| 76 | +## Tuning Guidelines | ||
| 77 | + | ||
| 78 | +| Parameter | When to Increase | When to Decrease | | ||
| 79 | +|-----------|------------------|------------------| | ||
| 80 | +| `MAX_CIRCLE_NUM` | When you need more aggressive tiling options | When NPU memory/compute constraints are tight | | ||
| 81 | +| `MIN_SUB_NUMEL` | When larger tiles are more efficient | When you need finer-grained search space | | ||
| 82 | +| `MAX_CONFIGS` | When you have more measurement budget | When measurement budget is limited | | ||
| 83 | + | ||
| 84 | +**Note:** Increasing `MAX_CONFIGS` increases the total measurement budget required by the dynamic filter algorithm downstream. | ||
| @@ -0,0 +1,254 @@ | |||
| 1 | +# Dynamic Filter Algorithm Parameters | ||
| 2 | + | ||
| 3 | +This document describes the configuration parameters used in the dynamic filter algorithm for efficient kernels tiling config selection via adaptive surrogate modeling. | ||
| 4 | + | ||
| 5 | +> **Note:** All FASTA_ parameters are environment-overridable using the `FASTA_<NAME>=value` syntax (e.g., `export FASTA_R1_PCT=0.5`). | ||
| 6 | + | ||
| 7 | +--- | ||
| 8 | + | ||
| 9 | +## Sampling & Budget Parameters | ||
| 10 | + | ||
| 11 | +### `FASTA_R1_PCT` | ||
| 12 | + | ||
| 13 | +- **Type:** float | ||
| 14 | +- **Default:** `0.3` | ||
| 15 | +- **Description:** The percentage of configs to sample in the initial R1 stratified sample phase. Higher values improve initial filter accuracy and parallel compilation throughput, but leave less budget for R2 exploration. See [Tuning Guide](#tuning-guide) for trade-offs. | ||
| 16 | + | ||
| 17 | +### `R1_FLOOR_PCT` | ||
| 18 | + | ||
| 19 | +- **Type:** float | ||
| 20 | +- **Default:** `0.10` | ||
| 21 | +- **Description:** Minimum floor percentage for R1 sampling. Ensures at least this fraction of configs are sampled even if `FASTA_R1_PCT` calculation yields fewer. | ||
| 22 | + | ||
| 23 | +### `FASTA_BASE_BUDGET` | ||
| 24 | + | ||
| 25 | +- **Type:** float | ||
| 26 | +- **Default:** `0.35` | ||
| 27 | +- **Description:** Base measurement budget as a fraction of total configs (N). Used for kernels with moderate complexity. See [Tuning Guide](#tuning-guide) for trade-offs with other budget parameters. | ||
| 28 | + | ||
| 29 | +### `FASTA_HIGH_BUDGET` | ||
| 30 | + | ||
| 31 | +- **Type:** float | ||
| 32 | +- **Default:** `0.4` | ||
| 33 | +- **Description:** Higher measurement budget for small, low-dimensional kernels. Controls the total R2 exploration budget. Higher values increase exploration at the cost of more measurements. See [Tuning Guide](#tuning-guide). | ||
| 34 | + | ||
| 35 | +### `FASTA_LOW_BUDGET` | ||
| 36 | + | ||
| 37 | +- **Type:** float | ||
| 38 | +- **Default:** `0.25` | ||
| 39 | +- **Description:** Lower measurement budget for large, high-dimensional kernels. Trade-off between exploration and measurement cost. See [Tuning Guide](#tuning-guide). | ||
| 40 | + | ||
| 41 | +### `R1_PCT_LOW` | ||
| 42 | + | ||
| 43 | +- **Type:** float | ||
| 44 | +- **Default:** `0.12` | ||
| 45 | +- **Description:** R1 sampling percentage for low-budget kernels. Used when routing determines a kernel should use `FASTA_LOW_BUDGET`. | ||
| 46 | + | ||
| 47 | +--- | ||
| 48 | + | ||
| 49 | +## Kernel Classification Thresholds | ||
| 50 | + | ||
| 51 | +### `HIGH_D_THRESH` | ||
| 52 | + | ||
| 53 | +- **Type:** int | ||
| 54 | +- **Default:** `4` | ||
| 55 | +- **Description:** Dimensionality threshold for classifying a kernel as high-dimensional. Kernels with D >= this value are considered high-dimensional. | ||
| 56 | + | ||
| 57 | +### `HIGH_NP_THRESH` | ||
| 58 | + | ||
| 59 | +- **Type:** int | ||
| 60 | +- **Default:** `5` | ||
| 61 | +- **Description:** Number of programs threshold for high-dimensional classification. Used in conjunction with `HIGH_D_THRESH`. | ||
| 62 | + | ||
| 63 | +### `HARD_D_THRESH` | ||
| 64 | + | ||
| 65 | +- **Type:** int | ||
| 66 | +- **Default:** `3` | ||
| 67 | +- **Description:** Dimensionality threshold for classifying a kernel as "hard". Small kernels with D < this threshold may get `FASTA_HIGH_BUDGET`. | ||
| 68 | + | ||
| 69 | +### `HARD_N_THRESH` | ||
| 70 | + | ||
| 71 | +- **Type:** int | ||
| 72 | +- **Default:** `150` | ||
| 73 | +- **Description:** Config count threshold for "hard" kernel classification. Small kernels with N < this threshold may get `FASTA_HIGH_BUDGET`. | ||
| 74 | + | ||
| 75 | +### `EASY_D_THRESH` | ||
| 76 | + | ||
| 77 | +- **Type:** int | ||
| 78 | +- **Default:** `4` | ||
| 79 | +- **Description:** Dimensionality threshold for classifying a kernel as "easy". Large kernels with D >= this threshold may get `FASTA_LOW_BUDGET`. | ||
| 80 | + | ||
| 81 | +### `EASY_N_THRESH` | ||
| 82 | + | ||
| 83 | +- **Type:** int | ||
| 84 | +- **Default:** `150` | ||
| 85 | +- **Description:** Config count threshold for "easy" kernel classification. Large kernels with N >= this threshold may get `FASTA_LOW_BUDGET`. | ||
| 86 | + | ||
| 87 | +### `MIN_VIABLE_NP` | ||
| 88 | + | ||
| 89 | +- **Type:** int | ||
| 90 | +- **Default:** `2` | ||
| 91 | +- **Description:** Minimum number of programs required for a kernel to be considered viable for optimization. | ||
| 92 | + | ||
| 93 | +--- | ||
| 94 | + | ||
| 95 | +## Surrogate Model Parameters | ||
| 96 | + | ||
| 97 | +### `TAU_MIN` | ||
| 98 | + | ||
| 99 | +- **Type:** float | ||
| 100 | +- **Default:** `0.3` | ||
| 101 | +- **Description:** Minimum tau value for Spearman rank correlation. Used in model selection to determine if a surrogate model is useful. | ||
| 102 | + | ||
| 103 | +### `TAU_RANGE` | ||
| 104 | + | ||
| 105 | +- **Type:** float | ||
| 106 | +- **Default:** `0.4` | ||
| 107 | +- **Description:** Range for tau-based model selection. The algorithm considers models with tau in the range `[TAU_MIN, TAU_MIN + TAU_RANGE]`. | ||
| 108 | + | ||
| 109 | +### `MARGINAL_D` | ||
| 110 | + | ||
| 111 | +- **Type:** int | ||
| 112 | +- **Default:** `3` | ||
| 113 | +- **Description:** Marginal dimensionality threshold. Used to determine when to add higher-order terms to the surrogate model. | ||
| 114 | + | ||
| 115 | +### `RIDGE_LAMBDA` | ||
| 116 | + | ||
| 117 | +- **Type:** float | ||
| 118 | +- **Default:** `0.1` | ||
| 119 | +- **Description:** Ridge regression regularization parameter. Prevents overfitting in surrogate models by penalizing large coefficients. | ||
| 120 | + | ||
| 121 | +--- | ||
| 122 | + | ||
| 123 | +## Convergence Parameters | ||
| 124 | + | ||
| 125 | +### `FASTA_MAX_ROUNDS` | ||
| 126 | + | ||
| 127 | +- **Type:** int | ||
| 128 | +- **Default:** `2` | ||
| 129 | +- **Description:** Maximum number of optimization rounds. Controls how many iterations of measure-model-propose cycles to perform. Increase when the algorithm struggles to converge and more R2 iterations are required. See [Tuning Guide](#tuning-guide). | ||
| 130 | + | ||
| 131 | +### `MARGIN_CONV` | ||
| 132 | + | ||
| 133 | +- **Type:** float | ||
| 134 | +- **Default:** `2.0` | ||
| 135 | +- **Description:** Margin threshold for convergence. When the best config's predicted margin exceeds this value, convergence is triggered. | ||
| 136 | + | ||
| 137 | +### `L1_CONV` | ||
| 138 | + | ||
| 139 | +- **Type:** float | ||
| 140 | +- **Default:** `0.01` | ||
| 141 | +- **Description:** L1 norm threshold for convergence. When the L1 norm of changes falls below this value, the algorithm converges. | ||
| 142 | + | ||
| 143 | +--- | ||
| 144 | + | ||
| 145 | +## Numerical Stability Parameters | ||
| 146 | + | ||
| 147 | +### `HIST_BINS` | ||
| 148 | + | ||
| 149 | +- **Type:** int | ||
| 150 | +- **Default:** `11` | ||
| 151 | +- **Description:** Number of bins for histogram-based computations. Used in distribution estimation and binning operations. | ||
| 152 | + | ||
| 153 | +### `K_TOLERANCE` | ||
| 154 | + | ||
| 155 | +- **Type:** float | ||
| 156 | +- **Default:** `1.05` | ||
| 157 | +- **Description:** Tolerance multiplier for kernel count. Allows slight variations in the expected number of good kernels (e.g., K * 1.05). | ||
| 158 | + | ||
| 159 | +### `UNDERFLOW_FLOOR` | ||
| 160 | + | ||
| 161 | +- **Type:** float | ||
| 162 | +- **Default:** `1e-300` | ||
| 163 | +- **Description:** Minimum value to prevent numerical underflow. Used to avoid log(0) or division by zero in probability computations. | ||
| 164 | + | ||
| 165 | +--- | ||
| 166 | + | ||
| 167 | +## Budget Routing Logic | ||
| 168 | + | ||
| 169 | +The algorithm routes kernels to different budgets based on their characteristics: | ||
| 170 | + | ||
| 171 | +| Kernel Type | Condition | Budget | | ||
| 172 | +|-------------|-----------|--------| | ||
| 173 | +| Small Low-D | D < `HARD_D_THRESH` AND N < `HARD_N_THRESH` | `FASTA_HIGH_BUDGET` | | ||
| 174 | +| Large High-D | D >= `EASY_D_THRESH` AND N >= `EASY_N_THRESH` | `FASTA_LOW_BUDGET` | | ||
| 175 | +| Moderate | Otherwise | `FASTA_BASE_BUDGET` | | ||
| 176 | + | ||
| 177 | +--- | ||
| 178 | + | ||
| 179 | +--- | ||
| 180 | + | ||
| 181 | +## Tuning Guide | ||
| 182 | + | ||
| 183 | +The dynamic filter algorithm operates in two phases: | ||
| 184 | + | ||
| 185 | +1. **R1 Phase (Initial Sampling):** Compiles and measures a stratified sample of configs in parallel | ||
| 186 | +2. **R2 Phase (Iterative Refinement):** Uses surrogate modeling to propose and evaluate new configs | ||
| 187 | + | ||
| 188 | +### Key Trade-offs | ||
| 189 | + | ||
| 190 | +The relationship between `FASTA_R1_PCT`, `FASTA_HIGH_BUDGET`, and `FASTA_MAX_ROUNDS` determines how the measurement budget is allocated: | ||
| 191 | + | ||
| 192 | +```text | ||
| 193 | +Total R2 budget = FASTA_HIGH_BUDGET - FASTA_R1_PCT | ||
| 194 | +R2 batch size per iteration = (FASTA_HIGH_BUDGET - FASTA_R1_PCT) / FASTA_MAX_ROUNDS | ||
| 195 | +``` | ||
| 196 | + | ||
| 197 | +Note: The total R2 budget is fixed (FASTA_HIGH_BUDGET - FASTA_R1_PCT), and FASTA_MAX_ROUNDS only controls how it's split across iterations. | ||
🔵 Low Priority 变更行: dynamic_filter_algo_params.md 第 192-197 行。 文档声称 R2 总预算和每轮 batch size 的计算公式为: Total R2 budget = FASTA_HIGH_BUDGET - FASTA_R1_PCT R2 batch size per iteration = (FASTA_HIGH_BUDGET - FASTA_R1_PCT) / FASTA_MAX_ROUNDS 但实际代码(dynamic_filter_if.py 第 513-515 行)的计算逻辑是: batch_size = max(1, self._total_budget // algo.df_cfg.max_rounds) remaining_budget = self._total_budget - self._used batch_size = min(batch_size, len(uidx), remaining_budget) 即先按 影响:试图理解预算分配逻辑的读者可能被误导,以为算法直接使用 建议:修正 R2 batch size 公式描述,说明实际代码使用 ![]() ![]() | |||
| 198 | + | ||
| 199 | +| Parameter | Effect When Increased | Trade-off | | ||
| 200 | +|-----------|----------------------|-----------| | ||
| 201 | +| `FASTA_R1_PCT` | More configs compiled in parallel during R1; better initial filter accuracy | Less total budget for R2 exploration (fixed FASTA_HIGH_BUDGET - FASTA_R1_PCT) | | ||
| 202 | +| `FASTA_HIGH_BUDGET` | More total budget for R2 exploration | Higher total measurement cost | | ||
| 203 | +| `FASTA_MAX_ROUNDS` | Finer-grained exploration across more iterations (same total R2 budget split differently) | More iterations may slow convergence | | ||
| 204 | + | ||
| 205 | +### Tuning Strategies | ||
| 206 | + | ||
| 207 | +#### High Compilation Throughput (Faster Compile Phase) | ||
| 208 | + | ||
| 209 | +```text | ||
| 210 | +FASTA_R1_PCT = 0.4-0.5 | ||
| 211 | +FASTA_HIGH_BUDGET = 0.5-0.6 | ||
| 212 | +FASTA_MAX_ROUNDS = 2 | ||
| 213 | +``` | ||
| 214 | + | ||
| 215 | +- Higher R1 means more configs are compiled in parallel at `.compile()` time | ||
| 216 | +- Better initial filter accuracy from more samples | ||
| 217 | +- Total R2 budget: 0.5-0.6 - 0.4-0.5 = ~0.1-0.2 | ||
| 218 | +- Suitable when compilation time dominates | ||
| 219 | + | ||
| 220 | +#### High Exploration (Better Kernel Discovery) | ||
| 221 | + | ||
| 222 | +```text | ||
| 223 | +FASTA_R1_PCT = 0.2-0.25 | ||
| 224 | +FASTA_HIGH_BUDGET = 0.45-0.5 | ||
| 225 | +FASTA_MAX_ROUNDS = 3-4 | ||
| 226 | +``` | ||
| 227 | + | ||
| 228 | +- Lower R1 leaves more total budget for R2 exploration (0.45-0.5 - 0.2-0.25 = ~0.2-0.3) | ||
| 229 | +- More iterations allow finer-grained exploitation of promising regions | ||
| 230 | +- Suitable when the optimal config is rare and needs extensive search | ||
| 231 | + | ||
| 232 | +#### Convergence Issues | ||
| 233 | + | ||
| 234 | +```text | ||
| 235 | +FASTA_R1_PCT = 0.3 | ||
| 236 | +FASTA_HIGH_BUDGET = 0.4 | ||
| 237 | +FASTA_MAX_ROUNDS = 3-4 | ||
| 238 | +``` | ||
| 239 | + | ||
| 240 | +- Increase `FASTA_MAX_ROUNDS` when the algorithm struggles to converge (same total R2 budget of 10%, split across more iterations) | ||
| 241 | +- More R2 iterations give the surrogate model more opportunities to refine predictions | ||
| 242 | +- Monitor `MARGIN_CONV` and `L1_CONV` to detect convergence | ||
| 243 | + | ||
| 244 | +### Default Configuration Rationale | ||
| 245 | + | ||
| 246 | +The defaults (`FASTA_R1_PCT=0.3`, `FASTA_HIGH_BUDGET=0.4`, `FASTA_MAX_ROUNDS=2`) provide: | ||
| 247 | + | ||
| 248 | +- ~30% of configs measured in R1 (fully parallel compilation) | ||
| 249 | +- ~10% total for all R2 iterations combined (FASTA_HIGH_BUDGET - FASTA_R1_PCT) | ||
| 250 | +- Total budget: ~40% of configs measured | ||
| 251 | +- With FASTA_MAX_ROUNDS=3, the 10% R2 budget would be split: 5% per iteration | ||
| 252 | +- Balanced trade-off between initial accuracy and iterative refinement | ||
| 253 | + | ||
| 254 | +--- | ||
| @@ -0,0 +1,1257 @@ | ||||||||||||||||||
| 1 | +# Dynamic Filter — Mathematical & Component Reference | |||||||||||||||||
| 2 | + | |||||||||||||||||
| 3 | +Contains full derivations for all mathematical components of the dynamic filter algorithm. | |||||||||||||||||
| 4 | + | |||||||||||||||||
| 5 | +Structure: each section starts with general textbook theory (what the method is, where it comes from), then shows how we specialized it for our problem. Every matrix operation includes shapes. Every component lists its inputs, outputs, and connections to other components. | |||||||||||||||||
| 6 | + | |||||||||||||||||
| 7 | +## Notation | |||||||||||||||||
| 8 | + | |||||||||||||||||
| 9 | +### Index conventions | |||||||||||||||||
| 10 | + | |||||||||||||||||
| 11 | +Subscripts on x (parameters within a single config): | |||||||||||||||||
| 12 | +xᵢ = the i-th tiling parameter of a single config. i ranges from 1 to D. Example: x₁ = Y0BLOCK, x₂ = Y0BLOCK_SUB, x₃ = core_num. | |||||||||||||||||
| 13 | + | |||||||||||||||||
| 14 | +Superscripts or separate index on configs: | |||||||||||||||||
| 15 | +x⁽ʲ⁾ or xⱼ = the j-th config in the dataset. j ranges from 1 to N (all configs) or 1 to n (measured configs). | |||||||||||||||||
| 16 | + | |||||||||||||||||
| 17 | +### Sets | |||||||||||||||||
| 18 | + | |||||||||||||||||
| 19 | +M = set of measured config indices (configs we have benchmarked). |M| = n. | |||||||||||||||||
| 20 | +U = set of unmeasured config indices (configs we have NOT benchmarked). |U| = N − n. | |||||||||||||||||
| 21 | +N = total configs. D = number of variable tiling parameters (1–6). K = number of "good" configs (duration ≤ 1.05 × d_best). | |||||||||||||||||
| 22 | + | |||||||||||||||||
| 23 | +### Matrices and vectors | |||||||||||||||||
| 24 | + | |||||||||||||||||
| 25 | +| Symbol | Shape | Description | | |||||||||||||||||
| 26 | +|--------|-------|-------------| | |||||||||||||||||
| 27 | +| X | (N, D) | raw feature matrix, all configs | | |||||||||||||||||
| 28 | +| Xₘ | (n, D) | raw features of measured configs | | |||||||||||||||||
| 29 | +| Xᵤ | (N−n, D) | raw features of unmeasured configs | | |||||||||||||||||
| 30 | +| Xₘ_norm | (n, D) | normalized features of measured configs | | |||||||||||||||||
| 31 | +| Xᵤ_norm | (N−n, D) | normalized features of unmeasured configs | | |||||||||||||||||
| 32 | +| Fₘ | (n, p) | basis-expanded features of measured configs | | |||||||||||||||||
| 33 | +| Fᵤ | (N−n, p) | basis-expanded features of unmeasured configs | | |||||||||||||||||
| 34 | +| d | (n,) | measured durations | | |||||||||||||||||
| 35 | +| d̂ | (N−n,) | predicted durations for unmeasured configs | | |||||||||||||||||
| 36 | +| β̂ | (p,) | estimated regression coefficients | | |||||||||||||||||
| 37 | +| G | (p, p) | Gram matrix FₘᵀFₘ + λI | | |||||||||||||||||
| 38 | +| H | (n, n) | hat matrix Fₘ G⁻¹ Fₘᵀ | | |||||||||||||||||
| 39 | +| p | scalar | number of basis features (varies by basis type) | | |||||||||||||||||
| 40 | + | |||||||||||||||||
| 41 | +Convention: plain symbols (d, X, F) are observed/constructed from data. Symbols with hat (d̂, β̂, R̂²) are estimated by the algorithm. Subscript m = measured set, u = unmeasured set. | |||||||||||||||||
| 42 | + | |||||||||||||||||
| 43 | +### Error metrics map | |||||||||||||||||
| 44 | + | |||||||||||||||||
| 45 | +Two distinct error metrics appear in the algorithm at different stages: | |||||||||||||||||
| 46 | + | |||||||||||||||||
| 47 | +| Metric | Formula | Used in | Purpose | | |||||||||||||||||
| 48 | +|--------|---------|---------|---------| | |||||||||||||||||
| 49 | +| SS_res (L²) | Σ(dᵢ − d̂ᵢ)² | R̂² computation (§9) | model quality → τ̂ adaptation | | |||||||||||||||||
| 50 | +| L₁ distance | Σ\|hₜ − hₜ₋₁\| | convergence (§13) | distribution stability → early stop | | |||||||||||||||||
| 51 | +| ρ_s (Spearman) | rank correlation | mode selection (§9) | basis competition criterion | | |||||||||||||||||
| 52 | + | |||||||||||||||||
| 53 | +Key distinction: R̂² measures how well the model fits training data (squared error between observed d and predicted d̂ on the measured set). L₁ measures how much the distribution of measured durations changed between rounds (histogram comparison, no model involved). ρ_s measures rank agreement between actual and cross-validated predictions (used to pick the best basis model). They are independent calculations used at different stages. | |||||||||||||||||
| 54 | + | |||||||||||||||||
| 55 | +--- | |||||||||||||||||
| 56 | + | |||||||||||||||||
| 57 | +## §1. Problem Formulation | |||||||||||||||||
| 58 | + | |||||||||||||||||
| 59 | +Given N tiling configurations for a Triton kernel, find the fastest one by measuring as few as possible. Each measurement costs compile time + benchmark time. We want to measure ~35% of N and still find a config within 5% of the true optimum. | |||||||||||||||||
| 60 | + | |||||||||||||||||
| 61 | +Formally: let d(·) be the unknown duration function. We observe d($j$) for a subset M ⊂ {1..N} of our choosing, sequentially. The goal is to find $i^*$ such that d($i^*$) ≤ 1.05 · minⱼ d($j$), while |M| ≤ 0.35N. | |||||||||||||||||
| 62 | + | |||||||||||||||||
| 63 | +This is a sequential experiment design problem, closely related to best-arm identification in multi-armed bandits, but with side information (the feature vectors of each config). | |||||||||||||||||
| 64 | + | |||||||||||||||||
| 65 | +### Evaluation metrics | |||||||||||||||||
| 66 | + | |||||||||||||||||
| 67 | +Catch rate: fraction of simulation trials where the algorithm found a config within 5% of the true optimum. Higher is better. 100% = always finds a near-optimal config. | |||||||||||||||||
| 68 | + | |||||||||||||||||
| 69 | +Savings: fraction of configs not measured. savings = 1 − |M|/N. Higher is better. | |||||||||||||||||
| 70 | + | |||||||||||||||||
| 71 | +Regret: how much worse the selected config is vs the true optimum. regret = (d($i^*$) − min d) / min d × 100%. Lower is better. | |||||||||||||||||
| 72 | + | |||||||||||||||||
| 73 | +K (needle count): number of configs with d ≤ 1.05 × min d. Low K = needle in a haystack. High K = easy landscape. | |||||||||||||||||
| 74 | + | |||||||||||||||||
| 75 | +--- | |||||||||||||||||
| 76 | + | |||||||||||||||||
| 77 | +## §2. Feature Extraction | |||||||||||||||||
| 78 | + | |||||||||||||||||
| 79 | +**Component 1.** Always runs first. No conditions, no gate. | |||||||||||||||||
| 80 | + | |||||||||||||||||
| 81 | +```text | |||||||||||||||||
| 82 | +inputs: configs (list of N objects with .kwargs dict) | |||||||||||||||||
| 83 | +outputs: X (N, D) — feature matrix | |||||||||||||||||
| 84 | + names (list of D strings) — feature names, sorted | |||||||||||||||||
| 85 | +feeds: stratified R1 sampling (X) | |||||||||||||||||
| 86 | + viability assessment (D) | |||||||||||||||||
| 87 | + budget allocation (D) | |||||||||||||||||
| 88 | + normalization (X) | |||||||||||||||||
| 89 | +``` | |||||||||||||||||
| 90 | + | |||||||||||||||||
| 91 | +Each config has a kwargs dict mapping parameter names to numeric values. We scan all configs, collect keys that are numeric, and keep only columns where the value varies (more than one unique value across configs). Categorical flags (compile_mode, multibuffer, split_k, etc.) are skipped. | |||||||||||||||||
| 92 | + | |||||||||||||||||
| 93 | +Output: X ∈ ℝ^(N×D), where N = number of configs, D = number of varying features. | |||||||||||||||||
| 94 | + | |||||||||||||||||
| 95 | +The old approach used a hardcoded list of 7 parameter names. This missed entire families (T0BLOCK, Z1BLOCK, Y1BLOCK, etc.). 28 of 35 test kernels had incorrect D under the hardcoded scheme. Auto-detection was the single largest impact fix (exp38). | |||||||||||||||||
| 96 | + | |||||||||||||||||
| 97 | +--- | |||||||||||||||||
| 98 | + | |||||||||||||||||
| 99 | +## §3. R1 Sizing: The Hypergeometric Distribution | |||||||||||||||||
| 100 | + | |||||||||||||||||
| 101 | +**Theoretical foundation for R1 sample size.** Determines how many configs to benchmark blind before the model kicks in. | |||||||||||||||||
| 102 | + | |||||||||||||||||
| 103 | +### 3.1 General theory | |||||||||||||||||
| 104 | + | |||||||||||||||||
| 105 | +The hypergeometric distribution models the number of successes in draws from a finite population without replacement. If a population of N items contains K "good" items, and we draw n without putting them back, the probability of getting exactly k good items is: | |||||||||||||||||
| 106 | + | |||||||||||||||||
| 107 | +```text | |||||||||||||||||
| 108 | +P(X = k) = C(K,k) · C(N−K, n−k) / C(N,n) | |||||||||||||||||
| 109 | +``` | |||||||||||||||||
| 110 | + | |||||||||||||||||
| 111 | +where C(a,b) = a! / [b!(a−b)!] is the binomial coefficient. | |||||||||||||||||
| 112 | + | |||||||||||||||||
| 113 | +Reference: [1] Rice (2006), Mathematical Statistics and Data Analysis, 3rd ed. Ch. 2: hypergeometric as sampling without replacement. | |||||||||||||||||
| 114 | + | |||||||||||||||||
| 115 | +Key distinction from the binomial: the binomial assumes each draw is independent (with replacement). The hypergeometric accounts for the shrinking pool — after drawing a good item, fewer good items remain. | |||||||||||||||||
| 116 | + | |||||||||||||||||
| 117 | +### 3.2 The special case: P(miss all good) | |||||||||||||||||
| 118 | + | |||||||||||||||||
| 119 | +We care about k=0 specifically: probability of drawing zero good items. | |||||||||||||||||
| 120 | + | |||||||||||||||||
| 121 | +```text | |||||||||||||||||
| 122 | +P(miss all) = C(N−K, n) / C(N, n) | |||||||||||||||||
| 123 | +``` | |||||||||||||||||
| 124 | + | |||||||||||||||||
| 125 | +Expanding into a computationally stable product form: | |||||||||||||||||
| 126 | + | |||||||||||||||||
| 127 | +```text | |||||||||||||||||
| 128 | +P(miss) = ∏ᵢ₌₀ⁿ⁻¹ (N − K − i) / (N − i) | |||||||||||||||||
| 129 | +``` | |||||||||||||||||
| 130 | + | |||||||||||||||||
| 131 | +Each factor is: "of the remaining unchosen configs, what fraction are bad?" At draw i=0: (N−K)/N are bad. At draw i=1: (N−K−1)/(N−1) are bad (one bad config was already drawn). And so on. | |||||||||||||||||
| 132 | + | |||||||||||||||||
| 133 | +### 3.3 Our application | |||||||||||||||||
| 134 | + | |||||||||||||||||
| 135 | +We set r₁ = 15% of N. The hypergeometric tells us what this implies: | |||||||||||||||||
| 136 | + | |||||||||||||||||
| 137 | +Hard kernel (K=2, N=143, add_layer_norm_0): r₁ = 21. P(miss) ≈ 0.74, so P(catch) ≈ 26%. R1 alone has a 1-in-4 chance. | |||||||||||||||||
| 138 | + | |||||||||||||||||
| 139 | +Easy kernel (K=47, N=1800, addmm_tanh_9): r₁ = 270. P(miss) ≈ 0.00003. Caught with certainty. | |||||||||||||||||
| 140 | + | |||||||||||||||||
| 141 | +Closed form for K=2: P(miss) = (N−n)(N−n−1) / [N(N−1)]. Requires n ≈ 90% of N to guarantee P(miss) ≤ 0.01. This is why R1 alone cannot solve K=2 kernels and why the estimator is essential. | |||||||||||||||||
| 142 | + | |||||||||||||||||
| 143 | +--- | |||||||||||||||||
| 144 | + | |||||||||||||||||
| 145 | +## §4. Stratified R1 Sampling | |||||||||||||||||
| 146 | + | |||||||||||||||||
| 147 | +**Component 2.** Always runs first round. No conditions. | |||||||||||||||||
| 148 | + | |||||||||||||||||
| 149 | +```text | |||||||||||||||||
| 150 | +inputs: X (N, D) — feature matrix | |||||||||||||||||
| 151 | + n_sample — number of configs to sample (r₁) | |||||||||||||||||
| 152 | + rng — random state | |||||||||||||||||
| 153 | +outputs: r1_idx (list of r₁ indices) | |||||||||||||||||
| 154 | +feeds: measure function → measured durations | |||||||||||||||||
| 155 | + viability assessment (n = |r1_idx|) | |||||||||||||||||
| 156 | + mode selection (training data) | |||||||||||||||||
| 157 | +``` | |||||||||||||||||
| 158 | + | |||||||||||||||||
| 159 | +R1 (the blind exploration phase, 15% of N) uses most-unique-first stratification: | |||||||||||||||||
| 160 | + | |||||||||||||||||
| 161 | +1. Find the dimension with the most unique values. | |||||||||||||||||
| 162 | +2. For each unique value in that dimension, randomly pick one config with that value. | |||||||||||||||||
| 163 | +3. Move to next-most-unique dimension. Repeat until budget exhausted. | |||||||||||||||||
| 164 | +4. Fill remaining budget randomly. | |||||||||||||||||
| 165 | + | |||||||||||||||||
| 166 | +Why most-unique-first? If x₁ has 50 unique values and x₃ has 3, covering x₁ first gives the polynomial better training data because that dimension drives the performance landscape. The 16 picks for x₁ randomly cover most of x₃'s 3 values anyway. Experiment 48: least-unique-first drops one kernel from 99% to 77.5%. | |||||||||||||||||
| 167 | + | |||||||||||||||||
| 168 | +--- | |||||||||||||||||
| 169 | + | |||||||||||||||||
| 170 | +## §5. Budget Allocation | |||||||||||||||||
| 171 | + | |||||||||||||||||
| 172 | +**Component 3.** Gate / decision point. Runs once at start. | |||||||||||||||||
| 173 | + | |||||||||||||||||
| 174 | +```text | |||||||||||||||||
| 175 | +inputs: N — total configs | |||||||||||||||||
| 176 | + D — number of features | |||||||||||||||||
| 177 | + r₁ — R1 sample size | |||||||||||||||||
| 178 | +outputs: total_budget — maximum configs to measure | |||||||||||||||||
| 179 | +feeds: per-round batch size calculation | |||||||||||||||||
| 180 | + total stopping criterion | |||||||||||||||||
| 181 | +``` | |||||||||||||||||
| 182 | + | |||||||||||||||||
| 183 | +Base budget: 35% of N. High budget: 50% for kernels with D ≥ 4 AND n/p_linear < 5. | |||||||||||||||||
| 184 | + | |||||||||||||||||
| 185 | +Why a step function at D=4, n/p<5? Experiment 49 showed that a smooth budget function (linear interpolation between 35% and 50%) kills clone_6 (99.5→91.0%). clone_6 has N=199, D=5 — right at the boundary. The smooth function gives it 35.6% budget, but it needs the full 50% because it has only K=4 needles in 199 configs. The step function at D=4 correctly gives it 50%. | |||||||||||||||||
| 186 | + | |||||||||||||||||
| 187 | +--- | |||||||||||||||||
| 188 | + | |||||||||||||||||
| 189 | +## §6. Viability Assessment | |||||||||||||||||
| 190 | + | |||||||||||||||||
| 191 | +**Component 4.** Binary gate: model path vs coverage fallback. Checked once after R1. | |||||||||||||||||
| 192 | + | |||||||||||||||||
| 193 | +```text | |||||||||||||||||
| 194 | +inputs: n — number of measured samples (|M|) | |||||||||||||||||
| 195 | + D — number of features | |||||||||||||||||
| 196 | +outputs: path ∈ {'model', 'coverage'} | |||||||||||||||||
| 197 | +feeds: coverage fallback (if path = 'coverage') | |||||||||||||||||
| 198 | + mode selection (if path = 'model') | |||||||||||||||||
| 199 | +``` | |||||||||||||||||
| 200 | + | |||||||||||||||||
| 201 | +Computes p_linear = 1 + D. If n / p_linear < 2, even the simplest linear model has barely more equations than unknowns — OLS is unstable and predictions are noise. This is the high-variance regime of the bias-variance tradeoff ([2] Hastie et al. Ch. 7.3): too many parameters relative to data means the model fits noise rather than signal. | |||||||||||||||||
| 202 | + | |||||||||||||||||
| 203 | +Reference: [11] Bühlmann & van de Geer (2011). Statistics for High-Dimensional Data. Ridge in the p > n regime. | |||||||||||||||||
| 204 | + | |||||||||||||||||
| 205 | +**v6 difference:** in v6, viability also chose the polynomial complexity (linear → quad → full) based on n/p thresholds for each mode. That branching is removed in v7 — the LOOCV mode competition (§9) handles basis selection autonomously. | |||||||||||||||||
| 206 | + | |||||||||||||||||
| 207 | +--- | |||||||||||||||||
| 208 | + | |||||||||||||||||
| 209 | +## §7. Coverage Fallback | |||||||||||||||||
| 210 | + | |||||||||||||||||
| 211 | +**Component 5.** Activates when viability fails (n/p_linear < 2). | |||||||||||||||||
| 212 | + | |||||||||||||||||
| 213 | +```text | |||||||||||||||||
| 214 | +inputs: N — total configs | |||||||||||||||||
| 215 | + measured — current measured set | |||||||||||||||||
| 216 | +outputs: best_idx — index of best config found | |||||||||||||||||
| 217 | + best_dur — duration of best config | |||||||||||||||||
| 218 | +feeds: algorithm return (terminates the kernel) | |||||||||||||||||
| 219 | +``` | |||||||||||||||||
| 220 | + | |||||||||||||||||
| 221 | +Random sampling at 50% budget. No model, no predictions. This saved softmax_5 (N=26, D=3) from 72% to 96% (exp42) — with only 26 configs and 3 features, any polynomial model is pure noise. | |||||||||||||||||
| 222 | + | |||||||||||||||||
| 223 | +--- | |||||||||||||||||
| 224 | + | |||||||||||||||||
| 225 | +## §8. Feature Normalization & Basis Construction | |||||||||||||||||
| 226 | + | |||||||||||||||||
| 227 | +**Component 6.** Runs every round on model path. Normalization computed once, basis constructed per candidate. | |||||||||||||||||
| 228 | + | |||||||||||||||||
| 229 | +```text | |||||||||||||||||
| 230 | +inputs: X (N, D) — raw feature matrix | |||||||||||||||||
| 231 | + M, U — measured/unmeasured index sets | |||||||||||||||||
| 232 | + mode ∈ {'linear', 'quad', 'full', 'fourier', 'cubic'} — basis type | |||||||||||||||||
| 233 | +outputs: Fₘ (n, p) — measured feature matrix | |||||||||||||||||
| 234 | + Fᵤ (N−n, p) — unmeasured feature matrix | |||||||||||||||||
| 235 | +feeds: ridge regression (Fₘ, Fᵤ) | |||||||||||||||||
| 236 | +``` | |||||||||||||||||
| 237 | + | |||||||||||||||||
| 238 | +### 8.1 Normalization | |||||||||||||||||
| 239 | + | |||||||||||||||||
| 240 | +Before basis features are built, each raw feature column is min-max normalized to [0,1]. Without normalization, features at different scales (Y0BLOCK in [32,1024] vs core_num in [1,8]) produce a Gram matrix with a large condition number, making the normal equations numerically unstable ([4] Strang Ch. 11.2): | |||||||||||||||||
| 241 | + | |||||||||||||||||
| 242 | +```text | |||||||||||||||||
| 243 | +xᵢⱼ_norm = (xᵢⱼ − minⱼ) / (maxⱼ − minⱼ) | |||||||||||||||||
| 244 | +``` | |||||||||||||||||
| 245 | + | |||||||||||||||||
| 246 | +If maxⱼ = minⱼ (constant column, already filtered out in feature extraction), set range = 1.0. | |||||||||||||||||
| 247 | + | |||||||||||||||||
| 248 | +Normalization uses all N configs' statistics. Unmeasured configs are normalized using the same min/max, so all values stay in [0,1]. The same normalization applies to Fₘ and Fᵤ construction. | |||||||||||||||||
| 249 | + | |||||||||||||||||
| 250 | +### 8.2 Linear basis | |||||||||||||||||
| 251 | + | |||||||||||||||||
| 252 | +```text | |||||||||||||||||
| 253 | +f(x) = [1, x₁, x₂, ..., x_D] | |||||||||||||||||
| 254 | +p = 1 + D | |||||||||||||||||
| 255 | +``` | |||||||||||||||||
| 256 | + | |||||||||||||||||
| 257 | +Shape: Fₘ is (n, 1+D), Fᵤ is (N−n, 1+D). | |||||||||||||||||
| 258 | + | |||||||||||||||||
| 259 | +The minimal viable model. Works well when one dimension dominates (e.g., core_num for D=1 kernels). Always in the candidate set. | |||||||||||||||||
| 260 | + | |||||||||||||||||
| 261 | +### 8.3 Quadratic basis | |||||||||||||||||
| 262 | + | |||||||||||||||||
| 263 | +```text | |||||||||||||||||
| 264 | +f(x) = [1, x₁, ..., x_D, x₁², ..., x_D²] | |||||||||||||||||
| 265 | +p = 1 + 2D | |||||||||||||||||
| 266 | +``` | |||||||||||||||||
| 267 | + | |||||||||||||||||
| 268 | +Shape: Fₘ is (n, 1+2D), Fᵤ is (N−n, 1+2D). | |||||||||||||||||
| 269 | + | |||||||||||||||||
| 270 | +Captures curvature but no interactions. Useful for kernels where block sizes have sweet spots (too small = overhead, too large = waste). The squared term x² captures U-shapes that linear terms miss. | |||||||||||||||||
| 271 | + | |||||||||||||||||
| 272 | +### 8.4 Full polynomial basis (with cross terms) | |||||||||||||||||
| 273 | + | |||||||||||||||||
| 274 | +```text | |||||||||||||||||
| 275 | +f(x) = [1, x₁, ..., x_D, x₁², ..., x_D², x₁x₂, x₁x₃, ..., x_{D-1}x_D] | |||||||||||||||||
| 276 | +p = 1 + 2D + D(D−1)/2 | |||||||||||||||||
| 277 | +``` | |||||||||||||||||
| 278 | + | |||||||||||||||||
| 279 | +Shape: Fₘ is (n, p), Fᵤ is (N−n, p). At D=6: p = 1 + 12 + 15 = 28. | |||||||||||||||||
| 280 | + | |||||||||||||||||
| 281 | +The Config O/M workhorse. The cross-term xᵢxⱼ captures joint effects: "Y0BLOCK matters more when core_num is high." | |||||||||||||||||
| 282 | + | |||||||||||||||||
| 283 | +Reference: [6] Box & Draper (2007). Response Surfaces, Mixtures, and Ridge Analyses, 2nd ed. Second-order polynomial models are the standard tool in response surface methodology. | |||||||||||||||||
| 284 | + | |||||||||||||||||
| 285 | +### 8.5 Fourier basis (new in exp59) | |||||||||||||||||
| 286 | + | |||||||||||||||||
| 287 | +```text | |||||||||||||||||
| 288 | +f(x) = [1, x₁, sin(πx₁), cos(πx₁), sin(2πx₁), cos(2πx₁), | |||||||||||||||||
| 289 | + x₂, sin(πx₂), cos(πx₂), sin(2πx₂), cos(2πx₂), | |||||||||||||||||
| 290 | + ..., | |||||||||||||||||
| 291 | + x_D, sin(πx_D), cos(πx_D), sin(2πx_D), cos(2πx_D)] | |||||||||||||||||
| 292 | +p = 1 + 5D | |||||||||||||||||
| 293 | +``` | |||||||||||||||||
| 294 | + | |||||||||||||||||
| 295 | +Shape: Fₘ is (n, 1+5D), Fᵤ is (N−n, 1+5D). At D=6: p = 31. | |||||||||||||||||
| 296 | + | |||||||||||||||||
| 297 | +### 8.5.1 General theory: Fourier series | |||||||||||||||||
| 298 | + | |||||||||||||||||
| 299 | +Any periodic function f(x) on [0,1] can be represented as a sum of sines and cosines: | |||||||||||||||||
| 300 | + | |||||||||||||||||
| 301 | +```text | |||||||||||||||||
| 302 | +f(x) = a₀ + Σₖ [aₖ cos(2πkx) + bₖ sin(2πkx)] | |||||||||||||||||
| 303 | +``` | |||||||||||||||||
| 304 | + | |||||||||||||||||
| 305 | +Reference: [14] Tolstov (1976). Fourier Series. Dover. Ch. 1: convergence of trigonometric series. | |||||||||||||||||
| 306 | + | |||||||||||||||||
| 307 | +Truncating to the first two harmonics (k=1,2) gives a finite basis that captures the dominant periodic structure without overfitting to noise. The raw x term is included alongside the trigonometric terms to capture monotone trends that the periodic terms miss. | |||||||||||||||||
| 308 | + | |||||||||||||||||
| 309 | +### 8.5.2 Our application | |||||||||||||||||
| 310 | + | |||||||||||||||||
| 311 | +NPU performance is periodic in tile sizes — tiles aligned to cache line boundaries or SRAM bank boundaries show regular performance peaks. A tile of 256 might hit a sweet spot, 512 another, etc. Polynomial basis can't capture this without impractically high degree (need degree ≥ 4 to approximate two full periods). Fourier captures it with 5 terms per dimension. | |||||||||||||||||
| 312 | + | |||||||||||||||||
| 313 | +Fourier wins on 8 of 35 kernels, including 3 hard kernels where polynomial fails. At D=6, p=31 — comparable to full polynomial (p=28) but orthogonal basis functions avoid collinearity issues that plague high-degree polynomials. | |||||||||||||||||
| 314 | + | |||||||||||||||||
| 315 | +Higher harmonics (sin(3πx), cos(3πx)) were tested in exp64–65 and caused regressions due to near-collinearity with existing terms when data is sparse. Two harmonics is the empirical sweet spot. | |||||||||||||||||
| 316 | + | |||||||||||||||||
| 317 | +### 8.6 Cubic polynomial basis (new in exp61) | |||||||||||||||||
| 318 | + | |||||||||||||||||
| 319 | +```text | |||||||||||||||||
| 320 | +f(x) = [1, // intercept | |||||||||||||||||
| 321 | + x₁, ..., x_D, // linear | |||||||||||||||||
| 322 | + x₁², ..., x_D², // squared | |||||||||||||||||
| 323 | + x₁x₂, ..., x_{D-1}x_D, // 2-way cross | |||||||||||||||||
| 324 | + x₁³, ..., x_D³, // cubic | |||||||||||||||||
| 325 | + x₁²x₂, x₁²x₃, ..., // squared × linear (D(D−1) terms) | |||||||||||||||||
| 326 | + x₁x₂x₃, ...] // 3-way cross (C(D,3) terms) | |||||||||||||||||
| 327 | +``` | |||||||||||||||||
| 328 | + | |||||||||||||||||
| 329 | +Parameter count: | |||||||||||||||||
| 330 | + | |||||||||||||||||
| 331 | +```text | |||||||||||||||||
| 332 | +p_cubic = 1 + D + D + C(D,2) + D + D(D−1) + C(D,3) | |||||||||||||||||
| 333 | + = 1 + D + D + D(D−1)/2 + D + D(D−1) + D(D−1)(D−2)/6 | |||||||||||||||||
| 334 | +``` | |||||||||||||||||
| 335 | + | |||||||||||||||||
| 336 | +At D=3: p = 1+3+3+3+3+6+1 = 20. At D=6: p = 1+6+6+15+6+30+20 = 84. | |||||||||||||||||
| 337 | + | |||||||||||||||||
| 338 | +Shape: Fₘ is (n, p_cubic), Fᵤ is (N−n, p_cubic). | |||||||||||||||||
| 339 | + | |||||||||||||||||
| 340 | +The cubic basis captures 3-way interactions (xᵢxⱼxₖ) that matter for high-D kernels where the performance landscape has complex multi-parameter interactions. The x²y terms capture asymmetric interactions: "the curvature in Y0BLOCK depends on core_num." | |||||||||||||||||
| 341 | + | |||||||||||||||||
| 342 | +### 8.7 Parameter count summary | |||||||||||||||||
| 343 | + | |||||||||||||||||
| 344 | +| Basis | p formula | D=1 | D=2 | D=3 | D=4 | D=5 | D=6 | | |||||||||||||||||
| 345 | +|-------|-----------|-----|-----|-----|-----|-----|-----| | |||||||||||||||||
| 346 | +| linear | 1+D | 2 | 3 | 4 | 5 | 6 | 7 | | |||||||||||||||||
| 347 | +| quad | 1+2D | 3 | 5 | 7 | 9 | 11 | 13 | | |||||||||||||||||
| 348 | +| full | 1+2D+C(D,2) | 3 | 6 | 10 | 15 | 21 | 28 | | |||||||||||||||||
| 349 | +| fourier | 1+5D | 6 | 11 | 16 | 21 | 26 | 31 | | |||||||||||||||||
| 350 | +| cubic | see above | 4 | 11 | 20 | 35 | 56 | 84 | | |||||||||||||||||
| 351 | + | |||||||||||||||||
| 352 | +--- | |||||||||||||||||
| 353 | + | |||||||||||||||||
| 354 | +## §9. Mode Selection via Spearman-Ranked LOOCV | |||||||||||||||||
| 355 | + | |||||||||||||||||
| 356 | +**Component 7.** The core estimator. Runs every round on model path. | |||||||||||||||||
| 357 | + | |||||||||||||||||
| 358 | +```text | |||||||||||||||||
| 359 | +inputs: Xₘ_norm (n, D) — normalized measured features | |||||||||||||||||
| 360 | + d (n,) — measured durations | |||||||||||||||||
| 361 | + Xᵤ_norm (N−n, D) — normalized unmeasured features | |||||||||||||||||
| 362 | + D — number of features | |||||||||||||||||
| 363 | +outputs: d̂ (N−n,) — predicted durations for unmeasured | |||||||||||||||||
| 364 | + R̂² (scalar) — coefficient of determination of winning model | |||||||||||||||||
| 365 | + best_mode (string) — name of winning basis type | |||||||||||||||||
| 366 | +feeds: softmax batch selection (d̂) | |||||||||||||||||
| 367 | + marginal voting (d̂ competes for batch allocation) | |||||||||||||||||
| 368 | + convergence check (d̂ for margin criterion) | |||||||||||||||||
| 369 | + temperature adaptation (R̂²) | |||||||||||||||||
| 370 | +``` | |||||||||||||||||
| 371 | + | |||||||||||||||||
| 372 | +This component replaces v6's fixed polynomial mode assignment. Instead of choosing a mode by n/p ratio, we compete up to 5 basis types via leave-one-out cross-validation, scored by Spearman rank correlation. | |||||||||||||||||
| 373 | + | |||||||||||||||||
| 374 | +### 9.1 Step 1: Build candidate set | |||||||||||||||||
| 375 | + | |||||||||||||||||
| 376 | +Always include: linear, quad, full, fourier. Include cubic only if p_cubic < n/2. | |||||||||||||||||
| 377 | + | |||||||||||||||||
| 378 | +The n/2 gate for cubic is the identifiability condition — see §9.5. | |||||||||||||||||
| 379 | + | |||||||||||||||||
| 380 | +### 9.2 Step 2: Fit each candidate model (ridge regression) | |||||||||||||||||
| 381 | + | |||||||||||||||||
| 382 | +For each basis type b in the candidate set: | |||||||||||||||||
| 383 | + | |||||||||||||||||
| 384 | +**Build feature matrix.** Construct Fₘ⁽ᵇ⁾ ∈ ℝ^(n × pᵇ) using the basis expansion from §8. | |||||||||||||||||
| 385 | + | |||||||||||||||||
| 386 | +**Compute Gram matrix with ridge.** | |||||||||||||||||
| 387 | + | |||||||||||||||||
| 388 | +```text | |||||||||||||||||
| 389 | +G⁽ᵇ⁾ = Fₘ⁽ᵇ⁾ᵀ Fₘ⁽ᵇ⁾ + λI | |||||||||||||||||
| 390 | +``` | |||||||||||||||||
| 391 | + | |||||||||||||||||
| 392 | +Shapes: Fₘ⁽ᵇ⁾ᵀ is (pᵇ, n), Fₘ⁽ᵇ⁾ is (n, pᵇ), so G⁽ᵇ⁾ is (pᵇ, pᵇ). λ = 0.1 for all bases (validated as globally Pareto-optimal in exp26). | |||||||||||||||||
| 393 | + | |||||||||||||||||
| 394 | +**Invert Gram matrix.** | |||||||||||||||||
| 395 | + | |||||||||||||||||
| 396 | +```text | |||||||||||||||||
| 397 | +G⁽ᵇ⁾⁻¹ = (Fₘ⁽ᵇ⁾ᵀ Fₘ⁽ᵇ⁾ + λI)⁻¹ shape: (pᵇ, pᵇ) | |||||||||||||||||
| 398 | +``` | |||||||||||||||||
| 399 | + | |||||||||||||||||
| 400 | +Adding λI guarantees invertibility even when features are collinear. Ridge bumps all eigenvalues of FᵀF up by λ, so 1/(σᵢ+λ) replaces 1/σᵢ — weak directions are clamped, strong directions barely change. | |||||||||||||||||
| 401 | + | |||||||||||||||||
| 402 | +Reference: [3] Hoerl & Kennard (1970). Ridge Regression: Biased Estimation for Nonorthogonal Problems. The original ridge regression paper. | |||||||||||||||||
| 403 | + | |||||||||||||||||
| 404 | +**Solve for coefficients.** | |||||||||||||||||
| 405 | + | |||||||||||||||||
| 406 | +```text | |||||||||||||||||
| 407 | +β̂⁽ᵇ⁾ = G⁽ᵇ⁾⁻¹ Fₘ⁽ᵇ⁾ᵀ d shape: (pᵇ,) | |||||||||||||||||
| 408 | +``` | |||||||||||||||||
| 409 | + | |||||||||||||||||
| 410 | +Shapes walkthrough: G⁻¹ is (p, p), Fₘᵀ is (p, n), d is (n,). So Fₘᵀd is (p,) and G⁻¹(Fₘᵀd) is (p,). Each element β̂[k] corresponds to one basis feature. | |||||||||||||||||
| 411 | + | |||||||||||||||||
| 412 | +### 9.2.1 General theory: the normal equation | |||||||||||||||||
| 413 | + | |||||||||||||||||
| 414 | +Given n observations in feature matrix F (n × p) and response vector d (n × 1), ordinary least squares minimizes ||Fβ − d||². The solution is: | |||||||||||||||||
| 415 | + | |||||||||||||||||
| 416 | +```text | |||||||||||||||||
| 417 | +β = (FᵀF)⁻¹ Fᵀd | |||||||||||||||||
| 418 | +``` | |||||||||||||||||
| 419 | + | |||||||||||||||||
| 420 | +F is rectangular (n × p, with n > p). You cannot invert a rectangular matrix. The trick: multiply both sides by Fᵀ (shape p × n): | |||||||||||||||||
| 421 | + | |||||||||||||||||
| 422 | +```text | |||||||||||||||||
| 423 | +FᵀF β = Fᵀd | |||||||||||||||||
| 424 | +``` | |||||||||||||||||
| 425 | + | |||||||||||||||||
| 426 | +Now FᵀF is (p × p) — square. This is the Gram matrix. And Fᵀd is (p,) — the right-hand side. | |||||||||||||||||
| 427 | + | |||||||||||||||||
| 428 | +What does FᵀF mean? Entry (i,j) = dot product of column i and column j of F across all n measured configs. It answers: "how much do basis features i and j overlap in the training data?" If two features are perfectly collinear, the Gram matrix is singular. | |||||||||||||||||
| 429 | + | |||||||||||||||||
| 430 | +What does Fᵀd mean? Entry i = dot product of column i with the observed duration vector. It answers: "how much does this basis feature correlate with the thing we're trying to predict?" | |||||||||||||||||
| 431 | + | |||||||||||||||||
| 432 | +Reference: [4] Strang (2006). Linear Algebra and Its Applications, 4th ed. Ch. 3 (Orthogonality and Least Squares). | |||||||||||||||||
| 433 | + | |||||||||||||||||
| 434 | +Gauss-Markov theorem: among all linear unbiased estimators, OLS has the smallest variance. This justifies the normal equations as the starting point. Ridge regression deliberately introduces bias to reduce variance — trading the Gauss-Markov guarantee for lower total error when the system is ill-conditioned. | |||||||||||||||||
| 435 | + | |||||||||||||||||
| 436 | +Reference: [4] Strang Ch. 4.3; [2] Hastie et al. Ch. 3.2. | |||||||||||||||||
| 437 | + | |||||||||||||||||
| 438 | +### 9.2.2 General theory: ill-conditioning and ridge | |||||||||||||||||
| 439 | + | |||||||||||||||||
| 440 | +FᵀF has an eigendecomposition (spectral theorem for symmetric matrices): | |||||||||||||||||
| 441 | + | |||||||||||||||||
| 442 | +```text | |||||||||||||||||
| 443 | +FᵀF = QΛQᵀ where Λ = diag(σ₁, σ₂, ..., σₚ) | |||||||||||||||||
| 444 | +``` | |||||||||||||||||
| 445 | + | |||||||||||||||||
| 446 | +Reference: [4] Strang Ch. 5. The spectral theorem guarantees real eigenvalues and orthogonal eigenvectors for symmetric matrices. | |||||||||||||||||
| 447 | + | |||||||||||||||||
| 448 | +If any eigenvalue σᵢ is small, 1/σᵢ is huge — that direction in feature space gets amplified enormously. Example: σ₁=500, σ₂=300, σ₃=0.001. Then 1/σ₃ = 1000, dominating the solution. | |||||||||||||||||
| 449 | + | |||||||||||||||||
| 450 | +Reference: [5] Golub & Van Loan (2013). Matrix Computations, 4th ed. Condition numbers and numerical stability. | |||||||||||||||||
| 451 | + | |||||||||||||||||
| 452 | +Ridge regression adds λ to every eigenvalue: | |||||||||||||||||
| 453 | + | |||||||||||||||||
| 454 | +```text | |||||||||||||||||
| 455 | +β = (FᵀF + λI)⁻¹ Fᵀd | |||||||||||||||||
| 456 | +``` | |||||||||||||||||
| 457 | + | |||||||||||||||||
| 458 | +In eigenvalue language: 1/(σᵢ+λ) replaces 1/σᵢ. Bias-variance tradeoff: ridge introduces bias (coefficients are systematically shrunk toward zero) but reduces variance (the solution is stable). For ranking unmeasured configs, low variance matters more than low bias — we need the ranking correct, not the absolute predictions. | |||||||||||||||||
| 459 | + | |||||||||||||||||
| 460 | +Reference: [2] Hastie et al. Ch. 3.4. | |||||||||||||||||
| 461 | + | |||||||||||||||||
| 462 | +### 9.2.3 Why λ=0.1 is globally optimal | |||||||||||||||||
| 463 | + | |||||||||||||||||
| 464 | +Exp26 tested 15 adaptive λ strategies (discrete lookup by n/p ratio, continuous formulas, round-adaptive, fixed sweep). λ=0.1 is the only config with zero regressions across all 35 kernels. | |||||||||||||||||
| 465 | + | |||||||||||||||||
| 466 | +The eigenvalue argument predicts D=6 needs stronger λ and D=1 could tolerate weaker. But D=1 kernels have only 5 discrete core_num values — a quadratic through 5 points overfits at low λ regardless of n/p. The discreteness confounds the n/p heuristic. Uniform λ=0.1 sits at the Pareto optimum. | |||||||||||||||||
| 467 | + | |||||||||||||||||
| 468 | +### 9.3 Step 3: LOOCV via hat matrix | |||||||||||||||||
| 469 | + | |||||||||||||||||
| 470 | +For each candidate basis b, compute leave-one-out cross-validation predictions without refitting. | |||||||||||||||||
| 471 | + | |||||||||||||||||
| 472 | +#### 9.3.1 General theory: leave-one-out cross-validation | |||||||||||||||||
| 473 | + | |||||||||||||||||
| 474 | +LOOCV removes one observation i, refits the model on the remaining n−1, and predicts the held-out observation. Repeating for all n gives n predictions. The LOOCV error is: | |||||||||||||||||
| 475 | + | |||||||||||||||||
| 476 | +```text | |||||||||||||||||
| 477 | +LOOCV_MSE = (1/n) Σᵢ (dᵢ − d̂₋ᵢ)² | |||||||||||||||||
| 478 | +``` | |||||||||||||||||
| 479 | + | |||||||||||||||||
| 480 | +where d̂₋ᵢ is the prediction for observation i from the model trained without i. | |||||||||||||||||
| 481 | + | |||||||||||||||||
| 482 | +Naively, this requires n separate regressions — prohibitively expensive. The hat matrix shortcut avoids all n refits. | |||||||||||||||||
| 483 | + | |||||||||||||||||
| 484 | +Reference: [2] Hastie et al. Ch. 7.10: cross-validation. [15] Allen (1974). "The Relationship Between Variable Selection and Data Augmentation and a Method for Prediction." Technometrics 16(1):125–127. The original PRESS (Predicted Residual Sum of Squares) result. | |||||||||||||||||
| 485 | + | |||||||||||||||||
| 486 | +#### 9.3.2 The hat matrix | |||||||||||||||||
| 487 | + | |||||||||||||||||
| 488 | +The hat matrix H "puts a hat on" y — it maps observed values to fitted values: | |||||||||||||||||
| 489 | + | |||||||||||||||||
| 490 | +```text | |||||||||||||||||
| 491 | +ŷ = Hy where H = F(FᵀF + λI)⁻¹Fᵀ | |||||||||||||||||
| 492 | +``` | |||||||||||||||||
| 493 | + | |||||||||||||||||
| 494 | +Shapes: F is (n, p), (FᵀF+λI)⁻¹ is (p, p), Fᵀ is (p, n). So H is (n, n). | |||||||||||||||||
| 495 | + | |||||||||||||||||
| 496 | +H is symmetric and (for ridge) satisfies 0 < Hᵢᵢ < 1. The diagonal element Hᵢᵢ is the leverage of observation i — how much it influences its own prediction. High leverage = the observation is unusual in feature space and strongly pulls the fit toward itself. | |||||||||||||||||
| 497 | + | |||||||||||||||||
| 498 | +#### 9.3.3 The LOOCV shortcut | |||||||||||||||||
| 499 | + | |||||||||||||||||
| 500 | +The key identity: the leave-one-out prediction for observation i equals: | |||||||||||||||||
| 501 | + | |||||||||||||||||
| 502 | +```text | |||||||||||||||||
| 503 | +d̂₋ᵢ = dᵢ − eᵢ / (1 − Hᵢᵢ) | |||||||||||||||||
| 504 | +``` | |||||||||||||||||
| 505 | + | |||||||||||||||||
| 506 | +where eᵢ = dᵢ − d̂ᵢ is the ordinary residual. Equivalently: | |||||||||||||||||
| 507 | + | |||||||||||||||||
| 508 | +```text | |||||||||||||||||
| 509 | +loocv_predᵢ = dᵢ − eᵢ / (1 − Hᵢᵢ) | |||||||||||||||||
| 510 | +``` | |||||||||||||||||
| 511 | + | |||||||||||||||||
| 512 | +Proof sketch: removing observation i from the training set changes the fit proportionally to how much i influences it (leverage Hᵢᵢ). The denominator (1 − Hᵢᵢ) corrects the residual for this leverage effect. When Hᵢᵢ is high (i is influential), the correction amplifies the residual — the LOO prediction moves further from dᵢ. | |||||||||||||||||
| 513 | + | |||||||||||||||||
| 514 | +Reference: [2] Hastie et al. Ch. 7.10. The derivation follows from the Sherman-Morrison-Woodbury formula applied to the rank-1 update of removing one observation. | |||||||||||||||||
| 515 | + | |||||||||||||||||
| 516 | +#### 9.3.4 Our application | |||||||||||||||||
| 517 | + | |||||||||||||||||
| 518 | +In code: | |||||||||||||||||
| 519 | + | |||||||||||||||||
| 520 | +```text | |||||||||||||||||
| 521 | +H = Fₘ @ G⁻¹ @ Fₘᵀ shape: (n, n) | |||||||||||||||||
| 522 | +h_diag = diag(H) shape: (n,) | |||||||||||||||||
| 523 | +residuals = d − Fₘ @ β̂ shape: (n,) | |||||||||||||||||
| 524 | +denom = max(1 − h_diag, 0.01) shape: (n,), clamped for stability | |||||||||||||||||
| 525 | +loocv_pred = d − residuals / denom shape: (n,) | |||||||||||||||||
| 526 | +``` | |||||||||||||||||
| 527 | + | |||||||||||||||||
| 528 | +The clamping at 0.01 prevents division by zero when a data point has leverage near 1.0 (it lies in a region of feature space where it's the only observation). Cost: one matrix multiply and element-wise operations — no refitting needed. Each basis type gets a vector of n LOOCV predictions at no extra cost beyond the single matrix inversion in step 2. | |||||||||||||||||
| 529 | + | |||||||||||||||||
| 530 | +### 9.4 Step 4: Rank by Spearman | |||||||||||||||||
| 531 | + | |||||||||||||||||
| 532 | +For each basis b, compute Spearman rank correlation between actual durations and LOOCV predictions: | |||||||||||||||||
| 533 | + | |||||||||||||||||
| 534 | +```text | |||||||||||||||||
| 535 | +ρ_s⁽ᵇ⁾ = spearman(d, loocv_pred⁽ᵇ⁾) | |||||||||||||||||
| 536 | +``` | |||||||||||||||||
| 537 | + | |||||||||||||||||
| 538 | +The basis with the highest ρ_s wins. | |||||||||||||||||
| 539 | + | |||||||||||||||||
| 540 | +#### 9.4.1 General theory: Spearman rank correlation | |||||||||||||||||
| 541 | + | |||||||||||||||||
| 542 | +Spearman's ρ converts both variables to ranks, then computes Pearson correlation on the ranks: | |||||||||||||||||
| 543 | + | |||||||||||||||||
| 544 | +```text | |||||||||||||||||
| 545 | +ρ_s = corr(rank(a), rank(b)) | |||||||||||||||||
| 546 | +``` | |||||||||||||||||
| 547 | + | |||||||||||||||||
| 548 | +Equivalently (when no ties): | |||||||||||||||||
| 549 | + | |||||||||||||||||
| 550 | +```text | |||||||||||||||||
| 551 | +ρ_s = 1 − 6·Σ(rₐ − r_b)² / [n(n²−1)] | |||||||||||||||||
| 552 | +``` | |||||||||||||||||
| 553 | + | |||||||||||||||||
| 554 | +where rₐ, r_b are the ranks. |ρ_s| close to 1 = perfect monotonic agreement. |ρ_s| near 0 = no monotonic relationship. | |||||||||||||||||
| 555 | + | |||||||||||||||||
| 556 | +Reference: [13] Spearman (1904). The Proof and Measurement of Association Between Two Things. The original rank correlation paper. Also [2] Hastie et al. Ch. 14.7 for its use in feature screening. | |||||||||||||||||
| 557 | + | |||||||||||||||||
| 558 | +#### 9.4.2 Why Spearman, not MSE (exp57–58) | |||||||||||||||||
| 559 | + | |||||||||||||||||
| 560 | +The key insight: we only need the *ranking* of unmeasured configs to be correct for softmax selection to work, not the absolute predicted durations. | |||||||||||||||||
| 561 | + | |||||||||||||||||
| 562 | +Example: model A predicts [100, 200, 300] for true durations [10, 20, 30] — terrible MSE (10× scale error), perfect ranking, correct softmax behavior (it selects the config predicted fastest, which IS the true fastest). | |||||||||||||||||
| 563 | + | |||||||||||||||||
| 564 | +Model B predicts [15, 25, 12] for true [10, 20, 30] — much better MSE, but wrong ranking (predicts config 3 is fastest when config 1 actually is). Softmax follows the wrong ranking. | |||||||||||||||||
| 565 | + | |||||||||||||||||
| 566 | +MSE penalizes scale errors that don't affect selection quality. Spearman measures exactly what matters — rank agreement. Exp57–58 showed switching from MSE to Spearman as the LOOCV criterion gives a clear improvement. | |||||||||||||||||
| 567 | + | |||||||||||||||||
| 568 | +### 9.5 The identifiability gate for cubic (p < n/2) | |||||||||||||||||
| 569 | + | |||||||||||||||||
| 570 | +The cubic basis only enters the competition when p_cubic < n/2. This prevents overfitting. | |||||||||||||||||
| 571 | + | |||||||||||||||||
| 572 | +#### 9.5.1 General theory: identifiability | |||||||||||||||||
| 573 | + | |||||||||||||||||
| 574 | +A model is identifiable when the data contains enough information to distinguish the true parameters from alternatives. With p parameters and n observations, the effective degrees of freedom for error estimation is n − p. When p > n/2, more than half the degrees of freedom are consumed by parameters — the model is fitting noise as much as signal. | |||||||||||||||||
| 575 | + | |||||||||||||||||
| 576 | +Reference: [2] Hastie et al. Ch. 7.3: effective degrees of freedom and the bias-variance tradeoff. | |||||||||||||||||
| 577 | + | |||||||||||||||||
| 578 | +#### 9.5.2 Our application | |||||||||||||||||
| 579 | + | |||||||||||||||||
| 580 | +Without the gate, cubic wins the LOOCV competition on small n via overfitting: LOOCV residuals look small because the hat matrix diagonal Hᵢᵢ approaches 1.0 (each observation fully determines its own prediction), and the (1 − Hᵢᵢ) correction cannot fully compensate when almost all observations are high-leverage. | |||||||||||||||||
| 581 | + | |||||||||||||||||
| 582 | +With the gate, cubic only enters when there's enough data for genuine 3rd-order structure to be distinguishable from noise. On our data: cubic enters and wins on 4 kernels where it captures real 3-way interactions, stays out on kernels where it would overfit. | |||||||||||||||||
| 583 | + | |||||||||||||||||
| 584 | +Exp63b tested a tighter gate (p < n/3) — too restrictive, cubic never enters on medium-sized kernels where it helps. Exp63d tested applying the gate to all modes — hurts, because linear and quad naturally satisfy p < n/2 at our sample sizes. The gate is cubic-specific. | |||||||||||||||||
| 585 | + | |||||||||||||||||
| 586 | +### 9.6 Winning model prediction | |||||||||||||||||
| 587 | + | |||||||||||||||||
| 588 | +After the competition selects a winner: | |||||||||||||||||
| 589 | + | |||||||||||||||||
| 590 | +```text | |||||||||||||||||
| 591 | +Fᵤ = build_features(Xᵤ_norm, D, best_mode) shape: (N−n, p_best) | |||||||||||||||||
| 592 | +d̂ = Fᵤ @ β̂_best shape: (N−n,) | |||||||||||||||||
| 593 | +``` | |||||||||||||||||
| 594 | + | |||||||||||||||||
| 595 | +R̂² is computed on training data for the winning model: | |||||||||||||||||
| 596 | + | |||||||||||||||||
| 597 | +```text | |||||||||||||||||
| 598 | +SS_res = Σ(dᵢ − Fₘ @ β̂_best)² | |||||||||||||||||
| 599 | +SS_tot = Σ(dᵢ − d̄)² | |||||||||||||||||
| 600 | +R̂² = max(0, min(1, 1 − SS_res / (SS_tot + ε))) | |||||||||||||||||
| 601 | +``` | |||||||||||||||||
| 602 | + | |||||||||||||||||
| 603 | +where ε = 10⁻¹² for numerical safety. R̂² feeds into temperature adaptation (§10), not into mode selection. | |||||||||||||||||
| 604 | + | |||||||||||||||||
| 605 | +### 9.7 Walkthrough: 3-parameter kernel (D=3, n=18, N=119) | |||||||||||||||||
| 606 | + | |||||||||||||||||
| 607 | +**Round 1 after R1 (n=18 measured, 101 unmeasured):** | |||||||||||||||||
| 608 | + | |||||||||||||||||
| 609 | +Build 4 candidates (cubic excluded: p_cubic=20, n/2=9, 20 > 9 → gated): | |||||||||||||||||
| 610 | + | |||||||||||||||||
| 611 | +- linear: Fₘ is (18, 4), p=4, G is (4,4) | |||||||||||||||||
| 612 | +- quad: Fₘ is (18, 7), p=7, G is (7,7) | |||||||||||||||||
| 613 | +- full: Fₘ is (18, 10), p=10, G is (10,10) | |||||||||||||||||
| 614 | +- fourier: Fₘ is (18, 16), p=16, G is (16,16) | |||||||||||||||||
| 615 | + | |||||||||||||||||
| 616 | +For each: solve β̂, compute H diagonal, get loocv_pred, compute ρ_s. | |||||||||||||||||
| 617 | + | |||||||||||||||||
| 618 | +Suppose results: ρ_s(linear)=0.72, ρ_s(quad)=0.78, ρ_s(full)=0.81, ρ_s(fourier)=0.85. | |||||||||||||||||
| 619 | + | |||||||||||||||||
| 620 | +Winner: fourier. Predict: d̂ = Fᵤ_fourier @ β̂_fourier, shape (101,). | |||||||||||||||||
| 621 | + | |||||||||||||||||
| 622 | +**Round 3 (n=42 measured, 77 unmeasured):** | |||||||||||||||||
| 623 | + | |||||||||||||||||
| 624 | +Now cubic enters: p_cubic=20, n/2=21, 20 < 21 → allowed. | |||||||||||||||||
| 625 | + | |||||||||||||||||
| 626 | +5 candidates compete. Suppose ρ_s(cubic)=0.91, beats all others. Winner changes to cubic. | |||||||||||||||||
| 627 | + | |||||||||||||||||
| 628 | +This is the key behavior: the LOOCV competition re-runs every round, and the winning basis can change as data accumulates. | |||||||||||||||||
| 629 | + | |||||||||||||||||
| 630 | +--- | |||||||||||||||||
| 631 | + | |||||||||||||||||
| 632 | +## §10. Temperature Adaptation | |||||||||||||||||
| 633 | + | |||||||||||||||||
| 634 | +**Component 8.** Always runs on model path. | |||||||||||||||||
| 635 | + | |||||||||||||||||
| 636 | +```text | |||||||||||||||||
| 637 | +inputs: R̂² (scalar) — from mode selection winning model | |||||||||||||||||
| 638 | + k_est — number of needle-range configs measured so far | |||||||||||||||||
| 639 | +outputs: τ̂ (scalar) — softmax temperature | |||||||||||||||||
| 640 | +feeds: softmax batch selection (τ̂) | |||||||||||||||||
| 641 | +``` | |||||||||||||||||
| 642 | + | |||||||||||||||||
| 643 | +```text | |||||||||||||||||
| 644 | +τ̂ = τ_min + τ_range × (1 − R̂²) × min(1, k_est / 4) | |||||||||||||||||
| 645 | +``` | |||||||||||||||||
| 646 | + | |||||||||||||||||
| 647 | +where τ_min = 0.3, τ_range = 0.4, k_est = |{j ∈ M : d($j$) ≤ 1.05 × min(d)}|. | |||||||||||||||||
| 648 | + | |||||||||||||||||
| 649 | +Three interacting factors: | |||||||||||||||||
| 650 | + | |||||||||||||||||
| 651 | +1. R̂² (model quality). Bad model (R̂² ≈ 0) → τ̂ high → explore widely. Good model (R̂² ≈ 1) → τ̂ low → exploit predictions. | |||||||||||||||||
| 652 | + | |||||||||||||||||
| 653 | +2. k_est (needle rarity). k_est = estimated number of near-optimal configs found so far. k_est ≤ 3: min(1, k/4) < 1 → squashes the range toward τ_min for tight exploitation ("needles are rare, focus search"). k_est ≥ 4: factor hits 1.0, full R̂²-adaptation. | |||||||||||||||||
| 654 | + | |||||||||||||||||
| 655 | +3. τ_min (floor). Even with a perfect model and rare needles, we keep some stochasticity to avoid deterministic dead ends. | |||||||||||||||||
| 656 | + | |||||||||||||||||
| 657 | +This unified formula (exp49) replaced 3 separate if-branches with zero regressions. | |||||||||||||||||
| 658 | + | |||||||||||||||||
| 659 | +Note: R̂² here comes from the winning basis model's training-set fit, not from the LOOCV Spearman score. Temperature adapts to how well the chosen model explains the data in absolute terms, not to cross-validated rank correlation. | |||||||||||||||||
| 660 | + | |||||||||||||||||
| 661 | +--- | |||||||||||||||||
| 662 | + | |||||||||||||||||
| 663 | +## §11. Softmax Batch Selection | |||||||||||||||||
| 664 | + | |||||||||||||||||
| 665 | +**Component 9a.** Always runs on model path. | |||||||||||||||||
| 666 | + | |||||||||||||||||
| 667 | +```text | |||||||||||||||||
| 668 | +inputs: d̂ (N−n,) — predicted durations from mode selection | |||||||||||||||||
| 669 | + τ̂ (scalar) — temperature from adaptation | |||||||||||||||||
| 670 | + batch_size — number of configs to select | |||||||||||||||||
| 671 | + rng — random state | |||||||||||||||||
| 672 | +outputs: selected_indices (array of local indices into unmeasured set) | |||||||||||||||||
| 673 | +feeds: measure function (configs to benchmark next) | |||||||||||||||||
| 674 | +``` | |||||||||||||||||
| 675 | + | |||||||||||||||||
| 676 | +### 11.1 General theory: softmax (Boltzmann) function | |||||||||||||||||
| 677 | + | |||||||||||||||||
| 678 | +The softmax function converts a vector of scores s = [s₁, ..., sₘ] into a probability distribution: | |||||||||||||||||
| 679 | + | |||||||||||||||||
| 680 | +```text | |||||||||||||||||
| 681 | +pᵢ = exp(sᵢ / τ) / Σⱼ exp(sⱼ / τ) | |||||||||||||||||
| 682 | +``` | |||||||||||||||||
| 683 | + | |||||||||||||||||
| 684 | +where τ > 0 is the temperature. τ → 0: concentrates on argmax (greedy). τ → ∞: uniform. | |||||||||||||||||
| 685 | + | |||||||||||||||||
| 686 | +Reference: [7] Sutton & Barto (2018). Reinforcement Learning, 2nd ed. Ch. 2.3: softmax action selection in bandits. The temperature parameter comes from the Boltzmann distribution in statistical mechanics. | |||||||||||||||||
| 687 | + | |||||||||||||||||
| 688 | +Numerical stability: subtract max(s) before exponentiating. Doesn't change probabilities but keeps exponents ≤ 0. | |||||||||||||||||
| 689 | + | |||||||||||||||||
| 690 | +Reference: [8] Goodfellow, Bengio, Courville (2016). Deep Learning. Ch. 4.1: numerical stability of softmax. | |||||||||||||||||
| 691 | + | |||||||||||||||||
| 692 | +### 11.2 Our application | |||||||||||||||||
| 693 | + | |||||||||||||||||
| 694 | +Input: d̂ vector from §9 (one predicted duration per unmeasured config). We want to select configs with LOW duration. | |||||||||||||||||
| 695 | + | |||||||||||||||||
| 696 | +```text | |||||||||||||||||
| 697 | +step 1: scores sᵢ = −d̂ᵢ (lower duration = higher score) | |||||||||||||||||
| 698 | +step 2: shift: sᵢ ← sᵢ − max(s) (numerical stability) | |||||||||||||||||
| 699 | +step 3: exponentiate: pᵢ = exp(sᵢ / τ̂) shape: (N−n,) | |||||||||||||||||
| 700 | +step 4: clamp: pᵢ = max(pᵢ, 10⁻³⁰⁰) (underflow floor) | |||||||||||||||||
| 701 | +step 5: normalize: pᵢ ← pᵢ / Σpⱼ shape: (N−n,), sums to 1 | |||||||||||||||||
| 702 | +step 6: sample batch_size configs without replacement from this distribution | |||||||||||||||||
| 703 | +``` | |||||||||||||||||
| 704 | + | |||||||||||||||||
| 705 | +### 11.3 Underflow protection | |||||||||||||||||
| 706 | + | |||||||||||||||||
| 707 | +When τ̂ is small and d̂ range is large, exp(−d̂/τ̂) can collapse to 0 for most configs. We clamp at 10⁻³⁰⁰ to prevent all-zero probability vectors. If partial underflow occurs (some but not all probabilities are non-zero), we use a hybrid approach: softmax selection for the non-zero pool, random fill for the zero pool. | |||||||||||||||||
| 708 | + | |||||||||||||||||
| 709 | +### 11.4 Why softmax, not hard top-k | |||||||||||||||||
| 710 | + | |||||||||||||||||
| 711 | +Hard top-k picks the same configs deterministically. If the model's ranking has a 10-position error, the batch misses the needle. Softmax gives the predicted-best HIGH probability but not certainty. Experiment 44 showed top-k was catastrophic: bmm_mul_4 dropped from 96% to 56%. | |||||||||||||||||
| 712 | + | |||||||||||||||||
| 713 | +Reference: [9] Chapelle & Li (2011). Connection between softmax selection and Thompson sampling in bandits. | |||||||||||||||||
| 714 | + | |||||||||||||||||
| 715 | +### 11.5 Batch allocation | |||||||||||||||||
| 716 | + | |||||||||||||||||
| 717 | +If D < 3: softmax gets 100% of the batch. If D ≥ 3: softmax gets 50%, marginal voting (§12) gets the other 50%. The two halves are merged via set union, capped to batch_size. | |||||||||||||||||
| 718 | + | |||||||||||||||||
| 719 | +--- | |||||||||||||||||
| 720 | + | |||||||||||||||||
| 721 | +## §12. Marginal Voting | |||||||||||||||||
| 722 | + | |||||||||||||||||
| 723 | +**Component 9b.** Conditional — activates when D ≥ 3 (MARGINAL_D threshold). | |||||||||||||||||
| 724 | + | |||||||||||||||||
| 725 | +```text | |||||||||||||||||
| 726 | +inputs: X_measured (n, D) — raw (not normalized) features of measured configs | |||||||||||||||||
| 727 | + d (n,) — measured durations | |||||||||||||||||
| 728 | + X_unmeasured (N−n, D) — raw features of unmeasured configs | |||||||||||||||||
| 729 | + D — number of features | |||||||||||||||||
| 730 | +outputs: marginal_scores (N−n,) — additive scores for unmeasured | |||||||||||||||||
| 731 | +feeds: softmax selection with fixed τ = τ_min = 0.3 | |||||||||||||||||
| 732 | + merged into batch (50% allocation) | |||||||||||||||||
| 733 | +``` | |||||||||||||||||
| 734 | + | |||||||||||||||||
| 735 | +### 12.1 The factored additive model | |||||||||||||||||
| 736 | + | |||||||||||||||||
| 737 | +```text | |||||||||||||||||
| 738 | +score(x) = Σ_{d=1}^{D} f_d(x_d) | |||||||||||||||||
| 739 | +``` | |||||||||||||||||
| 740 | + | |||||||||||||||||
| 741 | +where f_d(v) = mean measured duration of configs with feature d equal to v: | |||||||||||||||||
| 742 | + | |||||||||||||||||
| 743 | +```text | |||||||||||||||||
| 744 | +f_d(v) = mean({d(j) : j ∈ M, X[j,d] = v}) | |||||||||||||||||
| 745 | +``` | |||||||||||||||||
| 746 | + | |||||||||||||||||
| 747 | +If value v hasn't been seen in the measured set, f_d(v) = mean(d) (global mean fallback). | |||||||||||||||||
| 748 | + | |||||||||||||||||
| 749 | +Shape: for each unmeasured config, sum D lookup values to get one score. Output: (N−n,) vector. | |||||||||||||||||
| 750 | + | |||||||||||||||||
| 751 | +### 12.2 Why this complements the basis model | |||||||||||||||||
| 752 | + | |||||||||||||||||
| 753 | +The polynomial/fourier/cubic models capture interactions (xᵢxⱼ) but need enough data to estimate cross-term coefficients. The marginal scorer captures "X1BLOCK=32 is always slow" without needing interactions — it's a strictly additive model. For high-D kernels where the basis model is underdetermined, the marginal path provides a safety net. | |||||||||||||||||
| 754 | + | |||||||||||||||||
| 755 | +The marginal channel uses fixed τ = τ_min = 0.3 (always exploitative) because it has no model confidence to adapt — it's a simple lookup. | |||||||||||||||||
| 756 | + | |||||||||||||||||
| 757 | +Experiment 36 showed this improved bmm_mul_4 from 88% to 94%. | |||||||||||||||||
| 758 | + | |||||||||||||||||
| 759 | +--- | |||||||||||||||||
| 760 | + | |||||||||||||||||
| 761 | +## §13. Convergence Check | |||||||||||||||||
| 762 | + | |||||||||||||||||
| 763 | +**Component 10.** Always runs on model path, after each round. | |||||||||||||||||
| 764 | + | |||||||||||||||||
| 765 | +```text | |||||||||||||||||
| 766 | +inputs: d (n,) — measured durations (from common data) | |||||||||||||||||
| 767 | + d̂ (N−n,) — predicted durations (from mode selection) | |||||||||||||||||
| 768 | + best_measured — min(d) | |||||||||||||||||
| 769 | + prev_hist — histogram from previous round (or None) | |||||||||||||||||
| 770 | +outputs: stop (boolean) — whether to terminate | |||||||||||||||||
| 771 | + curr_hist — current histogram (for next round) | |||||||||||||||||
| 772 | +feeds: loop decision: stop → return best, continue → next round | |||||||||||||||||
| 773 | +``` | |||||||||||||||||
| 774 | + | |||||||||||||||||
| 775 | +Two independent early-stop criteria (either triggers stop): | |||||||||||||||||
| 776 | + | |||||||||||||||||
| 777 | +### 13.1 Signal 1: Estimation margin | |||||||||||||||||
| 778 | + | |||||||||||||||||
| 779 | +```text | |||||||||||||||||
| 780 | +est_margin = min(d̂_unmeasured) / min(d_measured) | |||||||||||||||||
| 781 | +``` | |||||||||||||||||
| 782 | + | |||||||||||||||||
| 783 | +If est_margin > 2.5, stop. The model predicts nothing unmeasured is even close to what we've found. Catches easy kernels where the optimum appears in R1. | |||||||||||||||||
| 784 | + | |||||||||||||||||
| 785 | +Reads from: mode selection output (d̂) + common data (d). | |||||||||||||||||
| 786 | + | |||||||||||||||||
| 787 | +### 13.2 Signal 2: Histogram L₁ distance | |||||||||||||||||
| 788 | + | |||||||||||||||||
| 789 | +```text | |||||||||||||||||
| 790 | +bins = linspace(min(d), max(d), 11) | |||||||||||||||||
| 791 | +hist_now = histogram(d, bins=bins, density=True) shape: (10,) | |||||||||||||||||
| 792 | +hist_now = hist_now / sum(hist_now) normalized | |||||||||||||||||
| 793 | +hist_L1 = Σᵢ |hist_now[i] − prev_hist[i]| | |||||||||||||||||
| 794 | +``` | |||||||||||||||||
| 795 | + | |||||||||||||||||
| 796 | +If hist_L1 < 0.01, less than 1% of probability mass shifted between rounds — the distribution of measured durations has stabilized. | |||||||||||||||||
| 797 | + | |||||||||||||||||
| 798 | +Reads from: common data (d, measured durations only). Does NOT use the estimator. | |||||||||||||||||
| 799 | + | |||||||||||||||||
| 800 | +### 13.3 General theory: L₁ distance (total variation) | |||||||||||||||||
| 801 | + | |||||||||||||||||
| 802 | +For two discrete distributions P and Q over the same bins: | |||||||||||||||||
| 803 | + | |||||||||||||||||
| 804 | +```text | |||||||||||||||||
| 805 | +d_L1(P, Q) = Σᵢ |Pᵢ − Qᵢ| | |||||||||||||||||
| 806 | +``` | |||||||||||||||||
| 807 | + | |||||||||||||||||
| 808 | +This equals twice the total variation distance. Intuitive meaning: total probability mass that "moved" between bins. | |||||||||||||||||
| 809 | + | |||||||||||||||||
| 810 | +Reference: [10] Levin, Peres & Wilmer (2009). Markov Chains and Mixing Times. Ch. 4: total variation distance. | |||||||||||||||||
| 811 | + | |||||||||||||||||
| 812 | +### 13.4 Why L₁, not L₂ | |||||||||||||||||
| 813 | + | |||||||||||||||||
| 814 | +L₁ measures total change. L₂ is dominated by the largest single-bin change. For "did the distribution change overall," L₁ is more informative. In our data (11 bins): L₁/L₂ ratio median 2.07 (range 1.42–3.05). L₁<0.01 catches 16 correct stops vs L₂<0.005's 14, both zero false. | |||||||||||||||||
| 815 | + | |||||||||||||||||
| 816 | +### 13.5 Threshold selection | |||||||||||||||||
| 817 | + | |||||||||||||||||
| 818 | +Thresholds found by brute-force sweep (exp25b): margin>2.5 OR L₁<0.01. Union: 118 correct stops, zero false, across 700 runs. Hard kernels correctly run all 5 rounds. | |||||||||||||||||
| 819 | + | |||||||||||||||||
| 820 | +### 13.6 Where error metrics enter the flow | |||||||||||||||||
| 821 | + | |||||||||||||||||
| 822 | +Per-round flow annotated with which metric is active: | |||||||||||||||||
| 823 | + | |||||||||||||||||
| 824 | +1. R1: blind stratified sample. No model, no error metrics. | |||||||||||||||||
| 825 | +2. Mode selection: fit models, LOOCV → Spearman ρ_s (mode competition). Compute R̂² (diagnostic). | |||||||||||||||||
| 826 | +3. Temperature adaptation: τ̂ from R̂². R̂²'s influence ends here. | |||||||||||||||||
| 827 | +4. Batch selection: softmax(−d̂ / τ̂). Measure selected configs. | |||||||||||||||||
| 828 | +5. Convergence: (a) margin from d̂ and d. (b) histogram L₁ from d only. | |||||||||||||||||
| 829 | +6. If not converged → loop to step 2 with expanded M. | |||||||||||||||||
| 830 | + | |||||||||||||||||
| 831 | +Summary: ρ_s lives in step 2 (mode competition). R̂² lives in steps 2–3. L₁ lives in step 5. They never interact directly. | |||||||||||||||||
| 832 | + | |||||||||||||||||
| 833 | +--- | |||||||||||||||||
| 834 | + | |||||||||||||||||
| 835 | +## §14. R² Diagnostic | |||||||||||||||||
| 836 | + | |||||||||||||||||
| 837 | +**Not a decision component.** Computed inside mode selection, used only by temperature adaptation. | |||||||||||||||||
| 838 | + | |||||||||||||||||
| 839 | +```text | |||||||||||||||||
| 840 | +R̂² = 1 − Σ(dᵢ − d̂ᵢ)² / Σ(dᵢ − d̄)² | |||||||||||||||||
| 841 | +``` | |||||||||||||||||
| 842 | + | |||||||||||||||||
| 843 | +Computed on measured configs (in-sample), for the winning basis model. R̂² = 1.0: perfect fit. R̂² = 0.0: no better than predicting the mean. | |||||||||||||||||
| 844 | + | |||||||||||||||||
| 845 | +Reference: [12] Draper & Smith (1998). Applied Regression Analysis, 3rd ed. Wiley. | |||||||||||||||||
| 846 | + | |||||||||||||||||
| 847 | +Key values from our data: clone_5 (D=1) R̂²≈0.45, no_t_2 (D=2) R̂²≈0.05, add_layer_norm_0 (D=6) R̂²≈0.90. Softmax selection doesn't need accurate d̂ values, just approximately correct ranking. R̂²=0.45 still produces 90% catch. | |||||||||||||||||
| 848 | + | |||||||||||||||||
| 849 | +We tested using R̂² to switch estimators in exp23 — it regressed. R̂² is diagnostic only. | |||||||||||||||||
| 850 | + | |||||||||||||||||
| 851 | +--- | |||||||||||||||||
| 852 | + | |||||||||||||||||
| 853 | +## §15. The Feedback Loop | |||||||||||||||||
| 854 | + | |||||||||||||||||
| 855 | +After measuring a batch, the algorithm loops back to mode selection (§9) with the expanded measured set. Key properties: | |||||||||||||||||
| 856 | + | |||||||||||||||||
| 857 | +- The LOOCV competition re-runs every round — the winning basis can change as data accumulates (e.g., linear → fourier → cubic as n grows past the identifiability threshold) | |||||||||||||||||
| 858 | +- The cubic gate may open in later rounds when n crosses the p_cubic/2 threshold | |||||||||||||||||
| 859 | +- R̂² typically improves each round, pushing τ̂ down (more exploitation) | |||||||||||||||||
| 860 | +- Convergence criteria are re-checked every round — easy kernels exit early | |||||||||||||||||
| 861 | + | |||||||||||||||||
| 862 | +The loop runs up to FASTA_MAX_ROUNDS=5 or until budget is exhausted or convergence triggers, whichever comes first. | |||||||||||||||||
| 863 | + | |||||||||||||||||
| 864 | +--- | |||||||||||||||||
| 865 | + | |||||||||||||||||
| 866 | +## §16. Component Data Flow Summary | |||||||||||||||||
| 867 | + | |||||||||||||||||
| 868 | +### Execution order per kernel | |||||||||||||||||
| 869 | + | |||||||||||||||||
| 870 | +```text | |||||||||||||||||
| 871 | +[1] Feature Extraction | |||||||||||||||||
| 872 | + X (N,D), names | |||||||||||||||||
| 873 | + │ | |||||||||||||||||
| 874 | + ├──→ [2] Stratified R1 Sampling ──→ measure(r1_idx) ──→ d_r1 | |||||||||||||||||
| 875 | + ├──→ [3] Budget Allocation ──→ total_budget | |||||||||||||||||
| 876 | + └──→ [4] Viability Assessment | |||||||||||||||||
| 877 | + │ | |||||||||||||||||
| 878 | + ├── path='coverage' ──→ [5] Coverage Fallback ──→ return best | |||||||||||||||||
| 879 | + │ | |||||||||||||||||
| 880 | + └── path='model' ──→ LOOP (up to 4 rounds): | |||||||||||||||||
| 881 | + │ | |||||||||||||||||
| 882 | + ├──→ [6] Normalization + Basis Construction | |||||||||||||||||
| 883 | + │ Fₘ (n,p) for each candidate basis | |||||||||||||||||
| 884 | + │ Fᵤ (N-n,p) for winning basis | |||||||||||||||||
| 885 | + │ | |||||||||||||||||
| 886 | + ├──→ [7] Mode Selection (§9) | |||||||||||||||||
| 887 | + │ fit all candidates → LOOCV → Spearman | |||||||||||||||||
| 888 | + │ outputs: d̂ (N-n,), R̂², best_mode | |||||||||||||||||
| 889 | + │ │ | |||||||||||||||||
| 890 | + │ ├──→ [8] Temperature Adaptation | |||||||||||||||||
| 891 | + │ │ τ̂ from R̂² and k_est | |||||||||||||||||
| 892 | + │ │ | |||||||||||||||||
| 893 | + │ ├──→ [9a] Softmax Selection | |||||||||||||||||
| 894 | + │ │ p = softmax(-d̂/τ̂) | |||||||||||||||||
| 895 | + │ │ sample batch (50-100%) | |||||||||||||||||
| 896 | + │ │ | |||||||||||||||||
| 897 | + │ ├──→ [9b] Marginal Voting (if D≥3) | |||||||||||||||||
| 898 | + │ │ additive scores → softmax | |||||||||||||||||
| 899 | + │ │ sample batch (0-50%) | |||||||||||||||||
| 900 | + │ │ | |||||||||||||||||
| 901 | + │ └──→ [10] Convergence Check | |||||||||||||||||
| 902 | + │ margin from d̂, L₁ from d | |||||||||||||||||
| 903 | + │ │ | |||||||||||||||||
| 904 | + │ ├── stop=True → return best | |||||||||||||||||
| 905 | + │ └── stop=False → measure batch | |||||||||||||||||
| 906 | + │ expand M | |||||||||||||||||
| 907 | + │ ↑ loop back | |||||||||||||||||
| 908 | + └──────────────────────────────────┘ | |||||||||||||||||
| 909 | +``` | |||||||||||||||||
| 910 | + | |||||||||||||||||
| 911 | +### Data shapes through the pipeline (D=3, N=119 example) | |||||||||||||||||
| 912 | + | |||||||||||||||||
| 913 | +| Stage | Object | Shape | Notes | | |||||||||||||||||
| 914 | +|-------|--------|-------|-------| | |||||||||||||||||
| 915 | +| Feature extraction | X | (119, 3) | raw features | | |||||||||||||||||
| 916 | +| R1 sampling | r1_idx | (18,) | 15% of 119 | | |||||||||||||||||
| 917 | +| After R1 | Xₘ | (18, 3) | measured features | | |||||||||||||||||
| 918 | +| After R1 | Xᵤ | (101, 3) | unmeasured features | | |||||||||||||||||
| 919 | +| After R1 | d | (18,) | measured durations | | |||||||||||||||||
| 920 | +| Normalization | Xₘ_norm | (18, 3) | [0,1] scaled | | |||||||||||||||||
| 921 | +| Normalization | Xᵤ_norm | (101, 3) | [0,1] scaled | | |||||||||||||||||
| 922 | +| Basis (linear) | Fₘ | (18, 4) | p=4 | | |||||||||||||||||
| 923 | +| Basis (quad) | Fₘ | (18, 7) | p=7 | | |||||||||||||||||
| 924 | +| Basis (full) | Fₘ | (18, 10) | p=10 | | |||||||||||||||||
| 925 | +| Basis (fourier) | Fₘ | (18, 16) | p=16 | | |||||||||||||||||
| 926 | +| Basis (cubic) | — | gated out | p=20 > n/2=9 | | |||||||||||||||||
| 927 | +| Gram matrix | G | (p, p) | per candidate | | |||||||||||||||||
| 928 | +| Gram inverse | G⁻¹ | (p, p) | per candidate | | |||||||||||||||||
| 929 | +| Coefficients | β̂ | (p,) | per candidate | | |||||||||||||||||
| 930 | +| Hat matrix | H | (18, 18) | per candidate | | |||||||||||||||||
| 931 | +| Hat diagonal | h_diag | (18,) | leverage values | | |||||||||||||||||
| 932 | +| LOOCV pred | loocv_pred | (18,) | per candidate | | |||||||||||||||||
| 933 | +| Spearman | ρ_s | scalar | per candidate | | |||||||||||||||||
| 934 | +| Winner prediction | d̂ | (101,) | for unmeasured | | |||||||||||||||||
| 935 | +| R̂² | scalar | | winning model | | |||||||||||||||||
| 936 | +| τ̂ | scalar | | temperature | | |||||||||||||||||
| 937 | +| Softmax probs | p | (101,) | selection weights | | |||||||||||||||||
| 938 | +| Batch indices | sel | (~7,) | per round | | |||||||||||||||||
| 939 | +| After round 1 | d | (25,) | expanded | | |||||||||||||||||
| 940 | +| After round 1 | Xₘ | (25, 3) | expanded | | |||||||||||||||||
| 941 | +| After round 1 | Xᵤ | (94, 3) | shrunk | | |||||||||||||||||
| 942 | + | |||||||||||||||||
| 943 | +### Input/output connectivity matrix | |||||||||||||||||
| 944 | + | |||||||||||||||||
| 945 | +| Component | Reads from | Writes to | | |||||||||||||||||
| 946 | +|-----------|-----------|-----------| | |||||||||||||||||
| 947 | +| Feature extraction | configs | X, D, names | | |||||||||||||||||
| 948 | +| R1 sampling | X, r₁ size | r1_idx | | |||||||||||||||||
| 949 | +| Budget allocation | N, D, r₁ | total_budget | | |||||||||||||||||
| 950 | +| Viability | n, D | path (model/coverage) | | |||||||||||||||||
| 951 | +| Coverage fallback | unmeasured set | best_idx (terminates) | | |||||||||||||||||
| 952 | +| Basis construction | Xₘ_norm, Xᵤ_norm, mode | Fₘ, Fᵤ | | |||||||||||||||||
| 953 | +| Mode selection | Fₘ, d, Fᵤ for each candidate | d̂, R̂², best_mode | | |||||||||||||||||
| 954 | +| Temperature | R̂², k_est | τ̂ | | |||||||||||||||||
| 955 | +| Softmax selection | d̂, τ̂, batch_size | selected indices | | |||||||||||||||||
| 956 | +| Marginal voting | X_measured, d, X_unmeasured, D | marginal scores → indices | | |||||||||||||||||
| 957 | +| Convergence | d̂ (from estimator), d (measured), prev_hist | stop boolean | | |||||||||||||||||
| 958 | + | |||||||||||||||||
| 959 | +--- | |||||||||||||||||
| 960 | + | |||||||||||||||||
| 961 | +## §17. Parameter Summary | |||||||||||||||||
| 962 | + | |||||||||||||||||
| 963 | +All independently validated constants: | |||||||||||||||||
| 964 | + | |||||||||||||||||
| 965 | +| Parameter | Value | Controls | Validated in | | |||||||||||||||||
| 966 | +|-----------|-------|----------|-------------| | |||||||||||||||||
| 967 | +| FASTA_R1_PCT | 0.15 | blind exploration fraction | exp25: 10% too aggressive, 25% wastes | | |||||||||||||||||
| 968 | +| R1_FLOOR_PCT | 0.10 | minimum R1 fraction | lower bound for small kernels | | |||||||||||||||||
| 969 | +| FASTA_BASE_BUDGET | 0.35 | total measurement budget | exp11: savings-catch sweet spot | | |||||||||||||||||
| 970 | +| FASTA_HIGH_BUDGET | 0.50 | budget for borderline kernels | exp45: closed last regressions | | |||||||||||||||||
| 971 | +| HIGH_D_THRESH | 4 | D threshold for high budget | exp47 | | |||||||||||||||||
| 972 | +| HIGH_NP_THRESH | 5 | n/p threshold for high budget | exp49: smooth function fails | | |||||||||||||||||
| 973 | +| MIN_VIABLE_NP | 2 | minimum n/p for any model | exp42: coverage gate | | |||||||||||||||||
| 974 | +| RIDGE_LAMBDA | 0.1 | regularization strength | exp26: global Pareto optimum | | |||||||||||||||||
| 975 | +| TAU_MIN | 0.3 | softmax τ̂ floor | exp44: greedy is catastrophic | | |||||||||||||||||
| 976 | +| TAU_RANGE | 0.4 | softmax τ̂ range | exp49: unified formula | | |||||||||||||||||
| 977 | +| MARGINAL_D | 3 | D threshold for marginal voting | exp36 | | |||||||||||||||||
| 978 | +| FASTA_MAX_ROUNDS | 5 | maximum R2 iterations | exp20c: peaks at 5 | | |||||||||||||||||
| 979 | +| MARGIN_CONV | 2.5 | convergence margin threshold | exp25b | | |||||||||||||||||
| 980 | +| L1_CONV | 0.01 | histogram L₁ threshold | exp27 | | |||||||||||||||||
| 981 | +| HIST_BINS | 11 | histogram resolution | exp22 | | |||||||||||||||||
| 982 | +| K_TOLERANCE | 1.05 | definition of "near-optimal" | exp15 | | |||||||||||||||||
| 983 | +| UNDERFLOW_FLOOR | 10⁻³⁰⁰ | softmax numerical safety | — | | |||||||||||||||||
🔵 Low Priority 变更文件: dynamic_filter_math_and_components.md §17 参数汇总表(第 961-983 行)。 数学文档中列出的多个参数默认值与生产代码实际默认值不一致:
此外,§15(第 862 行)也使用了 FASTA_MAX_ROUNDS=5。 影响:数学文档作为算法的权威参考,如果参数值与实际运行的不一致,会导致:
建议:更新数学文档 §17 及 §15 中的参数值,使其与 dynamic_filter_config.py 和 dynamic_filter_algo.py 中的实际默认值一致,或在文档开头声明这些是实验值。 ![]() ![]() | ||||||||||||||||||
| 984 | + | |||||||||||||||||
| 985 | +The cubic identifiability gate (p < n/2) is not a tunable constant — it follows from the bias-variance tradeoff (§9.5). | |||||||||||||||||
| 986 | + | |||||||||||||||||
| 987 | +--- | |||||||||||||||||
| 988 | + | |||||||||||||||||
| 989 | +## §A. API and Integration | |||||||||||||||||
| 990 | + | |||||||||||||||||
| 991 | +### A.1 `fasta_algo` public functions | |||||||||||||||||
| 992 | + | |||||||||||||||||
| 993 | +15 functions. All are pure math — no I/O, no logging, no framework dependencies. A production integration wraps these into a caller-driven loop (see A.2b). | |||||||||||||||||
| 994 | + | |||||||||||||||||
| 995 | +**Feature preparation:** | |||||||||||||||||
| 996 | + | |||||||||||||||||
| 997 | +| Function | Signature | Returns | Shape | | |||||||||||||||||
| 998 | +|----------|-----------|---------|-------| | |||||||||||||||||
| 999 | +| `extract_features(configs)` | list of config objects | X (N, D), names (list[str]) | scans kwargs, keeps numeric varying columns | | |||||||||||||||||
| 1000 | +| `build_features(X, D, mode)` | dispatcher | F (len(X), p) | mode ∈ {linear, quad, full, fourier, cubic} | | |||||||||||||||||
| 1001 | +| `build_poly_features(X, D, mode)` | linear/quad/full construction | F (len(X), p) | | | |||||||||||||||||
| 1002 | +| `build_fourier_features(X, D)` | fourier construction | F (len(X), 1+5D) | | | |||||||||||||||||
| 1003 | +| `build_cubic_features(X, D)` | cubic construction | F (len(X), p_cubic) | | | |||||||||||||||||
| 1004 | +| `cubic_param_count(D)` | parameter count | int | used by identifiability gate | | |||||||||||||||||
| 1005 | + | |||||||||||||||||
| 1006 | +**Sampling and viability:** | |||||||||||||||||
| 1007 | + | |||||||||||||||||
| 1008 | +| Function | Signature | Returns | | |||||||||||||||||
| 1009 | +|----------|-----------|---------| | |||||||||||||||||
| 1010 | +| `stratified_sample(X, n_sample, rng)` | most-unique-first R1 sampling | list of indices | | |||||||||||||||||
| 1011 | +| `assess_viability(n_samples, D)` | binary gate: model vs coverage | (path, poly_mode) | | |||||||||||||||||
| 1012 | + | |||||||||||||||||
| 1013 | +**Core estimator:** | |||||||||||||||||
| 1014 | + | |||||||||||||||||
| 1015 | +| Function | Signature | Returns | Shape | | |||||||||||||||||
| 1016 | +|----------|-----------|---------|-------| | |||||||||||||||||
| 1017 | +| `select_mode_and_predict(Xm_norm, dm, Xu_norm, D)` | compete bases via Spearman LOOCV | (d̂, R̂², best_mode) | d̂: (len(Xu_norm),) | | |||||||||||||||||
| 1018 | +| `numpy_spearman(a, b)` | Spearman rank correlation | float in [−1, 1] | returns 0.0 if n < 3 | | |||||||||||||||||
| 1019 | + | |||||||||||||||||
| 1020 | +**Batch selection:** | |||||||||||||||||
| 1021 | + | |||||||||||||||||
| 1022 | +| Function | Signature | Returns | | |||||||||||||||||
| 1023 | +|----------|-----------|---------| | |||||||||||||||||
| 1024 | +| `softmax_select(scores, tau, batch_size, rng)` | softmax sampling without replacement | array of local indices | | |||||||||||||||||
| 1025 | +| `marginal_scores(X_measured, d_measured, X_unmeasured, D)` | per-dim additive scoring | (len(X_unmeasured),) | | |||||||||||||||||
| 1026 | + | |||||||||||||||||
| 1027 | +**Temperature and convergence:** | |||||||||||||||||
| 1028 | + | |||||||||||||||||
| 1029 | +| Function | Signature | Returns | | |||||||||||||||||
| 1030 | +|----------|-----------|---------| | |||||||||||||||||
| 1031 | +| `compute_tau(r_sq, k_est)` | τ̂ from model quality + needle rarity | float | | |||||||||||||||||
| 1032 | +| `should_stop(dm_array, best_measured, d_hat_unmeasured, prev_hist)` | margin + histogram L₁ | (stop: bool, hist: array) | | |||||||||||||||||
| 1033 | + | |||||||||||||||||
| 1034 | +**Entry point:** | |||||||||||||||||
| 1035 | + | |||||||||||||||||
| 1036 | +| Function | Signature | Returns | | |||||||||||||||||
| 1037 | +|----------|-----------|---------| | |||||||||||||||||
| 1038 | +| `run(X, measure, rng)` | full algorithm loop | (best_idx, best_dur, n_rounds, n_measured, last_mode) | | |||||||||||||||||
| 1039 | + | |||||||||||||||||
| 1040 | +`measure` is either a callable `measure(indices) → list[float]` or an ndarray (N,) with ground truth durations. When it's an array, `run()` wraps it as a lookup internally. | |||||||||||||||||
| 1041 | + | |||||||||||||||||
| 1042 | +### A.2 What happens inside `run()` | |||||||||||||||||
| 1043 | + | |||||||||||||||||
| 1044 | +`run()` drives the full loop: R1 → viability → model/coverage path → iterate → return best. | |||||||||||||||||
| 1045 | + | |||||||||||||||||
| 1046 | +```python | |||||||||||||||||
| 1047 | +run(X, measure, rng): | |||||||||||||||||
| 1048 | + | |||||||||||||||||
| 1049 | + phase 1 — setup (runs once) | |||||||||||||||||
| 1050 | + r1_size = max(15%·N, 10%·N), clamped to N | |||||||||||||||||
| 1051 | + p_lin = 1 + D | |||||||||||||||||
| 1052 | + if D ≥ 4 AND r1_size / p_lin < 5: | |||||||||||||||||
| 1053 | + total_budget = 50%·N | |||||||||||||||||
| 1054 | + else: | |||||||||||||||||
| 1055 | + total_budget = 35%·N | |||||||||||||||||
| 1056 | + r1_idx = stratified_sample(X, r1_size, rng) | |||||||||||||||||
| 1057 | + r1_durations = measure(r1_idx) # blind benchmark | |||||||||||||||||
| 1058 | + M = set(r1_idx), U = {0..N−1} − M | |||||||||||||||||
| 1059 | + measured_d = {idx: dur for r1} | |||||||||||||||||
| 1060 | + | |||||||||||||||||
| 1061 | + phase 2 — viability gate (runs once) | |||||||||||||||||
| 1062 | + path, _ = assess_viability(|M|, D) | |||||||||||||||||
| 1063 | + if path == 'coverage': | |||||||||||||||||
| 1064 | + sample remaining budget randomly from U | |||||||||||||||||
| 1065 | + measure those, return best # no model, done | |||||||||||||||||
| 1066 | + | |||||||||||||||||
| 1067 | + phase 3 — normalize (runs once) | |||||||||||||||||
| 1068 | + xmin, xmax per column across all N configs | |||||||||||||||||
| 1069 | + Xn = (X − xmin) / (xmax − xmin) shape: (N, D), all in [0,1] | |||||||||||||||||
| 1070 | + | |||||||||||||||||
| 1071 | + phase 4 — model loop | |||||||||||||||||
| 1072 | + for round 1..4: | |||||||||||||||||
| 1073 | + refine(state, latest_durations) # see A.2b | |||||||||||||||||
| 1074 | + return best | |||||||||||||||||
| 1075 | +``` | |||||||||||||||||
| 1076 | + | |||||||||||||||||
| 1077 | +In the target design, `run()` calls `refine()` internally — the same `refine()` that production uses. This eliminates duplicated loop logic: | |||||||||||||||||
| 1078 | + | |||||||||||||||||
| 1079 | +```python | |||||||||||||||||
| 1080 | +run(X, measure, rng): | |||||||||||||||||
| 1081 | + state = init_state(X, rng) # phase 1–3: features, R1, normalize, viability | |||||||||||||||||
| 1082 | + batch_idx = state.r1_indices | |||||||||||||||||
| 1083 | + while batch_idx: | |||||||||||||||||
| 1084 | + durations = measure(batch_idx) | |||||||||||||||||
| 1085 | + batch_idx = refine(state, durations) # phase 4: one iteration | |||||||||||||||||
| 1086 | + return state.best_idx, state.best_dur, ... | |||||||||||||||||
| 1087 | +``` | |||||||||||||||||
| 1088 | + | |||||||||||||||||
| 1089 | +`refine()` becomes a module-level function taking explicit state. Both `run()` and production call it. One code path for the algorithm. | |||||||||||||||||
| 1090 | + | |||||||||||||||||
| 1091 | +### A.2b What happens inside `refine()` | |||||||||||||||||
| 1092 | + | |||||||||||||||||
| 1093 | +`refine()` is one iteration of the model loop. `run()` calls it internally with durations from its `measure()` callback. A production integration calls it directly with compile+benchmark results. Same function, two callers. | |||||||||||||||||
| 1094 | + | |||||||||||||||||
| 1095 | +```text | |||||||||||||||||
| 1096 | +refine(state, durations): | |||||||||||||||||
| 1097 | + step 1 — absorb results | |||||||||||||||||
| 1098 | + for each (index, duration) in zip(state.last_batch, durations): | |||||||||||||||||
| 1099 | + add to M, remove from U | |||||||||||||||||
| 1100 | + update best | |||||||||||||||||
| 1101 | + n = |M| | |||||||||||||||||
| 1102 | + | |||||||||||||||||
| 1103 | + step 2 — terminal check | |||||||||||||||||
| 1104 | + if U is empty → return [] | |||||||||||||||||
| 1105 | + | |||||||||||||||||
| 1106 | + step 3 — branch by path | |||||||||||||||||
| 1107 | + if path == 'coverage': | |||||||||||||||||
| 1108 | + pick remaining budget randomly from U → return final batch | |||||||||||||||||
| 1109 | + (one call, then done) | |||||||||||||||||
| 1110 | + | |||||||||||||||||
| 1111 | + if path == 'model': | |||||||||||||||||
| 1112 | + continue to step 4 | |||||||||||||||||
| 1113 | + | |||||||||||||||||
| 1114 | + step 4 — model iteration | |||||||||||||||||
| 1115 | + if round ≥ FASTA_MAX_ROUNDS (5) → return [] | |||||||||||||||||
| 1116 | + | |||||||||||||||||
| 1117 | + 4a. rebuild from current M, U | |||||||||||||||||
| 1118 | + Xm = Xn[sorted(M)] shape: (n, D) — all measured so far | |||||||||||||||||
| 1119 | + dm = durations[sorted(M)] shape: (n,) | |||||||||||||||||
| 1120 | + Xu = Xn[sorted(U)] shape: (N−n, D) | |||||||||||||||||
| 1121 | + | |||||||||||||||||
| 1122 | + 4b. compete basis models (§9) | |||||||||||||||||
| 1123 | + d̂, R̂², mode = select_mode_and_predict(Xm, dm, Xu, D) | |||||||||||||||||
| 1124 | + | |||||||||||||||||
| 1125 | + internally: build candidate set {linear, quad, full, fourier} | |||||||||||||||||
| 1126 | + + cubic if p_cubic < n/2 | |||||||||||||||||
| 1127 | + for each candidate: | |||||||||||||||||
| 1128 | + Fm = build_features(Xm, D, mode) shape: (n, p) | |||||||||||||||||
| 1129 | + G = Fm'Fm + λI shape: (p, p) | |||||||||||||||||
| 1130 | + β̂ = G⁻¹ Fm' dm shape: (p,) | |||||||||||||||||
| 1131 | + H = Fm G⁻¹ Fm' shape: (n, n) | |||||||||||||||||
| 1132 | + loocv_pred = dm − residuals / (1 − diag(H)) | |||||||||||||||||
| 1133 | + ρ_s = spearman(dm, loocv_pred) | |||||||||||||||||
| 1134 | + winner = argmax(ρ_s) | |||||||||||||||||
| 1135 | + d̂ = build_features(Xu, D, winner) @ β̂_winner | |||||||||||||||||
| 1136 | + | |||||||||||||||||
| 1137 | + 4c. check convergence (§13) | |||||||||||||||||
| 1138 | + if should_stop(dm, best, d̂, prev_hist) → return [] | |||||||||||||||||
| 1139 | + | |||||||||||||||||
| 1140 | + 4d. compute batch_size from remaining budget | |||||||||||||||||
| 1141 | + batch_size = total_budget / FASTA_MAX_ROUNDS | |||||||||||||||||
| 1142 | + clamped to min(batch_size, |U|, remaining_budget) | |||||||||||||||||
| 1143 | + | |||||||||||||||||
| 1144 | + 4e. select next batch (§11 + §12) | |||||||||||||||||
| 1145 | + τ̂ = compute_tau(R̂², k_est) | |||||||||||||||||
| 1146 | + if D ≥ 3: | |||||||||||||||||
| 1147 | + 50% → softmax_select(d̂, τ̂) model channel | |||||||||||||||||
| 1148 | + 50% → softmax_select(marginal, τ_min) marginal channel | |||||||||||||||||
| 1149 | + merge via set union, cap to batch_size | |||||||||||||||||
| 1150 | + else: | |||||||||||||||||
| 1151 | + 100% → softmax_select(d̂, τ̂) | |||||||||||||||||
| 1152 | + | |||||||||||||||||
| 1153 | + return batch indices (or [] if budget exhausted) | |||||||||||||||||
| 1154 | +``` | |||||||||||||||||
| 1155 | + | |||||||||||||||||
| 1156 | +The first `refine()` call receives R1 durations. Steps 4a–4e run for the first time, producing the first guided batch. Each subsequent call grows M (step 1), refits the model from scratch on all accumulated data (step 4a–4b), and selects the next batch. After at most 4 calls (rounds 2–5), or earlier if convergence fires or budget runs out, it returns `[]`. | |||||||||||||||||
| 1157 | + | |||||||||||||||||
| 1158 | +Compilation failures: the caller passes `float('inf')` as the duration for configs that failed to compile. From `refine()`'s perspective, those are very slow configs that softmax won't select again. | |||||||||||||||||
| 1159 | + | |||||||||||||||||
| 1160 | +### A.3 The growing measured matrix: what the estimator refits each round | |||||||||||||||||
| 1161 | + | |||||||||||||||||
| 1162 | +The estimator trains on all accumulated measurements — Fₘ grows every round. Here is the concrete progression for a D=3, N=119 kernel: | |||||||||||||||||
| 1163 | + | |||||||||||||||||
| 1164 | +| Round | Event | n (measured) | Fₘ shape | dm shape | Unmeasured | What's new in the training set | | |||||||||||||||||
| 1165 | +|-------|-------|-------------|----------|----------|------------|-------------------------------| | |||||||||||||||||
| 1166 | +| R1 | blind sample | 18 | (18, p) | (18,) | 101 | initial 15% stratified sample | | |||||||||||||||||
| 1167 | +| R2-1 | 1st guided | 25 | (25, p) | (25,) | 94 | +7 softmax-selected configs | | |||||||||||||||||
| 1168 | +| R2-2 | 2nd guided | 32 | (32, p) | (32,) | 87 | +7 more, biased toward predicted-fast | | |||||||||||||||||
| 1169 | +| R2-3 | 3rd guided | 39 | (39, p) | (39,) | 80 | +7 more | | |||||||||||||||||
| 1170 | +| R2-4 | 4th guided (or stop) | 42 | (42, p) | (42,) | 77 | +3 (budget exhausted at 35%) | | |||||||||||||||||
| 1171 | + | |||||||||||||||||
| 1172 | +Each round, `select_mode_and_predict` receives `Xm = Xn[sorted(M)]` — all configs measured so far, not just the latest batch. The Gram matrix FₘᵀFₘ is built from all n rows. β̂ is solved from scratch — no incremental update, no warm-start from the previous round's coefficients. | |||||||||||||||||
| 1173 | + | |||||||||||||||||
| 1174 | +The model at round R2-3 (n=39) sees the same 18 R1 configs that the model at round R2-1 (n=25) saw, plus 14 additional guided samples. R1 provides broad coverage of the parameter space. Guided samples fill in the fast region. Together they give the model both landscape shape and local precision near the optimum. | |||||||||||||||||
| 1175 | + | |||||||||||||||||
| 1176 | +### A.4 Why train on measured only, and why earlier approaches failed | |||||||||||||||||
| 1177 | + | |||||||||||||||||
| 1178 | +The regression trains on Fₘ (n rows, measured configs with known durations) and predicts on Fᵤ (N−n rows, unmeasured configs with unknown durations). We don't have durations for unmeasured configs, so they can't be training data. | |||||||||||||||||
| 1179 | + | |||||||||||||||||
| 1180 | +Two things the estimator does use from the full N-config set: | |||||||||||||||||
| 1181 | + | |||||||||||||||||
| 1182 | +1. **Normalization.** Min-max scaling to [0,1] uses all N configs' statistics. Measured and unmeasured share the same feature space. | |||||||||||||||||
| 1183 | +2. **Prediction targets.** Fᵤ is built from the same basis expansion as Fₘ, applied to unmeasured configs' (known) tiling parameters. The features x are known for all N — only the durations d are unknown for unmeasured ones. | |||||||||||||||||
| 1184 | + | |||||||||||||||||
| 1185 | +The tension is at low n. After R1 with D=6, we have n≈18 samples. The full polynomial has p=28 parameters — more unknowns than equations. Three approaches were tried: | |||||||||||||||||
| 1186 | + | |||||||||||||||||
| 1187 | +**Approach 1: force the richest model, let ridge handle it (exp52d).** Always use full cross-terms from round 1 regardless of n/p. Tested with λ=0.1, λ=1.0, and decaying λ. Failed — at n=18, p=28, even strong regularization can't extract ranking signal from an underdetermined system. Ridge shrinks coefficients toward zero, which means predictions converge toward the mean duration. The model is "stable" (low variance) but predicts everything as roughly equal (high bias in ranking), which is useless for softmax selection. | |||||||||||||||||
| 1188 | + | |||||||||||||||||
| 1189 | +**Approach 2: downgrade basis complexity based on n/p ratio (Config O/M).** Viability assessment picks linear when n/p < 2 for quad, quad when n/p < 3 for full. This works but is rigid — some kernels genuinely need full cross-terms even at moderate n/p, while others are better served by fourier even at high n/p. The n/p threshold is a proxy for model quality, and a coarse one. | |||||||||||||||||
| 1190 | + | |||||||||||||||||
| 1191 | +**Approach 3 (current): compete multiple bases, let LOOCV decide (exp55→63c).** Don't choose the basis by n/p arithmetic. Fit all viable bases, measure each one's ranking accuracy via LOOCV, pick the winner. At n=18: linear (p=4, n/p=4.5) and quad (p=7, n/p=2.6) are well-conditioned and compete on ranking quality. Full (p=10, n/p=1.8) is marginal but enters and might win if cross-terms genuinely matter. Fourier (p=16, n/p=1.1) enters and might win if periodic structure dominates. Cubic (p=20) is gated out (20 > 18/2). By round R2-3 (n=39), cubic enters (20 < 39/2) and all 5 bases compete. | |||||||||||||||||
| 1192 | + | |||||||||||||||||
| 1193 | +The key difference from approach 1: approach 1 forces one fixed complex model across all rounds. Approach 3 lets the data decide, per round, which complexity matches the available sample size. | |||||||||||||||||
| 1194 | + | |||||||||||||||||
| 1195 | +### A.5 When and why the winning mode changes between rounds | |||||||||||||||||
| 1196 | + | |||||||||||||||||
| 1197 | +The LOOCV competition re-runs every round. The winner can change for three reasons: | |||||||||||||||||
| 1198 | + | |||||||||||||||||
| 1199 | +**1. Sample size crosses an identifiability threshold.** Cubic requires p < n/2. At D=3, p_cubic=20. After R1 (n=18), cubic is gated out (20 > 9). After round R2-2 (n=32), it enters (20 < 16). If cubic genuinely captures 3-way structure, it wins from that round onward. This is the most common mode switch — a basis that was absent becomes available. | |||||||||||||||||
| 1200 | + | |||||||||||||||||
| 1201 | +**2. Measured set composition changes.** R1 is stratified — roughly uniform coverage. Guided rounds add configs biased toward the predicted-fast region (softmax selected those). A basis that captures broad periodic structure (fourier) might win on R1 data, while one that captures local curvature (full polynomial with interactions) might win once the fast-region detail fills in. | |||||||||||||||||
| 1202 | + | |||||||||||||||||
| 1203 | +**3. LOOCV stability improves with more data.** With n=18, Spearman ρ_s has high variance — a basis might win by noise. At n=40, the 40-fold LOOCV produces a more stable ranking and the winner reflects genuine model quality. | |||||||||||||||||
| 1204 | + | |||||||||||||||||
| 1205 | +In practice on 35 kernels: most settle on one mode by round 2 and stay. The ~5 kernels that switch are the ones where the landscape complexity depends on sample size, or where the cubic gate opens mid-run. | |||||||||||||||||
| 1206 | + | |||||||||||||||||
| 1207 | +### A.6 Cost and risk of carrying 5 basis types | |||||||||||||||||
| 1208 | + | |||||||||||||||||
| 1209 | +**Cost:** near zero. Each LOOCV evaluation is one matrix inversion O(p³) plus element-wise operations. At our sizes (p≤7 for linear, p≤31 for fourier, p≤84 for cubic when gated), the full 5-basis competition takes <0.5ms — less than a single config benchmark (~5ms). | |||||||||||||||||
| 1210 | + | |||||||||||||||||
| 1211 | +**Risk — vote splitting.** If two similar bases are in the pool, they can split LOOCV scores, letting an inferior third basis win. This happened in exp65 (multiple fourier variants with different harmonic counts — near-collinear bases produced similar ρ_s scores, and the wrong one occasionally won). With 5 structurally distinct basis types, the LOOCV scores separate cleanly. | |||||||||||||||||
| 1212 | + | |||||||||||||||||
| 1213 | +**Effect of removing modes:** | |||||||||||||||||
| 1214 | + | |||||||||||||||||
| 1215 | +| Configuration | Catch rate | Regressions | What's lost | | |||||||||||||||||
| 1216 | +|--------------|-----------|-------------|-------------| | |||||||||||||||||
| 1217 | +| all 5 (exp63c) | 99.69% | 0 | — | | |||||||||||||||||
| 1218 | +| drop cubic | ~99.4% | 0 | 4 high-D kernels with 3-way interactions | | |||||||||||||||||
| 1219 | +| drop fourier | ~99.2% | 0 | 3 hard kernels with periodic cache-boundary effects | | |||||||||||||||||
| 1220 | +| polynomial only (Config M) | 99.1% | 0 | combined: the kernels above | | |||||||||||||||||
| 1221 | +| linear only | ~97% | several | most kernels need at least curvature or interactions | | |||||||||||||||||
| 1222 | + | |||||||||||||||||
| 1223 | +The gains from each additional basis are small globally but concentrated on hard kernels — the cases where the algorithm is already closest to failure. | |||||||||||||||||
| 1224 | + | |||||||||||||||||
| 1225 | +--- | |||||||||||||||||
| 1226 | + | |||||||||||||||||
| 1227 | +## References | |||||||||||||||||
| 1228 | + | |||||||||||||||||
| 1229 | +[1] Rice, J.A. (2006). Mathematical Statistics and Data Analysis, 3rd ed. Duxbury/Thomson. Ch. 2: hypergeometric distribution. | |||||||||||||||||
| 1230 | + | |||||||||||||||||
| 1231 | +[2] Hastie, T., Tibshirani, R., Friedman, J. (2009). The Elements of Statistical Learning, 2nd ed. Springer. Ch. 3.4.1: ridge regression. Ch. 7.3: bias-variance. Ch. 7.10: cross-validation. Ch. 14.7: rank correlation. Free: https://hastie.su.domains/ElemStatLearn/ | |||||||||||||||||
| 1232 | + | |||||||||||||||||
| 1233 | +[3] Hoerl, A.E. & Kennard, R.W. (1970). Ridge Regression: Biased Estimation for Nonorthogonal Problems. Technometrics 12(1):55–67. | |||||||||||||||||
| 1234 | + | |||||||||||||||||
| 1235 | +[4] Strang, G. (2006). Linear Algebra and Its Applications, 4th ed. Thomson. Ch. 3 (Least Squares), Ch. 5 (Eigenvalues), Ch. 11.2 (Condition Numbers). | |||||||||||||||||
| 1236 | + | |||||||||||||||||
| 1237 | +[5] Golub, G.H. & Van Loan, C.F. (2013). Matrix Computations, 4th ed. Johns Hopkins. Condition numbers and numerical stability. | |||||||||||||||||
| 1238 | + | |||||||||||||||||
| 1239 | +[6] Box, G.E.P. & Draper, N.R. (2007). Response Surfaces, Mixtures, and Ridge Analyses, 2nd ed. Wiley. Second-order polynomial models. | |||||||||||||||||
| 1240 | + | |||||||||||||||||
| 1241 | +[7] Sutton, R.S. & Barto, A.G. (2018). Reinforcement Learning, 2nd ed. MIT Press. Ch. 2.3: softmax action selection. | |||||||||||||||||
| 1242 | + | |||||||||||||||||
| 1243 | +[8] Goodfellow, I., Bengio, Y., Courville, A. (2016). Deep Learning. MIT Press. Ch. 4.1: numerical stability of softmax. | |||||||||||||||||
| 1244 | + | |||||||||||||||||
| 1245 | +[9] Chapelle, O. & Li, L. (2011). An Empirical Evaluation of Thompson Sampling. NIPS 2011:2249–2257. | |||||||||||||||||
| 1246 | + | |||||||||||||||||
| 1247 | +[10] Levin, D.A., Peres, Y., Wilmer, E.L. (2009). Markov Chains and Mixing Times. AMS. Ch. 4: total variation distance. | |||||||||||||||||
| 1248 | + | |||||||||||||||||
| 1249 | +[11] Bühlmann, P. & van de Geer, S. (2011). Statistics for High-Dimensional Data. Springer. Ridge in the p > n regime. | |||||||||||||||||
| 1250 | + | |||||||||||||||||
| 1251 | +[12] Draper, N.R. & Smith, H. (1998). Applied Regression Analysis, 3rd ed. Wiley. R² and coefficient of determination. | |||||||||||||||||
| 1252 | + | |||||||||||||||||
| 1253 | +[13] Spearman, C. (1904). The Proof and Measurement of Association Between Two Things. American Journal of Psychology 15(1):72–101. | |||||||||||||||||
| 1254 | + | |||||||||||||||||
| 1255 | +[14] Tolstov, G.P. (1976). Fourier Series. Dover. Ch. 1: convergence of trigonometric series. | |||||||||||||||||
| 1256 | + | |||||||||||||||||
| 1257 | +[15] Allen, D.M. (1974). The Relationship Between Variable Selection and Data Augmentation and a Method for Prediction. Technometrics 16(1):125–127. The original PRESS statistic / LOOCV shortcut. | |||||||||||||||||
| @@ -0,0 +1,129 @@ | |||
| 1 | +import fcntl | ||
| 2 | +import os | ||
| 3 | +import csv | ||
| 4 | +import logging | ||
| 5 | +from datetime import datetime | ||
| 6 | +import psutil | ||
| 7 | + | ||
| 8 | + | ||
| 9 | +class AutotuneStatsManager: | ||
| 10 | + def __init__(self, enabled: bool, log: logging.Logger): | ||
| 11 | + self.enabled = enabled | ||
| 12 | + | ||
| 13 | + if not enabled: | ||
| 14 | + return | ||
| 15 | + | ||
| 16 | + self.run_ts = self._get_run_ts() | ||
| 17 | + self.logs_dir = self._create_logs_dir() | ||
| 18 | + | ||
| 19 | + self.csv_paths = {} | ||
| 20 | + self.schemas = {} | ||
| 21 | + | ||
| 22 | + self.log = log | ||
| 23 | + self._setup_logger() | ||
| 24 | + | ||
| 25 | + self.register_csv( | ||
| 26 | + "duration-stats", | ||
| 27 | + ["Kernel", "Stage", "Duration(ms)", "Configs", | ||
| 28 | + "Start TS", "End TS"], | ||
| 29 | + create_now=True | ||
| 30 | + ) | ||
| 31 | + | ||
| 32 | + | ||
| 33 | + def _get_run_ts(self): | ||
| 34 | + """ | ||
| 35 | + Return a stable timestamp string identifying the current logical run | ||
| 36 | + | ||
| 37 | + Notes | ||
| 38 | + ----- | ||
| 39 | + This implementation assumes the current torch.async_compile behavior. | ||
| 40 | + | ||
| 41 | + If in the future the first kernel is executed concurrently with other | ||
| 42 | + kernels, the run identifier should be derived from the grandparent | ||
| 43 | + process ID (i.e., write grandparent.pid as RUN_PID) to preserve a | ||
| 44 | + consistent run scope. | ||
| 45 | + """ | ||
| 46 | + def write_run_info(): | ||
| 47 | + ts = datetime.now().strftime('%Y-%m-%d-%H-%M-%S') | ||
| 48 | + with open("/tmp/run_info", "w") as f: | ||
| 49 | + fcntl.flock(f, fcntl.LOCK_EX) | ||
| 50 | + f.write(f"{proc.pid},{ts}\n") | ||
| 51 | + fcntl.flock(f, fcntl.LOCK_UN) | ||
| 52 | + return ts | ||
| 53 | + | ||
| 54 | + proc = psutil.Process(os.getpid()) | ||
| 55 | + grandparent = proc.parent().parent() | ||
| 56 | + | ||
| 57 | + try: | ||
| 58 | + | ||
| 59 | + with open("/tmp/run_info", "r") as f: | ||
| 60 | + fcntl.flock(f, fcntl.LOCK_SH) | ||
| 61 | + run_pid, ts = f.read().strip().split(",", 1) | ||
| 62 | + fcntl.flock(f, fcntl.LOCK_UN) | ||
| 63 | + except FileNotFoundError: | ||
| 64 | + return write_run_info() | ||
| 65 | + | ||
| 66 | + if not grandparent or grandparent.pid != int(run_pid): | ||
| 67 | + return write_run_info() | ||
| 68 | + | ||
| 69 | + return ts | ||
| 70 | + | ||
| 71 | + def _create_logs_dir(self): | ||
| 72 | + path = os.path.join(os.getcwd(), "autotune_logs") | ||
| 73 | + os.makedirs(path, exist_ok=True) | ||
| 74 | + return path | ||
| 75 | + | ||
| 76 | + def _setup_logger(self): | ||
| 77 | + if not self.log.handlers: | ||
| 78 | + log_file = os.path.join( | ||
| 79 | + self.logs_dir, | ||
| 80 | + f"npu_triton_heuristics-{self.run_ts}.log", | ||
| 81 | + ) | ||
| 82 | + | ||
| 83 | + handler = logging.FileHandler(log_file) | ||
| 84 | + handler.setFormatter(logging.Formatter( | ||
| 85 | + "%(asctime)s - %(levelname)s - %(message)s" | ||
| 86 | + )) | ||
| 87 | + | ||
| 88 | + self.log.addHandler(handler) | ||
| 89 | + self.log.setLevel(logging.INFO) | ||
| 90 | + print("log file:", log_file) | ||
| 91 | + | ||
| 92 | + def register_csv(self, name, headers, create_now=False): | ||
| 93 | + self.schemas[name] = headers | ||
| 94 | + | ||
| 95 | + if create_now: | ||
| 96 | + self._ensure_csv_created(name) | ||
| 97 | + | ||
| 98 | + def _ensure_csv_created(self, name): | ||
| 99 | + if name in self.csv_paths: | ||
| 100 | + return self.csv_paths[name] | ||
| 101 | + | ||
| 102 | + path = os.path.join( | ||
| 103 | + self.logs_dir, | ||
| 104 | + f"{name}-{self.run_ts}.csv" | ||
| 105 | + ) | ||
| 106 | + | ||
| 107 | + try: | ||
| 108 | + fd = os.open( | ||
| 109 | + path, | ||
| 110 | + os.O_CREAT | os.O_EXCL | os.O_WRONLY, | ||
| 111 | + 0o644 | ||
| 112 | + ) | ||
| 113 | + except FileExistsError: | ||
| 114 | + pass | ||
| 115 | + else: | ||
| 116 | + with os.fdopen(fd, "w", newline="") as f: | ||
| 117 | + csv.writer(f).writerow(self.schemas[name]) | ||
| 118 | + | ||
| 119 | + self.csv_paths[name] = path | ||
| 120 | + return path | ||
| 121 | + | ||
| 122 | + def write(self, name, row): | ||
| 123 | + if not self.enabled: | ||
| 124 | + return | ||
| 125 | + | ||
| 126 | + path = self._ensure_csv_created(name) | ||
| 127 | + | ||
| 128 | + with open(path, "a", newline="") as f: | ||
| 129 | + csv.writer(f).writerow(row) | ||
| @@ -0,0 +1,172 @@ | |||
| 1 | +"""Config optimizer for FASTA autotuning pipeline. | ||
| 2 | + | ||
| 3 | +Activated by environment variable FASTA_CONFIG_OPTIMIZER=1. | ||
| 4 | +Called after config generation, before add_mutibuffer_config(). | ||
| 5 | + | ||
| 6 | +Stage 2: NPU-Aware pruning | ||
| 7 | + - 2d. circle_num <= MAX_CIRCLE_NUM (default 4) | ||
| 8 | + - 2c. sub_numel >= MIN_SUB_NUMEL (default 32) — minimum tile size | ||
| 9 | + | ||
| 10 | +Stage 3: Diversity filter | ||
| 11 | + - Deduplicate by (circle_num, sub_block_pattern) | ||
| 12 | + - Cap at MAX_CONFIGS via uniform sub_numel sampling | ||
| 13 | +""" | ||
| 14 | + | ||
| 15 | +import math | ||
| 16 | +from torch_npu._inductor.experimental.dynamic_filter.dynamic_filter_config import fasta_config_optimizer | ||
| 17 | +from torch_npu._inductor.fasta_autotune import log | ||
| 18 | + | ||
| 19 | + | ||
| 20 | +MAX_CIRCLE_NUM = 4 | ||
| 21 | +MIN_SUB_NUMEL = 32 | ||
| 22 | +MAX_CONFIGS = 50 | ||
| 23 | + | ||
| 24 | + | ||
| 25 | +def _get_circle_num(cfg): | ||
| 26 | + """Get circle_num from a FastAConfig, computing it if needed.""" | ||
| 27 | + if hasattr(cfg, "circle_num") and cfg.circle_num > 0: | ||
| 28 | + return cfg.circle_num | ||
| 29 | + # Fallback: compute from kwargs | ||
| 30 | + kwargs = cfg.kwargs | ||
| 31 | + blocks = {} | ||
| 32 | + subs = {} | ||
| 33 | + for k, v in kwargs.items(): | ||
| 34 | + if isinstance(v, (int, float)): | ||
| 35 | + if "BLOCK" in k and "SUB" not in k: | ||
| 36 | + blocks[k.replace("BLOCK", "")] = v | ||
| 37 | + elif "BLOCK_SUB" in k: | ||
| 38 | + subs[k.replace("BLOCK_SUB", "")] = v | ||
| 39 | + circle = 1 | ||
| 40 | + for axis in blocks: | ||
| 41 | + if axis in subs and subs[axis] > 0: | ||
| 42 | + circle *= math.ceil(blocks[axis] / subs[axis]) | ||
| 43 | + return circle | ||
| 44 | + | ||
| 45 | + | ||
| 46 | +def _get_sub_numel(cfg): | ||
| 47 | + """Get product of all BLOCK_SUB values (tile footprint in elements).""" | ||
| 48 | + sub_numel = 1 | ||
| 49 | + for k, v in cfg.kwargs.items(): | ||
| 50 | + if "BLOCK_SUB" in k and isinstance(v, (int, float)) and v > 0: | ||
| 51 | + sub_numel *= int(v) | ||
| 52 | + return sub_numel | ||
| 53 | + | ||
| 54 | + | ||
| 55 | +def _get_sub_pattern(cfg): | ||
| 56 | + """Get sorted tuple of (key, value) for all BLOCK_SUB params.""" | ||
| 57 | + return tuple( | ||
| 58 | + sorted( | ||
| 59 | + (k, int(v)) | ||
| 60 | + for k, v in cfg.kwargs.items() | ||
| 61 | + if "BLOCK_SUB" in k and isinstance(v, (int, float)) | ||
| 62 | + ) | ||
| 63 | + ) | ||
| 64 | + | ||
| 65 | + | ||
| 66 | +def _dedup_configs(configs): | ||
| 67 | + """Deduplicate by (circle_num, sub_block_pattern). Keep first seen.""" | ||
| 68 | + seen = {} | ||
| 69 | + for cfg in configs: | ||
| 70 | + key = (_get_circle_num(cfg), _get_sub_pattern(cfg)) | ||
| 71 | + if key not in seen: | ||
| 72 | + seen[key] = cfg | ||
| 73 | + return list(seen.values()) | ||
| 74 | + | ||
| 75 | + | ||
| 76 | +def _sample_diverse(configs, max_configs): | ||
| 77 | + """Sample configs uniformly across sub_numel range for diversity. | ||
| 78 | + | ||
| 79 | + Sorts by sub_numel then picks evenly spaced indices to cover | ||
| 80 | + small, medium, and large tile sizes. | ||
| 81 | + """ | ||
| 82 | + if len(configs) <= max_configs: | ||
| 83 | + return configs | ||
| 84 | + configs_sorted = sorted(configs, key=_get_sub_numel) | ||
| 85 | + n = len(configs_sorted) | ||
| 86 | + # Evenly spaced indices including first and last | ||
| 87 | + indices = set() | ||
| 88 | + for i in range(max_configs): | ||
| 89 | + idx = int(i * (n - 1) / (max_configs - 1)) if max_configs > 1 else 0 | ||
| 90 | + indices.add(idx) | ||
| 91 | + # Sort indices to maintain sub_numel order | ||
| 92 | + return [configs_sorted[i] for i in sorted(indices)] | ||
| 93 | + | ||
| 94 | + | ||
| 95 | +def _is_expert(cfg): | ||
| 96 | + """Check if config originates from base TileGenerator (F0).""" | ||
| 97 | + return getattr(cfg, "from_expert", False) is True | ||
| 98 | + | ||
| 99 | + | ||
| 100 | +def optimize_configs(configs): | ||
| 101 | + """Apply optimization pipeline to config list. | ||
| 102 | + | ||
| 103 | + Expert configs (from base TileGenerator, from_expert=True) are preserved | ||
| 104 | + unconditionally — they cover the conservative UB budget range that FASTA | ||
| 105 | + configs may miss. Only FASTA-generated configs go through the filter, | ||
| 106 | + dedup, and sampling pipeline. | ||
| 107 | + | ||
| 108 | + Pipeline (FASTA configs only): | ||
| 109 | + 1. Circle number filter: keep configs with circle_num <= MAX_CIRCLE_NUM | ||
| 110 | + 2. Minimum tile size: keep configs with sub_numel >= MIN_SUB_NUMEL | ||
| 111 | + 3. Deduplicate by (circle_num, sub_block_pattern) | ||
| 112 | + 4. Cap at MAX_CONFIGS via uniform sub_numel sampling | ||
| 113 | + | ||
| 114 | + Args: | ||
| 115 | + configs: List of FastAConfig objects | ||
| 116 | + | ||
| 117 | + Returns: | ||
| 118 | + Filtered list of FastAConfig objects (expert + filtered FASTA) | ||
| 119 | + """ | ||
| 120 | + if not fasta_config_optimizer or not configs: | ||
| 121 | + return configs | ||
| 122 | + | ||
| 123 | + before = len(configs) | ||
| 124 | + | ||
| 125 | + # Separate expert configs (from base TileGenerator) from FASTA configs | ||
| 126 | + expert_configs = [cfg for cfg in configs if _is_expert(cfg)] | ||
| 127 | + fasta_configs = [cfg for cfg in configs if not _is_expert(cfg)] | ||
| 128 | + n_expert = len(expert_configs) | ||
| 129 | + | ||
| 130 | + # Apply filters only to FASTA configs | ||
| 131 | + filtered = [cfg for cfg in fasta_configs if _get_circle_num(cfg) <= MAX_CIRCLE_NUM] | ||
| 132 | + | ||
| 133 | + if MIN_SUB_NUMEL > 1: | ||
| 134 | + filtered = [cfg for cfg in filtered if _get_sub_numel(cfg) >= MIN_SUB_NUMEL] | ||
| 135 | + | ||
| 136 | + after_filter = len(filtered) | ||
| 137 | + | ||
| 138 | + # Dedup and sample only FASTA configs | ||
| 139 | + filtered = _dedup_configs(filtered) | ||
| 140 | + after_dedup = len(filtered) | ||
| 141 | + | ||
| 142 | + if MAX_CONFIGS > 0: | ||
| 143 | + filtered = _sample_diverse(filtered, MAX_CONFIGS) | ||
| 144 | + | ||
| 145 | + # Merge: expert configs first, then filtered FASTA configs | ||
| 146 | + result = expert_configs + filtered | ||
| 147 | + | ||
| 148 | + # Safety: never return empty list | ||
| 149 | + if not result: | ||
| 150 | + log.warning( | ||
| 151 | + "config_optimizer: all configs filtered out, keeping original %d", before | ||
| 152 | + ) | ||
| 153 | + return configs | ||
| 154 | + | ||
| 155 | + after = len(result) | ||
| 156 | + log.info( | ||
| 157 | + "config_optimizer: %d -> %d configs " | ||
| 158 | + "(expert: %d preserved, fasta: %d -> %d -> %d -> %d, " | ||
| 159 | + "filter: -%d, dedup: -%d, sample: -%d)", | ||
| 160 | + before, | ||
| 161 | + after, | ||
| 162 | + n_expert, | ||
| 163 | + len(fasta_configs), | ||
| 164 | + after_filter, | ||
| 165 | + after_dedup, | ||
| 166 | + after - n_expert, | ||
| 167 | + len(fasta_configs) - after_filter, | ||
| 168 | + after_filter - after_dedup, | ||
| 169 | + after_dedup - (after - n_expert), | ||
| 170 | + ) | ||
| 171 | + | ||
| 172 | + return result | ||
| @@ -0,0 +1,479 @@ | |||
| 1 | +""" | ||
| 2 | +dynamic filter algorithm: efficient kernel selection via adaptive | ||
| 3 | +surrogate modeling. | ||
| 4 | + | ||
| 5 | +pure math, minimal INFO logging, no other I/O, no framework deps beyond | ||
| 6 | +stdlib logging + numpy. | ||
| 7 | + | ||
| 8 | +motivation: kernel selection costs real wall-clock time. with N configs and | ||
| 9 | +limited measurement budget (8-16% of N typical), brute-force evaluation is | ||
| 10 | +infeasible. this algorithm trades early modeling (R1 stratified sample) for | ||
| 11 | +fast config proposal (softmax-weighted) to converge in 40-50 evals on hard | ||
| 12 | +kernels (K/N ≈ 1-2%). | ||
| 13 | + | ||
| 14 | +in: feature matrix X (N x D), measurement callback (live timing or offline | ||
| 15 | + precomputed durations). | ||
| 16 | +out: best config index + convergence stats (catch rate, regret, iterations). | ||
| 17 | + | ||
| 18 | +protocol: plugs into dynamic_algo_if for uniform online/offline dispatch—same | ||
| 19 | +code runs live on NPU or replays recorded measurements for validation. | ||
| 20 | + | ||
| 21 | +given N compiled tiling configs for a triton kernel, find the fastest while | ||
| 22 | +measuring as few as possible: | ||
| 23 | + 1. measure a stratified R1 sample (driven by identifiability) | ||
| 24 | + 2. compete basis models (linear/quad/full/fourier/cubic) by spearman-ranked | ||
| 25 | + loocv, keep the best surrogate | ||
| 26 | + 3. softmax-sample the next batch toward predicted-fast configs | ||
| 27 | + 4. repeat until the budget runs out or it converges | ||
| 28 | + | ||
| 29 | +current estimators (mean rank, quadratic form) capture typical | ||
| 30 | +harmonic + smooth patterns. future estimators (e.g., ratio estimator for tail | ||
| 31 | +risk, worse-kernel baseline) can be composed via bayesian model averaging or | ||
| 32 | +competing loss functions. contatct asaf.goldberg@huawei.com | ||
| 33 | + | ||
| 34 | +logging: | ||
| 35 | + log_algo_config() dumps the global constants once. | ||
| 36 | + run(..., kernel_name=...) emits the per-kernel start/end lines under the | ||
| 37 | + [FASTA_DYN_FILTER_ALGO] tag. regret is only real when ground-truth durations | ||
| 38 | + are passed (offline sim). | ||
| 39 | + | ||
| 40 | +""" | ||
| 41 | + | ||
| 42 | +# import time # unused: only the commented-out run()/_log_end referenced it | ||
| 43 | +import logging | ||
| 44 | +from torch_npu._inductor.experimental.dynamic_filter.dynamic_filter_config import fasta_dynamic_filter as df_cfg | ||
| 45 | +import numpy as np | ||
| 46 | + | ||
| 47 | +log = logging.getLogger("torch._inductor") | ||
| 48 | + | ||
| 49 | +_TAG = "[FASTA_DYN_FILTER_ALGO]" | ||
| 50 | + | ||
| 51 | + | ||
| 52 | +# constants | ||
| 53 | +R1_FLOOR_PCT: float = 0.10 | ||
| 54 | +R1_PCT_LOW: float = 0.12 | ||
| 55 | +HIGH_D_THRESH: int = 4 | ||
| 56 | +HIGH_NP_THRESH: int = 5 | ||
| 57 | +HARD_D_THRESH: int = 3 | ||
| 58 | +HARD_N_THRESH: int = 150 | ||
| 59 | +EASY_D_THRESH: int = 4 | ||
| 60 | +EASY_N_THRESH: int = 150 | ||
| 61 | +MIN_VIABLE_NP: int = 2 | ||
| 62 | +TAU_MIN: float = 0.3 | ||
| 63 | +TAU_RANGE: float = 0.4 | ||
| 64 | +MARGINAL_D: int = 3 | ||
| 65 | +RIDGE_LAMBDA: float = 0.1 | ||
| 66 | +MARGIN_CONV: float = 2.0 | ||
| 67 | +L1_CONV: float = 0.01 | ||
| 68 | +HIST_BINS: int = 11 | ||
| 69 | +K_TOLERANCE: float = 1.05 | ||
| 70 | +UNDERFLOW_FLOOR: float = 1e-300 | ||
| 71 | + | ||
| 72 | +SKIP_KWARGS = frozenset({ | ||
| 73 | + 'compile_mode', 'multibuffer', 'split_k', | ||
| 74 | + 'remain_programs', 'using_programs', | ||
| 75 | +}) | ||
| 76 | + | ||
| 77 | +BASE_MODES = ['linear', 'quad', 'full', 'fourier'] | ||
| 78 | + | ||
| 79 | +# config banner fires once even if called again | ||
| 80 | +_CONFIG_LOGGED = False | ||
| 81 | + | ||
| 82 | +def log_algo_config(force=False): | ||
| 83 | + """dump the global constants once, at INFO. only emits on the first call | ||
| 84 | + unless force=True.""" | ||
| 85 | + global _CONFIG_LOGGED | ||
| 86 | + if _CONFIG_LOGGED and not force: | ||
| 87 | + return | ||
| 88 | + _CONFIG_LOGGED = True | ||
| 89 | + log.info( | ||
| 90 | + _TAG + " event=config " | ||
| 91 | + "R1_PCT=%s R1_FLOOR_PCT=%s R1_PCT_LOW=%s " | ||
| 92 | + "BASE_BUDGET=%s HIGH_BUDGET=%s LOW_BUDGET=%s " | ||
| 93 | + "HARD_D_THRESH=%s HARD_N_THRESH=%s EASY_D_THRESH=%s EASY_N_THRESH=%s " | ||
| 94 | + "HIGH_D_THRESH=%s HIGH_NP_THRESH=%s MIN_VIABLE_NP=%s " | ||
| 95 | + "TAU_MIN=%s TAU_RANGE=%s MARGINAL_D=%s RIDGE_LAMBDA=%s " | ||
| 96 | + "MAX_ROUNDS=%s MARGIN_CONV=%s L1_CONV=%s HIST_BINS=%s " | ||
| 97 | + "K_TOLERANCE=%s UNDERFLOW_FLOOR=%s", | ||
| 98 | + df_cfg.r1_pct, R1_FLOOR_PCT, R1_PCT_LOW, | ||
| 99 | + df_cfg.base_budget, df_cfg.high_budget, df_cfg.low_budget, | ||
| 100 | + HARD_D_THRESH, HARD_N_THRESH, EASY_D_THRESH, EASY_N_THRESH, | ||
| 101 | + HIGH_D_THRESH, HIGH_NP_THRESH, MIN_VIABLE_NP, | ||
| 102 | + TAU_MIN, TAU_RANGE, MARGINAL_D, RIDGE_LAMBDA, | ||
| 103 | + df_cfg.max_rounds, MARGIN_CONV, L1_CONV, HIST_BINS, | ||
| 104 | + K_TOLERANCE, UNDERFLOW_FLOOR, | ||
| 105 | + ) | ||
| 106 | + | ||
| 107 | + | ||
| 108 | + | ||
| 109 | + | ||
| 110 | +# ===================================================================== | ||
| 111 | +# feature extraction | ||
| 112 | +# ===================================================================== | ||
| 113 | + | ||
| 114 | +def extract_features(configs): | ||
| 115 | + """build the feature matrix from config kwargs. | ||
| 116 | + | ||
| 117 | + scan every config's kwargs for numeric keys, keep only the columns that | ||
| 118 | + vary across configs. | ||
| 119 | + | ||
| 120 | + args: | ||
| 121 | + configs: list of objects with a .kwargs dict. | ||
| 122 | + | ||
| 123 | + returns: | ||
| 124 | + X: np.ndarray (N, D) feature matrix. | ||
| 125 | + names: list[str] of length D, sorted feature names. | ||
| 126 | + """ | ||
| 127 | + N = len(configs) | ||
| 128 | + all_keys = set() | ||
| 129 | + for cfg in configs: | ||
| 130 | + kw = cfg.kwargs if hasattr(cfg, 'kwargs') else {} | ||
| 131 | + for k, v in kw.items(): | ||
| 132 | + if k in SKIP_KWARGS: | ||
| 133 | + continue | ||
| 134 | + try: | ||
| 135 | + float(v) | ||
| 136 | + all_keys.add(k) | ||
| 137 | + except (TypeError, ValueError): | ||
| 138 | + pass | ||
| 139 | + | ||
| 140 | + if not all_keys or N == 0: | ||
| 141 | + return np.zeros((N, 0)), [] | ||
| 142 | + | ||
| 143 | + sorted_keys = sorted(all_keys) | ||
| 144 | + X_full = np.zeros((N, len(sorted_keys))) | ||
| 145 | + for i, cfg in enumerate(configs): | ||
| 146 | + kw = cfg.kwargs if hasattr(cfg, 'kwargs') else {} | ||
| 147 | + for j, k in enumerate(sorted_keys): | ||
| 148 | + val = kw.get(k, 0) | ||
| 149 | + try: | ||
| 150 | + X_full[i, j] = float(val) | ||
| 151 | + except (TypeError, ValueError): | ||
| 152 | + X_full[i, j] = 0.0 | ||
| 153 | + | ||
| 154 | + varying = [j for j in range(len(sorted_keys)) | ||
| 155 | + if len(np.unique(X_full[:, j])) > 1] | ||
| 156 | + names = [sorted_keys[j] for j in varying] | ||
| 157 | + X = X_full[:, varying] if varying else np.zeros((N, 0)) | ||
| 158 | + return X, names | ||
| 159 | + | ||
| 160 | + | ||
| 161 | +# ===================================================================== | ||
| 162 | +# viability | ||
| 163 | +# ===================================================================== | ||
| 164 | + | ||
| 165 | +def assess_viability(n_samples, D): | ||
| 166 | + """decide whether a polynomial model is viable, and at what complexity. | ||
| 167 | + | ||
| 168 | + returns: | ||
| 169 | + path: 'coverage' | 'model' | ||
| 170 | + poly_mode: 'full' | 'quad' | 'linear' | ||
| 171 | + """ | ||
| 172 | + if D == 0: | ||
| 173 | + return 'coverage', 'linear' | ||
| 174 | + p_lin = 1 + D | ||
| 175 | + if n_samples / p_lin < MIN_VIABLE_NP: | ||
| 176 | + return 'coverage', 'linear' | ||
| 177 | + p_full = 1 + 2 * D + D * (D - 1) // 2 | ||
| 178 | + if n_samples / p_full >= MIN_VIABLE_NP + 1: | ||
| 179 | + return 'model', 'full' | ||
| 180 | + p_quad = 1 + 2 * D | ||
| 181 | + if n_samples / p_quad >= MIN_VIABLE_NP: | ||
| 182 | + return 'model', 'quad' | ||
| 183 | + return 'model', 'linear' | ||
| 184 | + | ||
| 185 | + | ||
| 186 | +# ===================================================================== | ||
| 187 | +# feature bases: polynomial, fourier, cubic | ||
| 188 | +# ===================================================================== | ||
| 189 | + | ||
| 190 | +def build_poly_features(X, D, mode): | ||
| 191 | + """expand raw features into a polynomial basis (linear/quad/full).""" | ||
| 192 | + n = len(X) | ||
| 193 | + feats = [np.ones(n)] | ||
| 194 | + for i in range(D): | ||
| 195 | + feats.append(X[:, i]) | ||
| 196 | + if mode in ('quad', 'full'): | ||
| 197 | + for i in range(D): | ||
| 198 | + feats.append(X[:, i] ** 2) | ||
| 199 | + if mode == 'full': | ||
| 200 | + for i in range(D): | ||
| 201 | + for j in range(i + 1, D): | ||
| 202 | + feats.append(X[:, i] * X[:, j]) | ||
| 203 | + return np.column_stack(feats) | ||
| 204 | + | ||
| 205 | + | ||
| 206 | +def build_fourier_features(X, D): | ||
| 207 | + """fourier basis: x, sin(pi x), cos(pi x), sin(2 pi x), cos(2 pi x).""" | ||
| 208 | + n = len(X) | ||
| 209 | + feats = [np.ones(n)] | ||
| 210 | + for i in range(D): | ||
| 211 | + xi = X[:, i] | ||
| 212 | + feats.append(xi) | ||
| 213 | + feats.append(np.sin(np.pi * xi)) | ||
| 214 | + feats.append(np.cos(np.pi * xi)) | ||
| 215 | + feats.append(np.sin(2 * np.pi * xi)) | ||
| 216 | + feats.append(np.cos(2 * np.pi * xi)) | ||
| 217 | + return np.column_stack(feats) | ||
| 218 | + | ||
| 219 | + | ||
| 220 | +def build_cubic_features(X, D): | ||
| 221 | + """full cubic polynomial basis.""" | ||
| 222 | + n = len(X) | ||
| 223 | + feats = [np.ones(n)] | ||
| 224 | + for i in range(D): | ||
| 225 | + feats.append(X[:, i]) | ||
| 226 | + for i in range(D): | ||
| 227 | + feats.append(X[:, i] ** 2) | ||
| 228 | + for i in range(D): | ||
| 229 | + for j in range(i + 1, D): | ||
| 230 | + feats.append(X[:, i] * X[:, j]) | ||
| 231 | + for i in range(D): | ||
| 232 | + feats.append(X[:, i] ** 3) | ||
| 233 | + for i in range(D): | ||
| 234 | + for j in range(D): | ||
| 235 | + if j == i: | ||
| 236 | + continue | ||
| 237 | + feats.append(X[:, i] ** 2 * X[:, j]) | ||
| 238 | + for i in range(D): | ||
| 239 | + for j in range(i + 1, D): | ||
| 240 | + for k in range(j + 1, D): | ||
| 241 | + feats.append(X[:, i] * X[:, j] * X[:, k]) | ||
| 242 | + return np.column_stack(feats) | ||
| 243 | + | ||
| 244 | + | ||
| 245 | +def cubic_param_count(D): | ||
| 246 | + """param count of the cubic basis for D features.""" | ||
| 247 | + p = 1 + D + D + D * (D - 1) // 2 | ||
| 248 | + p += D + D * (D - 1) | ||
| 249 | + p += D * (D - 1) * (D - 2) // 6 if D >= 3 else 0 | ||
| 250 | + return p | ||
| 251 | + | ||
| 252 | + | ||
| 253 | +def build_features(X, D, mode): | ||
| 254 | + """dispatch to the right basis builder.""" | ||
| 255 | + if mode == 'fourier': | ||
| 256 | + return build_fourier_features(X, D) | ||
| 257 | + if mode == 'cubic': | ||
| 258 | + return build_cubic_features(X, D) | ||
| 259 | + return build_poly_features(X, D, mode) | ||
| 260 | + | ||
| 261 | + | ||
| 262 | +# ===================================================================== | ||
| 263 | +# spearman rank correlation | ||
| 264 | +# ===================================================================== | ||
| 265 | + | ||
| 266 | +def numpy_spearman(a, b): | ||
| 267 | + """spearman rank correlation between two vectors.""" | ||
| 268 | + n = len(a) | ||
| 269 | + if n < 3: | ||
| 270 | + return 0.0 | ||
| 271 | + order_a = np.argsort(a) | ||
| 272 | + ranks_a = np.empty(n, dtype=float) | ||
| 273 | + ranks_a[order_a] = np.arange(1, n + 1, dtype=float) | ||
| 274 | + order_b = np.argsort(b) | ||
| 275 | + ranks_b = np.empty(n, dtype=float) | ||
| 276 | + ranks_b[order_b] = np.arange(1, n + 1, dtype=float) | ||
| 277 | + std_a, std_b = np.std(ranks_a), np.std(ranks_b) | ||
| 278 | + if std_a < 1e-12 or std_b < 1e-12: | ||
| 279 | + return 0.0 | ||
| 280 | + corr = float(np.corrcoef(ranks_a, ranks_b)[0, 1]) | ||
| 281 | + return 0.0 if np.isnan(corr) else corr | ||
| 282 | + | ||
| 283 | + | ||
| 284 | +# ===================================================================== | ||
| 285 | +# mode selection: spearman-ranked loocv | ||
| 286 | +# ===================================================================== | ||
| 287 | + | ||
| 288 | +def select_mode_and_predict(Xm_norm, dm, Xu_norm, D): | ||
| 289 | + """compete basis modes by spearman-ranked loocv, predict the unmeasured set. | ||
| 290 | + | ||
| 291 | + args: | ||
| 292 | + Xm_norm: (n, D) normalized features of measured configs | ||
| 293 | + dm: (n,) measured durations | ||
| 294 | + Xu_norm: (m, D) normalized features of unmeasured configs | ||
| 295 | + D: int | ||
| 296 | + | ||
| 297 | + returns: | ||
| 298 | + d_hat: (m,) predicted durations for the unmeasured set | ||
| 299 | + best_r_sq: float, R^2 of the winning model | ||
| 300 | + best_mode: str, name of the winning mode | ||
| 301 | + """ | ||
| 302 | + n_meas = len(dm) | ||
| 303 | + p_cub = cubic_param_count(D) | ||
| 304 | + | ||
| 305 | + modes = list(BASE_MODES) | ||
| 306 | + if p_cub < n_meas / 2.0: | ||
| 307 | + modes.append('cubic') | ||
| 308 | + | ||
| 309 | + best_mode = 'linear' | ||
| 310 | + best_rank_corr = -2.0 | ||
| 311 | + best_beta = None | ||
| 312 | + best_r_sq = 0.0 | ||
| 313 | + | ||
| 314 | + for mode in modes: | ||
| 315 | + Fm = build_features(Xm_norm, D, mode) | ||
| 316 | + n, p = Fm.shape | ||
| 317 | + gram = Fm.T @ Fm + RIDGE_LAMBDA * np.eye(p) | ||
| 318 | + try: | ||
| 319 | + gram_inv = np.linalg.inv(gram) | ||
| 320 | + except np.linalg.LinAlgError: | ||
| 321 | + continue | ||
| 322 | + | ||
| 323 | + beta = gram_inv @ Fm.T @ dm | ||
| 324 | + residuals = dm - Fm @ beta | ||
| 325 | + | ||
| 326 | + H = Fm @ gram_inv @ Fm.T | ||
| 327 | + h_diag = np.diag(H) | ||
| 328 | + denom = np.maximum(1.0 - h_diag, 0.01) | ||
| 329 | + loocv_pred = dm - residuals / denom | ||
| 330 | + | ||
| 331 | + rank_corr = numpy_spearman(dm, loocv_pred) | ||
| 332 | + if rank_corr > best_rank_corr: | ||
| 333 | + best_rank_corr = rank_corr | ||
| 334 | + best_mode = mode | ||
| 335 | + best_beta = beta | ||
| 336 | + ss_res = np.sum(residuals ** 2) | ||
| 337 | + ss_tot = np.sum((dm - np.mean(dm)) ** 2) | ||
| 338 | + best_r_sq = max(0.0, min(1.0, 1.0 - ss_res / (ss_tot + 1e-12))) | ||
| 339 | + | ||
| 340 | + if best_beta is None: | ||
| 341 | + Fm = build_features(Xm_norm, D, 'linear') | ||
| 342 | + gram = Fm.T @ Fm + RIDGE_LAMBDA * np.eye(Fm.shape[1]) | ||
| 343 | + best_beta = np.linalg.solve(gram, Fm.T @ dm) | ||
| 344 | + best_mode = 'linear' | ||
| 345 | + best_r_sq = 0.0 | ||
| 346 | + | ||
| 347 | + Fu = build_features(Xu_norm, D, best_mode) | ||
| 348 | + d_hat = Fu @ best_beta | ||
| 349 | + return d_hat, best_r_sq, best_mode | ||
| 350 | + | ||
| 351 | + | ||
| 352 | +# ===================================================================== | ||
| 353 | +# softmax batch selection | ||
| 354 | +# ===================================================================== | ||
| 355 | + | ||
| 356 | +def softmax_select(scores, tau, batch_size, rng): | ||
| 357 | + """pick the next batch to measure, biased toward low scores.""" | ||
| 358 | + s = -scores / tau | ||
| 359 | + s -= s.max() | ||
| 360 | + p = np.exp(s) | ||
| 361 | + p = np.maximum(p, UNDERFLOW_FLOOR) | ||
| 362 | + p /= p.sum() | ||
| 363 | + n = min(batch_size, len(scores)) | ||
| 364 | + | ||
| 365 | + n_nonzero = int(np.sum(p > 0)) | ||
| 366 | + if n_nonzero >= n: | ||
| 367 | + return rng.choice(len(scores), size=n, replace=False, p=p) | ||
| 368 | + if n_nonzero == 0: | ||
| 369 | + return rng.choice(len(scores), size=n, replace=False) | ||
| 370 | + | ||
| 371 | + nonzero_mask = p > 0 | ||
| 372 | + nonzero_idx = np.where(nonzero_mask)[0] | ||
| 373 | + zero_idx = np.where(~nonzero_mask)[0] | ||
| 374 | + p_nz = p[nonzero_mask] | ||
| 375 | + p_nz /= p_nz.sum() | ||
| 376 | + softmax_picks = rng.choice(nonzero_idx, size=n_nonzero, replace=False, p=p_nz) | ||
| 377 | + n_random = n - n_nonzero | ||
| 378 | + random_picks = rng.choice(zero_idx, size=min(n_random, len(zero_idx)), replace=False) | ||
| 379 | + return np.concatenate([softmax_picks, random_picks]) | ||
| 380 | + | ||
| 381 | + | ||
| 382 | +# ===================================================================== | ||
| 383 | +# marginal voting | ||
| 384 | +# ===================================================================== | ||
| 385 | + | ||
| 386 | +def marginal_scores(X_measured, d_measured, X_unmeasured, D): | ||
| 387 | + """score unmeasured configs by per-dimension marginal averages.""" | ||
| 388 | + m = len(X_unmeasured) | ||
| 389 | + scores = np.zeros(m) | ||
| 390 | + for dim in range(D): | ||
| 391 | + val_to_durs = {} | ||
| 392 | + for j in range(len(X_measured)): | ||
| 393 | + v = X_measured[j, dim] | ||
| 394 | + if v not in val_to_durs: | ||
| 395 | + val_to_durs[v] = [] | ||
| 396 | + val_to_durs[v].append(d_measured[j]) | ||
| 397 | + val_to_mean = {v: np.mean(ds) for v, ds in val_to_durs.items()} | ||
| 398 | + global_mean = np.mean(d_measured) | ||
| 399 | + for j in range(m): | ||
| 400 | + v = X_unmeasured[j, dim] | ||
| 401 | + scores[j] += val_to_mean.get(v, global_mean) | ||
| 402 | + return scores | ||
| 403 | + | ||
| 404 | + | ||
| 405 | +# ===================================================================== | ||
| 406 | +# stratified R1 sampling | ||
| 407 | +# ===================================================================== | ||
| 408 | + | ||
| 409 | +def stratified_sample(X, n_sample, rng): | ||
| 410 | + """pick R1 indices: one representative per unique value, most-unique dim first.""" | ||
| 411 | + N, D = X.shape | ||
| 412 | + selected = set() | ||
| 413 | + used_dims = set() | ||
| 414 | + remaining = n_sample | ||
| 415 | + | ||
| 416 | + while remaining > 0 and len(used_dims) < D: | ||
| 417 | + best_dim, best_count = -1, -1 | ||
| 418 | + for dim in range(D): | ||
| 419 | + if dim in used_dims: | ||
| 420 | + continue | ||
| 421 | + n_unique = len(np.unique(X[:, dim])) | ||
| 422 | + if n_unique > best_count: | ||
| 423 | + best_dim, best_count = dim, n_unique | ||
| 424 | + if best_dim < 0: | ||
| 425 | + break | ||
| 426 | + used_dims.add(best_dim) | ||
| 427 | + for val in np.unique(X[:, best_dim]): | ||
| 428 | + if remaining <= 0: | ||
| 429 | + break | ||
| 430 | + candidates = [i for i in range(N) | ||
| 431 | + if X[i, best_dim] == val and i not in selected] | ||
| 432 | + if candidates: | ||
| 433 | + selected.add(rng.choice(candidates)) | ||
| 434 | + remaining -= 1 | ||
| 435 | + | ||
| 436 | + if remaining > 0: | ||
| 437 | + pool = [i for i in range(N) if i not in selected] | ||
| 438 | + if pool: | ||
| 439 | + n_pick = min(remaining, len(pool)) | ||
| 440 | + picks = rng.choice(pool, size=n_pick, replace=False) | ||
| 441 | + selected.update(picks.tolist()) | ||
| 442 | + | ||
| 443 | + return sorted(selected) | ||
| 444 | + | ||
| 445 | + | ||
| 446 | +# ===================================================================== | ||
| 447 | +# temperature | ||
| 448 | +# ===================================================================== | ||
| 449 | + | ||
| 450 | +def compute_tau(r_sq, k_est): | ||
| 451 | + """softmax temperature from model quality and needle rarity.""" | ||
| 452 | + k_factor = min(1.0, k_est / 4.0) | ||
| 453 | + return TAU_MIN + TAU_RANGE * (1.0 - r_sq) * k_factor | ||
| 454 | + | ||
| 455 | + | ||
| 456 | +# ===================================================================== | ||
| 457 | +# convergence | ||
| 458 | +# ===================================================================== | ||
| 459 | + | ||
| 460 | +def should_stop(dm_array, best_measured, d_hat_unmeasured, prev_hist): | ||
| 461 | + """margin and histogram-stability stopping checks.""" | ||
| 462 | + if len(dm_array) == 0: | ||
| 463 | + return False, prev_hist | ||
| 464 | + | ||
| 465 | + if len(d_hat_unmeasured) > 0: | ||
| 466 | + best_pred = np.min(d_hat_unmeasured) | ||
| 467 | + margin = best_pred / best_measured if best_measured > 0 else 0 | ||
| 468 | + if margin > MARGIN_CONV: | ||
| 469 | + return True, prev_hist | ||
| 470 | + | ||
| 471 | + bins = np.linspace(np.min(dm_array), np.max(dm_array), HIST_BINS) | ||
| 472 | + hist_now = np.histogram(dm_array, bins=bins, density=True)[0] | ||
| 473 | + hist_now = hist_now / (hist_now.sum() + 1e-12) | ||
| 474 | + if prev_hist is not None: | ||
| 475 | + l1 = np.sum(np.abs(hist_now - prev_hist)) | ||
| 476 | + if l1 < L1_CONV: | ||
| 477 | + return True, hist_now | ||
| 478 | + | ||
| 479 | + return False, hist_now | ||
| @@ -0,0 +1,27 @@ | |||
| 1 | +from dataclasses import dataclass, field | ||
| 2 | +import os | ||
| 3 | + | ||
| 4 | +# Additional info on _fasta_dynamic_filter params and tuning strategies can be found at | ||
| 5 | +# torch_npu/_inductor/docs/feature/autotuning_optimization/dynamic_filter_algo_params.md | ||
| 6 | + | ||
| 7 | +class _fasta_dynamic_filter: | ||
| 8 | + r1_pct: float = field(default_factory=lambda: float(os.getenv("FASTA_R1_PCT", 0.3))) | ||
| 9 | + base_budget: float = field(default_factory=lambda: float(os.getenv("FASTA_BASE_BUDGET", 0.35))) | ||
| 10 | + high_budget: float = field(default_factory=lambda: float(os.getenv("FASTA_HIGH_BUDGET", 0.4))) | ||
| 11 | + low_budget: float = field(default_factory=lambda: float(os.getenv("FASTA_LOW_BUDGET", 0.25))) | ||
| 12 | + max_rounds: int = field(default_factory=lambda: int(os.getenv("FASTA_MAX_ROUNDS", 2))) | ||
| 13 | + | ||
| 14 | +# Activate the dynamic filter algo - default=0 | ||
| 15 | +if os.getenv("FASTA_DYNAMIC_FILTER", "0") == "1": | ||
| 16 | + fasta_dynamic_filter = _fasta_dynamic_filter() | ||
| 17 | +else: | ||
| 18 | + fasta_dynamic_filter = None | ||
| 19 | + | ||
| 20 | +# Additional info on config_optimizer params and tuning strategies can be found at | ||
| 21 | +# torch_npu/_inductor/docs/feature/autotuning_optimization/config_optimizer_params.md | ||
| 22 | +# Activate the config optimizer - default=0 | ||
| 23 | +fasta_config_optimizer = os.getenv("FASTA_CONFIG_OPTIMIZER", "0") == "1" | ||
| 24 | +# Activate the autotune statistics - default=0 | ||
| 25 | +fasta_autotune_stats = os.getenv("FASTA_AUTOTUNE_STATS", "0") == "1" | ||
| 26 | +# Enable MSPTI profiler for autotuning benchmarking - default=0 | ||
| 27 | +fasta_mspti_en = os.getenv("FASTA_MSPTI_EN", "0") == "1" | ||
| @@ -0,0 +1,650 @@ | |||
| 1 | +""" | ||
| 2 | +dynamic filter — adaptive tiling config selection. | ||
| 3 | + | ||
| 4 | +thin wrapper over dynamic_filter_algo. the math lives in the algo module. | ||
| 5 | + | ||
| 6 | +the production caller (dynamic_filter_scheduler.py) drives the batch api | ||
| 7 | +directly: | ||
| 8 | + | ||
| 9 | + init phase (during model compilation): | ||
| 10 | + flt = DynamicFilter(all_configs, kernel_name) | ||
| 11 | + # caller compiles ALL configs via precompile_parallel() | ||
| 12 | + # flt is stored on the autotuner instance | ||
| 13 | + | ||
| 14 | + bench phase (first kernel.run()): | ||
| 15 | + batch = flt.r1_configs | ||
| 16 | + while batch: | ||
| 17 | + durations = bench(batch) | ||
| 18 | + batch = flt.refine(durations) | ||
| 19 | + # then read flt.stats / flt._best_idx | ||
| 20 | + | ||
| 21 | +the self-driving loop (run(bench_fn)) lives in the offline harness | ||
| 22 | +(offline_exp/run_filter.py), not here — production never calls it. | ||
| 23 | + | ||
| 24 | +env: | ||
| 25 | + ALGO_IF_LOG=1 — verbose debug logging (default 0) | ||
| 26 | +""" | ||
| 27 | +import os | ||
| 28 | +import time | ||
| 29 | +import numpy as np | ||
| 30 | +import torch_npu._inductor.experimental.dynamic_filter.dynamic_filter_algo as algo | ||
| 31 | +from torch_npu._inductor.config import log as _prod_log | ||
| 32 | + | ||
| 33 | +_TAG = "[FASTA_DYN_FILTER_IF]" | ||
| 34 | + | ||
| 35 | +ALGO_IF_LOG = int(os.environ.get('ALGO_IF_LOG', '0')) | ||
| 36 | + | ||
| 37 | + | ||
| 38 | +def _selector_log(msg, level='debug'): | ||
| 39 | + if not ALGO_IF_LOG: | ||
| 40 | + return | ||
| 41 | + from ...config import log | ||
| 42 | + full = f'[dyn filter] {msg}' | ||
| 43 | + if level == 'info': | ||
| 44 | + log.info(full) | ||
| 45 | + elif level == 'warning': | ||
| 46 | + log.warning(full) | ||
| 47 | + else: | ||
| 48 | + log.debug(full) | ||
| 49 | + | ||
| 50 | + | ||
| 51 | +# ===================================================================== | ||
| 52 | +# walk-through (read before changing the algorithm) | ||
| 53 | +# ===================================================================== | ||
| 54 | +# | ||
| 55 | +# PHASE I -- INIT (during model compilation, in the subprocess pool): | ||
| 56 | +# I.1 store items/kernel_name/rng/counters; assert the input contract | ||
| 57 | +# I.2 algo.extract_features(items) -> (X, feat_names); D = X.shape[1] | ||
| 58 | +# I.3 pick r1_size from R1_PCT and R1_FLOOR_PCT bounds | ||
| 59 | +# I.4 pick total_budget by routing: high (small low-D), low (large high-D), | ||
| 60 | +# high_legacy (high-D underflow), med (rest) | ||
| 61 | +# I.5 algo.stratified_sample(X, r1_size, rng) -> r1_indices (D>0) | ||
| 62 | +# else random.choice(N, r1_size) (D==0) | ||
| 63 | +# I.6 algo.assess_viability(r1_size, D) -> (path, poly_mode) | ||
| 64 | +# I.7 normalize X to [0,1] per column -> Xn | ||
| 65 | +# I.8 unmeasured = set(range(N)) - set(r1_indices) | ||
| 66 | +# | ||
| 67 | +# PHASE II -- R1 (once, just after init): | ||
| 68 | +# II.1 r1_configs (state-advancing, idempotent) returns items[r1_indices] | ||
| 69 | +# and sets _last_batch_indices so the next refine() can record | ||
| 70 | +# II.2 caller benchmarks them, returns durations in the same order | ||
| 71 | +# | ||
| 72 | +# PHASE III -- REFINE LOOP (repeat until an empty list comes back): | ||
| 73 | +# III.1 record durations; update _best_dur/_best_idx; check invariants | ||
| 74 | +# III.2 if all measured or _done -> return [] (terminal) | ||
| 75 | +# III.3 dispatch: | ||
| 76 | +# path == 'coverage': _coverage_step (one shot, then done) | ||
| 77 | +# else : _model_step (predict + softmax pick) | ||
| 78 | +# III.4 advance _round, store _last_batch_indices, return the next batch | ||
| 79 | +# | ||
| 80 | +# PHASE IV -- MODEL STEP: | ||
| 81 | +# IV.1 Xm = Xn[measured], dm = durations[measured]; Xu = Xn[unmeasured] | ||
| 82 | +# IV.2 algo.select_mode_and_predict(Xm, dm, Xu, D) -> (d_hat, R^2, mode) | ||
| 83 | +# IV.3 algo.should_stop(dm, best, d_hat, prev_hist) -> bool | ||
| 84 | +# IV.4 batch_size = total_budget // MAX_ROUNDS, capped by remaining | ||
| 85 | +# IV.5 D>=MARGINAL_D: half via softmax(d_hat), half via marginal score | ||
| 86 | +# else : all via softmax(d_hat) | ||
| 87 | +# IV.6 return uidx[merged] (indices into _all_items) | ||
| 88 | +# | ||
| 89 | +# every phase boundary is guarded by _check. failures raise FastaCheckError | ||
| 90 | +# carrying the kernel name and the condition. always on by design: catch the | ||
| 91 | +# bug at the first crime, not the second. | ||
| 92 | +# ===================================================================== | ||
| 93 | + | ||
| 94 | + | ||
| 95 | +class FastaCheckError(AssertionError): | ||
| 96 | + """raised when a filter input/invariant is violated.""" | ||
| 97 | + | ||
| 98 | + | ||
| 99 | +def _check(cond, kernel_name, fmt, *args): | ||
| 100 | + """always-on assertion. fmt uses %-format with optional args.""" | ||
| 101 | + if not cond: | ||
| 102 | + msg = fmt % args if args else fmt | ||
| 103 | + raise FastaCheckError(f"[FASTA-CHECK] {kernel_name}: {msg}") | ||
| 104 | + | ||
| 105 | + | ||
| 106 | +def _confidence_label(r_sq, margin, k_est): | ||
| 107 | + """confidence in the selected best, from the last round's kpis. | ||
| 108 | + | ||
| 109 | + the dangerous case is a sharp single winner (k_est==1) the surrogate could | ||
| 110 | + not model (low r_sq) and did not separate from the field (margin<1). the | ||
| 111 | + safe case is either many near-ties (any pick is fine) or a well-fit | ||
| 112 | + surrogate confident the winner is already in hand (margin>=1). | ||
| 113 | + """ | ||
| 114 | + # many near-optimal configs -> the pick barely matters | ||
| 115 | + if k_est >= 3: | ||
| 116 | + return 'high' | ||
| 117 | + # surrogate is confident the winner is already measured | ||
| 118 | + if margin >= 1.0 and r_sq >= 0.5: | ||
| 119 | + return 'high' | ||
| 120 | + if margin >= 1.0 or r_sq >= 0.6: | ||
| 121 | + return 'med' | ||
| 122 | + # sharp single winner the model could not explain -> least trustworthy | ||
| 123 | + if k_est <= 1 and r_sq < 0.3: | ||
| 124 | + return 'low' | ||
| 125 | + return 'med' | ||
| 126 | + | ||
| 127 | + | ||
| 128 | +class DynamicFilter: | ||
| 129 | + """adaptive config filter for autotuning. | ||
| 130 | + | ||
| 131 | + init entry (during model compilation): | ||
| 132 | + flt = DynamicFilter(configs, kernel_name) | ||
| 133 | + calls: algo.extract_features, algo.assess_viability, | ||
| 134 | + algo.stratified_sample | ||
| 135 | + | ||
| 136 | + batch api (driven by the production caller): | ||
| 137 | + batch = flt.r1_configs # state-advancing single-shot | ||
| 138 | + durations = my_benchmark(batch) | ||
| 139 | + batch = flt.refine(durations) # calls algo.select_mode_and_predict/should_stop/ | ||
| 140 | + ...until batch is empty... # compute_tau/softmax_select/marginal_scores | ||
| 141 | + # results via flt.stats | ||
| 142 | + """ | ||
| 143 | + | ||
| 144 | + def __init__(self, items, kernel_name='unknown'): | ||
| 145 | + """init entry — feature extraction, R1 planning, budget sizing. | ||
| 146 | + | ||
| 147 | + runs during model compilation. after this returns, the caller compiles | ||
| 148 | + R1 via precompile_parallel. | ||
| 149 | + | ||
| 150 | + args: | ||
| 151 | + items: list of config objects with a .kwargs dict. | ||
| 152 | + kernel_name: str for logging. | ||
| 153 | + """ | ||
| 154 | + self._t_init = time.perf_counter_ns() | ||
| 155 | + | ||
| 156 | + # PHASE I.1: input contract | ||
| 157 | + _check(items is not None, kernel_name, "items is None") | ||
| 158 | + _check(isinstance(kernel_name, str) and kernel_name, | ||
| 159 | + kernel_name or "<empty>", | ||
| 160 | + "kernel_name must be non-empty str, got %r", kernel_name) | ||
| 161 | + | ||
| 162 | + self._kernel_name = kernel_name | ||
| 163 | + self._all_items = list(items) | ||
| 164 | + self._N = len(self._all_items) | ||
| 165 | + | ||
| 166 | + _check(self._N > 0, kernel_name, "empty configs list") | ||
| 167 | + _bad = [i for i, c in enumerate(self._all_items) if not hasattr(c, 'kwargs')] | ||
| 168 | + _check(not _bad, kernel_name, | ||
| 169 | + "%d/%d configs lack .kwargs (first idx=%d)", | ||
| 170 | + len(_bad), self._N, _bad[0] if _bad else -1) | ||
| 171 | + # id() collisions break the strategy-side _launcher_map (keyed by id). | ||
| 172 | + _ids = [id(c) for c in self._all_items] | ||
| 173 | + _check(len(_ids) == len(set(_ids)), kernel_name, | ||
| 174 | + "duplicate config object ids in items (N=%d unique=%d)", | ||
| 175 | + self._N, len(set(_ids))) | ||
| 176 | + | ||
| 177 | + self._rng = np.random.RandomState() | ||
| 178 | + self._round = 0 | ||
| 179 | + self._measured = {} | ||
| 180 | + self._best_idx = None | ||
| 181 | + self._best_dur = float('inf') | ||
| 182 | + self._last_batch_indices = [] | ||
| 183 | + self._overhead_ns = 0 | ||
| 184 | + self._prev_hist = None | ||
| 185 | + self._round_estimators = [] # estimator (basis mode) picked per refine round | ||
| 186 | + self._round_records = [] # per-round kpi dicts: round/est/r_sq/margin/k_est/batch/best | ||
| 187 | + self._last_r_sq = 0.0 # last surrogate fit quality | ||
| 188 | + self._last_margin = float('inf') # last best_pred/best_measured ratio | ||
| 189 | + self._last_k_est = 0 # last count of configs within K_TOLERANCE of best | ||
| 190 | + self._t_algo_start = time.perf_counter_ns() # wall clock for the whole selection | ||
| 191 | + self._end_logged = False | ||
| 192 | + # configs that failed to compile during R1/R2. dropped from the pool: | ||
| 193 | + # not measured, not re-proposed. | ||
| 194 | + self.not_profiled_indices = [] | ||
| 195 | + | ||
| 196 | + # PHASE I.2: feature extraction | ||
| 197 | + self._X, self._feat_names = algo.extract_features(self._all_items) | ||
| 198 | + _check(self._X.ndim == 2, kernel_name, | ||
| 199 | + "extract_features X.ndim=%d expected 2", self._X.ndim) | ||
| 200 | + _check(self._X.shape[0] == self._N, kernel_name, | ||
| 201 | + "extract_features X.shape[0]=%d expected N=%d", | ||
| 202 | + self._X.shape[0], self._N) | ||
| 203 | + _check(len(self._feat_names) == self._X.shape[1], kernel_name, | ||
| 204 | + "feat_names len=%d != X.shape[1]=%d", | ||
| 205 | + len(self._feat_names), self._X.shape[1]) | ||
| 206 | + self._D = self._X.shape[1] if self._X.shape[1] > 0 else 0 | ||
| 207 | + | ||
| 208 | + # PHASE I.3 + I.4: budget sizing | ||
| 209 | + self._r1_size = max(int(algo.df_cfg.r1_pct * self._N), int(algo.R1_FLOOR_PCT * self._N)) | ||
| 210 | + self._r1_size = min(self._r1_size, self._N) | ||
| 211 | + _check(0 < self._r1_size <= self._N, kernel_name, | ||
| 212 | + "_r1_size=%d not in (0, %d]", self._r1_size, self._N) | ||
| 213 | + | ||
| 214 | + p_lin = 1 + self._D if self._D > 0 else 1 | ||
| 215 | + np_ratio = self._r1_size / p_lin | ||
| 216 | + # budget routing (mirrors algo.run): | ||
| 217 | + # high: small low-D kernels -> HIGH_BUDGET | ||
| 218 | + # low: large high-D kernels -> LOW_BUDGET, smaller R1 | ||
| 219 | + # high_legacy: high-D underflow fallback -> HIGH_BUDGET | ||
| 220 | + # med: rest -> BASE_BUDGET | ||
| 221 | + if self._D <= algo.HARD_D_THRESH and self._N <= algo.HARD_N_THRESH: | ||
| 222 | + self._total_budget = max(int(algo.df_cfg.high_budget * self._N), self._r1_size) | ||
| 223 | + self._budget_class = 'high' | ||
| 224 | + self._budget_frac = algo.df_cfg.high_budget | ||
| 225 | + elif self._D >= algo.EASY_D_THRESH and self._N >= algo.EASY_N_THRESH: | ||
| 226 | + self._total_budget = max(int(algo.df_cfg.low_budget * self._N), self._r1_size) | ||
| 227 | + self._r1_size = max(int(algo.R1_PCT_LOW * self._N), int(algo.R1_FLOOR_PCT * self._N)) | ||
| 228 | + self._r1_size = min(self._r1_size, self._N) | ||
| 229 | + self._budget_class = 'low' | ||
| 230 | + self._budget_frac = algo.df_cfg.low_budget | ||
| 231 | + elif self._D >= algo.HIGH_D_THRESH and np_ratio < algo.HIGH_NP_THRESH: | ||
| 232 | + self._total_budget = max(int(algo.df_cfg.high_budget * self._N), self._r1_size) | ||
| 233 | + self._budget_class = 'high_legacy' | ||
| 234 | + self._budget_frac = algo.df_cfg.high_budget | ||
| 235 | + else: | ||
| 236 | + self._total_budget = max(int(algo.df_cfg.base_budget * self._N), self._r1_size) | ||
| 237 | + self._budget_class = 'med' | ||
| 238 | + self._budget_frac = algo.df_cfg.base_budget | ||
| 239 | + | ||
| 240 | + _check(self._total_budget >= self._r1_size, kernel_name, | ||
| 241 | + "total_budget=%d < r1_size=%d", | ||
| 242 | + self._total_budget, self._r1_size) | ||
| 243 | + | ||
| 244 | + # PHASE I.5: stratified or random R1 sample | ||
| 245 | + if self._D > 0: | ||
| 246 | + self._r1_indices = algo.stratified_sample( | ||
| 247 | + self._X, self._r1_size, self._rng) | ||
| 248 | + else: | ||
| 249 | + self._r1_indices = sorted( | ||
| 250 | + self._rng.choice(self._N, size=min(self._r1_size, self._N), | ||
| 251 | + replace=False).tolist()) | ||
| 252 | + _check(len(self._r1_indices) > 0, kernel_name, | ||
| 253 | + "_r1_indices empty (r1_size=%d N=%d D=%d)", | ||
| 254 | + self._r1_size, self._N, self._D) | ||
| 255 | + _check(all(0 <= i < self._N for i in self._r1_indices), kernel_name, | ||
| 256 | + "_r1_indices out of [0,%d): %r", | ||
| 257 | + self._N, [i for i in self._r1_indices if not (0 <= i < self._N)][:5]) | ||
| 258 | + _check(len(self._r1_indices) == len(set(self._r1_indices)), kernel_name, | ||
| 259 | + "_r1_indices has duplicates: %d vs %d unique", | ||
| 260 | + len(self._r1_indices), len(set(self._r1_indices))) | ||
| 261 | + | ||
| 262 | + # PHASE I.6: viability classification | ||
| 263 | + self._path, self._poly_mode = algo.assess_viability( | ||
| 264 | + len(self._r1_indices), self._D) | ||
| 265 | + _check(self._path in ('coverage', 'model'), kernel_name, | ||
| 266 | + "assess_viability path=%r not in {coverage, model}", self._path) | ||
| 267 | + if self._path == 'coverage': | ||
| 268 | + self._poly_mode = 'coverage' # match algo.run telemetry | ||
| 269 | + | ||
| 270 | + # PHASE I.7: feature normalization. xmin/xmax/xrange are locals — only | ||
| 271 | + # used here to build _Xn, never read again. | ||
| 272 | + if self._D > 0: | ||
| 273 | + xmin = self._X.min(axis=0) | ||
| 274 | + xrange = self._X.max(axis=0) - xmin | ||
| 275 | + xrange[xrange == 0] = 1.0 | ||
| 276 | + self._Xn = (self._X - xmin) / xrange | ||
| 277 | + _check(self._Xn.shape == self._X.shape, kernel_name, | ||
| 278 | + "Xn shape %r != X shape %r", self._Xn.shape, self._X.shape) | ||
| 279 | + else: | ||
| 280 | + self._Xn = self._X | ||
| 281 | + | ||
| 282 | + # PHASE I.8: measured/unmeasured invariants | ||
| 283 | + self._unmeasured = set(range(self._N)) - set(self._r1_indices) | ||
| 284 | + _check(self._unmeasured.isdisjoint(set(self._r1_indices)), self._kernel_name, | ||
| 285 | + "_unmeasured overlaps _r1_indices") | ||
| 286 | + _check(len(self._unmeasured) + len(self._r1_indices) == self._N, self._kernel_name, | ||
| 287 | + "|unmeasured|+|r1|=%d != N=%d", | ||
| 288 | + len(self._unmeasured) + len(self._r1_indices), self._N) | ||
| 289 | + self._used = 0 | ||
| 290 | + self._done = False | ||
| 291 | + | ||
| 292 | + self._overhead_ns += time.perf_counter_ns() - self._t_init | ||
| 293 | + | ||
| 294 | + # config banner (once) | ||
| 295 | + algo.log_algo_config() | ||
| 296 | + # per-kernel start line | ||
| 297 | + _prod_log.info( | ||
| 298 | + _TAG + " event=start kernel=%s N=%d D=%d class=%s budget_frac=%.2f " | ||
| 299 | + "r1_size=%d planned_budget=%d path=%s", | ||
| 300 | + self._kernel_name, self._N, self._D, self._budget_class, | ||
| 301 | + self._budget_frac, self._r1_size, self._total_budget, self._path) | ||
| 302 | + | ||
| 303 | + _selector_log( | ||
| 304 | + f'{kernel_name}: N={self._N} D={self._D} ' | ||
| 305 | + f'feats={self._feat_names} r1={len(self._r1_indices)} ' | ||
| 306 | + f'budget={self._total_budget} path={self._path} ' | ||
| 307 | + f'poly={self._poly_mode} ' | ||
| 308 | + f'init_overhead={self._overhead_ns / 1e6:.2f}ms', | ||
| 309 | + level='info') | ||
| 310 | + | ||
| 311 | + # ================================================================= | ||
| 312 | + # batch api — driven by the production caller | ||
| 313 | + # | ||
| 314 | + # run(bench_fn) (the self-driving loop) moved to the offline harness | ||
| 315 | + # (offline_exp/run_filter.py); production never called it. | ||
| 316 | + # ================================================================= | ||
| 317 | + | ||
| 318 | + | ||
| 319 | + def r1_configs(self): | ||
| 320 | + """return R1 configs (PHASE II.1). state-advancing but idempotent. | ||
| 321 | + | ||
| 322 | + first read advances state (_last_batch_indices = r1_indices, _round=1) | ||
| 323 | + so refine() can record the durations against the right items. later | ||
| 324 | + reads are no-ops since the assigned values match what is already there. | ||
| 325 | + | ||
| 326 | + this single property replaces the old (r1_configs peek / r1_batch | ||
| 327 | + advance) split, which had a footgun: a caller reading r1_configs would | ||
| 328 | + never trigger refine()'s recording loop and the R1 timings would | ||
| 329 | + silently never enter _measured. | ||
| 330 | + """ | ||
| 331 | + if self._round == 0: | ||
| 332 | + # first read advances state; later reads converge to the same. | ||
| 333 | + self._last_batch_indices = list(self._r1_indices) | ||
| 334 | + self._round = 1 | ||
| 335 | + return [self._all_items[i] for i in self._r1_indices] | ||
| 336 | + | ||
| 337 | + def refine(self, durations): | ||
| 338 | + """feed durations from the last batch, return the next batch. | ||
| 339 | + | ||
| 340 | + PHASE III. returns [] when done (budget exhausted or converged). | ||
| 341 | + | ||
| 342 | + hard contract on `durations`: | ||
| 343 | + - sized sequence (list / tuple / ndarray) of numeric values | ||
| 344 | + - len must equal len(self._last_batch_indices) | ||
| 345 | + - values non-NaN, non-negative (use +inf for failed configs) | ||
| 346 | + - none of self._last_batch_indices may already be in self._measured | ||
| 347 | + (that means the same batch was issued twice — the bug we hunt) | ||
| 348 | + """ | ||
| 349 | + t0 = time.perf_counter_ns() | ||
| 350 | + | ||
| 351 | + # PHASE III.1.a: input contract | ||
| 352 | + _check(hasattr(durations, '__len__'), self._kernel_name, | ||
| 353 | + "refine() durations type=%s not sized", type(durations).__name__) | ||
| 354 | + _check(len(durations) == len(self._last_batch_indices), self._kernel_name, | ||
| 355 | + "refine() len(durations)=%d != len(last_batch)=%d", | ||
| 356 | + len(durations), len(self._last_batch_indices)) | ||
| 357 | + for k, d in enumerate(durations): | ||
| 358 | + try: | ||
| 359 | + fd = float(d) | ||
| 360 | + except (TypeError, ValueError): | ||
| 361 | + raise FastaCheckError( | ||
| 362 | + f"[FASTA-CHECK] {self._kernel_name}: refine() " | ||
| 363 | + f"durations[{k}] not numeric: type={type(d).__name__} val={d!r}") | ||
| 364 | + _check(fd == fd, self._kernel_name, | ||
| 365 | + "refine() durations[%d] is NaN", k) | ||
| 366 | + _check(fd >= 0.0, self._kernel_name, | ||
| 367 | + "refine() durations[%d]=%r is negative", k, fd) | ||
| 368 | + | ||
| 369 | + # PHASE III.1.b: re-bench detection | ||
| 370 | + already = [i for i in self._last_batch_indices if i in self._measured] | ||
| 371 | + _check(not already, self._kernel_name, | ||
| 372 | + "refine() got indices already measured: %r (round=%d)", | ||
| 373 | + already[:5], self._round) | ||
| 374 | + | ||
| 375 | + # PHASE III.1.c: record measurements | ||
| 376 | + for idx, dur in zip(self._last_batch_indices, durations): | ||
| 377 | + self._measured[idx] = float(dur) | ||
| 378 | + self._unmeasured.discard(idx) | ||
| 379 | + if dur < self._best_dur: | ||
| 380 | + self._best_dur = float(dur) | ||
| 381 | + self._best_idx = idx | ||
| 382 | + self._used = len(self._measured) | ||
| 383 | + | ||
| 384 | + # PHASE III.1.d: invariants after recording | ||
| 385 | + # disjointness must always hold. | ||
| 386 | + _check(set(self._measured.keys()).isdisjoint(self._unmeasured), | ||
| 387 | + self._kernel_name, | ||
| 388 | + "post-record: measured cap unmeasured non-empty") | ||
| 389 | + # item accounting. an item lives in exactly ONE of: | ||
| 390 | + # (a) measured -- already benchmarked | ||
| 391 | + # (b) unmeasured -- not yet handed out to the caller | ||
| 392 | + # (c) last_batch_pending -- handed out, awaiting the next refine() drain | ||
| 393 | + # after r1_configs is read (the first thing a caller does), the | ||
| 394 | + # _r1_indices subset moves from "implicit pending" to last_batch. no | ||
| 395 | + # leak possible: union of (a,b,c) must be exactly range(N). | ||
| 396 | + _accounted = (set(self._measured.keys()) | ||
| 397 | + | self._unmeasured | ||
| 398 | + | set(self._last_batch_indices)) | ||
| 399 | + _missing = (set(range(self._N)) | ||
| 400 | + - set(self.not_profiled_indices) | ||
| 401 | + - _accounted) | ||
| 402 | + _check(not _missing, self._kernel_name, | ||
| 403 | + "post-record: %d/%d indices unaccounted " | ||
| 404 | + "(measured=%d unmeasured=%d last_batch=%d). missing[:5]=%r", | ||
| 405 | + len(_missing), self._N, | ||
| 406 | + len(self._measured), len(self._unmeasured), | ||
| 407 | + len(self._last_batch_indices), sorted(_missing)[:5]) | ||
| 408 | + | ||
| 409 | + if self._done or not self._unmeasured: | ||
| 410 | + self._overhead_ns += time.perf_counter_ns() - t0 | ||
| 411 | + self._log_done() | ||
| 412 | + return [] | ||
| 413 | + | ||
| 414 | + if self._path == 'coverage': | ||
| 415 | + batch_indices = self._coverage_step() | ||
| 416 | + self._done = True | ||
| 417 | + self._last_batch_indices = batch_indices | ||
| 418 | + self._round += 1 | ||
| 419 | + self._overhead_ns += time.perf_counter_ns() - t0 | ||
| 420 | + if not batch_indices: | ||
| 421 | + self._log_done() | ||
| 422 | + return [] | ||
| 423 | + return [self._all_items[i] for i in batch_indices] | ||
| 424 | + | ||
| 425 | + batch_indices = self._model_step() | ||
| 426 | + if not batch_indices: | ||
| 427 | + self._done = True | ||
| 428 | + self._overhead_ns += time.perf_counter_ns() - t0 | ||
| 429 | + self._log_done() | ||
| 430 | + return [] | ||
| 431 | + | ||
| 432 | + self._last_batch_indices = batch_indices | ||
| 433 | + self._round += 1 | ||
| 434 | + self._overhead_ns += time.perf_counter_ns() - t0 | ||
| 435 | + | ||
| 436 | + _selector_log( | ||
| 437 | + f'{self._kernel_name}: round {self._round} ' | ||
| 438 | + f'batch={len(batch_indices)} measured={self._used} ' | ||
| 439 | + f'best={self._best_dur:.3f}') | ||
| 440 | + | ||
| 441 | + return [self._all_items[i] for i in batch_indices] | ||
| 442 | + | ||
| 443 | + # ================================================================= | ||
| 444 | + # internal | ||
| 445 | + # ================================================================= | ||
| 446 | + | ||
| 447 | + def _coverage_step(self): | ||
| 448 | + cov_budget = max(int(algo.df_cfg.high_budget * self._N), self._r1_size) | ||
| 449 | + remaining = cov_budget - self._used | ||
| 450 | + if remaining <= 0 or not self._unmeasured: | ||
| 451 | + return [] | ||
| 452 | + pool = sorted(self._unmeasured) | ||
| 453 | + n_pick = min(remaining, len(pool)) | ||
| 454 | + chosen = self._rng.choice(len(pool), size=n_pick, replace=False) | ||
| 455 | + return [pool[c] for c in chosen] | ||
| 456 | + | ||
| 457 | + def _model_step(self): | ||
| 458 | + if self._round >= algo.df_cfg.max_rounds: | ||
| 459 | + return [] | ||
| 460 | + | ||
| 461 | + midx = sorted(self._measured.keys()) | ||
| 462 | + uidx = sorted(self._unmeasured) | ||
| 463 | + if not uidx: | ||
| 464 | + return [] | ||
| 465 | + | ||
| 466 | + Xm = self._Xn[midx] | ||
| 467 | + dm = np.array([self._measured[i] for i in midx]) | ||
| 468 | + Xu = self._Xn[uidx] | ||
| 469 | + D = self._D | ||
| 470 | + | ||
| 471 | + d_hat, r_sq, chosen_mode = algo.select_mode_and_predict(Xm, dm, Xu, D) | ||
| 472 | + # PHASE IV.2 output check | ||
| 473 | + _check(d_hat.shape == (len(uidx),), self._kernel_name, | ||
| 474 | + "select_mode_and_predict d_hat.shape=%r expected (%d,)", | ||
| 475 | + d_hat.shape, len(uidx)) | ||
| 476 | + _check(not np.any(np.isnan(d_hat)), self._kernel_name, | ||
| 477 | + "select_mode_and_predict returned NaN d_hat (mode=%s r_sq=%r)", | ||
| 478 | + chosen_mode, r_sq) | ||
| 479 | + _check(0.0 <= r_sq <= 1.0, self._kernel_name, | ||
| 480 | + "select_mode_and_predict r_sq=%r outside [0,1]", r_sq) | ||
| 481 | + self._poly_mode = chosen_mode | ||
| 482 | + self._round_estimators.append(chosen_mode) | ||
| 483 | + | ||
| 484 | + best_m = self._best_dur | ||
| 485 | + # per-round kpis (computed regardless of the stop decision): | ||
| 486 | + # margin = predicted-best-unmeasured / best-measured. margin>=1 means | ||
| 487 | + # the surrogate believes the winner is already in hand. | ||
| 488 | + # r_sq = surrogate fit quality in [0,1]; high = landscape understood. | ||
| 489 | + # k_est = configs within K_TOLERANCE of best; high = many near-ties | ||
| 490 | + # (easy kernel, any pick fine); 1 = sharp single winner (high stakes). | ||
| 491 | + best_pred = float(np.min(d_hat)) if len(d_hat) else float('inf') | ||
| 492 | + margin = (best_pred / best_m) if best_m > 0 else float('inf') | ||
| 493 | + k_est = sum(1 for v in self._measured.values() | ||
| 494 | + if v <= best_m * algo.K_TOLERANCE) | ||
| 495 | + self._last_r_sq = float(r_sq) | ||
| 496 | + self._last_margin = float(margin) | ||
| 497 | + self._last_k_est = int(k_est) | ||
| 498 | + self._round_records.append({ | ||
| 499 | + 'round': self._round, 'est': chosen_mode, 'r_sq': float(r_sq), | ||
| 500 | + 'margin': float(margin), 'k_est': int(k_est), | ||
| 501 | + 'measured': self._used, 'best': float(best_m), | ||
| 502 | + }) | ||
| 503 | + _prod_log.info( | ||
| 504 | + _TAG + " event=round kernel=%s round=%d est=%s r_sq=%.3f " | ||
| 505 | + "margin=%.3f k_est=%d measured=%d/%d best_dur=%.4f", | ||
| 506 | + self._kernel_name, self._round, chosen_mode, float(r_sq), | ||
| 507 | + float(margin), int(k_est), self._used, self._N, best_m) | ||
| 508 | + | ||
| 509 | + stop, self._prev_hist = algo.should_stop(dm, best_m, d_hat, self._prev_hist) | ||
| 510 | + if stop: | ||
| 511 | + return [] | ||
| 512 | + | ||
| 513 | + batch_size = max(1, self._total_budget // algo.df_cfg.max_rounds) | ||
| 514 | + remaining_budget = self._total_budget - self._used | ||
| 515 | + batch_size = min(batch_size, len(uidx), remaining_budget) | ||
| 516 | + if batch_size <= 0: | ||
| 517 | + return [] | ||
| 518 | + | ||
| 519 | + tau = algo.compute_tau(r_sq, k_est) | ||
| 520 | + | ||
| 521 | + if D >= algo.MARGINAL_D: | ||
| 522 | + n_poly = max(1, batch_size // 2) | ||
| 523 | + n_marg = max(1, batch_size - n_poly) | ||
| 524 | + sel_poly = set(algo.softmax_select(d_hat, tau, n_poly, self._rng).tolist()) | ||
| 525 | + m_scores = algo.marginal_scores(self._X[midx], dm, self._X[uidx], D) | ||
| 526 | + sel_marg = set(algo.softmax_select( | ||
| 527 | + m_scores, algo.TAU_MIN, n_marg, self._rng).tolist()) | ||
| 528 | + merged = sorted(sel_poly | sel_marg)[:batch_size] | ||
| 529 | + else: | ||
| 530 | + merged = algo.softmax_select(d_hat, tau, batch_size, self._rng).tolist() | ||
| 531 | + | ||
| 532 | + return [uidx[c] for c in merged] | ||
| 533 | + | ||
| 534 | + def _log_done(self): | ||
| 535 | + # per-kernel end line (always on). regret is NA in production: the | ||
| 536 | + # filter never measures the unmeasured configs, so it cannot know the | ||
| 537 | + # true best — regret is offline-only. log what we have: real budget | ||
| 538 | + # used, rounds, per-round estimators, best_dur, quality, wall time. | ||
| 539 | + if self._end_logged: | ||
| 540 | + return | ||
| 541 | + self._end_logged = True | ||
| 542 | + _algo_wall_ms = (time.perf_counter_ns() - self._t_algo_start) / 1e6 | ||
| 543 | + _real_pct = (100.0 * self._used / self._N) if self._N else 0.0 | ||
| 544 | + _ests = self._round_estimators if self._round_estimators else [self._poly_mode] | ||
| 545 | + _conf = _confidence_label(self._last_r_sq, self._last_margin, self._last_k_est) | ||
| 546 | + # stop reason: budget exhausted vs converged early vs all measured | ||
| 547 | + _planned = self._total_budget | ||
| 548 | + _stop = 'budget' if self._used >= _planned else ('all' if not self._unmeasured else 'converged') | ||
| 549 | + _prod_log.info( | ||
| 550 | + _TAG + " event=end kernel=%s N=%d D=%d class=%s path=%s " | ||
| 551 | + "real_budget=%d/%d(%.1f%%) planned=%d rounds=%d estimators=[%s] " | ||
| 552 | + "best_dur=%.4f stop=%s quality=%s r_sq=%.3f margin=%.3f k_est=%d " | ||
| 553 | + "regret=NA catch5=NA filter_overhead_ms=%.2f algo_wall_ms=%.2f", | ||
| 554 | + self._kernel_name, self._N, self._D, | ||
| 555 | + getattr(self, '_budget_class', '?'), self._path, | ||
| 556 | + self._used, self._N, _real_pct, _planned, self._round, | ||
| 557 | + ",".join(_ests), self._best_dur, _stop, _conf, | ||
| 558 | + self._last_r_sq, self._last_margin, self._last_k_est, | ||
| 559 | + self._overhead_ns / 1e6, _algo_wall_ms) | ||
| 560 | + _selector_log( | ||
| 561 | + f'{self._kernel_name}: done. ' | ||
| 562 | + f'evals={self._used}/{self._N} ' | ||
| 563 | + f'savings={100 * (1 - self._used / self._N):.1f}% ' | ||
| 564 | + f'best_dur={self._best_dur:.3f} ' | ||
| 565 | + f'path={self._path} mode={self._poly_mode} ' | ||
| 566 | + f'overhead={self._overhead_ns / 1e6:.2f}ms', | ||
| 567 | + level='info') | ||
| 568 | + | ||
| 569 | + # ================================================================= | ||
| 570 | + # results | ||
| 571 | + # ================================================================= | ||
| 572 | + | ||
| 573 | + # best property removed with run() -> offline_exp/run_filter.py. production | ||
| 574 | + # reads the winner via stats / _best_idx, not via a (config, dur) tuple. | ||
| 575 | + | ||
| 576 | + | ||
| 577 | + def overhead_ms(self): | ||
| 578 | + return self._overhead_ns / 1e6 | ||
| 579 | + | ||
| 580 | + | ||
| 581 | + def stats(self): | ||
| 582 | + return { | ||
| 583 | + 'kernel': self._kernel_name, | ||
| 584 | + 'N': self._N, | ||
| 585 | + 'D': self._D, | ||
| 586 | + 'features': self._feat_names, | ||
| 587 | + 'evals': self._used, | ||
| 588 | + 'savings_pct': 100 * (1 - self._used / self._N) if self._N > 0 else 0, | ||
| 589 | + 'rounds_used': self._round, | ||
| 590 | + 'path': self._path, | ||
| 591 | + 'mode': self._poly_mode, | ||
| 592 | + 'best_dur': self._best_dur, | ||
| 593 | + 'overhead_ms': self.overhead_ms, | ||
| 594 | + 'r_sq': self._last_r_sq, | ||
| 595 | + 'margin': self._last_margin, | ||
| 596 | + 'k_est': self._last_k_est, | ||
| 597 | + 'confidence': _confidence_label(self._last_r_sq, self._last_margin, self._last_k_est), | ||
| 598 | + 'round_records': list(self._round_records), | ||
| 599 | + } | ||
| 600 | + | ||
| 601 | + def update_batch_indices(self, pvs): | ||
| 602 | + """reconcile _last_batch_indices with the actual pvs from the bench | ||
| 603 | + machinery. | ||
| 604 | + | ||
| 605 | + two jobs: | ||
| 606 | + 1. mark configs in the old _last_batch_indices that produced no | ||
| 607 | + profile value as compile-failed: move them to | ||
| 608 | + not_profiled_indices and discard from _unmeasured. | ||
| 609 | + 2. rebuild _last_batch_indices in pvs order so refine()'s | ||
| 610 | + zip(_last_batch_indices, durations) stays aligned. | ||
| 611 | + | ||
| 612 | + job 2 matters because precompile_parallel uses as_completed(), so | ||
| 613 | + launcher order != config submission order, and pvs is built in launcher | ||
| 614 | + order. the old in-place filter only handled job 1. | ||
| 615 | + """ | ||
| 616 | + # uniqueness asserts on inputs. | ||
| 617 | + batch_objs = [self._all_items[v] for v in self._last_batch_indices] | ||
| 618 | + _check(len(set(id(o) for o in batch_objs)) == len(batch_objs), | ||
| 619 | + self._kernel_name, | ||
| 620 | + "update_batch_indices: duplicate config object in last_batch") | ||
| 621 | + pv_configs = [pv.config for pv in pvs] | ||
| 622 | + _check(len(set(id(c) for c in pv_configs)) == len(pv_configs), | ||
| 623 | + self._kernel_name, | ||
| 624 | + "update_batch_indices: duplicate config object across pvs") | ||
| 625 | + | ||
| 626 | + # build id -> global index from the OLD last_batch only | ||
| 627 | + old_id_to_global = {id(self._all_items[v]): v | ||
| 628 | + for v in self._last_batch_indices} | ||
| 629 | + profiled_ids = {id(pv.config) for pv in pvs} | ||
| 630 | + | ||
| 631 | + # (1) failures: in the old batch but not in pvs | ||
| 632 | + for v in self._last_batch_indices: | ||
| 633 | + if id(self._all_items[v]) not in profiled_ids: | ||
| 634 | + self.not_profiled_indices.append(v) | ||
| 635 | + self._unmeasured.discard(v) | ||
| 636 | + | ||
| 637 | + # (2) rebuild in pvs order | ||
| 638 | + new_last_batch = [] | ||
| 639 | + for pv in pvs: | ||
| 640 | + g = old_id_to_global.get(id(pv.config)) | ||
| 641 | + _check(g is not None, self._kernel_name, | ||
| 642 | + "update_batch_indices: pv.config not in last_batch " | ||
| 643 | + "(kwargs=%r)", pv.config.kwargs) | ||
| 644 | + new_last_batch.append(g) | ||
| 645 | + self._last_batch_indices = new_last_batch | ||
| 646 | + | ||
| 647 | + _check(len(self._last_batch_indices) == len(pvs), | ||
| 648 | + self._kernel_name, | ||
| 649 | + "update_batch_indices: post-sync %d != pvs %d", | ||
| 650 | + len(self._last_batch_indices), len(pvs)) | ||
| @@ -0,0 +1,286 @@ | |||
| 1 | +import ast | ||
| 2 | +from enum import Enum, auto | ||
| 3 | +import functools | ||
| 4 | +import time | ||
| 5 | + | ||
| 6 | +from torch_npu._inductor.fasta_autotune import log | ||
| 7 | +from torch._inductor.codecache import _load_triton_kernel_from_source | ||
| 8 | +from torch._inductor.runtime.triton_heuristics import NoTritonConfigsError | ||
| 9 | +from torch_npu._inductor.runtime.triton_heuristics import stats | ||
| 10 | + | ||
| 11 | + | ||
| 12 | + | ||
| 13 | +class CompilationPhase(Enum): | ||
| 14 | + """ | ||
| 15 | + Representing the dynamic-filter compilation lifecycle | ||
| 16 | + the phases define how Triton kernel configs are compiled and benchmarked - | ||
| 17 | + when FASTA_DYNAMIC_FILTER is enabled | ||
| 18 | + | ||
| 19 | + Phases: | ||
| 20 | + R1: compile an initial subset of configs in async_compile (pre-run) | ||
| 21 | + benchmark in kernel.run() | ||
| 22 | + R2: iteratively compile and benchmark config batches from remaining configs | ||
| 23 | + DONE: final phase after the best launcher has been selected | ||
| 24 | + | ||
| 25 | + State transitions: | ||
| 26 | + R1 -> R2 -> DONE | ||
| 27 | + """ | ||
| 28 | + R1 = auto() | ||
| 29 | + R2 = auto() | ||
| 30 | + DONE = auto() | ||
| 31 | + | ||
| 32 | + def next_phase(self): | ||
| 33 | + transitions = { | ||
| 34 | + CompilationPhase.R1: CompilationPhase.R2, | ||
| 35 | + CompilationPhase.R2: CompilationPhase.DONE, | ||
| 36 | + CompilationPhase.DONE: CompilationPhase.DONE, | ||
| 37 | + } | ||
| 38 | + return transitions[self] | ||
| 39 | + | ||
| 40 | + | ||
| 41 | +class DynamicFilterScheduler: | ||
D during the new autotune time, do we need stream as the paramter? ![]() ![]() | |||
| 42 | + """ | ||
| 43 | + Manages the autotuning workflow for FASTA_DYNAMIC_FILTER=1, including R1/R2 | ||
| 44 | + autotuning, compilation, benchmarking, and schedule iterative refinement in R2. | ||
| 45 | + """ | ||
| 46 | + def __init__(self, fasta_setting): | ||
| 47 | + if fasta_setting.autotune_method != "Expert": | ||
| 48 | + raise ValueError("dynamic filter is not supported in non-Expert autotune_method") | ||
| 49 | + | ||
| 50 | + self.phase = CompilationPhase.R1 | ||
| 51 | + self.Rx = None | ||
| 52 | + self._selector = None | ||
| 53 | + self.phase_profiling_values = {} | ||
| 54 | + self.best_launchers = {} | ||
| 55 | + self.best_profiling_values = {} | ||
| 56 | + self.current_r2_configs = [] | ||
| 57 | + self.precompile_time_s = 0.0 | ||
| 58 | + | ||
| 59 | + def _build_minimal_kernel_src(self, filename: str) -> str: | ||
| 60 | + """ | ||
| 61 | + Extracts a minimal triton kernel source from a python file, | ||
| 62 | + used to reload kernel from main process | ||
| 63 | + it keeps only the kernel function definition and required imports | ||
| 64 | + and removes unrelated code to reduce compilation overhead | ||
| 65 | + | ||
| 66 | + Args: | ||
| 67 | + filename (str): path to the python file containing the triton kernel | ||
| 68 | + | ||
| 69 | + Returns: | ||
| 70 | + str: minimal kernel source code as a string. | ||
| 71 | + """ | ||
| 72 | + with open(filename) as f: | ||
| 73 | + src = f.read() | ||
| 74 | + tree = ast.parse(src) | ||
| 75 | + fn = next(n for n in tree.body if isinstance(n, ast.FunctionDef)) | ||
🔵 Low Priority
建议:添加带有明确错误信息的守卫:若不存在 FunctionDef 节点则抛出 ![]() ![]() | |||
| 76 | + fn.decorator_list = [d for d in fn.decorator_list if "triton.jit" in ast.unparse(d)] | ||
| 77 | + used = {n.id for n in ast.walk(fn) if isinstance(n, ast.Name)} | ||
| 78 | + | ||
| 79 | + imports = [] | ||
| 80 | + for node in tree.body: | ||
| 81 | + if not isinstance(node, (ast.Import, ast.ImportFrom)): | ||
| 82 | + continue | ||
| 83 | + names = [ | ||
| 84 | + a.asname or a.name.split(".")[0] | ||
| 85 | + for a in node.names | ||
| 86 | + ] | ||
| 87 | + if any(n in used for n in names): | ||
| 88 | + imports.append(node) | ||
| 89 | + | ||
| 90 | + module = ast.Module(body=imports + [fn], type_ignores=[]) | ||
| 91 | + return ast.unparse(module) | ||
| 92 | + | ||
| 93 | + def prepare_r1_configs(self, fast_autotuner): | ||
| 94 | + if self.phase is CompilationPhase.R1: | ||
| 95 | + self.Rx = "R1" | ||
| 96 | + from .experimental.dynamic_filter.dynamic_filter_if import DynamicFilter as _SelectorCls | ||
D here is a bug. the from .experimental.dynamic_filter.dynamic_filter_if import DynamicFilter as _SelectorCls will generate error., because here is a relative path. ![]() ![]() | |||
| 97 | + self._selector = _SelectorCls(fast_autotuner.configs, fast_autotuner.get_fn_name()) | ||
| 98 | + fast_autotuner.configs = self._selector.r1_configs | ||
| 99 | + log.info(f"r1_configs: {len(self._selector.r1_configs)}") | ||
| 100 | + | ||
| 101 | + def select_best_launcher_key(self, best_profiling_values, kernel_name): | ||
| 102 | + log.info( | ||
| 103 | + f"kernel: {kernel_name} - best launcher profiling time per batch: " | ||
| 104 | + f"{best_profiling_values}" | ||
| 105 | + ) | ||
| 106 | + valid = {k: v for k, v in best_profiling_values.items() if v is not None} | ||
| 107 | + if not valid: | ||
| 108 | + raise RuntimeError("No valid launchers found") | ||
| 109 | + return min(valid, key=valid.get) | ||
| 110 | + | ||
| 111 | + def store_phase_profiling_values(self, fast_autotuner): | ||
| 112 | + if fast_autotuner.profile_values: | ||
| 113 | + self.phase_profiling_values[self.Rx] = fast_autotuner.profile_values | ||
| 114 | + else: | ||
| 115 | + self.phase_profiling_values[self.Rx] = [] | ||
| 116 | + fast_autotuner.profile_values = None | ||
| 117 | + | ||
| 118 | + def compile_r2(self, fast_autotuner): | ||
| 119 | + """ | ||
| 120 | + Compiles r2 configs during kernel.run() | ||
| 121 | + builds minimal kernel source if needed and reload it, | ||
| 122 | + | ||
| 123 | + Args: | ||
| 124 | + fast_autotuner: NPUFastAutotuner kernel wrapper object | ||
| 125 | + """ | ||
| 126 | + fast_autotuner.skip_precompile = False | ||
| 127 | + load_kernel = None | ||
| 128 | + | ||
| 129 | + if getattr(fast_autotuner.fn, 'fn', None) is None: | ||
| 130 | + fn_src = self._build_minimal_kernel_src(filename=fast_autotuner.filename) | ||
| 131 | + kernel_name = fast_autotuner.fn.__name__ | ||
| 132 | + load_kernel = functools.partial(_load_triton_kernel_from_source, kernel_name, fn_src) | ||
| 133 | + | ||
| 134 | + fast_autotuner.clear_last_record() | ||
| 135 | + fast_autotuner.configs = self.current_r2_configs | ||
| 136 | + fast_autotuner.profiling_config_num += len(fast_autotuner.configs) | ||
| 137 | + log.info(f"kernel: {fast_autotuner.get_fn_name()} - Compiling {self.Rx}, {len(fast_autotuner.configs)} configs.") | ||
| 138 | + | ||
| 139 | + fast_autotuner.precompile(warm_cache_only=False, reload_kernel=load_kernel) | ||
| 140 | + | ||
| 141 | + def log_autotune_timing_summary(self, kernel_name, _total_wall_s, r1_wall_s, refine_acum): | ||
| 142 | + _sel = self._selector | ||
| 143 | + _sel_stats = _sel.stats if _sel else {} | ||
| 144 | + _n_failed = len(_sel.not_profiled_indices) if _sel else 0 | ||
| 145 | + _n_measured = len(_sel._measured) if _sel else 0 | ||
| 146 | + _sel_overhead = _sel.overhead_ms if _sel else 0.0 | ||
| 147 | + log.info( | ||
| 148 | + f"[FASTA_SUMMARY] kernel={kernel_name} " | ||
| 149 | + f"N={_sel_stats.get('N','?')} " | ||
| 150 | + f"D={_sel_stats.get('D','?')} " | ||
| 151 | + f"r1_size={len(_sel._r1_indices) if _sel else '?'} " | ||
| 152 | + f"measured={_n_measured} " | ||
| 153 | + f"failed={_n_failed} " | ||
| 154 | + f"savings={_sel_stats.get('savings_pct',0):.1f}% " | ||
| 155 | + f"rounds={_sel_stats.get('rounds_used','?')} " | ||
| 156 | + f"path={_sel_stats.get('path','?')} " | ||
| 157 | + f"mode={_sel_stats.get('mode','?')} " | ||
| 158 | + f"best_dur={_sel_stats.get('best_dur','?')} " | ||
| 159 | + f"total_wall={_total_wall_s*1000:.1f}ms " | ||
| 160 | + f"r1_wall={r1_wall_s*1000:.1f}ms " | ||
| 161 | + f"refine_total={refine_acum*1000:.1f}ms " | ||
| 162 | + f"selector_overhead={_sel_overhead:.2f}ms " | ||
| 163 | + ) | ||
| 164 | + | ||
| 165 | + def store_precompile_duration(self, precompile_time_s): | ||
| 166 | + if self.phase is CompilationPhase.R1: | ||
| 167 | + self.precompile_time_s = precompile_time_s | ||
| 168 | + | ||
| 169 | + def compile_and_benchmark(self, fast_autotuner, *args, **kwargs): | ||
| 170 | + """ | ||
| 171 | + Main entry point for the dynamic-filter compilation and benchmarking flow | ||
| 172 | + | ||
| 173 | + Depending on the current phase: | ||
| 174 | + - R1: initial benchmarking and transitions to R2 | ||
| 175 | + - R2: iteratively compiles and benchmarks remaining config batches, | ||
| 176 | + progression and early stopping are controlled by dynamic_filter_algo | ||
| 177 | + - DONE: reuses the cached best launcher | ||
| 178 | + | ||
| 179 | + Args: | ||
| 180 | + fast_autotuner: NPUFastAutotuner kernel wrapper object | ||
| 181 | + """ | ||
| 182 | + if self.phase is CompilationPhase.R1: | ||
| 183 | + autotune_start_time = time.perf_counter() | ||
| 184 | + self.autotuner(fast_autotuner, *args, **kwargs) | ||
| 185 | + r1_wall_s = time.perf_counter() - autotune_start_time | ||
| 186 | + kernel_name = fast_autotuner.get_fn_name() | ||
| 187 | + | ||
| 188 | + self.store_phase_profiling_values(fast_autotuner) | ||
| 189 | + self.phase = self.phase.next_phase() | ||
| 190 | + refine_acum = 0 | ||
| 191 | + r1_pvs = self.phase_profiling_values.get(self.Rx, []) | ||
| 192 | + self._selector.update_batch_indices(r1_pvs) | ||
| 193 | + r1_timings = [pv.profiler_time for pv in r1_pvs] | ||
| 194 | + batch_configs = self._selector.refine(r1_timings) | ||
| 195 | + | ||
| 196 | + i = 0 | ||
| 197 | + while batch_configs: | ||
| 198 | + self.Rx = f"{self.phase.name}_{i}" | ||
| 199 | + i = i + 1 | ||
| 200 | + | ||
| 201 | + log.info(f"[FASTA_R2] {kernel_name} {self.Rx}: " | ||
| 202 | + f"batch={len(batch_configs)} ") | ||
| 203 | + | ||
| 204 | + self.current_r2_configs = batch_configs | ||
| 205 | + self.autotuner(fast_autotuner, *args, **kwargs) | ||
| 206 | + | ||
| 207 | + self.store_phase_profiling_values(fast_autotuner) | ||
| 208 | + pvs = self.phase_profiling_values.get(self.Rx, []) | ||
| 209 | + self._selector.update_batch_indices(pvs) | ||
| 210 | + | ||
| 211 | + timings = [pv.profiler_time for pv in pvs] | ||
| 212 | + log.info(f"{kernel_name} b4 refine of {self.Rx} " | ||
| 213 | + f"batch_configs:{len(batch_configs)} timings:{len(timings)}") | ||
| 214 | + | ||
| 215 | + t0 = time.perf_counter() | ||
| 216 | + batch_configs = self._selector.refine(timings) | ||
| 217 | + refine_acum += time.perf_counter() - t0 | ||
| 218 | + | ||
| 219 | + _total_wall_s = time.perf_counter() - autotune_start_time | ||
| 220 | + self.log_autotune_timing_summary(kernel_name, _total_wall_s, r1_wall_s, refine_acum) | ||
| 221 | + | ||
| 222 | + self.phase = self.phase.next_phase() | ||
| 223 | + | ||
| 224 | + best_launcher_group = self.select_best_launcher_key(self.best_profiling_values, kernel_name) | ||
| 225 | + fast_autotuner.best_launcher = self.best_launchers.get(best_launcher_group) | ||
| 226 | + fast_autotuner.best_profiling_value = self.best_profiling_values.get(best_launcher_group) | ||
| 227 | + fast_autotuner.launchers = [fast_autotuner.best_launcher] | ||
| 228 | + log.info( | ||
| 229 | + f"{kernel_name} - " | ||
| 230 | + f"best config: {fast_autotuner.best_launcher.config.kwargs}, " | ||
| 231 | + f"from {best_launcher_group}, " | ||
| 232 | + f"cost time:{fast_autotuner.best_profiling_value}" | ||
| 233 | + ) | ||
| 234 | + | ||
| 235 | + self.autotune_time_taken_ns = (_total_wall_s + self.precompile_time_s) * 1e9 | ||
| 236 | + if fast_autotuner.save_cache_hook: | ||
| 237 | + fast_autotuner.save_cache_hook(fast_autotuner.launchers[0].config, self.autotune_time_taken_ns) | ||
| 238 | + | ||
| 239 | + if fast_autotuner.best_launcher is not None: | ||
| 240 | + fast_autotuner.launchers = [fast_autotuner.best_launcher] | ||
| 241 | + return | ||
| 242 | + | ||
| 243 | + def autotuner(self, fast_autotuner, *args, **kwargs): | ||
| 244 | + """ | ||
| 245 | + Executes autotuning for the current phase | ||
| 246 | + and stores the best launcher per phase | ||
| 247 | + | ||
| 248 | + Args: | ||
| 249 | + fast_autotuner: NPUFastAutotuner kernel wrapper object | ||
| 250 | + """ | ||
| 251 | + if self.phase is CompilationPhase.R2: | ||
| 252 | + self.compile_r2(fast_autotuner) | ||
| 253 | + if not fast_autotuner.launchers and not fast_autotuner.compile_results: | ||
| 254 | + self.best_launchers[self.Rx] = None | ||
| 255 | + log.warning(f"{self.Rx} produced no valid launchers") | ||
| 256 | + return | ||
| 257 | + | ||
| 258 | + if stats.enabled: | ||
| 259 | + fast_autotuner.cache_hit = False | ||
| 260 | + fast_autotuner.skip_precompile = False | ||
| 261 | + best_launcher = fast_autotuner.auto_tune_by_fasta_parallel(*args, **kwargs) | ||
| 262 | + | ||
| 263 | + fast_autotuner.best_launcher = best_launcher | ||
| 264 | + self.best_launchers[self.Rx] = fast_autotuner.best_launcher | ||
| 265 | + self.best_profiling_values[self.Rx] = fast_autotuner.best_profiling_value | ||
| 266 | + | ||
| 267 | + def catch_no_valid_triton_configs(self, kernel_name: str, e: NoTritonConfigsError): | ||
| 268 | + """ | ||
| 269 | + Suppress error if no valid Triton configs are found in R2_x | ||
| 270 | + """ | ||
| 271 | + if self.phase is CompilationPhase.R2: | ||
| 272 | + log.info(f"kernel: {kernel_name} - {self.Rx} has no valid configs\n, {e}") | ||
| 273 | + return | ||
| 274 | + raise e | ||
| 275 | + | ||
| 276 | + def get_compilation_desc(self): | ||
| 277 | + if self.phase is CompilationPhase.R1: | ||
| 278 | + return f"Precompile configs - {self.Rx}" | ||
| 279 | + if self.phase is CompilationPhase.R2: | ||
| 280 | + return f"Compile configs in run - {self.Rx}" | ||
| 281 | + return "Precompile configs" | ||
| 282 | + | ||
| 283 | + def get_benchmark_desc(self): | ||
| 284 | + if self.Rx is not None: | ||
| 285 | + return f"Benchmark configs - {self.Rx}" | ||
| 286 | + return "Benchmark configs" | ||
| @@ -24,10 +24,18 @@ from torch._inductor import config | |||||||||||||||
| 24 | import torch_npu | 24 | import torch_npu | ||||||||||||
| 25 | from .codegen.tile_generator import TileGenerator | 25 | from .codegen.tile_generator import TileGenerator | ||||||||||||
| 26 | from .config import log | 26 | from .config import log | ||||||||||||
| 27 | -from .runtime.triton_heuristics import NPUCachingAutotuner | ||||||||||||||
| 28 | from . import config as npu_config | 27 | from . import config as npu_config | ||||||||||||
| 29 | from .codegen.triton_utils import get_byte_per_numel, NPUKernelType | 28 | from .codegen.triton_utils import get_byte_per_numel, NPUKernelType | ||||||||||||
| 30 | from .profiler import simple_trace_handler | 29 | from .profiler import simple_trace_handler | ||||||||||||
| 30 | +from .runtime.triton_heuristics import NPUCachingAutotuner, stats | ||||||||||||||
| 31 | +from .experimental.dynamic_filter.dynamic_filter_config import fasta_dynamic_filter, fasta_mspti_en | ||||||||||||||
| 32 | +from .experimental.dynamic_filter.config_optimizer import optimize_configs | ||||||||||||||
| 33 | +from .experimental.dynamic_filter.dynamic_filter_scheduler import DynamicFilterScheduler | ||||||||||||||
| 34 | + | ||||||||||||||
| 35 | +KernelMonitor = None | ||||||||||||||
| 36 | + | ||||||||||||||
| 37 | +_BENCH_ACCUMULATOR = {"n_kernels": 0, "total_wall_ns": 0, "total_func_ns": 0} | ||||||||||||||
| 38 | +_BENCH_FAILURES = [] # list of (kernel_name, expected, got, backend) for timing/launcher mismatches | ||||||||||||||
| 31 | 39 | ||||||||||||||
| 32 | 40 | ||||||||||||||
| 33 | def fast_a_log_message(content, tag='autotuner', level='debug'): | 41 | def fast_a_log_message(content, tag='autotuner', level='debug'): | ||||||||||||
| @@ -130,6 +138,7 @@ class FastASetting: | |||||||||||||||
| 130 | 138 | ||||||||||||||
| 131 | 139 | ||||||||||||||
| 132 | FASTA_SETTING = FastASetting() | 140 | FASTA_SETTING = FastASetting() | ||||||||||||
| 141 | +FASTA_EVENT_BENCHMARK = os.environ.get("FASTA_EVENT_BENCHMARK", "0") == "1" | ||||||||||||||
| 133 | 142 | ||||||||||||||
| 134 | 143 | ||||||||||||||
| 135 | def get_ub_size(): | 144 | def get_ub_size(): | ||||||||||||
| @@ -163,8 +172,8 @@ class FastAConfig(Config): | |||||||||||||||
| 163 | 172 | ||||||||||||||
| 164 | def get_config_info(self): | 173 | def get_config_info(self): | ||||||||||||
| 165 | return "config: {}, core num {}, UB usage ratio {:.5f}, from expert {}".format( | 174 | return "config: {}, core num {}, UB usage ratio {:.5f}, from expert {}".format( | ||||||||||||
| 166 | - self.kwargs, self.vector_core_num, self.ub_usage, self.from_expert) | 175 | + self.kwargs, self.vector_core_num, self.ub_usage, self.from_expert | ||||||||||||
| 167 | - | 176 | + ) | ||||||||||||
| 168 | 177 | ||||||||||||||
| 169 | class TileConfig: | 178 | class TileConfig: | ||||||||||||
| 170 | def __init__(self): | 179 | def __init__(self): | ||||||||||||
| @@ -920,9 +929,14 @@ class NPUFastAutotuner(NPUCachingAutotuner): | |||||||||||||||
| 920 | self.skip_precompile = False | 929 | self.skip_precompile = False | ||||||||||||
| 921 | return | 930 | return | ||||||||||||
| 922 | 931 | ||||||||||||||
| 932 | + self.dynamic_filter = fasta_dynamic_filter | ||||||||||||||
| 933 | + if self.dynamic_filter: | ||||||||||||||
| 934 | + self.dynamic_filter_scheduler = DynamicFilterScheduler(FASTA_SETTING) | ||||||||||||||
| 935 | + | ||||||||||||||
| 923 | if FASTA_SETTING.autotune_method == "Expert": | 936 | if FASTA_SETTING.autotune_method == "Expert": | ||||||||||||
| 924 | self.skip_precompile = False | 937 | self.skip_precompile = False | ||||||||||||
| 925 | - self._precompile_for_expert() | 938 | + self._precompile_for_expert(reload_kernel=None) | ||||||||||||
| 939 | + | ||||||||||||||
| 926 | 940 | ||||||||||||||
| 927 | def _config_separation(self, configs): | 941 | def _config_separation(self, configs): | ||||||||||||
| 928 | self.expert_configs = [] | 942 | self.expert_configs = [] | ||||||||||||
| @@ -940,13 +954,27 @@ class NPUFastAutotuner(NPUCachingAutotuner): | |||||||||||||||
| 940 | self.origin_configs.append(cfg) | 954 | self.origin_configs.append(cfg) | ||||||||||||
| 941 | 955 | ||||||||||||||
| 942 | def add_mutibuffer_config(self): | 956 | def add_mutibuffer_config(self): | ||||||||||||
| 943 | - new_cfg = [] | 957 | + expanded = [] | ||||||||||||
| 944 | - for self_config in self.configs: | 958 | + for c in self.configs: | ||||||||||||
| 945 | - self_config.kwargs['multibuffer'] = False | 959 | + if 'multibuffer' in c.kwargs: | ||||||||||||
| 946 | - config_copied = copy.deepcopy(self_config) | 960 | + expanded.append(c) | ||||||||||||
| 947 | - config_copied.kwargs['multibuffer'] = True | 961 | + continue | ||||||||||||
| 948 | - new_cfg.append(config_copied) | 962 | + c.kwargs['multibuffer'] = False | ||||||||||||
| 949 | - self.configs.extend(new_cfg) | 963 | + twin = copy.deepcopy(c) | ||||||||||||
| 964 | + twin.kwargs['multibuffer'] = True | ||||||||||||||
| 965 | + expanded.append(c) | ||||||||||||||
| 966 | + expanded.append(twin) | ||||||||||||||
| 967 | + | ||||||||||||||
| 968 | + seen = set() | ||||||||||||||
| 969 | + out = [] | ||||||||||||||
| 970 | + for c in expanded: | ||||||||||||||
| 971 | + k = (tuple(sorted(c.kwargs.items())), c.num_warps, c.num_stages, | ||||||||||||||
| 972 | + getattr(c, 'num_ctas', 1)) | ||||||||||||||
| 973 | + if k in seen: | ||||||||||||||
| 974 | + continue | ||||||||||||||
| 975 | + seen.add(k) | ||||||||||||||
| 976 | + out.append(c) | ||||||||||||||
| 977 | + self.configs = out | ||||||||||||||
| 950 | 978 | ||||||||||||||
| 951 | def print_profile_value(self, this_pv: ProfileValue, full_info=False): | 979 | def print_profile_value(self, this_pv: ProfileValue, full_info=False): | ||||||||||||
| 952 | if not this_pv: | 980 | if not this_pv: | ||||||||||||
| @@ -982,10 +1010,10 @@ class NPUFastAutotuner(NPUCachingAutotuner): | |||||||||||||||
| 982 | this_pv.config.circle_num, | 1010 | this_pv.config.circle_num, | ||||||||||||
| 983 | this_pv.config.from_expert), tag="autotuner") | 1011 | this_pv.config.from_expert), tag="autotuner") | ||||||||||||
| 984 | 1012 | ||||||||||||||
| 985 | - def _precompile_for_expert(self): | 1013 | + def _precompile_for_expert(self, reload_kernel=None): | ||||||||||||
| 986 | fast_a_log_message(content='enter precompile for expert') | 1014 | fast_a_log_message(content='enter precompile for expert') | ||||||||||||
| 987 | self.bucket_dict = self._make_bucket_and_filter_with_binary() | 1015 | self.bucket_dict = self._make_bucket_and_filter_with_binary() | ||||||||||||
| 988 | - self._expert_configs_precompile() | 1016 | + self._expert_configs_precompile(reload_kernel=reload_kernel) | ||||||||||||
| 989 | 1017 | ||||||||||||||
| 990 | def autotuner(self, *args, stream, benchmark_run=False, **kwargs): | 1018 | def autotuner(self, *args, stream, benchmark_run=False, **kwargs): | ||||||||||||
| 991 | if self.use_origin_autotuner: | 1019 | if self.use_origin_autotuner: | ||||||||||||
| @@ -998,6 +1026,8 @@ class NPUFastAutotuner(NPUCachingAutotuner): | |||||||||||||||
| 998 | self.launchers = [self.best_launcher] | 1026 | self.launchers = [self.best_launcher] | ||||||||||||
| 999 | return | 1027 | return | ||||||||||||
| 1000 | 1028 | ||||||||||||||
| 1029 | + if stats.enabled: | ||||||||||||||
| 1030 | + self.cache_hit = False | ||||||||||||||
| 1001 | self.skip_precompile = False | 1031 | self.skip_precompile = False | ||||||||||||
| 1002 | best_launcher = self.auto_tune_by_fasta_parallel(*args, **kwargs) | 1032 | best_launcher = self.auto_tune_by_fasta_parallel(*args, **kwargs) | ||||||||||||
| 1003 | best_launcher_time = time.perf_counter() - self.autotune_start_time | 1033 | best_launcher_time = time.perf_counter() - self.autotune_start_time | ||||||||||||
| @@ -1299,7 +1329,7 @@ class NPUFastAutotuner(NPUCachingAutotuner): | |||||||||||||||
| 1299 | 1329 | ||||||||||||||
| 1300 | self.expert_method_choose_core_num_list = this_core_num_list[:FASTA_SETTING.expert_min_bucket_num] | 1330 | self.expert_method_choose_core_num_list = this_core_num_list[:FASTA_SETTING.expert_min_bucket_num] | ||||||||||||
| 1301 | 1331 | ||||||||||||||
| 1302 | - def _expert_configs_precompile(self): | 1332 | + def _expert_configs_precompile(self, reload_kernel=None): | ||||||||||||
| 1303 | need_compile_configs = copy.deepcopy(self.expert_configs) | 1333 | need_compile_configs = copy.deepcopy(self.expert_configs) | ||||||||||||
| 1304 | 1334 | ||||||||||||||
| 1305 | core_num_list = sorted(list(self.bucket_dict.keys()), reverse=True) | 1335 | core_num_list = sorted(list(self.bucket_dict.keys()), reverse=True) | ||||||||||||
| @@ -1315,19 +1345,32 @@ class NPUFastAutotuner(NPUCachingAutotuner): | |||||||||||||||
| 1315 | get_circle_num=tiling_axis_num)) | 1345 | get_circle_num=tiling_axis_num)) | ||||||||||||
| 1316 | 1346 | ||||||||||||||
| 1317 | self.configs = need_compile_configs | 1347 | self.configs = need_compile_configs | ||||||||||||
| 1348 | + | ||||||||||||||
| 1349 | + log.info(f"%s - Configs len before optimization = %s", self.get_fn_name(), len(self.configs)) | ||||||||||||||
| 1350 | + self.configs = optimize_configs(self.configs) | ||||||||||||||
| 1351 | + log.info(f"%s - Configs len after optimization = %s", self.get_fn_name(), len(self.configs)) | ||||||||||||||
| 1318 | self.add_mutibuffer_config() | 1352 | self.add_mutibuffer_config() | ||||||||||||
| 1353 | + | ||||||||||||||
| 1354 | + if self.dynamic_filter: | ||||||||||||||
| 1355 | + self.dynamic_filter_scheduler.prepare_r1_configs(self) | ||||||||||||||
| 1356 | + | ||||||||||||||
| 1319 | self.profiling_config_num += len(self.configs) | 1357 | self.profiling_config_num += len(self.configs) | ||||||||||||
| 1320 | - self.precompile() | 1358 | + self.precompile(reload_kernel=reload_kernel) | ||||||||||||
| 1321 | 1359 | ||||||||||||||
| 1322 | def profiling_and_get_best_config(self, *args, **kwargs): | 1360 | def profiling_and_get_best_config(self, *args, **kwargs): | ||||||||||||
| 1323 | timings = self.benchmark_all_configs_with_std(*args, **kwargs) | 1361 | timings = self.benchmark_all_configs_with_std(*args, **kwargs) | ||||||||||||
| 1324 | - profile_values = self.make_profile_values(timings) | 1362 | + self.profile_values = self.make_profile_values(timings) | ||||||||||||
| 1325 | best_profile = None | 1363 | best_profile = None | ||||||||||||
| 1326 | - best_profile = self.find_best_launcher(profile_values, best_profile) | 1364 | + best_profile = self.find_best_launcher(self.profile_values, best_profile) | ||||||||||||
| 1365 | + self._last_launchers = list(self.launchers) | ||||||||||||||
🔵 Low Priority 在 建议:移除 ![]() ![]() | |||||||||||||||
| 1327 | self.clear_last_record() | 1366 | self.clear_last_record() | ||||||||||||
| 1328 | - self.get_result_config_num += len(profile_values) | 1367 | + self.get_result_config_num += len(self.profile_values) | ||||||||||||
| 1329 | fast_a_log_message(content="now our best profile is", tag='expert') | 1368 | fast_a_log_message(content="now our best profile is", tag='expert') | ||||||||||||
| 1330 | self.print_profile_value(best_profile) | 1369 | self.print_profile_value(best_profile) | ||||||||||||
| 1370 | + log.info( | ||||||||||||||
| 1371 | + f"{self.get_fn_name()} - config:{best_profile.config.kwargs}, cost time:{best_profile.profiler_time}, " | ||||||||||||||
| 1372 | + f"sem:{best_profile.profiler_time_sem}, expert: {best_profile.config.from_expert}" | ||||||||||||||
| 1373 | + ) | ||||||||||||||
| 1331 | return best_profile | 1374 | return best_profile | ||||||||||||
| 1332 | 1375 | ||||||||||||||
| 1333 | def auto_tune_by_fasta_parallel(self, *args, **kwargs): | 1376 | def auto_tune_by_fasta_parallel(self, *args, **kwargs): | ||||||||||||
| @@ -1357,6 +1400,9 @@ class NPUFastAutotuner(NPUCachingAutotuner): | |||||||||||||||
| 1357 | break | 1400 | break | ||||||||||||
| 1358 | 1401 | ||||||||||||||
| 1359 | self.configs = need_compile_configs | 1402 | self.configs = need_compile_configs | ||||||||||||
| 1403 | + log.info(f"%s - Configs len before optimization = %s", self.get_fn_name(), len(self.configs)) | ||||||||||||||
| 1404 | + self.configs = optimize_configs(self.configs) | ||||||||||||||
| 1405 | + log.info(f"%s - Configs len after optimization = %s", self.get_fn_name(), len(self.configs)) | ||||||||||||||
| 1360 | self.add_mutibuffer_config() | 1406 | self.add_mutibuffer_config() | ||||||||||||
| 1361 | self.profiling_config_num += len(self.configs) | 1407 | self.profiling_config_num += len(self.configs) | ||||||||||||
| 1362 | ans = self.precompile() | 1408 | ans = self.precompile() | ||||||||||||
| @@ -1442,7 +1488,10 @@ class NPUFastAutotuner(NPUCachingAutotuner): | |||||||||||||||
| 1442 | return False | 1488 | return False | ||||||||||||
| 1443 | 1489 | ||||||||||||||
| 1444 | def benchmark_all_configs_with_std(self, *args, **kwargs): | 1490 | def benchmark_all_configs_with_std(self, *args, **kwargs): | ||||||||||||
| 1445 | - fast_a_log_message(content=f"candidate launcher count = {len(self.launchers)}", tag='benchmark profiling') | 1491 | + | ||||||||||||
| 1492 | + # Event-based benchmark: bypasses NPU profiler, time proportional to config count | ||||||||||||||
| 1493 | + if FASTA_EVENT_BENCHMARK: | ||||||||||||||
| 1494 | + return self._benchmark_all_configs_event(*args, **kwargs) | ||||||||||||||
| 1446 | 1495 | ||||||||||||||
| 1447 | tilling_kernel_list = [] | 1496 | tilling_kernel_list = [] | ||||||||||||
| 1448 | 1497 | ||||||||||||||
| @@ -1486,6 +1535,9 @@ class NPUFastAutotuner(NPUCachingAutotuner): | |||||||||||||||
| 1486 | md5_hash = hashlib.md5(random_uuid.encode()).hexdigest() | 1535 | md5_hash = hashlib.md5(random_uuid.encode()).hexdigest() | ||||||||||||
| 1487 | 1536 | ||||||||||||||
| 1488 | def do_batch_benchmark_kernel_isolate(this_tilling_kernel_list, this_active_num): | 1537 | def do_batch_benchmark_kernel_isolate(this_tilling_kernel_list, this_active_num): | ||||||||||||
| 1538 | + log.info(f"Running do_batch_benchmark_kernel_isolate") | ||||||||||||||
| 1539 | + t0 = time.perf_counter_ns() | ||||||||||||||
| 1540 | + exp_timing = {'func_total_ns': 0} | ||||||||||||||
| 1489 | this_stream = torch.npu.current_stream() | 1541 | this_stream = torch.npu.current_stream() | ||||||||||||
| 1490 | 1542 | ||||||||||||||
| 1491 | tiling_length = len(this_tilling_kernel_list) | 1543 | tiling_length = len(this_tilling_kernel_list) | ||||||||||||
| @@ -1536,19 +1588,168 @@ class NPUFastAutotuner(NPUCachingAutotuner): | |||||||||||||||
| 1536 | time_cost[tiling_index] = np.mean(data_list) | 1588 | time_cost[tiling_index] = np.mean(data_list) | ||||||||||||
| 1537 | time_std[tiling_index] = np.std(data_list) | 1589 | time_std[tiling_index] = np.std(data_list) | ||||||||||||
| 1538 | time_sem[tiling_index] = time_std[tiling_index] / math.sqrt(len(data_list)) | 1590 | time_sem[tiling_index] = time_std[tiling_index] / math.sqrt(len(data_list)) | ||||||||||||
| 1539 | - | 1591 | + if not stats.enabled: | ||||||||||||
| 1540 | - delete_file(torch_path) | 1592 | + delete_file(torch_path) | ||||||||||||
| 1541 | - return time_cost, time_sem, time_std | 1593 | + else: | ||||||||||||
| 1594 | + exp_timing['func_total_ns'] = time.perf_counter_ns() - t0 | ||||||||||||||
| 1595 | + return time_cost, time_sem, time_std, exp_timing | ||||||||||||||
| 1542 | 1596 | ||||||||||||||
| 1543 | delete_file(torch_path) | 1597 | delete_file(torch_path) | ||||||||||||
| 1544 | - return [], [], [] | 1598 | + return [], [], [], {} | ||||||||||||
| 1599 | + | ||||||||||||||
| 1600 | + def do_batch_benchmark_mspti(this_tilling_kernel_list, this_active_num): | ||||||||||||||
| 1601 | + """mspti-based benchmarking, same return type as do_batch_benchmark_kernel_isolate.""" | ||||||||||||||
| 1602 | + from collections import defaultdict | ||||||||||||||
| 1603 | + | ||||||||||||||
| 1604 | + fn_name = self.get_fn_name() if hasattr(self, 'get_fn_name') else 'unknown' | ||||||||||||||
| 1605 | + tiling_length = len(this_tilling_kernel_list) | ||||||||||||||
| 1606 | + WARMUP = 2 | ||||||||||||||
| 1607 | + ACTIVE = this_active_num | ||||||||||||||
| 1608 | + exp_timing = {'func_total_ns': 0} | ||||||||||||||
| 1609 | + | ||||||||||||||
| 1610 | + raw_records = [] | ||||||||||||||
| 1611 | + def callback(data): | ||||||||||||||
| 1612 | + raw_records.append((data.name, data.start, data.end)) | ||||||||||||||
| 1613 | + | ||||||||||||||
| 1614 | + t0 = time.perf_counter_ns() | ||||||||||||||
| 1615 | + monitor = KernelMonitor() | ||||||||||||||
| 1616 | + torch.npu.synchronize() | ||||||||||||||
| 1617 | + monitor.start(callback) | ||||||||||||||
| 1618 | + | ||||||||||||||
| 1619 | + | ||||||||||||||
| 1620 | + l2_cache_size = 192 * (1 << 20) | ||||||||||||||
| 1621 | + buffer = torch.empty(l2_cache_size // 4, dtype=torch.int, device="npu") | ||||||||||||||
| 1622 | + | ||||||||||||||
| 1623 | + _failed_positions = set() | ||||||||||||||
| 1624 | + for _pos, fn in enumerate(this_tilling_kernel_list): | ||||||||||||||
| 1625 | + buffer.zero_() | ||||||||||||||
| 1626 | + try: | ||||||||||||||
| 1627 | + for _ in range(WARMUP + ACTIVE): | ||||||||||||||
| 1628 | + fn() | ||||||||||||||
| 1629 | + except Exception as _e: | ||||||||||||||
| 1630 | + _failed_positions.add(_pos) | ||||||||||||||
| 1631 | + fast_a_log_message( | ||||||||||||||
| 1632 | + content=f"[BENCH_FAIL] {fn_name} config {_pos}/{tiling_length}: {type(_e).__name__}", | ||||||||||||||
| 1633 | + tag='benchmark profiling', level='warning') | ||||||||||||||
| 1634 | + torch.npu.synchronize() | ||||||||||||||
| 1635 | + monitor.stop() | ||||||||||||||
🔵 Low Priority
此外,如果 建议:添加 ![]() ![]() | |||||||||||||||
| 1636 | + | ||||||||||||||
| 1637 | + | ||||||||||||||
| 1638 | + # group by unique kernel name | ||||||||||||||
| 1639 | + durations_by_name = defaultdict(list) | ||||||||||||||
| 1640 | + ordered_triton_names = [] | ||||||||||||||
| 1641 | + for name, start_ns, end_ns in raw_records: | ||||||||||||||
| 1642 | + if name.startswith('triton_'): | ||||||||||||||
| 1643 | + if name not in durations_by_name: | ||||||||||||||
| 1644 | + ordered_triton_names.append(name) | ||||||||||||||
| 1645 | + durations_by_name[name].append(end_ns - start_ns) | ||||||||||||||
| 1646 | + | ||||||||||||||
| 1647 | + # compute per-config stats from the names that produced callbacks | ||||||||||||||
| 1648 | + time_cost = [] | ||||||||||||||
| 1649 | + time_sem = [] | ||||||||||||||
| 1650 | + time_std = [] | ||||||||||||||
| 1651 | + for name in ordered_triton_names: | ||||||||||||||
| 1652 | + durs = durations_by_name[name] | ||||||||||||||
| 1653 | + active_ns = durs[WARMUP:] if len(durs) > WARMUP else durs | ||||||||||||||
| 1654 | + active_us = [d / 1000.0 for d in active_ns] | ||||||||||||||
| 1655 | + avg = np.mean(active_us) | ||||||||||||||
| 1656 | + std = np.std(active_us) if len(active_us) > 1 else 0.0 | ||||||||||||||
| 1657 | + sem = std / math.sqrt(len(active_us)) if len(active_us) > 1 else 0.0 | ||||||||||||||
| 1658 | + time_cost.append(float(avg)) | ||||||||||||||
| 1659 | + time_sem.append(float(sem)) | ||||||||||||||
| 1660 | + time_std.append(float(std)) | ||||||||||||||
| 1661 | + | ||||||||||||||
| 1662 | + # track mismatches: caught exceptions + silent drops | ||||||||||||||
| 1663 | + n_failed = len(_failed_positions) | ||||||||||||||
| 1664 | + n_got = len(ordered_triton_names) | ||||||||||||||
| 1665 | + if n_got != tiling_length: | ||||||||||||||
| 1666 | + _BENCH_FAILURES.append((fn_name, tiling_length, n_got, 'mspti')) | ||||||||||||||
| 1667 | + fast_a_log_message( | ||||||||||||||
| 1668 | + content=f"[BENCH_MISMATCH] {fn_name}: expected={tiling_length} got={n_got} " | ||||||||||||||
| 1669 | + f"exceptions={n_failed}", | ||||||||||||||
| 1670 | + tag='benchmark profiling', level='warning') | ||||||||||||||
| 1671 | + | ||||||||||||||
| 1672 | + exp_timing['func_total_ns'] = time.perf_counter_ns() - t0 | ||||||||||||||
| 1673 | + return time_cost, time_sem, time_std, exp_timing | ||||||||||||||
| 1545 | 1674 | ||||||||||||||
| 1546 | try: | 1675 | try: | ||||||||||||
| 1547 | - this_benchmark_fun = do_batch_benchmark_kernel_isolate | 1676 | + if stats.enabled: | ||||||||||||
| 1548 | - timing_list, timing_sem_list, timing_std_list = this_benchmark_fun(tilling_kernel_list, | 1677 | + # wall time measurement | ||||||||||||
| 1549 | - this_active_num=profiler_active_num) | 1678 | + _bench_wall_start = time.perf_counter_ns() | ||||||||||||
| 1679 | + | ||||||||||||||
| 1680 | + # select benchmark backend | ||||||||||||||
| 1681 | + if fasta_mspti_en: | ||||||||||||||
| 1682 | + global KernelMonitor | ||||||||||||||
| 1683 | + if KernelMonitor is None: | ||||||||||||||
| 1684 | + try: | ||||||||||||||
| 1685 | + from mspti import KernelMonitor | ||||||||||||||
| 1686 | + except ImportError: | ||||||||||||||
| 1687 | + pass | ||||||||||||||
| 1688 | + if fasta_mspti_en and KernelMonitor is not None: | ||||||||||||||
| 1689 | + this_benchmark_fun = do_batch_benchmark_mspti | ||||||||||||||
| 1690 | + else: | ||||||||||||||
| 1691 | + this_benchmark_fun = do_batch_benchmark_kernel_isolate | ||||||||||||||
| 1692 | + | ||||||||||||||
| 1693 | + if stats.enabled: | ||||||||||||||
| 1694 | + bench_start_time = time.perf_counter() | ||||||||||||||
| 1695 | + | ||||||||||||||
| 1696 | + result = this_benchmark_fun(tilling_kernel_list, this_active_num=profiler_active_num) | ||||||||||||||
| 1697 | + | ||||||||||||||
| 1698 | + if stats.enabled: | ||||||||||||||
| 1699 | + stage = "Benchmark configs" | ||||||||||||||
| 1700 | + if self.dynamic_filter: | ||||||||||||||
| 1701 | + stage = self.dynamic_filter_scheduler.get_benchmark_desc() | ||||||||||||||
| 1702 | + bench_duration = time.perf_counter() - bench_start_time | ||||||||||||||
| 1703 | + log.info(f"{self.get_fn_name()} {stage} elapsed time {bench_duration}s") | ||||||||||||||
| 1704 | + stats.write( | ||||||||||||||
| 1705 | + "duration-stats", | ||||||||||||||
| 1706 | + [ | ||||||||||||||
| 1707 | + self.get_fn_name(), | ||||||||||||||
| 1708 | + stage, | ||||||||||||||
| 1709 | + bench_duration * 1000, | ||||||||||||||
| 1710 | + len(self.launchers), | ||||||||||||||
| 1711 | + None, | ||||||||||||||
| 1712 | + None, | ||||||||||||||
| 1713 | + ], | ||||||||||||||
| 1714 | + ) | ||||||||||||||
| 1715 | + | ||||||||||||||
| 1716 | + timing_list = result[0] if isinstance(result, tuple) and len(result) >= 4 else result[0] | ||||||||||||||
| 1717 | + timing_sem_list = result[1] if isinstance(result, tuple) and len(result) >= 4 else result[1] | ||||||||||||||
| 1718 | + timing_std_list = result[2] if isinstance(result, tuple) and len(result) >= 4 else result[2] | ||||||||||||||
| 1719 | + _bench_exp_timing = result[3] if isinstance(result, tuple) and len(result) >= 4 else {} | ||||||||||||||
| 1720 | + | ||||||||||||||
| 1721 | + if len(timing_list) != len(self.launchers): | ||||||||||||||
| 1722 | + _BENCH_FAILURES.append((self.get_fn_name(), len(self.launchers), len(timing_list), | ||||||||||||||
| 1723 | + 'mspti' if (fasta_mspti_en and KernelMonitor is not None) else 'msprof')) | ||||||||||||||
| 1724 | + fast_a_log_message( | ||||||||||||||
| 1725 | + content=f"[BENCH_MISMATCH] {self.get_fn_name()}: launchers={len(self.launchers)} " | ||||||||||||||
| 1726 | + f"timings={len(timing_list)}", | ||||||||||||||
| 1727 | + tag='benchmark profiling', level='warning') | ||||||||||||||
| 1728 | + # truncate to shorter length so zip doesn't silently drop | ||||||||||||||
| 1729 | + _min_len = min(len(timing_list), len(self.launchers)) | ||||||||||||||
| 1730 | + timing_list = timing_list[:_min_len] | ||||||||||||||
| 1731 | + timing_sem_list = timing_sem_list[:_min_len] | ||||||||||||||
| 1732 | + timing_std_list = timing_std_list[:_min_len] | ||||||||||||||
| 1733 | + self.launchers = self.launchers[:_min_len] | ||||||||||||||
| 1734 | + if stats.enabled: | ||||||||||||||
| 1735 | + # accumulate benchmark wall time | ||||||||||||||
| 1736 | + _bench_wall_ns = time.perf_counter_ns() - _bench_wall_start | ||||||||||||||
| 1737 | + _BENCH_ACCUMULATOR['n_kernels'] += 1 | ||||||||||||||
| 1738 | + _BENCH_ACCUMULATOR['total_wall_ns'] += _bench_wall_ns | ||||||||||||||
| 1739 | + if _bench_exp_timing: | ||||||||||||||
| 1740 | + _BENCH_ACCUMULATOR['total_func_ns'] += _bench_exp_timing.get('func_total_ns', 0) | ||||||||||||||
| 1741 | + | ||||||||||||||
| 1742 | + # log per-kernel benchmark stats | ||||||||||||||
| 1743 | + _backend = 'mspti' if (fasta_mspti_en and KernelMonitor is not None) else 'msprof' | ||||||||||||||
| 1744 | + fast_a_log_message( | ||||||||||||||
| 1745 | + content=f"[STATS] benchmark kernel={self.get_fn_name()} backend={_backend} " | ||||||||||||||
| 1746 | + f"launchers={len(timing_list)} wall={_bench_wall_ns/1e6:.1f}ms", | ||||||||||||||
| 1747 | + tag='stats', level='info') | ||||||||||||||
| 1748 | + | ||||||||||||||
| 1749 | + | ||||||||||||||
| 1550 | if not len(timing_list) == len(self.launchers): | 1750 | if not len(timing_list) == len(self.launchers): | ||||||||||||
| 1551 | raise RuntimeError(f"not {len(timing_list)} == {len(self.launchers)}") | 1751 | raise RuntimeError(f"not {len(timing_list)} == {len(self.launchers)}") | ||||||||||||
| 1752 | + | ||||||||||||||
| 1552 | timing_infos = {} | 1753 | timing_infos = {} | ||||||||||||
| 1553 | for launcher, timing, sem, std in zip(self.launchers, timing_list, timing_sem_list, timing_std_list): | 1754 | for launcher, timing, sem, std in zip(self.launchers, timing_list, timing_sem_list, timing_std_list): | ||||||||||||
| 1554 | timing_infos[launcher] = [timing, sem, std] | 1755 | timing_infos[launcher] = [timing, sem, std] | ||||||||||||
| @@ -1570,7 +1771,7 @@ class NPUFastAutotuner(NPUCachingAutotuner): | |||||||||||||||
| 1570 | need_test_index.append(idx) | 1771 | need_test_index.append(idx) | ||||||||||||
| 1571 | if need_test_index: | 1772 | if need_test_index: | ||||||||||||
| 1572 | need_test_times = min(10 * (need_test_times + 10 - 1) // 10, | 1773 | need_test_times = min(10 * (need_test_times + 10 - 1) // 10, | ||||||||||||
| 1573 | - FASTA_SETTING.re_profiling_max_times) | 1774 | + FASTA_SETTING.re_profiling_max_times) | ||||||||||||
| 1574 | fast_a_log_message( | 1775 | fast_a_log_message( | ||||||||||||
| 1575 | content="need re_profiling times is ({}) for index {}".format( | 1776 | content="need re_profiling times is ({}) for index {}".format( | ||||||||||||
| 1576 | need_test_times, need_test_index), tag='re_profiling') | 1777 | need_test_times, need_test_index), tag='re_profiling') | ||||||||||||
| @@ -1581,7 +1782,7 @@ class NPUFastAutotuner(NPUCachingAutotuner): | |||||||||||||||
| 1581 | this_active_num=profiler_active_num) | 1782 | this_active_num=profiler_active_num) | ||||||||||||
🟠 High Priority 在 触发条件: 建议:改为4变量解包,或使用索引访问(与主路径一致),并忽略 改动建议
![]() ![]() | |||||||||||||||
| 1582 | for idx, re_i in enumerate(need_test_index): | 1783 | for idx, re_i in enumerate(need_test_index): | ||||||||||||
| 1583 | timing_infos[self.launchers[re_i]] = [re_timing_list[idx], re_timing_sem_list[idx], | 1784 | timing_infos[self.launchers[re_i]] = [re_timing_list[idx], re_timing_sem_list[idx], | ||||||||||||
| 1584 | - re_timing_std_list[idx]] | 1785 | + re_timing_std_list[idx]] | ||||||||||||
| 1585 | 1786 | ||||||||||||||
| 1586 | except Exception as e: | 1787 | except Exception as e: | ||||||||||||
| 1587 | fast_a_log_message( | 1788 | fast_a_log_message( | ||||||||||||
| @@ -1601,6 +1802,51 @@ class NPUFastAutotuner(NPUCachingAutotuner): | |||||||||||||||
| 1601 | 1802 | ||||||||||||||
| 1602 | return timing_infos | 1803 | return timing_infos | ||||||||||||
| 1603 | 1804 | ||||||||||||||
| 1805 | + | ||||||||||||||
| 1806 | + def _benchmark_all_configs_event(self, *args, **kwargs): | ||||||||||||||
| 1807 | + """Event-based benchmark: no NPU profiler overhead, time proportional to config count. | ||||||||||||||
| 1808 | + | ||||||||||||||
| 1809 | + Activated by FASTA_EVENT_BENCHMARK=1. Uses NPU events (start/end record) | ||||||||||||||
| 1810 | + instead of torch_npu.profiler, eliminating ~34s fixed overhead per kernel. | ||||||||||||||
| 1811 | + """ | ||||||||||||||
| 1812 | + fast_a_log_message( | ||||||||||||||
| 1813 | + content=f"event-based benchmark for {len(self.launchers)} configs", | ||||||||||||||
| 1814 | + tag='benchmark event') | ||||||||||||||
| 1815 | + | ||||||||||||||
| 1816 | + if stats.enabled: | ||||||||||||||
| 1817 | + bench_start_time = time.perf_counter() | ||||||||||||||
| 1818 | + | ||||||||||||||
| 1819 | + timing_infos = {} | ||||||||||||||
| 1820 | + for launcher in self.launchers: | ||||||||||||||
| 1821 | + try: | ||||||||||||||
| 1822 | + timing_infos[launcher] = self.bench_event(launcher, *args, **kwargs) | ||||||||||||||
| 1823 | + except Exception as e: | ||||||||||||||
| 1824 | + fast_a_log_message( | ||||||||||||||
| 1825 | + content=f"event bench config ({launcher.config}) error: {e}", | ||||||||||||||
| 1826 | + tag='benchmark event', level='warning') | ||||||||||||||
| 1827 | + | ||||||||||||||
| 1828 | + if stats.enabled: | ||||||||||||||
| 1829 | + stage = "Benchmark configs" | ||||||||||||||
| 1830 | + if self.dynamic_filter: | ||||||||||||||
| 1831 | + stage = self.dynamic_filter_scheduler.get_benchmark_desc() | ||||||||||||||
| 1832 | + stats.write( | ||||||||||||||
| 1833 | + "duration-stats", | ||||||||||||||
| 1834 | + [ | ||||||||||||||
| 1835 | + self.get_fn_name(), | ||||||||||||||
| 1836 | + stage, | ||||||||||||||
| 1837 | + (time.perf_counter() - bench_start_time) * 1000, | ||||||||||||||
| 1838 | + len(self.launchers), | ||||||||||||||
| 1839 | + None, | ||||||||||||||
| 1840 | + None, | ||||||||||||||
| 1841 | + ], | ||||||||||||||
| 1842 | + ) | ||||||||||||||
| 1843 | + | ||||||||||||||
| 1844 | + # Common post-processing: cache results for coordinate descent tuner | ||||||||||||||
| 1845 | + for k, v in timing_infos.items(): | ||||||||||||||
| 1846 | + self.coordesc_tuner.cache_benchmark_result(k.config, v[0]) | ||||||||||||||
| 1847 | + | ||||||||||||||
| 1848 | + return timing_infos | ||||||||||||||
| 1849 | + | ||||||||||||||
| 1604 | def bench_event(self, launcher, *args, **kwargs): | 1850 | def bench_event(self, launcher, *args, **kwargs): | ||||||||||||
| 1605 | """Measure the performance of a given launcher""" | 1851 | """Measure the performance of a given launcher""" | ||||||||||||
| 1606 | 1852 | ||||||||||||||
| @@ -1623,7 +1869,7 @@ class NPUFastAutotuner(NPUCachingAutotuner): | |||||||||||||||
| 1623 | return self._do_bench_with_event(kernel_call) | 1869 | return self._do_bench_with_event(kernel_call) | ||||||||||||
| 1624 | 1870 | ||||||||||||||
| 1625 | 1871 | ||||||||||||||
| 1626 | - def _do_bench_with_event(fn, warmup_times=1, rep_times=2): | 1872 | + def _do_bench_with_event(fn, warmup_times=1, rep_times=10): | ||||||||||||
| 1627 | """ | 1873 | """ | ||||||||||||
| 1628 | Benchmark the runtime of the provided function. By default, return the median runtime of :code:`fn` along with | 1874 | Benchmark the runtime of the provided function. By default, return the median runtime of :code:`fn` along with | ||||||||||||
| 1629 | the 20-th and 80-th performance percentile. | 1875 | the 20-th and 80-th performance percentile. | ||||||||||||
| @@ -78,7 +78,6 @@ from torch._inductor.runtime.triton_heuristics import ( # noqa: F401 | |||||||||
| 78 | from torch._inductor.runtime.runtime_utils import triton_hash_to_path_key | 78 | from torch._inductor.runtime.runtime_utils import triton_hash_to_path_key | ||||||
| 79 | from triton.compiler import CompiledKernel | 79 | from triton.compiler import CompiledKernel | ||||||
| 80 | from torch._inductor.triton_bundler import TritonBundler | 80 | from torch._inductor.triton_bundler import TritonBundler | ||||||
| 81 | - | ||||||||
| 82 | try: | 81 | try: | ||||||
| 83 | from triton.backends.compiler import GPUTarget | 82 | from triton.backends.compiler import GPUTarget | ||||||
| 84 | from triton.runtime.autotuner import OutOfResources | 83 | from triton.runtime.autotuner import OutOfResources | ||||||
| @@ -97,6 +96,8 @@ from ..codegen.triton_utils import NPUKernelType | |||||||||
| 97 | from ..config import log, autotune_continue_on_failure | 96 | from ..config import log, autotune_continue_on_failure | ||||||
| 98 | from .. import config as npu_config | 97 | from .. import config as npu_config | ||||||
| 99 | from ..profiler import simple_trace_handler, mspti_batch_benchmark | 98 | from ..profiler import simple_trace_handler, mspti_batch_benchmark | ||||||
| 99 | +from ..experimental.dynamic_filter.dynamic_filter_config import fasta_autotune_stats | ||||||||
| 100 | +from ..experimental.dynamic_filter.autotune_stats import AutotuneStatsManager | ||||||||
| 100 | 101 | ||||||||
| 101 | kernel_idx = count() | 102 | kernel_idx = count() | ||||||
| 102 | 103 | ||||||||
| @@ -126,6 +127,11 @@ class CompileThreadPool: | |||||||||
| 126 | compile_thread_pool = CompileThreadPool() | 127 | compile_thread_pool = CompileThreadPool() | ||||||
| 127 | 128 | ||||||||
| 128 | 129 | ||||||||
| 130 | +stats = AutotuneStatsManager( | ||||||||
| 131 | + enabled=fasta_autotune_stats, log=log | ||||||||
| 132 | +) | ||||||||
| 133 | + | ||||||||
| 134 | + | ||||||||
| 129 | 135 | ||||||||
| 130 | def create_profiler(torch_path, wait=0, warmup=1, active=1, repeat=1, skip_first=1): | 136 | def create_profiler(torch_path, wait=0, warmup=1, active=1, repeat=1, skip_first=1): | ||||||
| 131 | experimental_config = torch_npu.profiler._ExperimentalConfig( | 137 | experimental_config = torch_npu.profiler._ExperimentalConfig( | ||||||
| @@ -667,7 +673,7 @@ class NPUCachingAutotuner(CachingAutotuner): | |||||||||
| 667 | def precompile( | 673 | def precompile( | ||||||
| 668 | self, | 674 | self, | ||||||
| 669 | warm_cache_only=False, | 675 | warm_cache_only=False, | ||||||
| 670 | - reload_kernel: Optional[Callable[[], CachingAutotuner]] = None, | 676 | + reload_kernel: Optional[Callable[[], CachingAutotuner]] = None | ||||||
| 671 | ): | 677 | ): | ||||||
| 672 | runtime_args, runtime_kwargs = self._resolve_costmodel_runtime_inputs() | 678 | runtime_args, runtime_kwargs = self._resolve_costmodel_runtime_inputs() | ||||||
| 673 | self._apply_costmodel_to_configs(*runtime_args, **runtime_kwargs) | 679 | self._apply_costmodel_to_configs(*runtime_args, **runtime_kwargs) | ||||||
| @@ -1489,7 +1495,8 @@ class NPUCachingAutotuner(CachingAutotuner): | |||||||||
| 1489 | for active_index in range(ACTIVE): | 1495 | for active_index in range(ACTIVE): | ||||||
| 1490 | row_index = kernel_index + kernel_count * active_index | 1496 | row_index = kernel_index + kernel_count * active_index | ||||||
| 1491 | time_cost[kernel_index] += triton_rows.iloc[row_index]['Duration(us)'] | 1497 | time_cost[kernel_index] += triton_rows.iloc[row_index]['Duration(us)'] | ||||||
| 1492 | - delete_file(autotune_path) | 1498 | + if not stats.enabled: | ||||||
| 1499 | + delete_file(autotune_path) | ||||||||
| 1493 | return [cost / ACTIVE for cost in time_cost] | 1500 | return [cost / ACTIVE for cost in time_cost] | ||||||
| 1494 | 1501 | ||||||||
| 1495 | delete_file(autotune_path) | 1502 | delete_file(autotune_path) | ||||||
| @@ -1708,6 +1715,24 @@ class NPUCachingAutotuner(CachingAutotuner): | |||||||||
| 1708 | ) | 1715 | ) | ||||||
| 1709 | timings = self._benchmark_candidate_entries(*args, **kwargs) | 1716 | timings = self._benchmark_candidate_entries(*args, **kwargs) | ||||||
| 1710 | benchmark_time_taken_ns = time.time_ns() - start_time | 1717 | benchmark_time_taken_ns = time.time_ns() - start_time | ||||||
| 1718 | + if stats.enabled: | ||||||||
| 1719 | + benchmark_time_taken_ms = benchmark_time_taken_ns / 1e6 | ||||||||
| 1720 | + stats.write( | ||||||||
| 1721 | + "duration-stats", | ||||||||
| 1722 | + [ | ||||||||
| 1723 | + self.get_fn_name(), | ||||||||
| 1724 | + "Benchmark configs", | ||||||||
| 1725 | + benchmark_time_taken_ms, | ||||||||
| 1726 | + len(self.launchers), | ||||||||
| 1727 | + None, | ||||||||
| 1728 | + None, | ||||||||
| 1729 | + ], | ||||||||
| 1730 | + ) | ||||||||
| 1731 | + log.info( | ||||||||
| 1732 | + "%s - Duration time of benchmarking all configs (ms): %.6f", | ||||||||
| 1733 | + self.get_fn_name(), | ||||||||
| 1734 | + benchmark_time_taken_ms, | ||||||||
| 1735 | + ) | ||||||||
| 1711 | candidate_map = { | 1736 | candidate_map = { | ||||||
| 1712 | candidate["candidate_id"]: candidate | 1737 | candidate["candidate_id"]: candidate | ||||||
| 1713 | for candidate in self.candidate_plan["candidate_entries"] | 1738 | for candidate in self.candidate_plan["candidate_entries"] | ||||||
| @@ -1797,6 +1822,11 @@ class NPUCachingAutotuner(CachingAutotuner): | |||||||||
| 1797 | def run( | 1822 | def run( | ||||||
| 1798 | self, *args, stream, benchmark_run=False, **kwargs | 1823 | self, *args, stream, benchmark_run=False, **kwargs | ||||||
| 1799 | ): # type:ignore[override] | 1824 | ): # type:ignore[override] | ||||||
| 1825 | + | ||||||||
| 1826 | + if stats.enabled: | ||||||||
| 1827 | + self.cache_hit = True | ||||||||
| 1828 | + start_time = time.perf_counter() | ||||||||
| 1829 | + | ||||||||
| 1800 | if self.triton_interpret: | 1830 | if self.triton_interpret: | ||||||
| 1801 | cfg = self.best_candidate_config or self.configs[0] | 1831 | cfg = self.best_candidate_config or self.configs[0] | ||||||
| 1802 | runtime_blocks = self.best_runtime_blocks | 1832 | runtime_blocks = self.best_runtime_blocks | ||||||
| @@ -1820,7 +1850,11 @@ class NPUCachingAutotuner(CachingAutotuner): | |||||||||
| 1820 | **kwargs, | 1850 | **kwargs, | ||||||
| 1821 | ) | 1851 | ) | ||||||
| 1822 | 1852 | ||||||||
| 1823 | - self.autotuner(*args, stream=stream, benchmark_run=benchmark_run, **kwargs) | 1853 | + if getattr(self, 'dynamic_filter', False) and \ | ||||||
| 1854 | + self.dynamic_filter_scheduler.phase.name not in 'DONE': | ||||||||
🔵 Low Priority 在 属于误导性代码,具有潜在回归风险。 建议:改用相等比较 改动建议
![]() ![]() | |||||||||
| 1855 | + self.dynamic_filter_scheduler.compile_and_benchmark(self, *args, **kwargs) | ||||||||
| 1856 | + else: | ||||||||
| 1857 | + self.autotuner(*args, stream=stream, benchmark_run=benchmark_run, **kwargs) | ||||||||
| 1824 | launcher = self.best_launcher if self.best_launcher is not None else self.launchers[0] | 1858 | launcher = self.best_launcher if self.best_launcher is not None else self.launchers[0] | ||||||
| 1825 | runtime_blocks = self.best_runtime_blocks | 1859 | runtime_blocks = self.best_runtime_blocks | ||||||
| 1826 | 1860 | ||||||||
| @@ -1828,7 +1862,7 @@ class NPUCachingAutotuner(CachingAutotuner): | |||||||||
| 1828 | self.save_gpu_kernel(stream, launcher) | 1862 | self.save_gpu_kernel(stream, launcher) | ||||||
| 1829 | 1863 | ||||||||
| 1830 | if self.dump_launch_params: | 1864 | if self.dump_launch_params: | ||||||
| 1831 | - _dump_launch_params(args, kwargs, launcher, self.fn.__name__) | 1865 | + _dump_launch_params(args, kwargs, launcher, self.fn.__name__, "") | ||||||
| 1832 | 1866 | ||||||||
| 1833 | launch_args = self._build_runtime_launch_args(args, runtime_blocks) | 1867 | launch_args = self._build_runtime_launch_args(args, runtime_blocks) | ||||||
| 1834 | if self.is_run_debug() and not self.heuristic_type == HeuristicType.USER_AUTOTUNE: | 1868 | if self.is_run_debug() and not self.heuristic_type == HeuristicType.USER_AUTOTUNE: | ||||||
| @@ -1858,6 +1892,26 @@ class NPUCachingAutotuner(CachingAutotuner): | |||||||||
| 1858 | stream=stream, | 1892 | stream=stream, | ||||||
| 1859 | ) | 1893 | ) | ||||||
| 1860 | else: | 1894 | else: | ||||||
| 1895 | + if stats.enabled: | ||||||||
| 1896 | + duration_ms = (time.perf_counter() - start_time) * 1000 | ||||||||
| 1897 | + log.info( | ||||||||
| 1898 | + "%s - Duration time of autotuning process (no tile gen): %.6f ms. cache hit: %s", | ||||||||
| 1899 | + self.get_fn_name(), | ||||||||
| 1900 | + duration_ms, | ||||||||
| 1901 | + self.cache_hit, | ||||||||
| 1902 | + ) | ||||||||
| 1903 | + if not self.cache_hit: | ||||||||
| 1904 | + stats.write( | ||||||||
| 1905 | + "duration-stats", | ||||||||
| 1906 | + [ | ||||||||
| 1907 | + self.get_fn_name(), | ||||||||
| 1908 | + "Autotuning (no tile gen)", | ||||||||
| 1909 | + duration_ms, | ||||||||
| 1910 | + len(self.launchers), | ||||||||
| 1911 | + None, | ||||||||
| 1912 | + None, | ||||||||
| 1913 | + ], | ||||||||
| 1914 | + ) | ||||||||
| 1861 | return launcher( | 1915 | return launcher( | ||||||
| 1862 | *launch_args, | 1916 | *launch_args, | ||||||
| 1863 | **kwargs, | 1917 | **kwargs, | ||||||
| @@ -1907,6 +1961,8 @@ class NPUCachingAutotuner(CachingAutotuner): | |||||||||
| 1907 | self.save_cache_hook(best_config, 0) | 1961 | self.save_cache_hook(best_config, 0) | ||||||
| 1908 | else: | 1962 | else: | ||||||
| 1909 | self.autotune_to_one_config(*args, **kwargs) | 1963 | self.autotune_to_one_config(*args, **kwargs) | ||||||
| 1964 | + if stats.enabled: | ||||||||
| 1965 | + self.cache_hit = False | ||||||||
| 1910 | log.info(f"{self.get_fn_name()} benchmark elapsed time {time.perf_counter() - autotune_start_time}s") | 1966 | log.info(f"{self.get_fn_name()} benchmark elapsed time {time.perf_counter() - autotune_start_time}s") | ||||||
| 1911 | 1967 | ||||||||
| 1912 | def _interpret_args_grid( | 1968 | def _interpret_args_grid( | ||||||
| @@ -2537,7 +2593,7 @@ def cached_autotune( | |||||||||
| 2537 | ) | 2593 | ) | ||||||
| 2538 | 2594 | ||||||||
| 2539 | if npu_config.fasta_autotune: | 2595 | if npu_config.fasta_autotune: | ||||||
| 2540 | - from .fasta_autotune import NPUFastAutotuner | 2596 | + from ..fasta_autotune import NPUFastAutotuner | ||||||
| 2541 | return NPUFastAutotuner( | 2597 | return NPUFastAutotuner( | ||||||
| 2542 | fn, | 2598 | fn, | ||||||
| 2543 | triton_meta=triton_meta, | 2599 | triton_meta=triton_meta, | ||||||
| @@ -2601,7 +2657,7 @@ def brutal_prune_tiling_configs_if_fast_run(configs, inductor_meta) -> List[Conf | |||||||||
| 2601 | 2657 | ||||||||
| 2602 | if max_num > 0 and len(configs) > max_num: | 2658 | if max_num > 0 and len(configs) > max_num: | ||||||
| 2603 | configs = configs[-1 * max_num:] | 2659 | configs = configs[-1 * max_num:] | ||||||
| 2604 | - logging.debug("[%s], prune tiling configs to [%s]", | 2660 | + log.debug("[%s], prune tiling configs to [%s]", | ||||||
| 2605 | inductor_meta["kernel_name"], | 2661 | inductor_meta["kernel_name"], | ||||||
| 2606 | len(configs)) | 2662 | len(configs)) | ||||||
| 2607 | return configs | 2663 | return configs | ||||||
| @@ -3013,8 +3069,10 @@ def _triton_config_npu_index_legacy( | |||||||||
| 3013 | npu_kernel_type = NPUKernelType(inductor_meta.get("npu_kernel_type", "simd")) | 3069 | npu_kernel_type = NPUKernelType(inductor_meta.get("npu_kernel_type", "simd")) | ||||||
| 3014 | size_hints = [size_hints.get(axis_name, 1) for axis_name in axis_names] | 3070 | size_hints = [size_hints.get(axis_name, 1) for axis_name in axis_names] | ||||||
| 3015 | 3071 | ||||||||
| 3072 | + if stats.enabled: | ||||||||
| 3073 | + start_split_tiling = time.perf_counter() | ||||||||
| 3016 | if npu_config.fasta_autotune: | 3074 | if npu_config.fasta_autotune: | ||||||
| 3017 | - from .fasta_autotune import FastATileGenerator | 3075 | + from ..fasta_autotune import FastATileGenerator | ||||||
| 3018 | tile_generator = FastATileGenerator(size_hints, axis_names, tiling_axis, no_loop_axis, split_axis, low_dims, | 3076 | tile_generator = FastATileGenerator(size_hints, axis_names, tiling_axis, no_loop_axis, split_axis, low_dims, | ||||||
| 3019 | persistent_reduction=is_persistent_reduction, | 3077 | persistent_reduction=is_persistent_reduction, | ||||||
| 3020 | dtype=split_axis_dtype, | 3078 | dtype=split_axis_dtype, | ||||||
| @@ -3073,12 +3131,33 @@ def _triton_config_npu_index_legacy( | |||||||||
| 3073 | "SUB") and tiling.startswith("R"): | 3131 | "SUB") and tiling.startswith("R"): | ||||||
| 3074 | tiling_cfg.kwargs[tiling.rstrip("_SUB")] = tling_value | 3132 | tiling_cfg.kwargs[tiling.rstrip("_SUB")] = tling_value | ||||||
| 3075 | 3133 | ||||||||
| 3076 | - logging.debug("[%s], generate candidate tiling count: [%s]", | 3134 | + log.debug("[%s], generate candidate tiling count: [%s]", | ||||||
| 3077 | inductor_meta["kernel_name"], | 3135 | inductor_meta["kernel_name"], | ||||||
| 3078 | len(configs)) | 3136 | len(configs)) | ||||||
| 3079 | 3137 | ||||||||
| 3080 | # if fast run, we prune the configs to the last max_num configs | 3138 | # if fast run, we prune the configs to the last max_num configs | ||||||
| 3081 | configs = brutal_prune_tiling_configs_if_fast_run(configs, inductor_meta) | 3139 | configs = brutal_prune_tiling_configs_if_fast_run(configs, inductor_meta) | ||||||
| 3140 | + | ||||||||
| 3141 | + if stats.enabled: | ||||||||
| 3142 | + end_split_tiling = time.perf_counter() | ||||||||
| 3143 | + duration_split_tiling_ms = (end_split_tiling - start_split_tiling) * 1000 | ||||||||
| 3144 | + stats.write( | ||||||||
| 3145 | + "duration-stats", | ||||||||
| 3146 | + [ | ||||||||
| 3147 | + inductor_meta['kernel_name'], | ||||||||
| 3148 | + "Tile generation", | ||||||||
| 3149 | + duration_split_tiling_ms, | ||||||||
| 3150 | + len(configs), | ||||||||
| 3151 | + start_split_tiling, | ||||||||
| 3152 | + end_split_tiling | ||||||||
| 3153 | + ] | ||||||||
| 3154 | + ) | ||||||||
| 3155 | + log.info( | ||||||||
| 3156 | + "%s - Tile generation - Duration time (ms): %.6f", | ||||||||
| 3157 | + inductor_meta["kernel_name"], | ||||||||
| 3158 | + duration_split_tiling_ms, | ||||||||
| 3159 | + ) | ||||||||
| 3160 | + | ||||||||
| 3082 | return configs | 3161 | return configs | ||||||
| 3083 | 3162 | ||||||||
| 3084 | 3163 | ||||||||
| @@ -3262,13 +3341,17 @@ def _benchmark_all_configs(self, *args, **kwargs): | |||||||||
| 3262 | def precompile_parallel( | 3341 | def precompile_parallel( | ||||||
| 3263 | self, | 3342 | self, | ||||||
| 3264 | warm_cache_only=False, | 3343 | warm_cache_only=False, | ||||||
| 3265 | - reload_kernel: Optional[Callable[[], CachingAutotuner]] = None, | 3344 | + reload_kernel: Optional[Callable[[], CachingAutotuner]] = None | ||||||
| 3266 | ): | 3345 | ): | ||||||
| 3267 | if reload_kernel is not None: | 3346 | if reload_kernel is not None: | ||||||
| 3268 | self._reload_kernel = reload_kernel | 3347 | self._reload_kernel = reload_kernel | ||||||
| 3269 | - start_time = time.perf_counter() | 3348 | + | ||||||
| 3349 | + precompile_start_time = time.perf_counter() | ||||||||
| 3350 | + log.info("kernel: %s enter precompile", self.get_fn_name()) | ||||||||
| 3351 | + | ||||||||
| 3270 | if hasattr(self, "skip_precompile"): | 3352 | if hasattr(self, "skip_precompile"): | ||||||
| 3271 | if self.skip_precompile: | 3353 | if self.skip_precompile: | ||||||
| 3354 | + log.info("kernel: %s self.skip_precompile", self.get_fn_name()) | ||||||||
| 3272 | return | 3355 | return | ||||||
| 3273 | 3356 | ||||||||
| 3274 | runtime_args, runtime_kwargs = self._resolve_costmodel_runtime_inputs() | 3357 | runtime_args, runtime_kwargs = self._resolve_costmodel_runtime_inputs() | ||||||
| @@ -3277,7 +3360,7 @@ def precompile_parallel( | |||||||||
| 3277 | if warm_cache_only: | 3360 | if warm_cache_only: | ||||||
| 3278 | self.kernel_name = self.get_fn_name() | 3361 | self.kernel_name = self.get_fn_name() | ||||||
| 3279 | self._precompile_worker_parallel() | 3362 | self._precompile_worker_parallel() | ||||||
| 3280 | - log.info(f"kernel: {self.get_fn_name()} precompile elapsed time: {time.perf_counter() - start_time}s") | 3363 | + log.info("kernel: %s precompile elapsed time: %ss", self.get_fn_name(), (time.perf_counter() - precompile_start_time)) | ||||||
| 3281 | return | 3364 | return | ||||||
| 3282 | 3365 | ||||||||
| 3283 | if self.compile_results: | 3366 | if self.compile_results: | ||||||
| @@ -3290,7 +3373,43 @@ def precompile_parallel( | |||||||||
| 3290 | self._refresh_variant_launchers() | 3373 | self._refresh_variant_launchers() | ||||||
| 3291 | return | 3374 | return | ||||||
| 3292 | 3375 | ||||||||
| 3293 | - self._precompile_worker_parallel() | 3376 | + if reload_kernel is not None and getattr(self.fn, 'fn', None) is None: | ||||||
| 3377 | + fresh_jit_fn = reload_kernel() | ||||||||
| 3378 | + # patch ONLY the two dead fields onto the hollow object | ||||||||
| 3379 | + # everything else (hash, cache, params...) stays from hollow | ||||||||
| 3380 | + self.fn.fn = fresh_jit_fn.fn | ||||||||
| 3381 | + self.fn.__globals__ = fresh_jit_fn.__globals__ | ||||||||
| 3382 | + | ||||||||
| 3383 | + try: | ||||||||
| 3384 | + self._precompile_worker_parallel() | ||||||||
| 3385 | + except NoTritonConfigsError as e: | ||||||||
| 3386 | + if getattr(self, 'dynamic_filter', False): | ||||||||
| 3387 | + self.dynamic_filter_scheduler.catch_no_valid_triton_configs(self.get_fn_name(), e) | ||||||||
| 3388 | + else: | ||||||||
| 3389 | + raise e | ||||||||
| 3390 | + | ||||||||
| 3294 | self._make_launchers() | 3391 | self._make_launchers() | ||||||
| 3295 | self._refresh_variant_launchers() | 3392 | self._refresh_variant_launchers() | ||||||
| 3296 | - log.info(f"kernel: {self.get_fn_name()} precompile elapsed time: {time.perf_counter() - start_time}s") | 3393 | + precompile_end_time = time.perf_counter() | ||||||
| 3394 | + precompile_duration = precompile_end_time - precompile_start_time | ||||||||
| 3395 | + | ||||||||
| 3396 | + if getattr(self, 'dynamic_filter', False): | ||||||||
| 3397 | + self.dynamic_filter_scheduler.store_precompile_duration(precompile_duration) | ||||||||
| 3398 | + | ||||||||
| 3399 | + log.info(f"kernel: {self.get_fn_name()} - precompile elapsed time: {precompile_duration}s.") | ||||||||
| 3400 | + if stats.enabled: | ||||||||
| 3401 | + if getattr(self, 'dynamic_filter', False): | ||||||||
| 3402 | + stage = self.dynamic_filter_scheduler.get_compilation_desc() | ||||||||
| 3403 | + else: | ||||||||
| 3404 | + stage = "Precompile configs" | ||||||||
| 3405 | + stats.write( | ||||||||
| 3406 | + "duration-stats", | ||||||||
| 3407 | + [ | ||||||||
| 3408 | + self.get_fn_name(), | ||||||||
| 3409 | + stage, | ||||||||
| 3410 | + (precompile_duration * 1000), | ||||||||
| 3411 | + len(self.compile_results), | ||||||||
| 3412 | + precompile_start_time, | ||||||||
| 3413 | + precompile_end_time, | ||||||||
| 3414 | + ], | ||||||||
| 3415 | + ) | ||||||||


🔵 Low Priority
变更行: test_config_optimizer.py 第 12 行,
import torch。该文件中所有实际使用的 torch 符号均来自
from torch.testing._internal.common_utils import ...(第 13-15 行)。顶层的import torch未在任何地方被引用,是一个无效的导入。影响:死代码,不影响运行时行为,但增加不必要的 import 开销和代码噪音。
建议:移除第 12 行
import torch,该模块未在文件中使用。