diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-21 22:44:53 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-21 22:44:53 +0800 |
| commit | 34c7ebc9ed8b6295ec109c177f75ba0803cc04f2 (patch) | |
| tree | 53f896ed5e299756431e3471e98cf767dfe417f0 /src/LivingVillage.Headless/Program.fs | |
| parent | 7c45d027a833a5038660952873424465b25e1e55 (diff) | |
| parent | 453b74c6b7f3f49b8220aa2bb9d8a2613ba46430 (diff) | |
| download | living-village-34c7ebc9ed8b6295ec109c177f75ba0803cc04f2.tar.gz | |
merge: 采纳 M3-1 流式可观测性+门槛缩尺+成本量化(独立复验全绿)
Diffstat (limited to 'src/LivingVillage.Headless/Program.fs')
| -rw-r--r-- | src/LivingVillage.Headless/Program.fs | 93 |
1 files changed, 74 insertions, 19 deletions
diff --git a/src/LivingVillage.Headless/Program.fs b/src/LivingVillage.Headless/Program.fs index 2a2482b..89f4f26 100644 --- a/src/LivingVillage.Headless/Program.fs +++ b/src/LivingVillage.Headless/Program.fs @@ -449,38 +449,75 @@ 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)。 + // 世界间相互独立(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 + Console.Out.Flush() + let wall = System.Diagnostics.Stopwatch.StartNew() + // 逐世界流式:每个世界一完成立刻打印该行(行尾追加 elapsed_s),再打印一行 + // [done k/N] 进度,并 flush,使长批次不再整轮零进度。结果仍按 world 序号落数组。 + let results = + LivingVillage.Headless.BatchOutput.runStreaming worlds workers + (fun k -> + let sw = System.Diagnostics.Stopwatch.StartNew() + let verdict = evalWorld days k + sw.Stop() + (verdict, sw.Elapsed.TotalSeconds)) + (fun k (verdict, seconds) doneCount -> + printfn "%s" (LivingVillage.Headless.BatchOutput.worldLine verdict.Line seconds) + printfn "%s" (LivingVillage.Headless.BatchOutput.doneLine doneCount worlds seconds k) + Console.Out.Flush()) + wall.Stop() let mutable passed = 0 let mutable failed = 0 let mutable oldPassed = 0 let ratios = ResizeArray<float> () let ginis = ResizeArray<float> () - let sw = System.Diagnostics.Stopwatch.StartNew() - 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() + for verdict, _ in results do + if verdict.Ok then passed <- passed + 1 else failed <- failed + 1 + if verdict.CheckOld then oldPassed <- oldPassed + 1 + ratios.Add verdict.Ratio + ginis.Add verdict.Gini 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" - worlds passed failed oldPassed worlds (Seq.min ratios) (Seq.max ratios) (Seq.min ginis) (Seq.max ginis) sw.Elapsed.TotalSeconds + printfn "%s" + (LivingVillage.Headless.BatchOutput.summaryLine + worlds passed failed oldPassed (Seq.min ratios) (Seq.max ratios) (Seq.min ginis) (Seq.max ginis) workers wall.Elapsed.TotalSeconds) printfn "M3_ACCEPTANCE=%s" (if allPass then "PASS" else "FAIL") + Console.Out.Flush() if allPass then 0 else 1 +/// `--cost-probe`:单世界按天数阶梯逐段真实测量(墙钟、ticks/s、分配与 GC), +/// 只读、不改任何模拟语义。天数阶梯可用 `--cost-probe D1,D2,...` 覆盖。 +let runCostProbe (ladder: int64 list) : int = + printfn "cost_probe ladder=%s npcs=%d" (ladder |> List.map string |> String.concat ",") Sim.npcCount + Console.Out.Flush() + for days in ladder do + let ticks = days * ticksPerDay + System.GC.Collect() + let allocBefore = System.GC.GetTotalAllocatedBytes(false) + let gen0Before = System.GC.CollectionCount 0 + let gen1Before = System.GC.CollectionCount 1 + let gen2Before = System.GC.CollectionCount 2 + let sw = System.Diagnostics.Stopwatch.StartNew() + let stats = runSimulation false days 42UL Sim.npcCount + sw.Stop() + let allocated = System.GC.GetTotalAllocatedBytes(false) - allocBefore + let line = + LivingVillage.Headless.BatchOutput.costLine + days ticks sw.Elapsed.TotalSeconds allocated + (int64 (System.GC.CollectionCount 0 - gen0Before)) + (int64 (System.GC.CollectionCount 1 - gen1Before)) + (int64 (System.GC.CollectionCount 2 - gen2Before)) + printfn "%s" line + printfn "cost_probe_check days=%d nonfinite=%d out_of_bounds=%d final_tick=%d" days stats.NonFinite stats.OutOfBounds stats.World.Tick + Console.Out.Flush() + 0 + let runExisting (argv: string[]) = let rec parse (i: int) @@ -544,14 +581,32 @@ let runExisting (argv: string[]) = if stats.NonFinite > 0L || stats.OutOfBounds > 0L || not replayOk then 1 else 0 | Error msg -> eprintfn $"headless: {msg}" - eprintfn "usage: dotnet run -c Release --project src/LivingVillage.Headless -- --days N --seed S [--npc N] [--dump-relations] [--dump-rumors] [--replay-rumors] | --m5-smoke | --m6a-smoke | --batch K D | --performance-baseline" + eprintfn "usage: dotnet run -c Release --project src/LivingVillage.Headless -- --days N --seed S [--npc N] [--dump-relations] [--dump-rumors] [--replay-rumors] | --m5-smoke | --m6a-smoke | --batch K D | --performance-baseline | --cost-probe [D1,D2,...]" 2 +let private parseCostLadder (argv: string[]) : int64 list = + if argv.Length >= 2 then + let parsed = + argv.[1].Split(',') + |> Array.toList + |> List.map (fun token -> + match Int64.TryParse(token.Trim()) with + | true, days when days > 0L -> Some days + | _ -> None) + if parsed <> [] && List.forall Option.isSome parsed then + parsed |> List.map Option.get + else + LivingVillage.Headless.BatchOutput.defaultCostLadder + else + LivingVillage.Headless.BatchOutput.defaultCostLadder + [<EntryPoint>] let main argv = if argv.Length = 1 && argv.[0] = "--m6a-smoke" then runM6aSmoke () elif argv.Length = 1 && argv.[0] = "--performance-baseline" then LivingVillage.Headless.PerformanceProbe.runDefault () + elif argv.Length >= 1 && argv.[0] = "--cost-probe" then + runCostProbe (parseCostLadder argv) else runExisting argv |
