From ef1f6b3d27e292a6b5e005411844eb8e14cbe4b2 Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Sat, 19 Sep 2026 19:20:07 +0800 Subject: infra: P0 监督加固 — m3_supervise.py flock防重入/原子状态/冻结Release全依赖+sha256清单/真实child退出码(sh -c exitfile)/PID+starttime身份/收紧PASS判据(批次参数+world行数全OK+batch_summary+verdict token+清单hash+commit绑定)/失败停机并写diagnosis报告, selftest 33项合成断言; completion_notify.py 新completion-notice.md hash仅打印一次缺失静默, selftest 7项; Headless --batch 世界级并行(LV_BATCH_WORKERS 默认modest 4, 逐世界确定性, 输出按序, world行与串行逐字节一致); .gitignore 补冻结/锁/通知状态文件 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/LivingVillage.Headless/Program.fs | 97 ++++++++++++++++++++++------------- 1 file changed, 61 insertions(+), 36 deletions(-) (limited to 'src/LivingVillage.Headless/Program.fs') diff --git a/src/LivingVillage.Headless/Program.fs b/src/LivingVillage.Headless/Program.fs index be9c325..bcf2e41 100644 --- a/src/LivingVillage.Headless/Program.fs +++ b/src/LivingVillage.Headless/Program.fs @@ -175,6 +175,48 @@ let gini (xs: int64[]) : float = num <- num + abs (float xi - float xj) num / (2.0 * n * n * (s / n)) +type WorldVerdict = + { Line: string + Ok: bool + CheckOld: bool + Ratio: float + Gini: float } + +let evalWorld (days: int64) (k: int) : WorldVerdict = + let seed = 42UL + uint64 k + let stats = runSimulation false days seed Sim.npcCount + let m = Sim.relationMatrix stats.World + let n = Array2D.length1 m + let mutable hasNonFinite = stats.NonFinite > 0L + for i in 0 .. n - 1 do + for j in 0 .. n - 1 do + if Single.IsNaN m.[i, j] || Single.IsInfinity m.[i, j] then hasNonFinite <- true + let chats = stats.ChatsPerNpc + let chatsTotal = Array.sum chats + let chatsAvg = float chatsTotal / float chats.Length + let chatsMax = if chats.Length > 0 then Array.max chats else 0L + let ratio = if chatsAvg > 0.0 then float chatsMax / chatsAvg else 0.0 + let g = gini chats + let counts = Sim.relationCounts m + let cntMax = if counts.Length > 0 then Array.max counts else 0 + let cntMin = if counts.Length > 0 then Array.min counts else 0 + let connected = Array.fold (fun acc c -> if c > 0 then acc + 1 else acc) 0 counts + let connectedNeed = int (System.Math.Ceiling(0.6 * float chats.Length)) + let checkA = ratio > 2.0 + let checkB = connected >= connectedNeed + let checkC = not hasNonFinite + let checkOld = cntMax > 2 * cntMin + let ok = checkA && checkB && checkC + let line = + sprintf "world=%d seed=%d chats_max=%d chats_avg=%.2f ratio=%.3f gini=%.3f relcnt_max=%d relcnt_min=%d relconn=%d/%d nonfinite=%d check_a=%s check_b=%s check_c=%s old_crit=%s %s" + k seed chatsMax chatsAvg ratio g cntMax cntMin connected connectedNeed stats.NonFinite + (if checkA then "PASS" else "FAIL") + (if checkB then "PASS" else "FAIL") + (if checkC then "PASS" else "FAIL") + (if checkOld then "PASS" else "FAIL") + (if ok then "OK" else "BAD") + { Line = line; Ok = ok; CheckOld = checkOld; Ratio = ratio; Gini = g } + let runBatch (worlds: int) (days: int64) : int = printfn "batch worlds=%d days=%d half_life_ticks=%d rel_threshold=%.2f" worlds days Sim.relationHalfLifeTicks Sim.relationThreshold // 判据语义(M3c 修正): @@ -184,48 +226,31 @@ let runBatch (worlds: int) (days: int64) : int = // 明星集中型分布过严,relcnt_max=0 即判死,与"非均匀分布"的验收目标不符) // (c) 无 NaN/Inf printfn "checks: (a) chats max/avg > 2.0 (b) npcs_with_relation >= 0.6*npc_count (c) no NaN (old) relcnt_max > 2*relcnt_min (report only)" + // 世界间相互独立(seed=42+k,逐世界确定性),可安全并行;输出仍按 world 序号 + // 顺序打印,逐行内容与串行版本逐字节一致。LV_BATCH_WORKERS 可覆盖(默认 modest 4)。 + let workers = + match Environment.GetEnvironmentVariable "LV_BATCH_WORKERS" with + | null | "" -> min 4 Environment.ProcessorCount + | v -> match Int32.TryParse v with true, n when n > 0 -> min n Environment.ProcessorCount | _ -> min 4 Environment.ProcessorCount + printfn "workers=%d" workers + let results = Array.zeroCreate worlds + System.Threading.Tasks.Parallel.For + (0, worlds, + System.Threading.Tasks.ParallelOptions(MaxDegreeOfParallelism = workers), + fun k -> results.[k] <- evalWorld days k) + |> ignore let mutable passed = 0 let mutable failed = 0 let mutable oldPassed = 0 let ratios = ResizeArray () let ginis = ResizeArray () let sw = System.Diagnostics.Stopwatch.StartNew() - for k in 0 .. worlds - 1 do - let seed = 42UL + uint64 k - let stats = runSimulation false days seed Sim.npcCount - let m = Sim.relationMatrix stats.World - let n = Array2D.length1 m - let mutable hasNonFinite = stats.NonFinite > 0L - for i in 0 .. n - 1 do - for j in 0 .. n - 1 do - if Single.IsNaN m.[i, j] || Single.IsInfinity m.[i, j] then hasNonFinite <- true - let chats = stats.ChatsPerNpc - let chatsTotal = Array.sum chats - let chatsAvg = float chatsTotal / float chats.Length - let chatsMax = if chats.Length > 0 then Array.max chats else 0L - let ratio = if chatsAvg > 0.0 then float chatsMax / chatsAvg else 0.0 - let g = gini chats - let counts = Sim.relationCounts m - let cntMax = if counts.Length > 0 then Array.max counts else 0 - let cntMin = if counts.Length > 0 then Array.min counts else 0 - let connected = Array.fold (fun acc c -> if c > 0 then acc + 1 else acc) 0 counts - let connectedNeed = int (System.Math.Ceiling(0.6 * float chats.Length)) - let checkA = ratio > 2.0 - let checkB = connected >= connectedNeed - let checkC = not hasNonFinite - let checkOld = cntMax > 2 * cntMin - let ok = checkA && checkB && checkC - if ok then passed <- passed + 1 else failed <- failed + 1 - if checkOld then oldPassed <- oldPassed + 1 - ratios.Add ratio - ginis.Add g - printfn "world=%d seed=%d chats_max=%d chats_avg=%.2f ratio=%.3f gini=%.3f relcnt_max=%d relcnt_min=%d relconn=%d/%d nonfinite=%d check_a=%s check_b=%s check_c=%s old_crit=%s %s" - k seed chatsMax chatsAvg ratio g cntMax cntMin connected connectedNeed stats.NonFinite - (if checkA then "PASS" else "FAIL") - (if checkB then "PASS" else "FAIL") - (if checkC then "PASS" else "FAIL") - (if checkOld then "PASS" else "FAIL") - (if ok then "OK" else "BAD") + for r in results do + printfn "%s" r.Line + if r.Ok then passed <- passed + 1 else failed <- failed + 1 + if r.CheckOld then oldPassed <- oldPassed + 1 + ratios.Add r.Ratio + ginis.Add r.Gini sw.Stop() let allPass = failed = 0 printfn "batch_summary worlds=%d passed=%d failed=%d old_passed=%d/%d ratio_min=%.3f ratio_max=%.3f gini_min=%.3f gini_max=%.3f elapsed_s=%.1f" -- cgit v1.2.3