import { chmodSync, copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { Type, type Static } from "typebox"; import { Value } from "typebox/value"; import type { HypothesisMetric, Json, JudgedResult, Measured, MetricDefinition, ProcessResult, RunDefinition, } from "../vocab/index.ts"; import type { TaskFacts } from "./facts.ts"; import { measured, readMetricsFile, unmeasured } from "./metrics-file.ts"; import type { DiagnosisEnding, DiagnosisExperiment, DiagnosisExperimentJudged, DiagnosisJudged, DiagnosisJudgingStep, LightCheckOutcome, Task, TaskImplementation, } from "./types.ts"; // 填圆:在单位正方形里放 26 个圆,最大化半径之和。任务说明见 tasks/circle_packing/TASK.md。 // 凡是要执行任务代码的事(正式运行、校验器、轻量检查)都只交出运行定义,由调用方交给 runner 在任务镜像里跑; // 这里只读写文件、解读产物(docs/结构.md 第二部分"runner 只管怎么跑,tasks 只管怎么判")。 const CIRCLES = 26; const ENTRY = "initial.py"; const HARNESS = "harness.py"; const DIAGNOSIS_HARNESS = "harness-diagnosis.py"; const VALIDATOR = "validate.py"; const CHECK_SCRIPT = "check-candidate.py"; const METRICS_SPEC = "metrics-spec.json"; const METRICS = "metrics.json"; const SOLUTION = "solution.json"; const VERDICT = "verdict.json"; const CHECK_RESULT = "check.json"; /** 诊断运行:原始产物目录、每个实验的答案目录、harness 写的每实验用时报告(路径都相对运行目录)。 */ const BASELINE_ARTIFACT = "baseline-artifact"; const ANSWERS = "answers"; const DIAGNOSIS_REPORT = "diagnosis.json"; /** 判分目录里每个实验写的校验器退出码文件(同一个目录下还有这个实验的 solution.json、verdict.json)。 */ const EXIT_FILE = "exit"; /** 轻量检查目录里放候选代码的子目录;检查脚本放在它的上一层,免得扫到自己。 */ const CHECK_CANDIDATE = "candidate"; /** 运行目录挂进容器的位置。 */ const WORKDIR = "/work"; /** 校验器的时限。26 个圆两两比较加合法化,实测一秒以内;它超时说明宿主的工具出了问题。 */ const VALIDATOR_SECONDS = 60; /** 合法化前先按容差 0 判原产物(方法.md 第八部分"合法化"第 1 条)。 */ const TOLERANCE = "0"; // 常规辅助指标的编号(方法.md 第五部分"指标")。前三个来自 runner 的资源用量,后四个来自校验器。 const WALL = "wall-seconds"; const CPU = "cpu-seconds"; const MEMORY = "peak-memory-bytes"; const CLEARANCE = "min-pairwise-clearance"; const RAW_SCORE = "raw-score"; const RAW_VIOLATION = "raw-violation"; const DEDUCTION = "legalization-deduction"; const FROM_VALIDATOR = [CLEARANCE, RAW_SCORE, RAW_VIOLATION, DEDUCTION]; const SIDE = "单位正方形的边长"; const PER_RUN = "每次运行结束后由宿主读一次,直接取值"; function auxiliary( id: string, meaning: string, unit: string, direction: "maximize" | "minimize", howMeasured: string, ): MetricDefinition { return { id, layer: "auxiliary", meaning, unit, direction, howMeasured, missing: "recordMissing" }; } function metricDefinitions(facts: TaskFacts): MetricDefinition[] { const validator = `宿主的校验器从 ${SOLUTION} 重算,${PER_RUN}`; return [ { id: facts.finalMetric.id, layer: "final", meaning: "合法摆法的半径之和,由宿主的独立校验器从实际产物重算;原产物差一点不合法时,先按 TASK.md 的只缩不移规则修,再按修后的半径算", unit: SIDE, direction: facts.finalMetric.direction, howMeasured: validator, missing: "invalidateRun", }, auxiliary(WALL, "正式运行的墙钟时间", "秒", "minimize", `容器里的计量程序记录,${PER_RUN}`), auxiliary(CPU, "正式运行用掉的 CPU 秒(容器 cgroup 的 usage_usec)", "秒", "minimize", `容器里的计量程序记录,${PER_RUN}`), auxiliary(MEMORY, "正式运行的峰值内存(容器 cgroup 的 memory.peak)", "字节", "minimize", `容器里的计量程序记录,${PER_RUN}`), auxiliary(CLEARANCE, "所有圆对中「圆心距减两半径之和」的最小值,大于等于 0 表示不重叠;修过的取修后的摆法", SIDE, "maximize", validator), auxiliary(RAW_SCORE, "合法化之前原产物的半径之和", SIDE, "maximize", validator), auxiliary(RAW_VIOLATION, "原产物的最大违反量:两圆重叠的量和圆出边的量里最大的一个,0 表示没有违反", SIDE, "minimize", validator), auxiliary(DEDUCTION, "只缩不移合法化扣掉的分:原分数减最终成绩,原产物合法时是 0", SIDE, "minimize", validator), ]; } /** 正式运行、校验器、轻量检查共用的运行定义。数学库的线程数和核数一致,否则会开多个线程抢核。 */ function definition(task: Task, imageDigest: string, command: string[], wallSeconds: number, env: Record): RunDefinition { const cores = String(task.facts.run.cores); return { command, workdir: WORKDIR, image: { digest: imageDigest, runtime: task.facts.run.runtime }, wallSeconds, cores: task.facts.run.cores, inheritEnv: false, env: { OMP_NUM_THREADS: cores, OPENBLAS_NUM_THREADS: cores, MKL_NUM_THREADS: cores, ...env }, }; } // 解释器不加 -I:-I 会忽略全部 PYTHON* 环境变量,连同 runner 设的 PYTHON_CPU_COUNT, // 于是 os.cpu_count() 报回宿主核数(本机实测 16)。容器里的环境变量全部由运行定义给出,用不着 -I。 // -B:不写 __pycache__,不往运行目录里添文件。 function prepareRun(task: Task, candidateDir: string, runDir: string, hypothesisMetrics: HypothesisMetric[]): void { cpSync(candidateDir, runDir, { recursive: true }); // harness 和指标说明以宿主的为准,候选目录里有同名文件也被盖掉(这两个名字在 task.json 的不许改名单里)。 copyFileSync(join(task.dir, HARNESS), join(runDir, HARNESS)); writeFileSync(join(runDir, METRICS_SPEC), `${JSON.stringify(hypothesisMetrics, null, 2)}\n`); } function formalRun(task: Task, imageDigest: string, seed: string): RunDefinition { const command = ["python", "-B", `${WORKDIR}/${HARNESS}`, "--program", ENTRY, "--output", SOLUTION]; return definition(task, imageDigest, command, task.facts.run.wallSeconds, { INFORSI_SEED: seed }); } function lightCheck(task: Task, candidateDir: string, checkDir: string, imageDigest: string, seconds: number): RunDefinition { cpSync(candidateDir, join(checkDir, CHECK_CANDIDATE), { recursive: true }); copyFileSync(join(task.dir, CHECK_SCRIPT), join(checkDir, CHECK_SCRIPT)); const command = ["python", "-B", CHECK_SCRIPT, CHECK_CANDIDATE, ENTRY, CHECK_RESULT]; return definition(task, imageDigest, command, seconds, {}); } /** 检查脚本写的结果文件。valid 与 reasons 是否为空必须一致。 */ const CheckResult = Type.Object({ valid: Type.Boolean(), reasons: Type.Array(Type.String()) }); /** 宿主的工具(检查脚本、校验器)没正常跑完时报错停下:这不是候选的失败(任务书 05 第三部分)。 */ function requireExit(what: string, result: ProcessResult, codes: number[]): number { if (result.ending === "exited" && codes.includes(result.exitCode)) return result.exitCode; const how = result.ending === "notStarted" ? `没启动:${result.reason}` : result.ending === "exited" ? `退出码 ${result.exitCode}` : result.ending; throw new Error(`${what}没有正常跑完(${how}),这是宿主一侧的工具出了问题,不能当成候选的失败`); } function readJson(path: string): unknown { try { return JSON.parse(readFileSync(path, "utf8")); } catch (error) { throw new Error(`读不出 ${path}:${(error as Error).message}`, { cause: error }); } } function readLightCheck(checkDir: string, result: ProcessResult): LightCheckOutcome { requireExit("轻量检查脚本", result, [0]); const path = join(checkDir, CHECK_RESULT); const check = readJson(path); if (!Value.Check(CheckResult, check) || check.valid !== (check.reasons.length === 0)) { throw new Error(`${path} 的内容认不出:${JSON.stringify(check)}`); } return check.valid ? { passed: true } : { passed: false, reasons: check.reasons }; } /** 正式运行没正常结束的原因;正常退出(退出码 0)时为 null。 */ function processFailure(task: Task, formal: Exclude): string | null { if (formal.ending === "timeout") return `超过单次时限 ${task.facts.run.wallSeconds} 秒,被结束`; if (formal.ending === "cancelled") return "容器从外面被结束(关机、手动 docker kill 等),没有留下计量结果"; if (formal.exitCode !== 0) return `正式运行以退出码 ${formal.exitCode} 结束`; return null; } function started(formal: ProcessResult): Exclude { if (formal.ending === "notStarted") throw new Error(`没启动的运行不判分:${formal.reason}`); return formal; } /** * 拼出 JudgedResult:资源用量来自正式运行,假设专用指标和两个保留键来自候选写的 metrics.json。 * 有"缺了整次作废"的假设专用指标测不到时,原本算数的也改判不算数(方法.md 第五部分"指标")。 */ function assemble( runDir: string, formal: Exclude, hypothesisMetrics: HypothesisMetric[], verdict: { valid: boolean; reason: string }, score: Measured, fromValidator: Record, extra: Json, ): JudgedResult { const file = readMetricsFile(join(runDir, METRICS), METRICS, hypothesisMetrics); const voided = hypothesisMetrics.flatMap((m) => { const value = file.hypothesis[m.id]!; return m.missing === "invalidateRun" && value.status === "unmeasured" ? [`${m.id}(${value.reason})`] : []; }); const result = verdict.valid && voided.length > 0 ? { valid: false, reason: `登记为"缺了整次作废"的假设专用指标测不到:${voided.join(";")}` } : verdict; return { verdict: result.valid ? "valid" : "invalid", verdictReason: result.reason, score, metrics: { [WALL]: formal.wallSeconds, [CPU]: formal.cpuSeconds, [MEMORY]: formal.peakMemoryBytes, ...fromValidator, ...file.hypothesis, }, answerPath: file.answerPath, safeguardTriggers: file.safeguardTriggers, extra, }; } function prepareJudging( task: Task, runDir: string, judgeDir: string, imageDigest: string, formal: ProcessResult, hypothesisMetrics: HypothesisMetric[], ): ReturnType { const run = started(formal); const solution = join(runDir, SOLUTION); const failure = processFailure(task, run) ?? (existsSync(solution) ? null : `没有产物:运行目录里没有 ${SOLUTION}`); if (failure !== null) { const notValidated = Object.fromEntries(FROM_VALIDATOR.map((id) => [id, unmeasured(`没有跑校验器:${failure}`)])); const result = assemble(runDir, run, hypothesisMetrics, { valid: false, reason: failure }, unmeasured(failure), notValidated, { legalization: null, }); return { status: "judged", result }; } copyFileSync(solution, join(judgeDir, SOLUTION)); copyFileSync(join(task.dir, VALIDATOR), join(judgeDir, VALIDATOR)); const command = [ "python", "-B", VALIDATOR, "--circles", String(CIRCLES), "--solution", SOLUTION, "--atol", TOLERANCE, "--legalize", "--verdict", VERDICT, ]; return { status: "validate", definition: definition(task, imageDigest, command, VALIDATOR_SECONDS, {}) }; } const NumberOrNull = Type.Union([Type.Number(), Type.Null()]); /** validate.py --legalize 写的判定里,这里要读的部分;其余键(evidence、legal_radii 等)不读。 */ const ValidatorVerdict = Type.Object({ valid: Type.Boolean(), computed_sum: NumberOrNull, reasons: Type.Array(Type.String()), diagnostics: Type.Object({ min_pairwise_clearance: NumberOrNull }), legalization: Type.Object({ raw_valid: Type.Boolean(), raw_score: NumberOrNull, raw_violation: NumberOrNull, applied: Type.Boolean(), repaired: Type.Boolean(), shrunk_circles: Type.Array(Type.Integer()), reasons: Type.Array(Type.String()), }), }); type ValidatorVerdict = Static; /** 校验器在形状不对、含非有限数或负半径时提前返回,几何量没算。 */ const NOT_COMPUTED = "原产物形状不对、含非有限数或负半径,校验器没算这个量"; function orUnmeasured(value: number | null, reason: string): Measured { return value === null ? unmeasured(reason) : measured(value); } function readVerdict(judgeDir: string, exitCode: number): ValidatorVerdict { const path = join(judgeDir, VERDICT); const verdict = readJson(path); if (!Value.Check(ValidatorVerdict, verdict)) throw new Error(`${path} 的内容认不出:${JSON.stringify(verdict)}`); // 退出码 0 = 合法、1 = 不合法;合法化之后的结论就是最终判定。对不上说明跑的不是这份校验器。 const consistent = verdict.valid === (exitCode === 0) && verdict.legalization.repaired === verdict.valid && (!verdict.valid || verdict.computed_sum !== null); if (!consistent) throw new Error(`${path} 与退出码 ${exitCode} 或自身前后对不上:${JSON.stringify(verdict)}`); return verdict; } /** 校验器判定读成的结论:算不算数、理由、最终分数、校验器给的辅助指标、合法化明细。 */ interface Scored { valid: boolean; reason: string; score: Measured; fromValidator: Record; extra: Json; } function scoreVerdict(verdict: ValidatorVerdict): Scored { const legal = verdict.legalization; let deduction: Measured; let score: Measured; if (verdict.valid) { const final = verdict.computed_sum!; score = measured(final); // legal.repaired 为真时 raw_score 一定有值:原产物能算出分数,才可能合法或被修好。 const raw = legal.raw_score!; // 只缩半径只会降分;修后反而更高,说明修法写错了(方法.md 第八部分"合法化"第 3 条)。 if (final > raw) throw new Error(`合法化后分数 ${final} 高于原分数 ${raw}:只缩半径只会降分,修法出错了`); deduction = measured(raw - final); } else { const reason = `不算数:${verdict.reasons.concat(legal.reasons).join(";")}`; score = unmeasured(reason); deduction = unmeasured(`修不了,没有修后分数:${legal.reasons.join(";")}`); } const rawScore = orUnmeasured(legal.raw_score, NOT_COMPUTED); const rawViolation = orUnmeasured(legal.raw_violation, NOT_COMPUTED); const fromValidator = { [CLEARANCE]: orUnmeasured(verdict.diagnostics.min_pairwise_clearance, NOT_COMPUTED), [RAW_SCORE]: rawScore, [RAW_VIOLATION]: rawViolation, [DEDUCTION]: deduction, }; const reason = verdict.valid ? legal.raw_valid ? "校验器按容差 0 判原产物合法" : `原产物不合法,按只缩不移规则修好后合法(缩了 ${legal.shrunk_circles.length} 个圆)` : `校验器判不合法:${verdict.reasons.concat(legal.reasons).join(";")}`; const extra = { legalization: { rawValid: legal.raw_valid, applied: legal.applied, repaired: legal.repaired, shrunkCircles: legal.shrunk_circles, reasons: legal.reasons, rawScore, rawViolation, deduction, }, }; return { valid: verdict.valid, reason, score, fromValidator, extra }; } function judge( _task: Task, runDir: string, judgeDir: string, formal: ProcessResult, validator: ProcessResult, hypothesisMetrics: HypothesisMetric[], ): JudgedResult { const run = started(formal); const scored = scoreVerdict(readVerdict(judgeDir, requireExit("校验器", validator, [0, 1]))); return assemble(runDir, run, hypothesisMetrics, { valid: scored.valid, reason: scored.reason }, scored.score, scored.fromValidator, scored.extra); } // --------------------------------------------------------------------------- // 诊断运行(方法.md 4.3"诊断"一段):一个容器、一次登记、一次扣次,里面按顺序跑几个实验。 // 全部是新增的一套函数,用自己的 harness(harness-diagnosis.py),不碰上面正式运行的任何一步。 // --------------------------------------------------------------------------- /** 诊断程序存版本时跳过的名字:正式运行那份加上原始产物目录(它是宿主放进工作目录的,不是实验员的代码)。 */ function diagnosisUnversionedNames(facts: TaskFacts): string[] { return [...facts.unversionedNames, BASELINE_ARTIFACT]; } /** * 把产出当前基线的那次正式运行的原始产物(solution.json)放进 dir 下的 baseline-artifact/,设成只读。 * 实验员的工作目录和诊断运行的运行目录都用它:实验员能看到实验要接着算的那份答案,运行时实验读的是同一份。 * 只读靠文件权限,防的是实验顺手改了它,不是安全边界。 */ function placeBaselineArtifact(_task: Task, baselineRunDir: string, dir: string): void { const target = join(dir, BASELINE_ARTIFACT); mkdirSync(target); const file = join(target, SOLUTION); copyFileSync(join(baselineRunDir, SOLUTION), file); chmodSync(file, 0o444); } function prepareDiagnosisRun(task: Task, programDir: string, baselineRunDir: string, runDir: string, experiments: DiagnosisExperiment[]): void { cpSync(programDir, runDir, { recursive: true }); // 两份 harness 以宿主的为准;指标说明是全部实验登记的指标合在一起,每个实验只写自己的那几条。 copyFileSync(join(task.dir, HARNESS), join(runDir, HARNESS)); copyFileSync(join(task.dir, DIAGNOSIS_HARNESS), join(runDir, DIAGNOSIS_HARNESS)); writeFileSync(join(runDir, METRICS_SPEC), `${JSON.stringify(experiments.flatMap((e) => e.metrics), null, 2)}\n`); placeBaselineArtifact(task, baselineRunDir, runDir); mkdirSync(join(runDir, ANSWERS)); } /** 总时限、核数照任务规定,实验按顺序跑,每个实验分到总时限 ÷ 实验数(由 harness 执行)。 */ function diagnosisRun(task: Task, imageDigest: string, seed: string, experimentIds: string[]): RunDefinition { const eachSeconds = task.facts.run.wallSeconds / experimentIds.length; const command = [ "python", "-B", `${WORKDIR}/${DIAGNOSIS_HARNESS}`, "--program", ENTRY, "--baseline", BASELINE_ARTIFACT, "--answers", ANSWERS, "--seconds-each", String(eachSeconds), "--report", DIAGNOSIS_REPORT, "--experiments", ...experimentIds, ]; return definition(task, imageDigest, command, task.facts.run.wallSeconds, { INFORSI_SEED: seed }); } /** 一个实验在运行目录里的结局:harness 报告里的一项,或者整次运行先结束、没轮到它。 */ type ExperimentOutcome = { ending: "finished" | "crashed" | "timeout"; wallSeconds: number } | { ending: "notRun"; reason: string }; /** harness 写的报告(我们自己的程序,按约定的格式直接读)。 */ interface ReportEntry { id: string; status: "finished" | "crashed" | "timeout"; wall_seconds: number; } function experimentOutcomes( task: Task, runDir: string, process: Exclude, experiments: DiagnosisExperiment[], ): ExperimentOutcome[] { const path = join(runDir, DIAGNOSIS_REPORT); const report = existsSync(path) ? (readJson(path) as ReportEntry[]) : []; return experiments.map((e) => { const entry = report.find((r) => r.id === e.id); if (entry !== undefined) return { ending: entry.status, wallSeconds: entry.wall_seconds }; const failure = processFailure(task, process); if (failure === null) throw new Error(`诊断运行正常退出,但 ${DIAGNOSIS_REPORT} 里没有实验 ${e.id}`); return { ending: "notRun", reason: `没轮到:整次诊断运行${failure}` }; }); } /** 没有答案可判的实验:不算数,辅助指标和假设专用指标都记测不到并附原因。 */ function unjudgedExperiment( id: string, ending: Exclude, wall: Measured, reason: string, hypothesisMetrics: HypothesisMetric[], ): DiagnosisExperimentJudged { const metrics = { ...Object.fromEntries(FROM_VALIDATOR.map((m) => [m, unmeasured(`没有跑校验器:${reason}`)])), ...Object.fromEntries(hypothesisMetrics.map((m) => [m.id, unmeasured(`没有结果:${reason}`)])), }; return { id, ending, wallSeconds: wall, verdict: "invalid", verdictReason: reason, score: unmeasured(reason), metrics, extra: { legalization: null } }; } function judgedExperiments( task: Task, runDir: string, judgeDir: string, experiments: DiagnosisExperiment[], outcomes: ExperimentOutcome[], ): DiagnosisJudged { const eachSeconds = task.facts.run.wallSeconds / experiments.length; return { experiments: experiments.map((e, i): DiagnosisExperimentJudged => { const outcome = outcomes[i]!; if (outcome.ending === "notRun") return unjudgedExperiment(e.id, "notRun", unmeasured(outcome.reason), outcome.reason, e.metrics); const wall = measured(outcome.wallSeconds); if (outcome.ending === "timeout") { return unjudgedExperiment(e.id, "timeout", wall, `超过分到的时间 ${eachSeconds} 秒,被结束`, e.metrics); } if (outcome.ending === "crashed") { return unjudgedExperiment(e.id, "crashed", wall, `实验的进程以非零退出码结束(报错见这次运行的 stderr.log)`, e.metrics); } const dir = join(judgeDir, e.id); const exitCode = Number(readFileSync(join(dir, EXIT_FILE), "utf8").trim()); const scored = scoreVerdict(readVerdict(dir, exitCode)); const hypothesis = readMetricsFile(join(runDir, ANSWERS, e.id, METRICS), `${ANSWERS}/${e.id}/${METRICS}`, e.metrics).hypothesis; return { id: e.id, ending: "finished", wallSeconds: wall, verdict: scored.valid ? "valid" : "invalid", verdictReason: scored.reason, score: scored.score, metrics: { ...scored.fromValidator, ...hypothesis }, extra: scored.extra, }; }), }; } /** 逐个实验判分。校验器一个容器、按顺序对每个有答案的实验各跑一次,退出码各存一个文件。 */ const VALIDATE_EACH = `for id in "$@"; do python -B ${VALIDATOR} --circles ${CIRCLES} --solution "$id/${SOLUTION}" --atol ${TOLERANCE} --legalize --verdict "$id/${VERDICT}" echo $? > "$id/${EXIT_FILE}" done`; function prepareDiagnosisJudging( task: Task, runDir: string, judgeDir: string, imageDigest: string, process: ProcessResult, experiments: DiagnosisExperiment[], ): DiagnosisJudgingStep { const run = started(process); const outcomes = experimentOutcomes(task, runDir, run, experiments); const finished = experiments.filter((_, i) => outcomes[i]!.ending === "finished"); if (finished.length === 0) return { status: "judged", result: judgedExperiments(task, runDir, judgeDir, experiments, outcomes) }; for (const e of finished) { mkdirSync(join(judgeDir, e.id)); copyFileSync(join(runDir, ANSWERS, e.id, SOLUTION), join(judgeDir, e.id, SOLUTION)); } copyFileSync(join(task.dir, VALIDATOR), join(judgeDir, VALIDATOR)); const command = ["sh", "-c", VALIDATE_EACH, "inforsi-validate-each", ...finished.map((e) => e.id)]; return { status: "validate", definition: definition(task, imageDigest, command, VALIDATOR_SECONDS * finished.length, {}) }; } function judgeDiagnosis( task: Task, runDir: string, judgeDir: string, process: ProcessResult, validator: ProcessResult | null, experiments: DiagnosisExperiment[], ): DiagnosisJudged { const run = started(process); const outcomes = experimentOutcomes(task, runDir, run, experiments); if (outcomes.some((o) => o.ending === "finished")) { if (validator === null) throw new Error("有实验交出了答案,却没有校验器的运行结果"); requireExit("校验器", validator, [0]); } return judgedExperiments(task, runDir, judgeDir, experiments, outcomes); } export const circlePacking: TaskImplementation = { metricDefinitions, prepareRun, formalRun, lightCheck, readLightCheck, prepareJudging, judge, diagnosisUnversionedNames, placeBaselineArtifact, prepareDiagnosisRun, diagnosisRun, prepareDiagnosisJudging, judgeDiagnosis, };