dsh-fast-mode 1.5.25 → 1.5.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/BENCHMARK.md CHANGED
@@ -2299,6 +2299,35 @@ python3 bench/block_score.py LEN3 def15 nodisc
2299
2299
  - 所以**纪律提醒的独立份额目前不可判定**,不能对外报 1.54× / 1.79× 这类数字。能报的只有池化主效应(138 对 0.823×),其中「明细律」与其它提醒是同一个机制包里一起生效的,无法在单块里分离。
2300
2300
  - 这条给「不要再加提醒时机、不要再加重复提醒」提供了同样甚至更强的支持:连唯一那条提醒的份额都测不出来,继续加同类触发点没有可验证的收益。
2301
2301
 
2302
+ ## 多个开关一起开:默认之上再叠任何一个都是 0(59 个块、183 对池化的消融表)
2303
+
2304
+ 单块的 ±20 % 噪声(见上一节 LEN3)意味着「一次只测一个开关、7 对下结论」必然出假信号。已经跑过的所有块加起来其实足够回答另一个问题:**在已发布的默认配置之上,再打开某个开关,还能不能更快?** 新增 `bench/ablate.py` 把所有块的全部臂按「同块、同任务、同 pair」对上锚点臂池化,锚点优先 `off`、缺了回落到 `def15`:
2305
+
2306
+ ```
2307
+ python3 bench/ablate.py --ci 2000 # 相对 off
2308
+ python3 bench/ablate.py --ci 2000 --anchor def15 # 相对默认配置(叠开关的边际值)
2309
+ ```
2310
+
2311
+ 以 `def15` 为锚(59 个块;几何比 / 95 % bootstrap CI / 快过锚点的对数 / 符号检验 p):
2312
+
2313
+ | 臂 = 默认 + 打开 | n | wall 几何比 | CI95 | 中位 | 步数 | 输出 | 更快 | p |
2314
+ |---|---|---|---|---|---|---|---|---|
2315
+ | `d2`:明细律提前到第 2 步 | 41 | **0.695** | [0.565, 0.863] | 0.700 | 0.960 | 0.723 | 27/41 | 0.060 |
2316
+ | `d2g`:同上 + 第 1 步 ≥2 次调用才提前 | 21 | **0.756** | [0.605, 0.939] | 0.719 | 0.885 | 0.704 | 14/21 | 0.189 |
2317
+ | `map15` +`workspaceMap` | 23 | 0.802 | [0.648, 0.999] | 0.790 | 1.048 | 0.824 | 13/23 | 0.678 |
2318
+ | `open15` +`openBatch` | 53 | 0.882 | [0.740, 1.049] | 0.911 | 0.831 | 0.915 | 30/53 | 0.410 |
2319
+ | `bv15` +`batchVerify` | 39 | 0.963 | [0.813, 1.144] | 0.968 | 0.935 | 0.961 | 20/39 | 1.000 |
2320
+ | `bud8` +`stepBudget:8` | 26 | 0.971 | [0.783, 1.200] | 1.110 | 0.990 | 0.973 | 11/26 | 0.557 |
2321
+ | `pp16` +`preferPatch` | 34 | 0.988 | [0.803, 1.216] | 1.012 | 0.911 | 0.995 | 17/34 | 1.000 |
2322
+ | `wide15` 并行宽度拉大 | 20 | 1.075 | [0.796, 1.491] | 0.991 | 1.262 | 1.076 | 10/20 | 1.000 |
2323
+ | `nodisc` 关掉明细律 | 19 | 1.101 | [0.808, 1.492] | 1.110 | 0.998 | 1.049 | 8/19 | 0.648 |
2324
+
2325
+ - **结论 1:默认配置已经在平台上。** 除时点类两臂外,每个可选开关的 CI 都跨 1,几何比落在 0.80–1.08;把 `openBatch`、`workspaceMap`、`batchVerify`、`preferPatch`、更宽并行、步数预算一起打开,一个百分点都拿不到。所以「多开几个开关会更快」是错的——它们的收益在默认里已经兑现,或者从来就不存在。
2326
+ - **结论 2:唯一在池化下站得住的杠杆是「同一句提醒的时点」。** `d2` 41 对 0.695×(CI 上限 0.863 仍 < 1),但它是任务相关的:分族 `debugfix` 0.619、`ledger` 0.506、`migrate` 0.569,`codeimpl` **1.073**、`shellpipeline` **1.111**。加条件闸门后 `d2g` 把 `codeimpl` 收到 0.889,同时把 `debugfix` 的 0.619 抬到 0.903 ⇒ 池化 0.756×(21 对)——**净收益变小的同时方差也变小**。
2327
+ - **结论 3:这两臂仍是 HOLD,不改默认。** `d2` 在 `codeimpl` 上是负收益,`d2g` 的闸门常量为 2、只在 3 个家族验过;按预注册判据(`D2_IMPL` 1.073×、3/8 更快)默认保持 `disciplineAfterSteps: 4`。这张表的作用是说明**没有别的开关可以在默认之上继续叠加**,以及下一版要做的是把 `d2g` 的闸门在 `ledger`/`shellpipeline` 上补测,而不是再加开关。
2328
+
2329
+ **这张表的两个诚实边界**:不同臂覆盖的任务组合不同(跑一遍命令就能看到 `tasks` 列,`d2` 含 `codeimpl:8`、`map15` 只有 `codebase/debugfix/perfrefactor`),所以行与行之间不能直接排大小;以及每个块只采样一次,`n < 20` 的行只算方向提示。
2330
+
2302
2331
  ## 同一配置再跑一遍会差多少:块级差 4 %,任务级差 15–31 %(REP2,10 对)
