已合并
[feat]Inductor Autotuning Optimization for Triton Ascend Compilation #37655
[feat]Inductor Autotuning Optimization for Triton Ascend Compilation #37655
已合并
yanf81创建于 6月5日
15 个文件变更+4284-54
Atest/_inductor/test_config_optimizer.py+189-0
@@ -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
atomgit-bot
atomgit-botatomgit-bot6月23日

🔵 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,该模块未在文件中使用。

likedislike
yanf81
7月6日 评论:
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+ @parametrize("circle_num,kept", [(2, True), (4, True), (5, False), (8, False)])
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()
Atest/_inductor/test_dynamic_filter.py+336-0
@@ -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
atomgit-bot
atomgit-botatomgit-bot6月23日

🔵 Low Priority

变更行: test_dynamic_filter.py 第 147-148 行,_regret_pct 函数。

函数实现为 100.0 * (chosen - true_best) / chosen,以 chosen 作为分母。但数学文档(dynamic_filter_math_and_components.md 第 71 行)明确定义 regret = (d(i*) − min d) / min d × 100%,即应以 true_best 为分母。

影响:使用 chosen 作为分母会系统性地低估 regret 值(因为 chosen ≥ true_best,所以 chosen 作为分母时比值更小)。虽然当前测试阈值是基于该公式校准的(测试能通过),但如果将来有人按文档定义"修正"公式,测试阈值将需要重新校准。这会造成误导。

建议:将分母从 chosen 改为 true_best 以匹配文档定义,或添加注释说明此非标准公式是有意为之。

