profile: per-term legacy FI/TN sample profiler (L76); passage/support is ~75% of field time
Opt-in at build time (VOXELFORGE_PROFILE_LEGACY_TERMS=1; compiled out by default, ~2% when on). Per legacy sample: passage/support composer ~1.05 us vs primitive scan ~0.19 us, boundary band ~0.06 us, noise ~0.01 us. Resolved-passage constants are cached at field binding (arithmetic order unchanged); whole-view effect within noise. Per-tile hashes identical to L75 in all 24 compiler x worker x view comparisons; fast tier PASS; Linux UE host builds. R40 researcher notes attached. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
This commit is contained in:
1 parent
9d5455ec68
commit
90378a585f
13 files changed
+1070
-86
No files matched your search
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Measure sampled per-term profiler overhead against the same clean candidate."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
DATA = ROOT / "Standalone/data/owner_world.json"
|
||||
OUT = ROOT / os.environ.get("VOXELFORGE_L76_PROFILE_OVERHEAD_OUT",
|
||||
"Saved/Linux/l76/term-profiler-overhead")
|
||||
BINARIES = {
|
||||
"clean": ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-l76-clang",
|
||||
"profile": ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-l76-termprofile-clang",
|
||||
}
|
||||
CAMERAS = {
|
||||
1: (25.8369140625, 3.19921875, -737.0),
|
||||
2: (19.166015625, -14.2001953125, -753.3740234375),
|
||||
}
|
||||
KV = re.compile(r"([a-zA-Z0-9_]+)=([^ ]+)")
|
||||
|
||||
|
||||
def run(kind: str, strate: int, pair: int) -> dict:
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
log = OUT / f"strate{strate}-pair{pair}-{kind}.log"
|
||||
camera = ",".join(f"{value:.12f}" for value in CAMERAS[strate]) + ",0,0"
|
||||
command = [
|
||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", "timeout",
|
||||
"1740s", str(BINARIES[kind]), str(DATA), "--world-assembler-prototype",
|
||||
"--stream-benchmark", "--stream-pipeline", "--force-generate", "--workers=8",
|
||||
"--clip-radius=3", "--max-clip-level=8", "--max-level=8", f"--camera={camera}",
|
||||
"--profile-lod-work", "--instrument-tile-drain", "--profile-passage-composer",
|
||||
]
|
||||
started = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as output:
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=output,
|
||||
stderr=subprocess.STDOUT, check=False, timeout=1740)
|
||||
pipeline = {}
|
||||
with log.open(encoding="utf-8", errors="replace") as stream:
|
||||
for line in stream:
|
||||
if line.startswith("[StreamPipeline]"):
|
||||
pipeline = dict(KV.findall(line))
|
||||
if result.returncode != 0 or "whole_view_ready" not in pipeline:
|
||||
raise SystemExit(f"profiler-overhead run failed; see {log}")
|
||||
return {
|
||||
"kind": kind, "strate": strate, "pair": pair, "exit_code": result.returncode,
|
||||
"process_wall_s": round(time.monotonic() - started, 3),
|
||||
"whole_view_ready_s": float(pipeline["whole_view_ready"].removesuffix("s")),
|
||||
"log": str(log.relative_to(ROOT)),
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
missing = [str(path) for path in (*BINARIES.values(), DATA) if not path.is_file()]
|
||||
if missing:
|
||||
raise SystemExit("missing input: " + ", ".join(missing))
|
||||
rows = []
|
||||
for strate in (1, 2):
|
||||
for pair in range(3):
|
||||
order = ("clean", "profile") if pair % 2 == 0 else ("profile", "clean")
|
||||
pair_rows = {kind: run(kind, strate, pair) for kind in order}
|
||||
delta = pair_rows["profile"]["whole_view_ready_s"] \
|
||||
- pair_rows["clean"]["whole_view_ready_s"]
|
||||
result = {
|
||||
"strate": strate, "pair": pair,
|
||||
"clean_s": pair_rows["clean"]["whole_view_ready_s"],
|
||||
"profile_s": pair_rows["profile"]["whole_view_ready_s"],
|
||||
"delta_s": round(delta, 4),
|
||||
"delta_percent": round(100.0 * delta / pair_rows["clean"]["whole_view_ready_s"], 2),
|
||||
"runs": [pair_rows[kind] for kind in order],
|
||||
}
|
||||
rows.append(result)
|
||||
print("[L76TermProfilerOverhead] " + json.dumps(result, sort_keys=True), flush=True)
|
||||
(OUT / "summary.json").write_text(json.dumps(rows, indent=2) + "\n", encoding="utf-8")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,38 @@
|
||||
## Revised ranked list
|
||||
|
||||
Ranked by opportunity, **not measured speedup**. **Changed:** I retract R40’s ranking of the per-column detail context as the clearest next win. The line-based detail pass already has that context, while L73 reports large repeated passage-composer workloads. L73 does not time individual terms, so none of these ranks predicts seconds saved.
|
||||
|
||||
1. **[INFERENCE] [CODE] Reuse passage relief fields within one sample.** When multiple passages reach the relief calculation at the same `(X,Y,Z)`, they call the same three functions with the same inputs: passage-wall relief and two surface-relief fields. Each function evaluates three 3D value-noise fields, so its result depends on Z; it cannot be reused across a column’s different Z samples. A sample-scoped cache could reuse each result across qualifying passages while leaving their density-composition order intact. **[REPORT]** L73 counted 54.5M FI / 46.9M TN passage relief-field calls, but did not time them separately. [Relief calls in the passage loop](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:1404>), [noise definitions](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/VoxelForgeFieldShared.h:247>), [L73 counts and their limitation](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux73/REPORT.md:61>)
|
||||
|
||||
**Falsifier:** term-level timing shows these calls are a small part of composer time, or the hot samples rarely reach more than one qualifying passage.
|
||||
|
||||
2. **[INFERENCE] [CODE] Make tile passage and crossing candidates drive the sample loops.** Tile scope already builds passage features, but turns route and column features into per-passage flags. The sample path still checks every passage for upper-mouth proximity, loops over every passage for carves, and scans all void crossings inside that loop. An ordered per-tile list could avoid those visits while preserving passage order and retaining the existing sample-level tests. Mouth-support segments already have a per-tile candidate list and are filtered by sample bounds. **[REPORT]** L73 counted 63.4M FI / 54.2M TN carve visits, 20.4M / 15.5M support visits, and 24.6M / 20.4M mouth-route checks; these are counts, not time shares. [Tile scope](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:194>), [upper-mouth scan](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:1003>), [passage and crossing loops](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:1099>), [per-tile support segments](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:1078>)
|
||||
|
||||
**Falsifier:** most tiles retain nearly every passage, or direct timing shows the remaining loop and crossing checks are cheap beside route-distance work.
|
||||
|
||||
3. **[INFERENCE] [CODE] Extend Z-line reuse only where scalar sampling still pays for it.** The scalar analytic path calls `DetailNoise`, which creates a fresh owner-noise context per sample. Its surface relief has three 3D value-noise fields; owner roughness may add 3D Perlin calls, domain warps, or cellular searches. However, `ApplyDetailBand` already creates a detail context per column, reuses value-noise planes and owner endpoint cores, and passes its macro value from fade into detail. So this is a candidate only for hot callers that still use the scalar path, not a general missing optimization. [Scalar detail path](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:11697>), [existing per-column detail context](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:10425>), [macro reuse in that pass](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:10550>), [owner noise and Z-dependent warp inputs](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/VoxelForgeFieldShared.h:420>)
|
||||
|
||||
**Falsifier:** the scalar path contributes little to L73’s field time, or owner roughness is rarely active. The current report does not separate those costs.
|
||||
|
||||
4. **[INFERENCE] [CODE] Cache selected XY subterms, with strict scope.** `FloorAt(P,X,Y)` is XY-only for a fixed primitive, and cavern-edge closest-point parameter search uses X and Y. But primitive selection and bounds checks depend on Z; cavern floor distances, caps, noise, and feature eligibility also depend on Z. In addition, the batched primitive path already computes a primitive’s floor once for its Z lanes. These are narrower opportunities than R40 implied. [XY-only floor function](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:9166>), [batched floor reuse](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:11296>), [cavern XY projection](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:8155>), [Z-dependent cavern distance](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:8173>)
|
||||
|
||||
**Falsifier:** those XY subterms are a small share of time, or few features pass the per-sample bounds checks.
|
||||
|
||||
## Dependency and exactness audit
|
||||
|
||||
| Term | What the code verifies |
|
||||
|---|---|
|
||||
| `StrataDistance(Z)`, `FindLayerField(Z)`, bound-stratum lookup | **[CODE] Verified Z-only selectors/values.** They can be shared across X/Y samples on the same Z plane. The selected field can change when Z crosses a layer or stratum boundary. [Strata and layer lookup](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:11028>) |
|
||||
| `FloorAt(P,X,Y)` | **[CODE] Verified XY-only for fixed `P`.** This does not make floor blending XY-only: the primitive’s per-sample selection, `Floor-Z`, and floor blend tests still depend on Z. [Floor function](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:9166>), [Z-dependent blend](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:11086>) |
|
||||
| 3D value noise and relief | **[CODE] Verified partial XY reuse only.** `X0`, `Y0`, `TX`, and `TY` use X/Y; `Z0`, `TZ`, the corner planes, and the final nested lerp use Z. Owner roughness also puts effective Z into its main and fine coordinates and into domain-warp Perlin inputs. [Value-noise interpolation](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ResearchTunnelsInternal.h:51>), [owner domain warp](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/VoxelForgeFieldShared.h:428>), [owner core](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/VoxelForgeFieldShared.h:415>) |
|
||||
| Primitive selection | **[CODE] Verified Z-dependent.** `PointBin` includes Z, and the sample checks primitive Z bounds before evaluating distances. Only the X/Y parts of the bin lookup are column-stable. [3D bin key](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:9924>), [per-sample primitive scan](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:11061>) |
|
||||
| Cavern and mouth-support projections | **[CODE] Verified XY-only for the projection parameter in these paths.** Their full results are not XY-only: cavern floor height/caps and mouth-support distances use Z. Feature and primitive selection also still use per-sample bounds. [Cavern projection and floor](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:8155>), [mouth-support projection and Z distance](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:1043>) |
|
||||
| Tile candidates and boundary flags | **[CODE] Verified tile-constant candidate data.** Passage, stamp, and cavern candidate sets are built from tile bounds. The boundary and transition entries are conservative tile flags, not exact per-sample eligibility; the boundary calculation still uses X, Y, and Z. [Tile scope and flags](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:190>), [boundary calculation](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:820>) |
|
||||
|
||||
**Bit identity:** I cannot certify any proposed code motion as bit-identical from source inspection alone. The safest candidates are memoization that calls the same helper with exactly the same inputs, or reuses its returned value without rearranging density arithmetic. The existing line-context value-noise code preserves the visible nested lerp order, but moving work into another context can change inlining or floating-point contraction. I did not verify compiler FP settings or generated code, so FMA/reassociation parity remains **inferred, not verified**. Reordering composer features is especially risky because the density updates are sequential. [Value-noise line context](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:11650>), [composition order](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:2011>)
|
||||
|
||||
**Candidate-list correction:** primitive queries use 32-voxel spatial bins, not a per-tile primitive list; each sample scans the references in its selected 3D bin. Mouth-support segments do have per-tile candidates. The active legacy FI has no matching native `IslandPlan`, so native island feature lists and the native island column cache do not serve these FI samples. [Bin size](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:2646>), [FI plan guard](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:7434>), [L73 path finding](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux73/REPORT.md:67>)
|
||||
|
||||
Dropping detail octaves or changing float/double arithmetic remains **[INFERENCE] [WORLD-CHANGING]**: even if visually similar, those changes alter field bits and may alter signs near zero.
|
||||
|
||||
Read-only source/report inspection only; no program, build, or tests were run.
|
||||
@@ -0,0 +1,76 @@
|
||||
# Linux76 report: legacy FI/TN sample cost
|
||||
|
||||
## Per-term profile
|
||||
|
||||
The term profiler sampled 1 in 256 density points and scaled timed calls by their observed counts. Worker-ms are summed across LODs; ns/sample divides by legacy FI/TN field evaluations. These are sampled estimates, not wall time. Before is L75; after is the final constants-only source.
|
||||
|
||||
| Term | FI worker-ms / ns per sample, before → after | TN worker-ms / ns per sample, before → after |
|
||||
|---|---:|---:|
|
||||
| Primitive scan | 1,116 / 186 → 1,133 / 189 | 997 / 179 → 1,037 / 186 |
|
||||
| DetailNoise call (pre-noise fade excluded) | 38 / 6.4 → 39 / 6.5 | 63 / 11.3 → 68 / 12.2 |
|
||||
| ↳ Surface relief noise, inside detail | 7.6 / 1.3 → 7.7 / 1.3 | 12.3 / 2.2 → 13.2 / 2.4 |
|
||||
| ↳ Owner noise and warp, inside detail | 26.2 / 4.4 → 26.9 / 4.5 | 43.2 / 7.8 → 46.7 / 8.4 |
|
||||
| Composer cavern | 142 / 23.6 → 145 / 24.1 | 122 / 21.8 → 127 / 22.8 |
|
||||
| Composer boundary band | 338 / 56.3 → 350 / 58.2 | 306 / 55.0 → 321 / 57.8 |
|
||||
| Composer passage/support | 6,462 / 1,076 → 6,323 / 1,053 | 5,475 / 984 → 5,522 / 993 |
|
||||
| Composer stamps | 0 / 0 → 0 / 0 | 0 / 0 → 0 / 0 |
|
||||
| Edit brushes | 133 / 22.1 → 135 / 22.5 | 114 / 20.5 → 119 / 21.3 |
|
||||
| Smooth-min across legacy evaluation | 166 / 27.6 → 170 / 28.3 | 143 / 25.6 → 151 / 27.2 |
|
||||
|
||||
FI used 6,005,989 legacy samples and TN 5,562,334. The primitive bins supplied 2.75 FI / 3.85 TN candidate references per sample; only 0.251 / 0.237 per sample reached primitive-distance evaluation. The code scans references in the selected 3D bin, not the full primitive set. Smooth-min ran about 1.27 times per legacy sample and overlaps the primitive/composer rows; do not add it again. The noise subrows are inside `DetailNoise`. Its timer excludes the preceding fade and layer lookup. Surface relief evaluates 3D value-noise wavelengths 32, 16, and 4; noise timers cover helper calls, not individual octave loops.
|
||||
|
||||
Passage/support dominates: about 1.05–1.08 μs per sample versus 0.18–0.19 μs for primitive scan. Per-tile filtering of the upper-mouth scan matched hashes but regressed TN by 5.43% in the first whole-view timing set, so it was removed. Broad per-tile passage-loop filtering failed hashes and was rejected. The remaining relief-call counter includes path-relief calls and does not establish repeated triplets for the same sample; no sample cache was shipped.
|
||||
|
||||
The profiler adds about 1.9% on FI and 2.1% on TN by the median of three paired W8 runs against the same clean build. Pair deltas were FI +1.90%, −3.33%, +2.19%; TN +2.10%, +7.00%, −1.91%. This spread is visible instrumentation cost, not field speedup. Profile inputs and summaries are in `Saved/Linux/l76/term-profile-before/`, `term-profile-constants-only/`, and `term-profiler-overhead-constants-only/`.
|
||||
|
||||
## Invariants and changes
|
||||
|
||||
- `StrataDistance(Z)`, layer lookup, and bound-stratum lookup are Z-only. `FloorAt(P,X,Y)` and some projection parameters are XY-only for a fixed primitive/feature, but primitive selection, floor blending, and the resulting composer values still depend on Z.
|
||||
- Surface and passage relief use 3D noise; owner warps also depend on effective Z. The SW column cache is not reused for these values.
|
||||
- Cached resolved-passage metadata once at field binding: internal-chain and landing flags, first/last floor Q, lower-mouth index, carve margin, and mouth search limits. Per-sample arithmetic order is unchanged. The profiler is opt-in at build time with `VOXELFORGE_PROFILE_LEGACY_TERMS=1`; timer bodies compile out in default builds.
|
||||
- The whole-view effect of the constants hoist is mixed and near noise. No FP reordering was made, hashes stayed identical, and no version bump was needed.
|
||||
|
||||
## Hashes
|
||||
|
||||
Final Clang/GCC candidate per-tile hashes matched the L75 reference for density, mesh, payload, and topology in all 24 compiler × worker × view comparisons (W1/W8/W12, four views). Each compiler/worker set covered 9,661 tiles.
|
||||
|
||||
Final Clang/GCC source stamp: `285080616b0c5b65a670a72858e3b7e922b302df183b247c238504d58aa2b82f`.
|
||||
|
||||
| View | Tiles | Density | Mesh | Payload | Topology |
|
||||
|---|---:|---|---|---|---|
|
||||
| SW | 2,850 | `40b76d0072a2adcd` | `8559df0850d4d7f1` | `7700965ff31ac702` | `0e7f2a16b4cc71a9` |
|
||||
| FI | 2,249 | `99ca213b21ce86e4` | `2977525405b79fff` | `3d7d2efb581710bd` | `af1916c949224871` |
|
||||
| TN | 2,233 | `8d23c26f5c092be1` | `15c3aad2e29d9ea0` | `3d48f68d511c21df` | `5b1584676884edd8` |
|
||||
| VS | 2,329 | `876b44981d106be3` | `aaef57cb08bd6845` | `b19617df9f4acbb1` | `e8eee991260adff4` |
|
||||
|
||||
The rows above are the matching W8 Clang whole-view hashes; GCC and the other worker counts matched each corresponding L75 tile hash as well. Per-tile comparison summary: `Saved/Linux/l76/hashes/final-clean/summary.json`.
|
||||
|
||||
## Whole-view timing
|
||||
|
||||
Three interleaved W8 pairs per view; seconds are `(L75 → L76)` in pair order. The delta is the median of paired percentage changes.
|
||||
|
||||
| View | Pair times | Median paired delta |
|
||||
|---|---|---:|
|
||||
| SW | 2.428→2.404, 2.499→2.456, 2.343→2.347 | −0.024 s (−0.99%) |
|
||||
| FI | 4.505→4.668, 4.778→4.635, 4.414→4.413 | −0.001 s (−0.02%) |
|
||||
| TN | 4.389→4.289, 4.132→4.408, 4.417→4.337 | −0.080 s (−1.81%) |
|
||||
| VS | 2.454→2.489, 2.353→2.426, 2.365→2.461 | +0.073 s (+3.10%) |
|
||||
|
||||
The largest median paired change is +0.073 s (VS); the best is −0.080 s (TN). Whole views remain 2.4–4.6 s, far from the ~1 s goal. Passage/support remains the next cost target.
|
||||
|
||||
Timing rows and logs are in `Saved/Linux/l76/timing-constants-only/summary.json`.
|
||||
|
||||
## Audits and builds
|
||||
|
||||
- Final `verify.py fast`: PASS in 381.8 s; passage audits, fast-tier hash gates, and controls completed.
|
||||
- Final fast-tier summary: `Saved/Linux/l76/verify/fast-summary.json`.
|
||||
- Linux UE host `VFHostEditor Linux Development`: succeeded in 121.98 s with `taskset -c 0-11`, `-MaxParallelActions=4`, and no address-space limit. Three existing RealtimeMesh deprecation warnings were emitted. Log: `Saved/Linux/l76/ue-build.log`.
|
||||
- All headless runs used `taskset -c 0-11` and `prlimit --as=6442450944`; no run exceeded 30 minutes.
|
||||
|
||||
## Failed runs
|
||||
|
||||
- Broad per-tile passage-loop filtering failed Clang W1 SW hashes: 612 density mismatches and 24 each in mesh, payload, and topology. It was removed.
|
||||
- The first fast-tier launch stopped before tests because its output-directory executable had not been built. After building the requested binaries, the final fast tier passed.
|
||||
- The first UE invocation used a nonexistent direct `Engine/Binaries/DotNET/dotnet` path. The engine `Build.sh` wrapper set up bundled DotNet and the host build succeeded.
|
||||
|
||||
No commit, push, or stash was made.
|
||||
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Recheck the final candidate against the L75 logs captured by RunHashMatrixL76."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import RunHashMatrixL76 as matrix
|
||||
|
||||
|
||||
REFERENCE_OUT = matrix.OUT
|
||||
FINAL_OUT = matrix.ROOT / "Saved/Linux/l76/hashes/final-clean"
|
||||
matrix.OUT = FINAL_OUT
|
||||
|
||||
|
||||
def main() -> int:
|
||||
FINAL_OUT.mkdir(parents=True, exist_ok=True)
|
||||
rows, comparisons = [], []
|
||||
for compiler in matrix.BINARIES:
|
||||
for workers in (1, 8, 12):
|
||||
for strate in range(4):
|
||||
reference = REFERENCE_OUT / compiler / f"w{workers}" / f"strate{strate}-before.log"
|
||||
candidate = matrix.run(compiler, workers, strate, "after")
|
||||
rows.append(candidate)
|
||||
comparison = matrix.compare(reference, matrix.ROOT / candidate["log"])
|
||||
comparison.update(compiler=compiler, workers=workers, strate=strate)
|
||||
comparisons.append(comparison)
|
||||
print("[L76HashFinalCompare] " + json.dumps(comparison, sort_keys=True), flush=True)
|
||||
if not comparison["equal"]:
|
||||
raise SystemExit(f"hash mismatch; see {comparison['candidate']}")
|
||||
result = {"candidate_runs": rows, "comparisons": comparisons,
|
||||
"equal": all(row["equal"] for row in comparisons),
|
||||
"reference_matrix": str(REFERENCE_OUT.relative_to(matrix.ROOT))}
|
||||
(FINAL_OUT / "summary.json").write_text(json.dumps(result, indent=2) + "\n",
|
||||
encoding="utf-8")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,127 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compare exact per-tile hashes for L75 reference and L76 on Clang/GCC W1/W8/W12."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l76/hashes"
|
||||
DATA = ROOT / "Standalone/data/owner_world.json"
|
||||
BINARIES = {
|
||||
"clang": {
|
||||
"before": ROOT / "Saved/Linux/l75/bin/VoxelForgeCoreHeadless-l75-clang",
|
||||
"after": ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-l76-clang",
|
||||
},
|
||||
"gcc": {
|
||||
"before": ROOT / "Saved/Linux/l75/bin-gcc/VoxelForgeCoreHeadless-l75-gcc",
|
||||
"after": ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-l76-gcc",
|
||||
},
|
||||
}
|
||||
CAMERAS = {
|
||||
0: (8.0, 16.0, -372.2626953125),
|
||||
1: (25.8369140625, 3.19921875, -737.0),
|
||||
2: (19.166015625, -14.2001953125, -753.3740234375),
|
||||
3: (-20.0625, -5.068359375, -900.353515625),
|
||||
}
|
||||
HASH = re.compile(
|
||||
r"\[PipelineTileHash\] lod=(\d+) x=(-?\d+) y=(-?\d+) z=(-?\d+) "
|
||||
r"density=(0x[0-9a-fA-F]+) mesh=(0x[0-9a-fA-F]+) "
|
||||
r"payload=(0x[0-9a-fA-F]+) topology=(0x[0-9a-fA-F]+)"
|
||||
)
|
||||
PAIR = re.compile(r"([a-zA-Z0-9_]+)=([^ ]+)")
|
||||
|
||||
|
||||
def read_hashes(path: Path) -> dict[tuple[int, int, int, int], dict[str, str]]:
|
||||
result = {}
|
||||
with path.open(encoding="utf-8", errors="replace") as stream:
|
||||
for line in stream:
|
||||
match = HASH.search(line)
|
||||
if match:
|
||||
lod, x, y, z = (int(match.group(index)) for index in range(1, 5))
|
||||
result[(lod, x, y, z)] = {
|
||||
"density": match.group(5).lower(), "mesh": match.group(6).lower(),
|
||||
"payload": match.group(7).lower(), "topology": match.group(8).lower(),
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
def run(compiler: str, workers: int, strate: int, mode: str) -> dict:
|
||||
folder = OUT / compiler / f"w{workers}"
|
||||
folder.mkdir(parents=True, exist_ok=True)
|
||||
log = folder / f"strate{strate}-{mode}.log"
|
||||
camera = ",".join(f"{value:.12f}" for value in CAMERAS[strate]) + ",0,0"
|
||||
command = [
|
||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", "timeout",
|
||||
"1740s", str(BINARIES[compiler][mode]), str(DATA), "--world-assembler-prototype",
|
||||
"--stream-benchmark", "--stream-pipeline", "--force-generate",
|
||||
f"--workers={workers}", "--clip-radius=3", "--max-clip-level=8",
|
||||
"--max-level=8", f"--camera={camera}", "--stream-hash-tiles",
|
||||
]
|
||||
started = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as output:
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=output,
|
||||
stderr=subprocess.STDOUT, check=False, timeout=1740)
|
||||
hashes = read_hashes(log)
|
||||
if result.returncode != 0 or not hashes:
|
||||
raise SystemExit(f"hash generation failed or no tile hashes; see {log}")
|
||||
row = {
|
||||
"compiler": compiler, "workers": workers, "strate": strate, "mode": mode,
|
||||
"exit_code": result.returncode, "wall_s": round(time.monotonic() - started, 3),
|
||||
"tile_hashes": len(hashes), "log": str(log.relative_to(ROOT)),
|
||||
}
|
||||
print("[L76HashRun] " + json.dumps(row, sort_keys=True), flush=True)
|
||||
return row
|
||||
|
||||
|
||||
def compare(reference: Path, candidate: Path) -> dict:
|
||||
expected, actual = read_hashes(reference), read_hashes(candidate)
|
||||
missing, extra = expected.keys() - actual.keys(), actual.keys() - expected.keys()
|
||||
mismatches = {name: 0 for name in ("density", "mesh", "payload", "topology")}
|
||||
for key in expected.keys() & actual.keys():
|
||||
for name in mismatches:
|
||||
if expected[key][name] != actual[key][name]:
|
||||
mismatches[name] += 1
|
||||
return {
|
||||
"reference": str(reference.relative_to(ROOT)),
|
||||
"candidate": str(candidate.relative_to(ROOT)),
|
||||
"reference_tiles": len(expected), "candidate_tiles": len(actual),
|
||||
"missing_keys": len(missing), "extra_keys": len(extra),
|
||||
"mismatches": mismatches,
|
||||
"equal": not missing and not extra and not any(mismatches.values()),
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
missing = [str(path) for compiler in BINARIES.values()
|
||||
for path in compiler.values() if not path.is_file()]
|
||||
if not DATA.is_file():
|
||||
missing.append(str(DATA))
|
||||
if missing:
|
||||
raise SystemExit("missing input: " + ", ".join(missing))
|
||||
rows, comparisons = [], []
|
||||
for compiler in BINARIES:
|
||||
for workers in (1, 8, 12):
|
||||
for strate in range(4):
|
||||
before = run(compiler, workers, strate, "before")
|
||||
after = run(compiler, workers, strate, "after")
|
||||
rows.extend((before, after))
|
||||
comparison = compare(ROOT / before["log"], ROOT / after["log"])
|
||||
comparison.update(compiler=compiler, workers=workers, strate=strate)
|
||||
comparisons.append(comparison)
|
||||
print("[L76HashCompare] " + json.dumps(comparison, sort_keys=True), flush=True)
|
||||
if not comparison["equal"]:
|
||||
raise SystemExit(f"hash mismatch; see {comparison['candidate']}")
|
||||
result = {"runs": rows, "comparisons": comparisons,
|
||||
"equal": all(row["equal"] for row in comparisons)}
|
||||
(OUT / "matrix-summary.json").write_text(json.dumps(result, indent=2) + "\n",
|
||||
encoding="utf-8")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Capture sampled legacy FI/TN field-term costs for the four owner views."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / os.environ.get("VOXELFORGE_L76_PROFILE_OUT",
|
||||
"Saved/Linux/l76/term-profile-before")
|
||||
DATA = ROOT / "Standalone/data/owner_world.json"
|
||||
BIN = ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-l76-termprofile-clang"
|
||||
CAMERAS = {
|
||||
0: (8.0, 16.0, -372.2626953125),
|
||||
1: (25.8369140625, 3.19921875, -737.0),
|
||||
2: (19.166015625, -14.2001953125, -753.3740234375),
|
||||
3: (-20.0625, -5.068359375, -900.353515625),
|
||||
}
|
||||
KV = re.compile(r"([a-zA-Z0-9_]+)=([^ ]+)")
|
||||
|
||||
|
||||
def run(strate: int) -> dict:
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
log = OUT / f"strate{strate}-w8.log"
|
||||
camera = ",".join(f"{value:.12f}" for value in CAMERAS[strate]) + ",0,0"
|
||||
command = [
|
||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", "timeout",
|
||||
"1740s", str(BIN), str(DATA), "--world-assembler-prototype", "--stream-benchmark",
|
||||
"--stream-pipeline", "--force-generate", "--workers=8", "--clip-radius=3",
|
||||
"--max-clip-level=8", "--max-level=8", f"--camera={camera}",
|
||||
"--profile-lod-work", "--instrument-tile-drain", "--profile-passage-composer",
|
||||
]
|
||||
started = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as output:
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=output,
|
||||
stderr=subprocess.STDOUT, check=False, timeout=1740)
|
||||
rows: dict[int, dict[str, dict]] = {}
|
||||
pipeline = {}
|
||||
with log.open(encoding="utf-8", errors="replace") as stream:
|
||||
for line in stream:
|
||||
if line.startswith("[WorldAssemblerLODLegacyField]"):
|
||||
row = dict(KV.findall(line))
|
||||
rows.setdefault(int(row["lod"]), {})["field"] = row
|
||||
elif line.startswith("[WorldAssemblerLODLegacyTerm]"):
|
||||
row = dict(KV.findall(line))
|
||||
rows.setdefault(int(row["lod"]), {}).setdefault("terms", {})[row["term"]] = row
|
||||
elif line.startswith("[WorldAssemblerLODClassification]"):
|
||||
row = dict(KV.findall(line))
|
||||
rows.setdefault(int(row["lod"]), {})["classification"] = row
|
||||
elif line.startswith("[WorldAssemblerLODWorker]"):
|
||||
row = dict(KV.findall(line))
|
||||
rows.setdefault(int(row["lod"]), {})["worker"] = row
|
||||
elif line.startswith("[StreamPipeline]"):
|
||||
pipeline = dict(KV.findall(line))
|
||||
if result.returncode != 0 or not pipeline or not rows:
|
||||
raise SystemExit(f"profile run failed or summary missing; see {log}")
|
||||
row = {
|
||||
"strate": strate,
|
||||
"exit_code": result.returncode,
|
||||
"process_wall_s": round(time.monotonic() - started, 3),
|
||||
"whole_view_ready_s": float(pipeline["whole_view_ready"].removesuffix("s")),
|
||||
"whole_tiles": int(pipeline["whole_tiles"].split("/")[0]),
|
||||
"lod": rows,
|
||||
"log": str(log.relative_to(ROOT)),
|
||||
}
|
||||
(OUT / f"strate{strate}-w8.json").write_text(json.dumps(row, indent=2) + "\n",
|
||||
encoding="utf-8")
|
||||
print("[L76TermProfileRun] " + json.dumps({k: v for k, v in row.items() if k != "lod"},
|
||||
sort_keys=True), flush=True)
|
||||
return row
|
||||
|
||||
|
||||
def main() -> int:
|
||||
missing = [str(path) for path in (DATA, BIN) if not path.is_file()]
|
||||
if missing:
|
||||
raise SystemExit("missing input: " + ", ".join(missing))
|
||||
rows = [run(strate) for strate in range(4)]
|
||||
(OUT / "summary.json").write_text(json.dumps(rows, indent=2) + "\n",
|
||||
encoding="utf-8")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run three interleaved whole-view timing pairs for L75 and L76 Clang builds."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import statistics
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / os.environ.get("VOXELFORGE_L76_TIMING_OUT", "Saved/Linux/l76/timing")
|
||||
DATA = ROOT / "Standalone/data/owner_world.json"
|
||||
BINARIES = {
|
||||
"before": ROOT / "Saved/Linux/l75/bin/VoxelForgeCoreHeadless-l75-clang",
|
||||
"after": ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-l76-clang",
|
||||
}
|
||||
CAMERAS = {
|
||||
0: (8.0, 16.0, -372.2626953125),
|
||||
1: (25.8369140625, 3.19921875, -737.0),
|
||||
2: (19.166015625, -14.2001953125, -753.3740234375),
|
||||
3: (-20.0625, -5.068359375, -900.353515625),
|
||||
}
|
||||
ORDERS = (("before", "after"), ("after", "before"), ("before", "after"))
|
||||
PIPELINE = re.compile(r"\[StreamPipeline\] (.*)")
|
||||
PAIR = re.compile(r"([a-zA-Z0-9_]+)=([^ ]+)")
|
||||
|
||||
|
||||
def run(strate: int, pair: int, mode: str) -> dict:
|
||||
label = f"strate{strate}-pair{pair}-{mode}"
|
||||
log = OUT / f"{label}.log"
|
||||
camera = ",".join(f"{value:.12f}" for value in CAMERAS[strate]) + ",0,0"
|
||||
command = [
|
||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", "timeout",
|
||||
"1740s", str(BINARIES[mode]), str(DATA), "--world-assembler-prototype",
|
||||
"--stream-benchmark", "--stream-pipeline", "--force-generate", "--workers=8",
|
||||
"--clip-radius=3", "--max-clip-level=8", "--max-level=8", f"--camera={camera}",
|
||||
]
|
||||
started = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as output:
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=output,
|
||||
stderr=subprocess.STDOUT, check=False, timeout=1740)
|
||||
content = log.read_text(encoding="utf-8", errors="replace")
|
||||
match = PIPELINE.search(content)
|
||||
stamp = re.search(r"^\[VoxelForgeSourceStamp\] sha256=(\S+)$", content, re.M)
|
||||
if result.returncode != 0 or match is None:
|
||||
raise SystemExit(f"timing run failed or summary missing; see {log}")
|
||||
pipeline = dict(PAIR.findall(match.group(1)))
|
||||
row = {
|
||||
"strate": strate,
|
||||
"pair": pair,
|
||||
"mode": mode,
|
||||
"exit_code": result.returncode,
|
||||
"process_wall_s": round(time.monotonic() - started, 3),
|
||||
"whole_view_ready_s": float(pipeline["whole_view_ready"].removesuffix("s")),
|
||||
"stream_wall_s": float(pipeline["stream_wall"].removesuffix("s")),
|
||||
"whole_tiles": int(pipeline["whole_tiles"].split("/")[0]),
|
||||
"source_stamp": stamp.group(1) if stamp else None,
|
||||
"log": str(log.relative_to(ROOT)),
|
||||
}
|
||||
print("[L76TimingPairRun] " + json.dumps(row, sort_keys=True), flush=True)
|
||||
return row
|
||||
|
||||
|
||||
def main() -> int:
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
missing = [str(path) for path in [DATA, *BINARIES.values()] if not path.is_file()]
|
||||
if missing:
|
||||
raise SystemExit("missing input: " + ", ".join(missing))
|
||||
rows = []
|
||||
for strate in range(4):
|
||||
for pair, order in enumerate(ORDERS, 1):
|
||||
for mode in order:
|
||||
rows.append(run(strate, pair, mode))
|
||||
summary = []
|
||||
for strate in range(4):
|
||||
group = [row for row in rows if row["strate"] == strate]
|
||||
before = [row["whole_view_ready_s"] for row in group if row["mode"] == "before"]
|
||||
after = [row["whole_view_ready_s"] for row in group if row["mode"] == "after"]
|
||||
summary.append({
|
||||
"strate": strate,
|
||||
"pairs": len(before),
|
||||
"before_s": [round(value, 3) for value in before],
|
||||
"after_s": [round(value, 3) for value in after],
|
||||
"before_median_s": round(statistics.median(before), 3),
|
||||
"after_median_s": round(statistics.median(after), 3),
|
||||
"change_pct": round(100.0 * (statistics.median(after)
|
||||
/ statistics.median(before) - 1.0), 2),
|
||||
})
|
||||
result = {"runs": rows, "summary": summary}
|
||||
(OUT / "summary.json").write_text(json.dumps(result, indent=2) + "\n",
|
||||
encoding="utf-8")
|
||||
for row in summary:
|
||||
print("[L76TimingPairSummary] " + json.dumps(row, sort_keys=True), flush=True)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
BIN
Binary file not shown.
Reference in new issue
Block a user