2303
2332
 
2304
2333
  外部评审提的第一条是稳定性。这一块不新增任何开关,只把 `migrate`+`debugfix` 的同一批格子**用同一套配置再跑一遍**(`REP2`,20 次运行,顺序 ABBA 与之前的块一致),看同配置的两次独立测量互相差多少:
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.5.26(把「多开几个开关会不会更快」变成一张可复算的表)
4
+
5
+ - **新增 `bench/ablate.py`**:把 59 个块的全部臂按「同块、同任务、同 pair」对锚点臂池化,输出每个开关相对默认配置(`--anchor def15`)或相对 `off` 的几何比、95 % bootstrap CI、中位、步数/输出比、更快对数与符号检验。这是第一次用一张表回答「多个开关一起开的效果」。
6
+ - **结论:默认之上再叠任何开关都拿不到收益。** `openBatch` 0.882 [0.740, 1.049]、`batchVerify` 0.963、`stepBudget:8` 0.971、`preferPatch` 0.988、更宽并行 1.075、`workspaceMap` 0.802 [0.648, 0.999]、关掉明细律 1.101 —— 每个 CI 都跨 1。收益已经在默认配置里兑现,或者从来就不存在。
7
+ - **唯一站得住的杠杆仍是「同一句提醒的时点」**:`d2`(`disciplineAfterSteps: 2`)41 对 **0.695×** [0.565, 0.863],但分族方向相反(`ledger` 0.506 / `debugfix` 0.619 / `migrate` 0.569,而 `codeimpl` 1.073、`shellpipeline` 1.111);带闸门的 `d2g` 把 `codeimpl` 收到 0.889、同时削掉 `debugfix` 的收益 ⇒ 池化 0.756× [0.605, 0.939]。两臂按预注册判据仍是 HOLD,默认不放宽。
8
+ - **没有行为变更**:只加一个只读分析脚本与文档;默认值、提醒时机、提示词与 1.5.25 完全一致。
9
+
3
10
  ## 1.5.25(收回一个测不出来的结论;两个分析脚本补上推理字符)
4
11
 