改动建议
148
+ def _regret_pct(chosen, true_best):
148
- return 100.0 * (chosen - true_best) / chosen
149
+ return 100.0 * (chosen - true_best) / true_best
应用建议
likedislike
yanf81
7月6日 评论:
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+ @parametrize("d", [2, 3])
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+ @parametrize("D,n,expect_path", [
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+ @parametrize("d", [2, 3])
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+ @parametrize("n,d,expect_class", [
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+ @parametrize("d", [2, 3])
235+ @parametrize("difficulty", ["easy", "medium", "worst"])
236+ @parametrize("n", [60, 130, 300])
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+ @parametrize("d", [2, 3])
261+ @parametrize("difficulty,regret_bound", [
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+ @parametrize("d", [2, 3])
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)
atomgit-bot
atomgit-botatomgit-bot6月23日

🔵 Low Priority

变更行: test_dynamic_filter.py 第 310-318 行,test_refine_rejects_negative_and_nan 测试。

测试名称声明同时验证"negative"和"nan"两种非法输入被拒绝,但测试体(第 315-318 行)仅构造了包含 -1.0 的 batch,只覆盖了 refine()fd >= 0.0 的负值检查路径(dynamic_filter_if.py 第 366-367 行),未覆盖 NaN 检查路径(dynamic_filter_if.py 第 364-365 行 fd == fd 对 NaN 为 False)。

影响:NaN 输入的防御代码缺少测试覆盖,如果将来有人修改了 NaN 检查逻辑,该回归不会被捕获。测试名与实现不匹配,也造成代码审查时的困惑。

建议:添加 NaN 测试用例,或重命名测试以准确反映所覆盖的行为。

likedislike
yanf81
7月6日 评论:
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()
Mtest/npu/test_public_bindings.py+8-0
@@ -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 }
atomgit-bot
atomgit-botatomgit-bot6月23日

🔵 Low Priority

test_public_bindings.py 中新增的模块排除列表包含了 "torch_npu._inductor.dvm.util"。但 diff 中添加的 dynamic_filter_config.py 模块不在排除列表中(该模块仅通过 from dataclasses import dataclass, fieldos 导入,不依赖原生扩展,因此可以在 binding 检查中存活)。然而,dynamic_filter_config.py 中 line 16-18 的条件逻辑 if os.getenv(...) == "1": fasta_dynamic_filter = _fasta_dynamic_filter() else: fasta_dynamic_filter = None 在模块级别执行,当 FASTA_DYNAMIC_FILTER 环境变量未设置或不为 "1" 时,fasta_dynamic_filterNone。公共 binding 检查会导入这些模块并检查顶层符号。fasta_dynamic_filter = None 不应导致问题。

无需要报告的文档安全问题——三份文档(config_optimizer_params.md, dynamic_filter_algo_params.md, dynamic_filter_math_and_components.md)均为纯参数说明/算法数学推导,不含危险命令、不安全配置、可疑依赖或误导性测试指令。

建议:无需修改。新模块的排除列表正确,文档无安全问题。此为补充说明。

likedislike
yanf81
7月6日 评论:
711 719 
712 # No new entries should be added to this list.720 # No new entries should be added to this list.
Mtorch_npu/_inductor/codegen/tile_generator.py+5-11
@@ -1,8 +1,7 @@
1import copy1import copy
2import functools2import functools
3-import math
4-import os
5import sys3import sys
4+ 
6from torch._inductor.runtime.runtime_utils import next_power_of_25from torch._inductor.runtime.runtime_utils import next_power_of_2
7from torch._inductor.runtime.triton_heuristics import Config6from torch._inductor.runtime.triton_heuristics import Config
8 7 
@@ -65,7 +64,7 @@ class TileGenerator:
65 continue64 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 continue66 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 continue82 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 continue84 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_splits97 last_blocks = (self.numels[axis] + last_splits - 1) // last_splits
99 return last_blocks98 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_threshold101 max_numel = self.max_numel_threshold
106 return total_numel <= max_numel102 return total_numel <= max_numel
@@ -327,7 +323,6 @@ class TileGenerator:
327 323 
328 return reached_stop_numel324 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_type393 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 True402 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 tiling433 # 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 preferrable434+ # 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 (
Atorch_npu/_inductor/docs/feature/autotuning_optimization/config_optimizer_params.md+84-0
@@ -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`).
atomgit-bot
atomgit-botatomgit-bot6月23日

🔵 Low Priority

变更行: config_optimizer_params.md 第 5 行。

文档声明 "All FASTA_ parameters are environment-overridable using the FASTA_<NAME>=value syntax",暗示 MAX_CIRCLE_NUMMIN_SUB_NUMELMAX_CONFIGS 三个参数也可以通过环境变量覆盖。但实际代码(config_optimizer.py 第 20-22 行)将它们定义为模块级硬编码常量,完全不读取环境变量。测试文件(test_config_optimizer.py 第 19-21 行)的注释也明确指出 "The pruning bounds ... are private constants in config_optimizer and cannot be set by the user."

影响:用户阅读文档后可能尝试设置 FASTA_MAX_CIRCLE_NUM=8 等环境变量,期望生效但实际无效,造成困惑和错误的性能调优。

建议:修改文档第 5 行的注释,明确指出 MAX_CIRCLE_NUM/MIN_SUB_NUMEL/MAX_CONFIGS 为硬编码常量,不可通过环境变量覆盖。

likedislike
yanf81
7月6日 评论:
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.
Atorch_npu/_inductor/docs/feature/autotuning_optimization/dynamic_filter_algo_params.md+254-0
@@ -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.
atomgit-bot
atomgit-botatomgit-bot6月23日

🔵 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)

即先按 total_budget // max_rounds 计算,再由 remaining_budget 钳位。文档公式仅针对 "high" budget class 且忽略了 remaining_budget 钳位的作用。虽然在实际默认参数下最终结果等价(因为 remaining_budget 钳位总是更紧),但文档的推导过程不准确,且无法推广到 max_rounds=1 或自定义 budget 配置的场景。

影响:试图理解预算分配逻辑的读者可能被误导,以为算法直接使用 (high_budget - r1_pct) / max_rounds 作为 batch size。

建议:修正 R2 batch size 公式描述,说明实际代码使用 total_budget // max_rounds 并由 remaining_budget 钳位,而非简单使用 (high_budget - r1_pct) / max_rounds

likedislike
yanf81
7月6日 评论:
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+---
Atorch_npu/_inductor/docs/feature/autotuning_optimization/dynamic_filter_math_and_components.md+1257-0
@@ -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 | — |
atomgit-bot
atomgit-botatomgit-bot6月23日

🔵 Low Priority

变更文件: dynamic_filter_math_and_components.md §17 参数汇总表(第 961-983 行)。

数学文档中列出的多个参数默认值与生产代码实际默认值不一致:

参数 数学文档值 生产代码默认值(config.py + algo.py)
FASTA_R1_PCT 0.15 0.3
FASTA_HIGH_BUDGET 0.50 0.4
FASTA_MAX_ROUNDS 5 2
MARGIN_CONV 2.5 2.0

此外,§15(第 862 行)也使用了 FASTA_MAX_ROUNDS=5。

影响:数学文档作为算法的权威参考,如果参数值与实际运行的不一致,会导致:

  1. 用户按文档调优参数时,基于错误的前提假设
  2. 文档中的性能分析(如 "R2 budget = FASTA_HIGH_BUDGET - FASTA_R1_PCT = 0.5 - 0.15 = 0.35")与实际行为不符
  3. 算法行为复现和调试困难

建议:更新数学文档 §17 及 §15 中的参数值,使其与 dynamic_filter_config.py 和 dynamic_filter_algo.py 中的实际默认值一致,或在文档开头声明这些是实验值。

likedislike
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.
Atorch_npu/_inductor/experimental/dynamic_filter/autotune_stats.py+129-0
@@ -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)
Atorch_npu/_inductor/experimental/dynamic_filter/config_optimizer.py+172-0
@@ -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
Atorch_npu/_inductor/experimental/dynamic_filter/dynamic_filter_algo.py+479-0
@@ -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
Atorch_npu/_inductor/experimental/dynamic_filter/dynamic_filter_config.py+27-0
@@ -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+@dataclass
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"
Atorch_npu/_inductor/experimental/dynamic_filter/dynamic_filter_if.py+650-0
@@ -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+ @property
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+ @property
577+ def overhead_ms(self):
578+ return self._overhead_ns / 1e6
579+ 
580+ @property
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))
Atorch_npu/_inductor/experimental/dynamic_filter/dynamic_filter_scheduler.py+286-0
@@ -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
Ddezheng8897月14日

during the new autotune time, do we need stream as the paramter?

likedislike
yanf81
7月14日 评论:
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))
atomgit-bot
atomgit-botatomgit-bot6月23日

🔵 Low Priority

_build_minimal_kernel_src 在 line 75 使用 next(n for n in tree.body if isinstance(n, ast.FunctionDef)) 获取第一个函数定义。若源文件中没有任何函数定义(例如纯导入文件或格式异常的 kernel 源),next() 将抛出 StopIteration,而非携带有意义错误信息的异常。虽在生产路径中 kernel 源必然包含至少一个函数,但添加守卫可以避免模糊的 stop-iteration 错误妨碍调试。

建议:添加带有明确错误信息的守卫:若不存在 FunctionDef 节点则抛出 ValueError("no function definition found in kernel source")

likedislike
yanf81
7月6日 评论:
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
Ddezheng8897月14日

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.

likedislike
yanf81
7月14日 评论:
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"
Mtorch_npu/_inductor/fasta_autotune.py+275-29
@@ -24,10 +24,18 @@ from torch._inductor import config
24import torch_npu24import torch_npu
25from .codegen.tile_generator import TileGenerator25from .codegen.tile_generator import TileGenerator
26from .config import log26from .config import log
27-from .runtime.triton_heuristics import NPUCachingAutotuner
28from . import config as npu_config27from . import config as npu_config
29from .codegen.triton_utils import get_byte_per_numel, NPUKernelType28from .codegen.triton_utils import get_byte_per_numel, NPUKernelType
30from .profiler import simple_trace_handler29from .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 
33def fast_a_log_message(content, tag='autotuner', level='debug'):41def fast_a_log_message(content, tag='autotuner', level='debug'):
@@ -130,6 +138,7 @@ class FastASetting:
130 138 
131 139 
132FASTA_SETTING = FastASetting()140FASTA_SETTING = FastASetting()
141+FASTA_EVENT_BENCHMARK = os.environ.get("FASTA_EVENT_BENCHMARK", "0") == "1"
133 142 
134 143 
135def get_ub_size():144def 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 
169class TileConfig:178class TileConfig:
170 def __init__(self):179 def __init__(self):
@@ -920,9 +929,14 @@ class NPUFastAutotuner(NPUCachingAutotuner):
920 self.skip_precompile = False929 self.skip_precompile = False
921 return930 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 = False937 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'] = False959+ if 'multibuffer' in c.kwargs:
946- config_copied = copy.deepcopy(self_config)960+ expanded.append(c)
947- config_copied.kwargs['multibuffer'] = True961+ 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 return1027 return
1000 1028 
1029+ if stats.enabled:
1030+ self.cache_hit = False
1001 self.skip_precompile = False1031 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_time1033 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_configs1347 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 = None1363 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)
atomgit-bot
atomgit-botatomgit-bot6月23日

🔵 Low Priority

profiling_and_get_best_config 中(line 1365),self._last_launchers = list(self.launchers) 被赋值,但全项目搜索显示 _last_launchers 从未被读取(无任何引用)。该属性赋值是死代码,可能属于遗留未清理代码或被遗忘的调试赋值。

建议:移除 self._last_launchers = list(self.launchers) 这一行死代码。

likedislike
yanf81
7月6日 评论:
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_profile1374 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 break1400 break
1358 1401 
1359 self.configs = need_compile_configs1402 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 False1488 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_std1593+ 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()
atomgit-bot
atomgit-botatomgit-bot6月23日

🔵 Low Priority

do_batch_benchmark_mspti 分配了 NPU 显存 buffer(line 1621)但函数结尾从未调用 del buffer 释放。对比同文件中 do_batch_benchmark_kernel_isolate 在 line 1568 显式 del buffer。虽然 Python GC 最终会回收,但 NPU 设备内存的显式释放对于长时间运行的 autotune 工作流有助于减少峰值内存压力并避免 OOM。

此外,如果 torch.empty(...) 在 line 1621 抛出异常(NPU OOM 等情况),line 1635 的 monitor.stop() 永远不会执行,导致 mspti 监控器资源泄漏。建议使用 try/finally 确保 monitor.stop() 在任何情况下都被调用。

建议:添加 del buffer 在 monitor.stop() 之后;用 try/finally 包裹 monitor.start/stop 以确保异常安全释放 mspti 资源。

likedislike
yanf81
7月6日 评论:
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_isolate1676+ 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)
atomgit-bot
atomgit-botatomgit-bot6月23日

🟠 High Priority

benchmark_all_configs_with_std 的重测(re-profiling)代码路径中(line 1781),this_benchmark_fun 的返回值被解包为 3 个变量: 但本轮 diff 已将 do_batch_benchmark_kernel_isolatedo_batch_benchmark_mspti 的返回值从 3 个扩展到 4 个(增加了 exp_timing)。主调用路径(line 1696)正确地使用索引访问 result[0]/result[1]/result[2]/result[3],但重测路径仍使用旧的三元组解包。当任一内核触发重测逻辑时(即存在性能接近的候选配置时),将抛出 ValueError: too many values to unpack,导致 autotuning 崩溃。

触发条件:need_test_index 非空(即存在与最优配置性能相近的候选配置需要重测)且使用非事件式 benchmark 路径。

建议:改为4变量解包,或使用索引访问(与主路径一致),并忽略 exp_timing(重测路径不需要它):

改动建议
1782
+ re_result = this_benchmark_fun(re_kernel_list,
1782
- this_active_num=profiler_active_num)
1783
+ this_active_num=profiler_active_num)
1784
+ re_timing_list = re_result[0] if isinstance(re_result, tuple) and len(re_result) >= 4 else re_result[0]
1785
+ re_timing_sem_list = re_result[1] if isinstance(re_result, tuple) and len(re_result) >= 4 else re_result[1]
1786
+ re_timing_std_list = re_result[2] if isinstance(re_result, tuple) and len(re_result) >= 4 else re_result[2]
应用建议
likedislike
yanf81
7月6日 评论:
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_infos1803 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 @staticmethod1871 @staticmethod
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 with1874 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.
Mtorch_npu/_inductor/runtime/triton_heuristics.py+133-14
@@ -78,7 +78,6 @@ from torch._inductor.runtime.triton_heuristics import ( # noqa: F401
78from torch._inductor.runtime.runtime_utils import triton_hash_to_path_key78from torch._inductor.runtime.runtime_utils import triton_hash_to_path_key
79from triton.compiler import CompiledKernel79from triton.compiler import CompiledKernel
80from torch._inductor.triton_bundler import TritonBundler80from torch._inductor.triton_bundler import TritonBundler
81- 
82try:81try:
83 from triton.backends.compiler import GPUTarget82 from triton.backends.compiler import GPUTarget
84 from triton.runtime.autotuner import OutOfResources83 from triton.runtime.autotuner import OutOfResources
@@ -97,6 +96,8 @@ from ..codegen.triton_utils import NPUKernelType
97from ..config import log, autotune_continue_on_failure96from ..config import log, autotune_continue_on_failure
98from .. import config as npu_config97from .. import config as npu_config
99from ..profiler import simple_trace_handler, mspti_batch_benchmark98from ..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 
101kernel_idx = count()102kernel_idx = count()
102 103 
@@ -126,6 +127,11 @@ class CompileThreadPool:
126compile_thread_pool = CompileThreadPool()127compile_thread_pool = CompileThreadPool()
127 128 
128 129 
130+stats = AutotuneStatsManager(
131+ enabled=fasta_autotune_stats, log=log
132+)
133+ 
134+ 
129@contextmanager135@contextmanager
130def create_profiler(torch_path, wait=0, warmup=1, active=1, repeat=1, skip_first=1):136def 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_index1496 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_time1717 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"]: candidate1737 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, **kwargs1823 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_blocks1832 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':
atomgit-bot
atomgit-botatomgit-bot6月23日

🔵 Low Priority

triton_heuristics.py line 1790: 这里使用了字符串成员运算符 not in 而不是相等比较 !=CompilationPhase.DONE.name 的值是字符串 'DONE',当 phase.name == 'DONE' 时,'DONE' in 'DONE' 为 True,not in 条件为 False;当 phase.name'R1''R2' 时,'DONE' in 'R1' 为 False,条件为 True。目前三个阶段名称恰好与检查兼容,但这完全依赖巧合——若未来有人重命名阶段(如 'COMPLETE'),或添加名称中含有 D/O/N/E 任一字符的阶段,该检查将意外失败或意外通过,导致 dynamic filter 被错误地跳过或不正确地重复执行。

属于误导性代码,具有潜在回归风险。

建议:改用相等比较 self.dynamic_filter_scheduler.phase != CompilationPhase.DONE,明确表达"非 DONE 阶段"的意图。

改动建议
1854
+ if getattr(self, 'dynamic_filter', False) and \
1854
- self.dynamic_filter_scheduler.phase.name not in 'DONE':
1855
+ self.dynamic_filter_scheduler.phase != CompilationPhase.DONE:
应用建议
likedislike
yanf81
7月6日 评论:
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_blocks1859 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 NPUFastAutotuner2596+ 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 configs2663 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 FastATileGenerator3075+ 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_value3132 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 configs3138 # 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 configs3161 return configs
3083 3162 
3084 3163 
@@ -3262,13 +3341,17 @@ def _benchmark_all_configs(self, *args, **kwargs):
3262def precompile_parallel(3341def 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_kernel3347 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 return3355 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 return3364 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 return3374 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+ )