5
12
  - **撤回「纪律提醒 1.54×」这个数字**:`LEN3` 把 `LEN_DBG` 那一块(debugfix、7 对、`def15` vs `nodisc`,两臂只差 `discipline:'full' → 'off'`)同题同配再跑一遍,几何比从 **1.790** 变成 **0.648**——方向相反,符号检验两遍都不显著(p = 0.125 / 0.453),块级比值由一两个离群单元格决定(一遍在 `nodisc`,一遍在 `def15`,单格 165 s / 196 s)。所以纪律提醒的**独立份额目前不可判定**,对外只能报池化主效应 0.823×(138 对)。
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env python3
2
+ """Pool every block and report each arm's marginal cost against its own block's anchor.
3
+
4
+ `pool.py` answers one question ("how does arm X compare with off, on the clean
5
+ blocks"). This script answers the question a user actually asks when they look at
6
+ the switch list: *what does each switch do on its own, and what happens when
7
+ several are on at once?*
8
+
9
+ Every suite block is an independent experiment with its own anchor arm, and the
10
+ arm names are the switch sets. So for each block, pick an anchor (`off` when the
11
+ block has it, otherwise `def15`), then pair each other arm with that anchor on
12
+ `(tag, task, pair)`. Ratios from different blocks are pooled because they are all
13
+ "arm ÷ anchor of the same block, same task, same pair" — the block, the clock and
14
+ the task family cancel out inside each pair.
15
+
16
+ Two honest caveats, printed with the table:
17
+
18
+ * arms are not tested on the same task mix; the `tasks` column is the whole
19
+ reason a row can look good (an arm measured only on `ledger` is not a
20
+ statement about `migrate`);
21
+ * a block is one sampling. `REP2` and `LEN3` showed block-level ratios swing
22
+ 0.65x-1.79x in *both* directions, so anything with n below ~20 pairs is a
23
+ direction hint, not a number. The sign test p-value is printed for that
24
+ reason and nothing here is significant without it.
25
+
26
+ python3 bench/ablate.py [--anchor off] [--min-pairs 1] [--json]
27
+ """
28
+ import argparse
29
+ import glob
30
+ import json
31
+ import math
32
+ import pathlib
33
+ import statistics
34
+ from collections import defaultdict
35
+
36
+ HERE = pathlib.Path(__file__).resolve().parent
37
+ RESULTS = HERE / 'results'
38
+
39
+ # Blocks whose fixtures leaked; their `llm` column is an artefact (see pool.py).
40
+ RETRACTED = ('V15', 'TA3', 'TA4')
41
+
42
+
43
+ def valid(row):
44
+ return (row.get('exit') == 0 and row.get('steps', 0) >= 1
45
+ and row.get('out', 0) > 0
46
+ and row.get('hasSessionIds', row.get('exact'))
47
+ and not row.get('leak'))
48
+
49
+
50
+ def load_all():
51
+ """tag -> (task, pair) -> {arm: row}, last write wins like the report."""
52
+ blocks = defaultdict(lambda: defaultdict(dict))
53
+ for path in sorted(glob.glob(str(RESULTS / 'suite-*.jsonl'))):
54
+ tag = path.split('suite-')[1][:-len('.jsonl')]
55
+ if tag in RETRACTED:
56
+ continue
57
+ for line in open(path, encoding='utf-8'):
58
+ line = line.strip()
59
+ if not line:
60
+ continue
61
+ row = json.loads(line)
62
+ blocks[tag][(row['task'], row['pair'])][row['arm']] = row
63
+ return blocks
64
+
65
+
66
+ def anchor_for(arms, wanted):
67
+ if wanted in arms:
68
+ return wanted
69
+ if 'off' in arms:
70
+ return 'off'
71
+ if 'def15' in arms:
72
+ return 'def15'
73
+ return None
74
+
75
+
76
+ def sign_p(k, n):
77
+ from math import comb
78
+ if n == 0:
79
+ return None
80
+ tail = sum(comb(n, i) for i in range(0, min(k, n - k) + 1)) / 2 ** n
81
+ return round(min(1.0, 2 * tail), 4)
82
+
83
+
84
+ def main():
85
+ ap = argparse.ArgumentParser()
86
+ ap.add_argument('--anchor', default='off', help="preferred anchor arm name")
87
+ ap.add_argument('--min-pairs', type=int, default=1)
88
+ ap.add_argument('--ci', type=int, default=0,
89
+ help='bootstrap resamples for a 95%% CI on the geometric mean')
90
+ ap.add_argument('--json', action='store_true')
91
+ args = ap.parse_args()
92
+
93
+ blocks = load_all()
94
+ per_arm = defaultdict(lambda: {'ratios': [], 'steps': [], 'outs': [],
95
+ 'tasks': defaultdict(int), 'tags': defaultdict(int),
96
+ 'anchors': defaultdict(int), 'walls': 0.0, 'anchor_walls': 0.0})
97
+ for tag, cells in blocks.items():
98
+ for (task, pair), arms in cells.items():
99
+ anchor = anchor_for(arms, args.anchor)
100
+ if not anchor:
101
+ continue
102
+ base = arms[anchor]
103
+ if not valid(base):
104
+ continue
105
+ for name, row in arms.items():
106
+ if name == anchor or not valid(row):
107
+ continue
108
+ a = per_arm[name]
109
+ a['ratios'].append(row['wall'] / base['wall'])
110
+ a['steps'].append(row['steps'] / base['steps'] if base['steps'] else 1.0)
111
+ a['outs'].append(row['out'] / base['out'] if base['out'] else 1.0)
112
+ a['tasks'][task] += 1
113
+ a['tags'][tag] += 1
114
+ a['anchors'][anchor] += 1
115
+ a['walls'] += row['wall']
116
+ a['anchor_walls'] += base['wall']
117
+
118
+ rows = []
119
+ for name, a in per_arm.items():
120
+ n = len(a['ratios'])
121
+ if n < args.min_pairs:
122
+ continue
123
+ geo = math.exp(sum(math.log(r) for r in a['ratios']) / n)
124
+ ratio = a['walls'] / a['anchor_walls']
125
+ wins = sum(1 for r in a['ratios'] if r < 1)
126
+ ci = None
127
+ if args.ci > 0 and n >= 3:
128
+ import random
129
+ rnd = random.Random(20261029)
130
+ draws = []
131
+ for _ in range(args.ci):
132
+ sample = [a['ratios'][rnd.randrange(n)] for _ in range(n)]
133
+ draws.append(math.exp(sum(math.log(r) for r in sample) / n))
134
+ draws.sort()
135
+ lo, hi = int(0.025 * len(draws)), int(0.975 * len(draws)) - 1
136
+ ci = [round(draws[lo], 4), round(draws[hi], 4)]
137
+ rows.append({
138
+ 'arm': name,
139
+ 'pairs': n,
140
+ 'wall_geo': round(geo, 4),
141
+ 'wall_total': round(ratio, 4),
142
+ 'wall_median': round(statistics.median(a['ratios']), 4),
143
+ 'steps_geo': round(math.exp(sum(math.log(s) for s in a['steps']) / n), 4),
144
+ 'out_geo': round(math.exp(sum(math.log(o) for o in a['outs']) / n), 4),
145
+ 'faster': wins,
146
+ 'sign_p': sign_p(wins, n),
147
+ 'geo_ci95': ci,
148
+ 'tasks': dict(sorted(a['tasks'].items(), key=lambda kv: -kv[1])),
149
+ 'blocks': len(a['tags']),
150
+ 'anchors': dict(a['anchors']),
151
+ })
152
+ rows.sort(key=lambda r: r['wall_geo'])
153
+
154
+ if args.json:
155
+ print(json.dumps(rows, indent=2, ensure_ascii=False))
156
+ return
157
+
158
+ print(f"anchor={args.anchor} (falls back to off/def15 per block), blocks={len(blocks)}")
159
+ print(f"{'arm':10s} {'n':>4s} {'geo (ci95)':>22s} {'median':>7s} {'total':>7s} "
160
+ f"{'steps':>6s} {'out':>6s} {'faster':>9s} {'p':>6s} {'blk':>4s} tasks")
161
+ for r in rows:
162
+ tasks = ', '.join(f'{k}:{v}' for k, v in list(r['tasks'].items())[:3])
163
+ ci = f" [{r['geo_ci95'][0]:.3f},{r['geo_ci95'][1]:.3f}]" if r['geo_ci95'] else ''
164
+ print(f"{r['arm']:10s} {r['pairs']:4d} {r['wall_geo']:7.3f}{ci:<15s} {r['wall_median']:7.3f} "
165
+ f"{r['wall_total']:7.3f} {r['steps_geo']:6.3f} {r['out_geo']:6.3f} "
166
+ f"{r['faster']:4d}/{r['pairs']:<4d} {str(r['sign_p']):>6s} {r['blocks']:4d} {tasks}")
167
+ print('\nRead this as a direction table, not as per-switch numbers: rows are '
168
+ 'measured on different task mixes and each block is one sampling.')
169
+
170
+
171
+ if __name__ == '__main__':
172
+ main()
package/docs/REFERENCE.md CHANGED
@@ -94,7 +94,7 @@
94
94
  - **扇出目标里的数字就是调度器算出的宽度。** `auto` 下提示段和看门狗共用同一个 `autoSchedule` 结果,所以模型读到的数字和被执行的数字不会漂移。
95
95
  - **预取只在宽度 ≥ 4 时出现。** 宽度不够时多取一个调用只会挤占别的工作。
96
96
  - **有界调用那一段只在 `stallGuard !== false` 时出现**,因为它描述的规则和看门狗是一套东西。
97
- - **输出纪律有两档**:`light` 给旧档位 `fast` 用,`full` 给 `auto`/`turbo`/`max`/`ludicrous` 用。两档要求模型不预告调用、不复述结果、不写进度报告;最后一步的答案仍然要完整写出。`full` 只多一句"不需要文字时只发调用"。**它不会让答案变短,更不会让模型跳过检查。** 口径要说清:可见正文只占「正文+工具参数」字符的 1.19 %(def15 全量 385 会话;不含 reasoning 与工具返回;`python3 bench/textshare.py --limit 0` 可复算),所以它的价值不在"删掉的字"本身,而在它连带把每步的推理压短(池化 168 对里 `reasoning` 字符 0.75×、`emitted` 0.82×)。把这一项单独关掉(`bench/arms/nodisc.yml`)的份额目前**测不出来**:同一块(debugfix 7 对)跑两遍,`nodisc/def15` 一次 1.79×、一次 0.648×,方向相反、符号检验都不显著,块级几何比由一两个离群单元格决定(见 `BENCHMARK.md` 的 LEN3 一节)。能报的只有池化主效应 0.823×,其中明细律与其它提醒在一个机制包里一起生效,无法在单块里分离;同样不能由此宣称"工作内容不受影响"。
97
+ - **输出纪律有两档**:`light` 给旧档位 `fast` 用,`full` 给 `auto`/`turbo`/`max`/`ludicrous` 用。两档要求模型不预告调用、不复述结果、不写进度报告;最后一步的答案仍然要完整写出。`full` 只多一句"不需要文字时只发调用"。**它不会让答案变短,更不会让模型跳过检查。** 口径要说清:可见正文只占「正文+工具参数」字符的 1.19 %(def15 全量 385 会话;不含 reasoning 与工具返回;`python3 bench/textshare.py --limit 0` 可复算),所以它的价值不在"删掉的字"本身,而在它连带把每步的推理压短(池化 168 对里 `reasoning` 字符 0.75×、`emitted` 0.82×)。把这一项单独关掉(`bench/arms/nodisc.yml`)的份额目前**测不出来**:同一块(debugfix 7 对)跑两遍,`nodisc/def15` 一次 1.79×、一次 0.648×,方向相反、符号检验都不显著,块级几何比由一两个离群单元格决定(见 `BENCHMARK.md` 的 LEN3 一节)。能报的只有池化主效应 0.823×,其中明细律与其它提醒在一个机制包里一起生效,无法在单块里分离;同样不能由此宣称"工作内容不受影响"。要判断「在默认之上再打开某个开关还有没有用」,用 `python3 bench/ablate.py --ci 2000 --anchor def15`:它把全部块按同块、同任务、同 pair 池化成每个开关相对默认的几何比与 95 % CI(当前读数:所有可选开关的 CI 都跨 1,叠上去拿不到收益)。
98
98
  - **`parallel: 1` 是"开着但不要求扇出"的正式写法**:开关仍然生效于看门狗,提示段整体消失。
99
99
 
100
100
  成本是明码标价的:`auto` 下这一段约 2.7 千字符,每个请求都要付这部分输入 token。这是有意的交换——用输入 token 换更少的往返。
package/index.js CHANGED
@@ -97,7 +97,7 @@ const TAG = '[fast-mode]'
97
97
  * Shipped identity of this build. Traced into the debug log so a stale install is
98
98
  * visible at a glance, and pinned by a test against `package.json`.
99
99
  */
100
- export const VERSION = '1.5.25'
100
+ export const VERSION = '1.5.26'
101
101
  const GLOBAL_AGENT = '__global__'
102
102
 
103
103
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-fast-mode",
3
- "version": "1.5.25",
3
+ "version": "1.5.26",
4
4
  "description": "DeepSeek Harness fast mode: same thinking effort, fewer round trips. An adaptive parallel tool fan-out, output discipline that never trims the answer, and a cancellable stall guard. Never changes reasoning effort, the model, or the request body. Provider-agnostic, one dependency (@deepseek-ai/schemastery).",
5
5
  "repository": {
6
6
  "type": "git",
@@ -43,6 +43,7 @@
43
43
  "bench/textshare.py",
44
44
  "bench/reminders.py",
45
45
  "bench/patchshare.py",
46
+ "bench/ablate.py",
46
47
  "docs/",
47
48
  "LICENSE"
48
49
  ],