Merge remote-tracking branch 'refs/remotes/origin/master'

This commit is contained in:
Harry Chen
2026-09-29 14:41:35 -04:00
29 changed files with 1929 additions and 54 deletions
+6 -1
View File
@@ -175,6 +175,8 @@ src/
│ seeding -> step loop -> eval. Both the CLI and
│ the GUI drive this; it lives in the engine
│ library (cmake/sources.txt), not the app targets.
│ TrainForecast.{h,cpp} beside it is the run's ETA
│ and VRAM forecast -- docs/notes/train-forecast.md
├── config/ TrainConfig.h — the training config's single source
│ of truth: one X-macro row per flag, hand-written.
│ TrainConfigJson.h is the one flat-JSON encoding
@@ -584,7 +586,10 @@ no ceremony — do not ask, do not leave a note saying you removed it.
`SS_POOL_ALIAS_POISON=1` fills the arena at every phase switch so a read
that outlives its phase becomes NaNs a parity test catches, and
`SS_POOL_ALIAS=0` turns the whole thing off. Read
`docs/notes/vram-splat-x-img.md` before adding a row.
`docs/notes/vram-splat-x-img.md` before adding a row. A buffer whose
length follows the LIVE splat count goes in `POOL_LIVE_SPLAT_TABLE`, so it
is sized for `cap_max` once: the trainer's VRAM forecast assumes only
`splat x img` grows during a run.
- **`SS_PROFILE=1`** enables the per-stage backend timing breakdown
(H2D / D2H / D2D / memset / device / host), header-only, both backends, plus
a per-category VRAM breakdown after any run that trained. What the biggest
+2
View File
@@ -39,6 +39,7 @@ src/data/*.cpp
src/data/parsers/*.cpp
src/app/webviewer/*.cpp
src/app/TrainerCore.cpp
src/app/TrainForecast.cpp
src/app/EvalMetrics.cpp
src/external/*.cpp
src/external/*.c
@@ -55,6 +56,7 @@ src/data/*.cpp
src/data/parsers/*.cpp
src/app/webviewer/*.cpp
src/app/TrainerCore.cpp
src/app/TrainForecast.cpp
src/app/EvalMetrics.cpp
src/external/*.cpp
src/external/*.c
+99
View File
@@ -0,0 +1,99 @@
# Training ETA and VRAM forecast
`app/TrainForecast.{h,cpp}` predicts, while a run trains, how long it has left
and how much device memory it will peak at. `TrainerSession::train()` feeds it
one sample per step; the CLI's progress line, `/progress`, the GUI's status
strip and the VRAM bar's hover card read it. `train_forecast_test` checks both
against synthetic runs whose answers are known, and `SS_FORECAST_LOG=1` prints
its state every 100 steps for checking it against a real run.
Both models lean on the same fact: until the model reaches `cap_max`, the
splat count still to come is **known**. `engine_densify_step` grows it by
`densify_grows_at` / `densify_target` (`engine/EngineConfig.h`), and
`SplatSchedule` replays that same rule forward. The SH degree schedule
(`min(step / sh_degree_warmup_every, sh_degree)`) is known the same way. So
nothing extrapolates the count or the degree -- only what they cost.
## ETA
A step costs `a + (b0 + b1 * k) * N^gamma`: `N` in millions of splats, `k`
the SH coefficients the step trains (0, 3, 8, 15), `a` everything that does
not scale with the model (loss, bilateral grids, PPISP, data loading, host
overhead). Per window of `refine_every` steps -- so every window holds the
same share of densify steps -- a Kalman filter updates `[a, b0, b1, c]` from:
- the window's wall time per step, outliers past 3x the median dropped (the
pipeline builds when the SH degree steps up are one-offs);
- the GPU time of the splat stages, `c + (b0 + b1 * k) * N^gamma`.
`SplatStageTimer` (`engine/EngineStepTiming.cpp`) brackets the forward, the
raster / projection backward, the optimizer and densify with timestamp
events on one step in ten; on Vulkan every bracket is a queue submission.
Those stages are NOT all per-splat: rasterization has a large per-pixel part,
~45% of their time on bonsai at 1/4 resolution and nearly all of it at 1/2.
Treating their time as `b * N` overshot the ETA there by +130%. Hence the
intercept `c`: the GPU measurement pins the slope once the count moves (it
has less host noise than the wall clock), and says nothing about it before.
While the count holds still the slope is a prior -- half the splat stages'
time, +-70% -- and the ETA's sigma says so.
No single `gamma` fits: whole-run fits gave 1.0 on bonsai at 1/4 resolution,
0.85 at 1/2 and 0.8 on garden. So three filters run, at 0.75, 0.875 and 1.0,
each scored on how well it predicted each window before seeing it
(discounted by 0.9 a window), and the ETA is the weighted mixture. Updates
are gated at 2 sigma with a 10% floor: the laptop GPU these were measured on
ran whole 100-step windows 40-60% slow, four times in one run.
The ETA also adds the checkpoint saves still to come, at the seconds per
million splats the saves so far took; a save is excluded from its step's
wall time for that reason.
Replayed on three recorded runs, error in the time left:
| step | 300 | 1000 | 2000 | 3000 | 4000 | 5000 |
|---|---|---|---|---|---|---|
| bonsai 1/4, naive (last 100 steps) | -66% | -65% | -59% | -50% | -29% | +3% |
| bonsai 1/4, model | -25% | -14% | -10% | -12% | -4% | +4% |
| bonsai 1/2, naive | -62% | -63% | -61% | -54% | -43% | -36% |
| bonsai 1/2, model | +27% | +55% | +16% | -10% | -3% | -1% |
| garden 1/4, naive | -23% | -25% | -23% | -30% | -15% | -24% |
| garden 1/4, model | +34% | +22% | +23% | +30% | 0% | -3% |
The naive estimate is biased low everywhere the count still grows; the model
is not biased, and its early error is what the prior cannot know.
## VRAM
Only `splat x img` grows during training (docs/notes/vram-splat-x-img.md),
so ours at a future step is everything else now plus the growth, projected.
Two measurements decide what "the growth" is:
- It is read off the process **high-water**, not the pool. A step that
overflows the alias arena holds private buffers beside it until the next
phase switch, so the real peak sits above the steady cap: 179 against 156
MiB on bonsai at 1/4, from one step near the end.
- The first densify step allocates `splat x img` scratch of its own -- 10 ->
55 MiB on garden, count unchanged -- so the fit starts after it. Before
that the forecast is provisional: growth all per splat, +-70%, plus 15% +-
8% of the splat category for the live-splat scratch the step reserves
(14.9% measured on bonsai).
The growth is `g0 + g1 * N`, a Kalman filter over `[g0, g1]` from one
high-water measurement per window, starting at `g0 = 0`: fitting a fixed
chunk from the first windows under-read bonsai's peak by 14 sigma, while
putting it all per splat over-reads garden's (its 55 MiB chunk) by up to
36% -- the safe side. Two terms widen the projection: the expected largest
per-step demand over the draws still to come at each count, minus what a
window already saw (the pool keeps the largest draw, and a view that sees
more of the model sets it); and 0.3 relative sigma per e-fold the count is
extrapolated.
"Others" is device use minus ours (on CUDA that includes the context), an
exponential average with its spread. The chance of running out is the normal
tail of `peak + others` above 99% of the device -- the last percent is
fragmentation. Low below 10%, medium below 50%, high above; the trainer logs a
warning the first time the risk reaches each of medium and high.
On the three runs, from the first densify step on, the projected peak stayed
within 2 sigma or above the truth, by at most 8% (bonsai 1/4), 10% (bonsai
1/2) and 36% (garden) of it.
+15
View File
@@ -15,6 +15,21 @@ Three numbers describe a step, and every buffer below is one of them:
| `nnz` | pairs that survive the visibility test | 15.1M |
| `n_isects` | (splat, tile) pairs the raster consumes | 22M - 36M |
## Only this category grows
Splat buffers are sized for `cap_max`, so everything else a run allocates is
fixed once the first densify step has run -- which is what lets the trainer
forecast its peak from this category alone. Scratch requested per LIVE splat
(the densify draws, `raster_bwd.accum_weight`, `fused_proj_bwd.cam_bounds`)
is Splat too: `POOL_LIVE_SPLAT_TABLE` in `core/PoolSlots.h` makes the pool
allocate it for `cap_max` on first use, scaling the request by cap / live, and
`POOL_SPLAT_DRAW_TABLE` gives the two index lists whose length is a draw count
`cap_max` entries outright. Measured on bonsai at 1/4 resolution, 228k -> 666k
splats: splat stays at 1042.6 MiB (revised) / 1019.7 MiB (MCMC); without the
tables it climbs 11 MiB over the same run and `other` another 15 MiB. A new buffer whose length
follows the live count goes in one of those two tables, or the forecast
under-reads the peak.
## What each buffer costs
Per element, after the 2026-08-30 pass:
+520
View File
@@ -0,0 +1,520 @@
// TrainForecast.cpp -- see TrainForecast.h and docs/notes/train-forecast.md.
#include "app/TrainForecast.h"
#include <algorithm>
#include <cmath>
namespace spirula {
namespace {
// One scalar measurement y = h . x with variance r. `gate` > 0 inflates r
// until the innovation is at most `gate` sigma, so one bad window nudges the
// state instead of yanking it.
template <int D>
void kf_update(double (&x)[D], double (&P)[D][D], const double (&h)[D], double y,
double r, double gate = 0.0) {
double ph[D];
double s = r, pred = 0.0;
for (int i = 0; i < D; ++i) {
ph[i] = 0.0;
for (int j = 0; j < D; ++j) ph[i] += P[i][j] * h[j];
s += h[i] * ph[i];
pred += h[i] * x[i];
}
if (!(s > 0.0)) return;
const double innov = y - pred;
if (gate > 0.0 && innov * innov > gate * gate * s) s = innov * innov / (gate * gate);
for (int i = 0; i < D; ++i) x[i] += ph[i] / s * innov;
for (int i = 0; i < D; ++i)
for (int j = 0; j < D; ++j) P[i][j] -= ph[i] * ph[j] / s;
}
template <int D>
double quad(const double (&P)[D][D], const double (&h)[D]) {
double q = 0.0;
for (int i = 0; i < D; ++i)
for (int j = 0; j < D; ++j) q += h[i] * P[i][j] * h[j];
return std::max(0.0, q);
}
double norm_cdf(double z) { return 0.5 * std::erfc(-z / std::sqrt(2.0)); }
double norm_pdf(double z) {
return std::exp(-0.5 * z * z) / std::sqrt(2.0 * 3.14159265358979323846);
}
// Acklam's rational approximation, relative error < 1.2e-9.
double norm_quantile(double p) {
static const double a[] = {-3.969683028665376e+01, 2.209460984245205e+02,
-2.759285104469687e+02, 1.383577518672690e+02,
-3.066479806614716e+01, 2.506628277459239e+00};
static const double b[] = {-5.447609879822406e+01, 1.615858368580409e+02,
-1.556989798598866e+02, 6.680131188771972e+01,
-1.328068155288572e+01};
static const double c[] = {-7.784894002430293e-03, -3.223964580411365e-01,
-2.400758277161838e+00, -2.549732539343734e+00,
4.374664141464968e+00, 2.938163982698783e+00};
static const double d[] = {7.784695709041462e-03, 3.224671290700398e-01,
2.445134137142996e+00, 3.754408661907416e+00};
p = std::min(std::max(p, 1e-12), 1.0 - 1e-12);
if (p < 0.02425) {
const double q = std::sqrt(-2.0 * std::log(p));
return (((((c[0] * q + c[1]) * q + c[2]) * q + c[3]) * q + c[4]) * q + c[5]) /
((((d[0] * q + d[1]) * q + d[2]) * q + d[3]) * q + 1.0);
}
if (p > 1.0 - 0.02425) return -norm_quantile(1.0 - p);
const double q = p - 0.5, r = q * q;
return (((((a[0] * r + a[1]) * r + a[2]) * r + a[3]) * r + a[4]) * r + a[5]) * q /
(((((b[0] * r + b[1]) * r + b[2]) * r + b[3]) * r + b[4]) * r + 1.0);
}
// Expected largest of n standard normals (Blom's plotting position).
double expected_max(double n) {
if (n <= 1.0) return 0.0;
return norm_quantile((n - 0.375) / (n + 0.25));
}
// max(c, X) for X ~ N(mu, sigma^2), moment-matched back to a normal.
void max_with(double mu, double sigma, double c, double& mean, double& sd) {
if (sigma <= 0.0) { mean = std::max(mu, c); sd = 0.0; return; }
const double d = (mu - c) / sigma;
const double P = norm_cdf(d), phi = norm_pdf(d);
mean = mu * P + sigma * phi + c * (1.0 - P);
const double m2 = (mu * mu + sigma * sigma) * P + (mu + c) * sigma * phi +
c * c * (1.0 - P);
sd = std::sqrt(std::max(0.0, m2 - mean * mean));
}
// Mean and its variance, over the samples within 3x the median: a pipeline
// build when the SH degree steps up is a one-off, not what later steps cost.
void robust_mean(const std::vector<double>& v, double& mean, double& var) {
std::vector<double> s = v;
std::nth_element(s.begin(), s.begin() + s.size() / 2, s.end());
const double cut = 3.0 * s[s.size() / 2];
double sum = 0.0, sq = 0.0;
int k = 0;
for (double x : v)
if (x <= cut) { sum += x; sq += x * x; ++k; }
mean = sum / k;
var = std::max(0.0, sq / k - mean * mean) / k;
}
// Live-splat scratch the first densify step allocates for cap_max, as a
// share of the Splat category before it: 14.9% on bonsai (revised, SH 3).
constexpr double kFirstDensifySplatShare = 0.15;
constexpr double kFirstDensifySplatSigma = 0.08;
// Pipeline creation and first-touch allocation: bonsai's first 100 steps ran
// 17% slower than the next 500 at the same splat count.
constexpr int kWarmupSteps = 50;
// How the splat stages' cost scales with the count. 1.0 fit bonsai at 1/4
// resolution, 0.85 bonsai at 1/2 and 0.8 garden at 1/4; each run weighs them.
constexpr double kGammas[3] = {0.75, 0.875, 1.0};
// Picked by replaying bonsai at 1/4 and 1/2 and garden at 1/4 resolution;
// docs/notes/train-forecast.md has the errors each one bought.
constexpr double kTimeFloor = 0.1; // wall-time noise floor, per window
constexpr double kTimeDrift = 0.05; // state drift, per window
constexpr double kGate = 2.0; // innovations past this many sigma shrink
constexpr double kForget = 0.9; // per window, of each exponent's score
// Growth starts all per splat (g0 = 0): fitting a fixed chunk under-read
// bonsai's peak by 14 sigma.
constexpr double kMemFloor = 0.05; // high-water noise floor
constexpr double kMemDrift = 0.02; // [g0, g1] drift, per window
constexpr double kChunkSigma = 0.5; // prior sigmas, as shares of the growth
constexpr double kPerSplatSigma = 0.7;
constexpr double kReach = 0.3; // extra sigma per e-fold of extrapolation
} // namespace
// ================
// SplatSchedule
// ================
int64_t SplatSchedule::final_count(int from, int64_t live) const {
int64_t last = live;
segments(from, live, [&](int, int, int64_t n) { last = n; });
return last;
}
int SplatSchedule::first_densify(int from) const {
if (_d.refine_every <= 0) return -1;
int g = std::max(from, _d.refine_start_iter + 1);
g = (g + _d.refine_every - 1) / _d.refine_every * _d.refine_every;
return densify_grows_at(_d, g, _total) ? g : -1;
}
// ================
// TrainForecast: time
// ================
void TrainForecast::reset(const ForecastSetup& s) {
std::lock_guard<std::mutex> lk(_mu);
_setup = s;
const int every = s.schedule.refine_every();
_window_len = std::min(500, std::max(20, every > 0 ? every : 100));
_warmup_left = kWarmupSteps;
_tw = Window{};
_time_init = false;
for (int i = 0; i < 3; ++i) {
_tm[i] = TimeModel{};
_tm[i].gamma = kGammas[i];
}
_fallback_wall = 0.0;
_fallback_n = 0;
_save_per_msplat = 0.0;
_saves = 0;
_mw = MemWindow{};
_mem_init = false;
_demand_sd = 0.0;
_ours_hw = _grow_hw = 0.0;
_history.clear();
_projection.clear();
_history_stride = 1;
_history_skip = 0;
_others_mean = _others_sq = 0.0;
_others_n = 0;
_vram = VramForecast{};
}
int TrainForecast::sh_coeffs(int step) const {
const int every = _setup.sh_degree_every;
const int d = every > 0 ? std::min(step / every, _setup.sh_degree) : _setup.sh_degree;
return (d + 1) * (d + 1) - 1;
}
void TrainForecast::add_step(int step, double wall_s, int64_t splats,
double splat_gpu_s) {
std::lock_guard<std::mutex> lk(_mu);
// The first step builds the pipelines: seconds, not milliseconds.
if (_warmup_left < kWarmupSteps) {
_fallback_wall += wall_s;
++_fallback_n;
}
if (_warmup_left > 0) { --_warmup_left; return; }
_tw.wall.push_back(wall_s);
_tw.gpu.push_back(splat_gpu_s);
_tw.msplats.push_back((double)splats / 1e6);
_tw.coeffs.push_back(sh_coeffs(step));
if ((int)_tw.wall.size() >= _window_len) close_time_window();
}
void TrainForecast::close_time_window() {
Window w;
std::swap(w, _tw);
double y = 0.0, var_y = 0.0, g = 0.0, var_g = 0.0;
robust_mean(w.wall, y, var_y);
// Laptop GPUs change clocks for whole windows at a time: bonsai at 1/2
// resolution ran 40-60% slow for 100 steps at a stretch, four times.
var_y += (kTimeFloor * y) * (kTimeFloor * y);
std::vector<double> timed;
for (double v : w.gpu)
if (v >= 0.0) timed.push_back(v);
const bool has_gpu = timed.size() >= 2;
if (has_gpu) {
robust_mean(timed, g, var_g);
var_g += (0.05 * g) * (0.05 * g);
}
for (TimeModel& t : _tm)
update_time_model(t, w, y, var_y, g, var_g, has_gpu, !_time_init);
_time_init = true;
}
void TrainForecast::update_time_model(TimeModel& t, const Window& w, double y,
double var_y, double g, double var_g,
bool timed, bool first) {
double n = 0.0, nk = 0.0;
for (size_t i = 0; i < w.msplats.size(); ++i) {
const double load = std::pow(w.msplats[i], t.gamma);
n += load;
nk += load * w.coeffs[i];
}
n = std::max(1e-9, n / w.msplats.size());
nk /= w.msplats.size();
if (first) {
// Until the count moves, nothing measures how cost scales with it.
// Half the splat stages' GPU time is the prior, +-70%: their per-pixel
// share measured ~45% at 1/4 resolution and ~100% at 1/2 on bonsai.
const double splat_part = timed ? 0.5 * std::min(g, y) : 0.5 * y;
t.x[1] = splat_part / n;
// The full SH band (15 coefficients) doubling the per-splat cost,
// +-100%: whole-run fits on bonsai gave +127% at 1/4 resolution.
t.x[2] = t.x[1] / 15.0;
t.x[0] = y - splat_part;
t.x[3] = timed ? g - splat_part : 0.0;
for (auto& row : t.P) for (double& p : row) p = 0.0;
t.P[0][0] = (0.5 * y) * (0.5 * y);
t.P[1][1] = (0.7 * t.x[1]) * (0.7 * t.x[1]);
t.P[2][2] = t.x[2] * t.x[2];
t.P[3][3] = (0.5 * g) * (0.5 * g) + 1e-12;
} else {
// Drift: splats shrink and the screen footprint changes as training runs.
for (int i = 0; i < 4; ++i) t.P[i][i] += (kTimeDrift * t.x[i]) * (kTimeDrift * t.x[i]) + 1e-12;
// Each exponent is scored on how well it predicted this window's wall
// time before seeing it; the clamp keeps one bad window from deciding.
const double pred = t.x[0] + t.x[1] * n + t.x[2] * nk;
const double s = quad(t.P, {1.0, n, nk, 0.0}) + var_y;
const double z2 = std::min((y - pred) * (y - pred) / s, 4.0 * kGate * kGate);
t.score = kForget * t.score - 0.5 * (z2 + std::log(s));
}
kf_update(t.x, t.P, {1.0, n, nk, 0.0}, y, var_y, kGate);
// The splat stages' GPU time has less host noise than the wall clock, so
// it pins the slope sooner once the count moves -- through its own
// intercept c, the per-pixel work inside those same stages.
if (timed) kf_update(t.x, t.P, {0.0, n, nk, 1.0}, g, var_g, kGate);
for (double& v : t.x) v = std::max(0.0, v);
}
void TrainForecast::add_save(double seconds, int64_t splats) {
std::lock_guard<std::mutex> lk(_mu);
if (splats <= 0) return;
const double per = seconds / ((double)splats / 1e6);
++_saves;
_save_per_msplat += (per - _save_per_msplat) / _saves;
}
EtaForecast TrainForecast::eta(int next_step, int64_t live) const {
std::lock_guard<std::mutex> lk(_mu);
EtaForecast out;
const SplatSchedule& sch = _setup.schedule;
const int T = sch.total_steps();
if (next_step >= T) { out.seconds = out.sigma = 0.0; return out; }
const int sps = _setup.steps_per_save;
const int every = _setup.sh_degree_every;
double steps = 0.0, save_msplat = 0.0;
double load[3] = {0, 0, 0}, load_k[3] = {0, 0, 0};
int64_t last = live;
sch.segments(next_step, live, [&](int f, int e, int64_t n) {
const double m = (double)n / 1e6;
steps += e - f;
for (int i = 0; i < 3; ++i) {
const double l = std::pow(m, _tm[i].gamma);
load[i] += (e - f) * l;
for (int t = f; t < e;) {
const int end = every > 0 && t / every < _setup.sh_degree
? std::min(e, (t / every + 1) * every) : e;
load_k[i] += (end - t) * l * sh_coeffs(t);
t = end;
}
}
if (sps > 0) {
const int lo = std::max(f, 1);
if (e > lo) save_msplat += ((e - 1) / sps - (lo - 1) / sps) * m;
}
last = n;
});
if (sps != 0) save_msplat += (double)last / 1e6;
if (_time_init) {
double best = _tm[0].score;
for (const TimeModel& t : _tm) best = std::max(best, t.score);
double wsum = 0.0, mean = 0.0, second = 0.0;
for (int i = 0; i < 3; ++i) {
const TimeModel& t = _tm[i];
const double wi = std::exp(t.score - best);
const double sec = t.x[0] * steps + t.x[1] * load[i] + t.x[2] * load_k[i];
const double var = quad(t.P, {steps, load[i], load_k[i], 0.0});
wsum += wi;
mean += wi * sec;
second += wi * (var + sec * sec);
}
out.seconds = mean / wsum;
out.sigma = std::sqrt(std::max(0.0, second / wsum - out.seconds * out.seconds));
} else if (_fallback_n > 0) {
out.seconds = _fallback_wall / _fallback_n * steps;
} else {
return out;
}
if (_saves > 0) out.seconds += _save_per_msplat * save_msplat;
return out;
}
// ================
// TrainForecast: VRAM
// ================
void TrainForecast::add_memory(const MemorySample& m) {
std::lock_guard<std::mutex> lk(_mu);
const int sxi = (int)VramCategory::SplatXImg;
if (m.has_used && m.has_process) {
const double o = (double)m.used_bytes - (double)m.process_bytes;
const double a = _others_n == 0 ? 1.0 : 0.05;
_others_mean += a * (o - _others_mean);
_others_sq += a * (o * o - _others_sq);
++_others_n;
}
if (m.splats_ran > 0 && m.pool_used[sxi] > 0) {
const double lr = std::log((double)m.pool_used[sxi] / (double)m.splats_ran);
_mw.log_ratio += lr;
_mw.log_ratio_sq += lr * lr;
}
// What grows is measured off the process high-water, not the pool: a step
// that overflows the alias arena holds private buffers beside it for a
// while, and that transient -- not the steady cap -- is the real peak.
double pool = (double)m.scratch;
for (size_t c : m.pool_cap) pool += (double)c;
const double ours = m.has_process ? (double)m.process_bytes : pool;
const double steady = (double)m.pool_cap[sxi] + (double)m.scratch;
_ours_hw = std::max(_ours_hw, ours);
_grow_hw = std::max(steady, _ours_hw - (ours - steady));
if (++_mw.steps >= _window_len) close_mem_window(m);
refresh_vram(m);
}
void TrainForecast::close_mem_window(const MemorySample& m) {
const MemWindow w = _mw;
_mw = MemWindow{};
if (w.steps >= 2) {
const double mean = w.log_ratio / w.steps;
const double sd =
std::sqrt(std::max(0.0, w.log_ratio_sq / w.steps - mean * mean));
_demand_sd = _demand_sd > 0.0 ? 0.7 * _demand_sd + 0.3 * sd : sd;
}
// The first densify step allocates splat x img scratch of its own -- 10
// -> 55 MiB on garden with the count unchanged -- so nothing before it
// says how the rest grows.
const int first = _setup.schedule.first_densify(_setup.start_step);
if (_grow_hw <= 0.0 || m.splats_ran <= 0 || (first >= 0 && m.step <= first))
return;
const double n = (double)m.splats_ran / 1e6;
if (!_mem_init) {
_g[0] = 0.0;
_g[1] = _grow_hw / n;
_G[0][0] = (kChunkSigma * _grow_hw) * (kChunkSigma * _grow_hw);
_G[1][1] = (kPerSplatSigma * _g[1]) * (kPerSplatSigma * _g[1]);
_G[0][1] = _G[1][0] = 0.0;
_mem_init = true;
} else {
_G[0][0] += (kMemDrift * _g[0]) * (kMemDrift * _g[0]);
_G[1][1] += (kMemDrift * _g[1]) * (kMemDrift * _g[1]);
}
kf_update(_g, _G, {1.0, n}, _grow_hw, (kMemFloor * _grow_hw) * (kMemFloor * _grow_hw), 3.0);
_g[0] = std::max(0.0, _g[0]);
_g[1] = std::max(0.0, _g[1]);
}
void TrainForecast::refresh_vram(const MemorySample& m) {
VramForecast v;
const int nc = (int)VramCategory::Count;
const int sxi = (int)VramCategory::SplatXImg;
double pool_total = (double)m.scratch;
for (int c = 0; c < nc; ++c) {
v.category[c] = (double)m.pool_cap[c];
pool_total += (double)m.pool_cap[c];
}
v.scratch = (double)m.scratch;
const double ours = m.has_process ? (double)m.process_bytes : pool_total;
v.unpooled = std::max(0.0, ours - pool_total);
v.ours_bytes = ours;
v.total_bytes = m.has_total ? (double)m.total_bytes : 0.0;
if (_others_n > 0) {
v.others_bytes = std::max(0.0, _others_mean);
v.others_sigma = std::max(
std::sqrt(std::max(0.0, _others_sq - _others_mean * _others_mean)),
0.01 * v.total_bytes);
}
if (_history_skip++ % _history_stride == 0)
_history.push_back({m.step, ours, v.others_bytes});
if (_history.size() > 1024) {
std::vector<VramForecast::Sample> half;
for (size_t i = 0; i < _history.size(); i += 2) half.push_back(_history[i]);
_history.swap(half);
_history_stride *= 2;
}
const double fixed = ours - (v.category[sxi] + v.scratch);
const SplatSchedule& sch = _setup.schedule;
const int next = m.step + 1;
const int first = sch.first_densify(_setup.start_step);
v.provisional = first >= next;
const double extra_mean =
v.provisional ? kFirstDensifySplatShare * v.category[(int)VramCategory::Splat] : 0.0;
const double extra_sd =
v.provisional ? kFirstDensifySplatSigma * v.category[(int)VramCategory::Splat] : 0.0;
if (next >= sch.total_steps() || m.splats_ran <= 0 || _grow_hw <= 0.0) {
v.peak_mean = _ours_hw;
v.valid = next >= sch.total_steps();
_projection.clear();
_vram = std::move(v);
return;
}
// Before the filter starts: the prior it will start from.
const double n_now = (double)m.splats_ran / 1e6;
double g[2] = {_g[0], _g[1]};
double G[2][2] = {{_G[0][0], _G[0][1]}, {_G[1][0], _G[1][1]}};
if (!_mem_init) {
g[0] = 0.0;
g[1] = _grow_hw / n_now;
G[0][0] = G[0][1] = G[1][0] = 0.0;
G[1][1] = (kPerSplatSigma * g[1]) * (kPerSplatSigma * g[1]);
}
const double C = std::max(1, _setup.distinct_batches);
const double seen = expected_max(std::min<double>(_window_len, C));
double hw_mean = _grow_hw, hw_sd = 0.0;
auto band = [&](int step, double g_mean, double g_sd) {
const bool extra = v.provisional && step >= first;
const double mean = fixed + g_mean + (extra ? extra_mean : 0.0);
const double sd = std::sqrt(g_sd * g_sd + (extra ? extra_sd * extra_sd : 0.0));
v.projection.push_back({step, mean, sd});
};
band(next, hw_mean, 0.0);
sch.segments(next, m.splats_next, [&](int f, int e, int64_t count) {
const double n = (double)count / 1e6;
const double per_splat = g[1] * n;
// The pool keeps the largest draw: more draws at one count, larger peak.
const double corr = std::max(
0.0, _demand_sd * (expected_max(std::min<double>(e - f, C)) - seen));
// Bytes per splat drift as splats shrink; the further the count is
// extrapolated, the less the fit so far says.
const double reach = kReach * std::log(n / n_now) * per_splat;
const double g_mean = g[0] + per_splat * (1.0 + corr);
const double g_sd = std::sqrt(quad(G, {1.0, n}) + reach * reach +
0.25 * corr * corr * per_splat * per_splat);
double mean, sd;
max_with(g_mean, g_sd, hw_mean, mean, sd);
const double keep = hw_sd > 0.0
? 1.0 - norm_cdf((g_mean - hw_mean) / std::max(g_sd, 1.0)) : 0.0;
sd = std::sqrt(sd * sd + keep * keep * hw_sd * hw_sd);
if (f > next) band(f, hw_mean, hw_sd);
hw_mean = mean;
hw_sd = sd;
band(e - 1, hw_mean, hw_sd);
});
v.valid = true;
v.grow_peak_mean = hw_mean;
v.peak_mean = std::max(_ours_hw, v.projection.back().mean);
v.peak_sigma = v.projection.back().sigma;
if (v.total_bytes > 0.0) {
// Allocator fragmentation keeps the last percent out of reach.
const double limit = 0.99 * v.total_bytes - v.others_bytes;
const double sd = std::sqrt(v.peak_sigma * v.peak_sigma +
v.others_sigma * v.others_sigma);
v.p_oom = sd > 0.0 ? 1.0 - norm_cdf((limit - v.peak_mean) / sd)
: (v.peak_mean > limit ? 1.0 : 0.0);
v.risk = v.p_oom < 0.1 ? OomRisk::Low
: v.p_oom < 0.5 ? OomRisk::Medium : OomRisk::High;
}
_projection.swap(v.projection);
v.projection.clear();
_vram = std::move(v);
}
VramForecast TrainForecast::vram(bool series) const {
std::lock_guard<std::mutex> lk(_mu);
VramForecast out = _vram;
if (series) {
out.history = _history;
out.projection = _projection;
}
return out;
}
} // namespace spirula
+196
View File
@@ -0,0 +1,196 @@
#pragma once
// TrainForecast -- where a training run is heading: the wall time it has left
// and the VRAM it will peak at, fitted online from the steps already run and
// the densify schedule still to come. The models and the measurements behind
// their constants: docs/notes/train-forecast.md.
#include "core/PoolSlots.h"
#include "engine/EngineConfig.h"
#include <algorithm>
#include <array>
#include <cstdint>
#include <mutex>
#include <vector>
namespace spirula {
// The splat count each step trains with, replayed from the densify schedule.
class SplatSchedule {
public:
SplatSchedule() = default;
SplatSchedule(const DensifyConfig& d, int total_steps, int64_t cap)
: _d(d), _total(total_steps), _cap(cap) {}
// f(first, end, n): steps [first, end) run with n splats, covering
// [from, total_steps) given `live` splats entering step `from`.
template <class F>
void segments(int from, int64_t live, F&& f) const;
int64_t final_count(int from, int64_t live) const;
// The first densify step >= from, or -1. It runs even at the cap: it
// relocates, and allocates the live-splat scratch.
int first_densify(int from) const;
int total_steps() const { return _total; }
int refine_every() const { return _d.refine_every; }
int64_t cap() const { return _cap; }
private:
DensifyConfig _d;
int _total = 0;
int64_t _cap = 0;
};
enum class OomRisk : uint8_t { Unknown, Low, Medium, High };
struct EtaForecast {
double seconds = -1.0; // < 0 while unknown
double sigma = -1.0;
};
// All byte figures are device memory. "Ours" is this process; "others" is
// everything else on the device, which includes the backend's own context.
struct VramForecast {
bool valid = false;
// No densify step has run yet, so the live-splat scratch the first one
// allocates is an estimate rather than a measurement.
bool provisional = false;
double total_bytes = 0.0;
double ours_bytes = 0.0;
double others_bytes = 0.0;
double others_sigma = 0.0;
struct Sample { int step; double ours; double others; };
std::vector<Sample> history;
struct Band { int step; double mean; double sigma; };
std::vector<Band> projection; // ours, from the next step to the end
double peak_mean = 0.0; // ours at the last step
double peak_sigma = 0.0;
double p_oom = 0.0; // chance ours + others exceeds the device
OomRisk risk = OomRisk::Unknown;
// Ours now, split by pool category, plus scratch and what the pool does
// not account for; and the splat x img share projected at the peak.
std::array<double, (int)VramCategory::Count> category{};
double scratch = 0.0;
double unpooled = 0.0;
double grow_peak_mean = 0.0;
};
struct ForecastSetup {
SplatSchedule schedule;
int start_step = 0;
int steps_per_save = 0; // 0: never saves; < 0: only the final one
int distinct_batches = 1; // views a step can draw, for the peak draw
int sh_degree = 0; // the step's SH degree is
int sh_degree_every = 0; // min(step / sh_degree_every, sh_degree)
};
struct MemorySample {
int step = 0;
int64_t splats_ran = 0; // what the step trained
int64_t splats_next = 0; // what the next one will, after densify
std::array<size_t, (int)VramCategory::Count> pool_used{};
std::array<size_t, (int)VramCategory::Count> pool_cap{};
size_t scratch = 0;
bool has_process = false, has_used = false, has_total = false;
uint64_t process_bytes = 0, used_bytes = 0, total_bytes = 0;
};
class TrainForecast {
public:
void reset(const ForecastSetup& s);
// A finished step: its wall time without any checkpoint save, the splats
// it ran with, and the GPU seconds of its splat-proportional stages
// (< 0 when not timed).
void add_step(int step, double wall_s, int64_t splats, double splat_gpu_s);
void add_save(double seconds, int64_t splats);
void add_memory(const MemorySample& m);
// Time left from `next_step` with `live` splats entering it.
EtaForecast eta(int next_step, int64_t live) const;
// Without the history and projection series when `series` is false.
VramForecast vram(bool series = true) const;
private:
struct Window {
std::vector<double> wall, gpu; // gpu < 0: step not timed
std::vector<double> msplats;
std::vector<int> coeffs;
};
// Step time = a + (b0 + b1 * sh_coeffs) * msplats^gamma, and the splat
// stages' GPU time = c + the same term: a Kalman filter over
// [a, b0, b1, c], one per exponent, weighted by how well each predicted.
struct TimeModel {
double gamma = 1.0;
double x[4] = {0, 0, 0, 0};
double P[4][4] = {};
double score = 0.0; // discounted log-likelihood
};
struct MemWindow {
int steps = 0;
double log_ratio = 0.0, log_ratio_sq = 0.0;
};
int sh_coeffs(int step) const;
void close_time_window();
void update_time_model(TimeModel& t, const Window& w, double y, double var_y,
double g, double var_g, bool timed, bool first);
void close_mem_window(const MemorySample& m);
void refresh_vram(const MemorySample& m);
mutable std::mutex _mu;
ForecastSetup _setup;
int _window_len = 100;
int _warmup_left = 0;
Window _tw;
bool _time_init = false;
std::array<TimeModel, 3> _tm;
double _fallback_wall = 0.0;
int _fallback_n = 0;
double _save_per_msplat = 0.0;
int _saves = 0;
// splat x img + scratch, at its high-water = g0 + g1 * msplats: a Kalman
// filter over [g0, g1], started once the first densify step has run.
MemWindow _mw;
bool _mem_init = false;
double _g[2] = {0, 0};
double _G[2][2] = {};
double _demand_sd = 0.0;
double _ours_hw = 0.0, _grow_hw = 0.0;
std::vector<VramForecast::Sample> _history;
std::vector<VramForecast::Band> _projection;
int _history_stride = 1;
int _history_skip = 0;
double _others_mean = 0.0, _others_sq = 0.0;
int _others_n = 0;
VramForecast _vram;
};
template <class F>
void SplatSchedule::segments(int from, int64_t live, F&& f) const {
int s = from;
int64_t n = live;
const int stop = std::min(
_total, std::max(_d.refine_stop_iter, _total - _d.refine_stop_num_iter));
if (_d.refine_every > 0 && n < _cap) {
int g = std::max(s, _d.refine_start_iter + 1);
g = (g + _d.refine_every - 1) / _d.refine_every * _d.refine_every;
for (; g < stop && n < _cap; g += _d.refine_every) {
const int64_t next = densify_target(_d, n, _cap);
if (next == n) break;
f(s, g + 1, n);
n = next;
s = g + 1;
}
}
if (s < _total) f(s, _total, n);
}
} // namespace spirula
+100 -7
View File
@@ -1,6 +1,9 @@
// TrainerCore.cpp -- see TrainerCore.h.
#include "app/TrainerCore.h"
#include "backend/api/BackendRuntime.h"
#include "core/Env.h"
#include "core/Tensor.h"
#include "data/SceneTransform.h"
#include "app/EvalMetrics.h"
#include "checkpoint/Adapt.h"
@@ -1128,6 +1131,7 @@ void TrainerSession::setup_engine() {
// Batch-size policy.
double n_batch = std::max((double)num_train / std::max(cfg.max_batch_per_epoch, 1), 1.0);
int train_bs = std::max(1, (int)(n_batch + 0.5));
_batches_per_epoch = (int)std::max<int64_t>(1, (num_train + train_bs - 1) / train_bs);
int val_bs = 1;
if (num_val > 0)
val_bs = std::max(1, (int)std::ceil(n_batch * (double)num_val / (double)num_train));
@@ -1396,10 +1400,45 @@ double TrainerSession::avg_step_latency() const {
}
double TrainerSession::eta_seconds() const {
const double avg = avg_step_latency();
const int step = cur_step.load();
if (avg < 0.0 || step <= 0) return -1.0;
return std::max(0, cfg.num_iterations - step) * avg;
if (step <= 0) return -1.0;
return _forecast.eta(step, _live_splats.load()).seconds;
}
void TrainerSession::observe_memory(int step, int64_t splats_ran) {
MemorySample m;
m.step = step;
m.splats_ran = splats_ran;
m.splats_next = _live_splats.load();
const DevicePool::CategoryBytes pool = DevicePool::global().category_bytes();
for (int c = 0; c < (int)VramCategory::Count; ++c) {
m.pool_used[c] = pool.used[c];
m.pool_cap[c] = pool.cap[c];
}
m.scratch = engine_get_scratch_bytes();
const backend::MemoryUsage mu = backend::memory_usage();
m.has_process = mu.has_process;
m.has_used = mu.has_used;
m.has_total = mu.has_total;
m.process_bytes = mu.process_bytes;
m.used_bytes = mu.used_bytes;
m.total_bytes = mu.total_bytes;
_forecast.add_memory(m);
// Once per level, so a run that stays at risk says it once.
const VramForecast v = _forecast.vram(false);
if (!v.valid || (int)v.risk <= (int)_warned_risk) return;
_warned_risk = v.risk;
auto gib = [](double b) {
char s[32];
std::snprintf(s, sizeof s, "%.2f", b / (1024.0 * 1024.0 * 1024.0));
return std::string(s);
};
char pct[16];
std::snprintf(pct, sizeof pct, "%.0f", v.p_oom * 100.0);
log(lfmt(lmsg::vram_forecast_warn,
{gib(v.peak_mean), gib(v.peak_sigma),
gib(0.99 * v.total_bytes - v.others_bytes), std::string(pct)}));
}
void TrainerSession::train(const TrainerCallbacks& cb) {
@@ -1410,6 +1449,20 @@ void TrainerSession::train(const TrainerCallbacks& cb) {
_paused_s = 0.0;
}
{
ForecastSetup fs;
fs.schedule = SplatSchedule(build_step_config(cfg, st, start_step).densify,
cfg.num_iterations, engine_get_max_num_splats());
fs.start_step = start_step;
fs.steps_per_save = cfg.steps_per_save;
fs.distinct_batches = _batches_per_epoch;
fs.sh_degree = cfg.sh_degree;
fs.sh_degree_every = cfg.sh_degree_warmup_every;
_forecast.reset(fs);
_live_splats = engine_get_cur_num_splats();
_warned_risk = OomRisk::Low;
}
int step = start_step;
for (; step < cfg.num_iterations; step++) {
// Pause gate + render-fairness yield: give viewer render workers an
@@ -1430,15 +1483,27 @@ void TrainerSession::train(const TrainerCallbacks& cb) {
std::map<std::string, float> losses;
std::string data_error;
double save_s = 0.0, splat_gpu_s = -1.0;
int64_t splats_ran = 0;
// One step in ten: on Vulkan each bracket is a queue submission.
const bool timed = step % 10 == 0;
{
std::lock_guard<std::mutex> lk(engine_mutex);
if (step > 0 && cfg.steps_per_save > 0 && step % cfg.steps_per_save == 0)
if (step > 0 && cfg.steps_per_save > 0 && step % cfg.steps_per_save == 0) {
const auto t0 = std::chrono::steady_clock::now();
save_checkpoint(step);
save_s = std::chrono::duration<double>(
std::chrono::steady_clock::now() - t0).count();
}
splats_ran = engine_get_cur_num_splats();
if (timed) engine_step_timing_arm();
try {
losses = train_step(step);
} catch (const DataDecodeError& e) {
data_error = e.what();
}
if (timed) splat_gpu_s = engine_step_timing_read();
_live_splats = engine_get_cur_num_splats();
}
// Asking outside the lock: the front end may sit on this for minutes
// while the user puts the dataset back, and the viewport still wants
@@ -1464,6 +1529,24 @@ void TrainerSession::train(const TrainerCallbacks& cb) {
_step_latencies.push_back(latency);
if (_step_latencies.size() > 100) _step_latencies.pop_front();
}
_forecast.add_step(step, latency - save_s, splats_ran, splat_gpu_s);
if (save_s > 0.0) _forecast.add_save(save_s, splats_ran);
observe_memory(step, splats_ran);
// SS_FORECAST_LOG=1: the forecast's state every 100 steps, English,
// for checking it against how the run actually ends.
static const bool forecast_log = spirula::env("FORECAST_LOG") != nullptr;
if (forecast_log && (step + 1) % 100 == 0) {
const EtaForecast e = _forecast.eta(step + 1, _live_splats.load());
const VramForecast v = _forecast.vram(false);
const double gib = 1024.0 * 1024.0 * 1024.0;
std::fprintf(stderr,
"[forecast] step %d eta %.1f +- %.1f s ours %.3f GiB peak %.3f +- %.3f GiB"
" others %.3f total %.3f p_oom %.3f%s\n",
step + 1, e.seconds, e.sigma,
v.ours_bytes / gib,
v.peak_mean / gib, v.peak_sigma / gib, v.others_bytes / gib,
v.total_bytes / gib, v.p_oom, v.provisional ? " provisional" : "");
}
if (cb.on_step) {
TrainerProgress p;
@@ -1507,21 +1590,31 @@ std::string TrainerSession::progress_json() {
double elapsed = elapsed_seconds();
double avg = avg_step_latency();
double eta = eta_seconds();
const VramForecast v = _forecast.vram(false);
static const char* const kRisk[] = {"unknown", "low", "medium", "high"};
char tail[160];
if (v.valid)
std::snprintf(tail, sizeof tail,
", \"vram_peak_bytes\": %.0f, \"vram_peak_sigma\": %.0f, "
"\"oom_probability\": %.4f, \"oom_risk\": \"%s\"}",
v.peak_mean, v.peak_sigma, v.p_oom, kRisk[(int)v.risk]);
else
std::snprintf(tail, sizeof tail, ", \"oom_risk\": \"unknown\"}");
char buf[256];
if (eta >= 0.0) {
std::snprintf(buf, sizeof buf,
"{\"step\": %d, \"total_steps\": %d, \"elapsed_time\": %.3f, "
"\"eta\": %.3f, \"latency_ms\": %.3f, \"paused\": %s}",
"\"eta\": %.3f, \"latency_ms\": %.3f, \"paused\": %s",
step, cfg.num_iterations, elapsed, eta, avg * 1000.0,
paused.load() ? "true" : "false");
} else {
std::snprintf(buf, sizeof buf,
"{\"step\": %d, \"total_steps\": %d, \"elapsed_time\": %.3f, "
"\"eta\": null, \"latency_ms\": null, \"paused\": %s}",
"\"eta\": null, \"latency_ms\": null, \"paused\": %s",
step, cfg.num_iterations, elapsed,
paused.load() ? "true" : "false");
}
return buf;
return std::string(buf) + tail;
}
ViewerRenderConfig TrainerSession::make_viewer_config() const {
+11 -2
View File
@@ -18,6 +18,7 @@
// setup_engine() calls engine_reset(), so a fresh session can follow a
// finished one in the same process (the GUI's "train again" path).
#include "app/TrainForecast.h"
#include "engine/Engine.h"
#include "core/ColorSpace.h"
#include "data/DatasetParser.h"
@@ -298,9 +299,11 @@ public:
// train() starts, frozen once it returns.
double elapsed_seconds() const;
// Remaining wall clock over the last 100 steps' average, or -1 before
// the first step lands.
// Remaining wall clock, from TrainForecast's step-time model and the
// densify schedule still to come; -1 before the first step lands.
double eta_seconds() const;
// The run's ETA and VRAM forecast, fed by train().
const TrainForecast& forecast() const { return _forecast; }
// The /progress response body.
std::string progress_json();
@@ -333,6 +336,12 @@ private:
mutable std::mutex _progress_mutex; // guards the latency window
std::deque<double> _step_latencies; // last 100, seconds
bool _diverged_loss_reported = false;
void observe_memory(int step, int64_t splats_ran);
TrainForecast _forecast;
int _batches_per_epoch = 1;
std::atomic<int64_t> _live_splats{0};
OomRisk _warned_risk = OomRisk::Low;
};
} // namespace spirula
+36 -3
View File
@@ -18,6 +18,7 @@
#include "app/gui/Subprocess.h"
#include "mesh/MeshImport.h"
#include "app/gui/Ui.h"
#include "app/gui/VramForecastView.h"
#include "i18n/Locale.h"
#include "i18n/catalog/Brand.h"
@@ -9355,8 +9356,35 @@ void GuiApp::draw_vram_readout(float x0, float avail) {
format_gib(m.total_bytes) + " GiB"
: "VRAM " + part(m.has_process, m.process_bytes) + " GiB";
// A run in progress (or just finished) has a forecast: its risk goes in
// front of the bar, and hovering either shows the projection behind it.
spirula::TrainerSession* session = nullptr;
{
const TrainRunner::Phase ph = _runner.phase();
if ((ph == TrainRunner::Phase::Training || ph == TrainRunner::Phase::Done) &&
_runner.engine_ready())
session = _runner.session();
}
const spirula::VramForecast brief =
session ? session->forecast().vram(false) : spirula::VramForecast{};
const spirula::i18n::Msg* risk = session ? oom_risk_label(brief.risk) : nullptr;
auto hover = [&] {
if (!session) { ui::help_on_hover(msg::vram_help); return; }
if (!ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort |
ImGuiHoveredFlags_NoSharedDelay) ||
!ImGui::BeginTooltip())
return;
ImGui::PushTextWrapPos(px(480.0f));
ui::TextDisabled(msg::vram_help);
ImGui::PopTextWrapPos();
ImGui::Separator();
vram_forecast_card(session->forecast().vram(), session->cfg.num_iterations);
ImGui::EndTooltip();
};
const ImGuiStyle& st = ImGui::GetStyle();
const float text_w = ImGui::CalcTextSize(label.c_str()).x;
float text_w = ImGui::CalcTextSize(label.c_str()).x;
if (risk) text_w += ImGui::CalcTextSize(risk->get()).x + st.ItemSpacing.x;
ImGui::SameLine();
// The bar is the first thing to give when the row is short -- the numbers
// beside it say everything it does. Without this the readout ran off the
@@ -9370,6 +9398,11 @@ void GuiApp::draw_vram_readout(float x0, float avail) {
}
if (target > ImGui::GetCursorPosX()) ImGui::SetCursorPosX(target);
if (risk) {
ui::TextColored(oom_risk_color(brief.risk), *risk);
hover();
ImGui::SameLine();
}
if (bar_w > 0.0f) {
const float h = ImGui::GetTextLineHeight();
const ImVec2 p = ImGui::GetCursorScreenPos();
@@ -9389,11 +9422,11 @@ void GuiApp::draw_vram_readout(float x0, float avail) {
dl->AddRect(p, ImVec2(p.x + bar_w, p.y + h),
ImGui::GetColorU32(ImGuiCol_Border), r);
ui::InvisibleButtonRaw("##vram", ImVec2(bar_w, h));
ui::help_on_hover(msg::vram_help);
hover();
ImGui::SameLine(0.0f, gap);
}
ui::TextColoredRaw(sized ? kDim : color, label);
ui::help_on_hover(msg::vram_help);
hover();
}
// ---------------------------------------------------------------------------
+1 -4
View File
@@ -48,10 +48,7 @@ double TrainRunner::avg_step_latency() {
}
double TrainRunner::eta_seconds() {
std::lock_guard<std::mutex> lk(_mu);
const double avg = avg_latency_locked();
if (avg < 0.0 || _latest.total_steps <= 0) return -1.0;
return avg * std::max(0, _latest.total_steps - (_latest.step + 1));
return _session ? _session->eta_seconds() : -1.0;
}
double TrainRunner::elapsed_seconds() {
+4 -4
View File
@@ -88,11 +88,11 @@ public:
// Latest per-step progress (copy).
spirula::TrainerProgress latest_progress();
// Mean over the last 100 steps, which is also what the ETA is built from.
// A single step's latency swings several-fold with whether a viewer
// render landed on it, so it is not a number to put on screen.
// Mean over the last 100 steps. A single step's latency swings
// several-fold with whether a viewer render landed on it, so it is not a
// number to put on screen.
double avg_step_latency();
double eta_seconds(); // < 0 when unknown
double eta_seconds(); // TrainerSession::eta_seconds; < 0 when unknown
// Time spent in the step loop, pauses excluded: < 0 before a session
// exists, 0 until the loop starts, frozen once it ends.
double elapsed_seconds();
+257
View File
@@ -0,0 +1,257 @@
// VramForecastView.cpp -- see VramForecastView.h.
#include "app/gui/VramForecastView.h"
#include "i18n/catalog/Gui.h"
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <string>
namespace gui {
namespace msg = spirula::i18n::msg::gui;
using spirula::OomRisk;
using spirula::VramForecast;
namespace {
// The status strip's own pressure colours (GuiApp.cpp).
const ImVec4 kOk(0.35f, 0.85f, 0.45f, 1.0f);
const ImVec4 kWarn(0.95f, 0.75f, 0.30f, 1.0f);
const ImVec4 kErr(1.0f, 0.42f, 0.42f, 1.0f);
const ImVec4 kOthers(0.55f, 0.55f, 0.60f, 1.0f);
constexpr double kGiB = 1024.0 * 1024.0 * 1024.0;
std::string gib(double bytes) {
char s[32];
std::snprintf(s, sizeof s, "%.2f", bytes / kGiB);
return s;
}
ImU32 with_alpha(ImVec4 c, float a) {
c.w = a;
return ImGui::GetColorU32(c);
}
void dashed_line(ImDrawList* dl, ImVec2 a, ImVec2 b, ImU32 col, float thick) {
const float dx = b.x - a.x, dy = b.y - a.y;
const float len = std::sqrt(dx * dx + dy * dy);
const float dash = px(4.0f);
for (float t = 0.0f; t < len; t += 2.0f * dash) {
const float t1 = std::min(len, t + dash);
dl->AddLine(ImVec2(a.x + dx * t / len, a.y + dy * t / len),
ImVec2(a.x + dx * t1 / len, a.y + dy * t1 / len), col, thick);
}
}
void swatch(ImU32 col) {
const float h = ImGui::GetTextLineHeight();
const ImVec2 p = ImGui::GetCursorScreenPos();
ImGui::GetWindowDrawList()->AddRectFilled(
ImVec2(p.x, p.y + h * 0.25f), ImVec2(p.x + h * 0.5f, p.y + h * 0.75f), col);
ImGui::Dummy(ImVec2(h * 0.5f, h));
ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x);
}
// A gridline spacing of 10^k, 2*10^k or 5*10^k GiB that draws at most five.
double tick_step(double max_gib) {
double step = std::pow(10.0, std::floor(std::log10(std::max(max_gib, 1e-3))));
for (double m : {1.0, 2.0, 5.0, 10.0})
if (max_gib / (step * m) <= 5.0) return step * m;
return step * 10.0;
}
void chart(const VramForecast& v, int total_steps) {
ImDrawList* dl = ImGui::GetWindowDrawList();
const ImGuiStyle& st = ImGui::GetStyle();
const float line = ImGui::GetTextLineHeight();
const float W = px(460.0f), H = px(170.0f);
const ImVec2 p0 = ImGui::GetCursorScreenPos();
ImGui::Dummy(ImVec2(W, H + line + st.ItemInnerSpacing.y));
double y_max = v.total_bytes;
for (const auto& s : v.history) y_max = std::max(y_max, s.ours + s.others);
for (const auto& b : v.projection)
y_max = std::max(y_max, v.others_bytes + b.mean + 2.0 * b.sigma);
y_max = std::max(y_max * 1.05, 1.0);
const double T = std::max(1, total_steps);
const float label_w = ImGui::CalcTextSize("00.0").x + st.ItemInnerSpacing.x;
const ImVec2 r0(p0.x + label_w, p0.y);
const ImVec2 r1(p0.x + W, p0.y + H);
auto X = [&](double step) {
return r0.x + (r1.x - r0.x) * (float)std::min(1.0, std::max(0.0, step / T));
};
auto Y = [&](double bytes) {
return r1.y - (r1.y - r0.y) * (float)std::min(1.0, std::max(0.0, bytes / y_max));
};
dl->AddRectFilled(r0, r1, ImGui::GetColorU32(ImGuiCol_FrameBg));
const ImU32 grid = ImGui::GetColorU32(ImGuiCol_Border);
const ImU32 text_dim = ImGui::GetColorU32(ImGuiCol_TextDisabled);
const double tick = tick_step(y_max / kGiB);
for (double g = 0.0; g * kGiB <= y_max; g += tick) {
const float y = Y(g * kGiB);
dl->AddLine(ImVec2(r0.x, y), ImVec2(r1.x, y), grid);
char s[16];
std::snprintf(s, sizeof s, tick < 1.0 ? "%.1f" : "%.0f", g);
const ImVec2 sz = ImGui::CalcTextSize(s);
dl->AddText(ImVec2(r0.x - sz.x - st.ItemInnerSpacing.x, y - sz.y * 0.5f),
text_dim, s);
}
// Other programs at the bottom, this run stacked on top: the gap to the
// capacity line is what the run has left.
const ImU32 others_fill = with_alpha(kOthers, 0.35f);
for (size_t i = 1; i < v.history.size(); ++i) {
const auto& a = v.history[i - 1];
const auto& b = v.history[i];
dl->AddQuadFilled(ImVec2(X(a.step), Y(0)), ImVec2(X(a.step), Y(a.others)),
ImVec2(X(b.step), Y(b.others)), ImVec2(X(b.step), Y(0)),
others_fill);
}
const double last_step = v.history.empty() ? 0.0 : v.history.back().step;
if (!v.projection.empty())
dl->AddRectFilled(ImVec2(X(last_step), Y(v.others_bytes)), ImVec2(r1.x, Y(0)),
others_fill);
const ImVec4 run = ImGui::GetStyleColorVec4(ImGuiCol_PlotLines);
for (size_t i = 1; i < v.projection.size(); ++i) {
const auto& a = v.projection[i - 1];
const auto& b = v.projection[i];
const double o = v.others_bytes;
dl->AddQuadFilled(ImVec2(X(a.step), Y(o + a.mean + 2.0 * a.sigma)),
ImVec2(X(b.step), Y(o + b.mean + 2.0 * b.sigma)),
ImVec2(X(b.step), Y(o + b.mean - 2.0 * b.sigma)),
ImVec2(X(a.step), Y(o + a.mean - 2.0 * a.sigma)),
with_alpha(run, 0.22f));
dashed_line(dl, ImVec2(X(a.step), Y(o + a.mean)),
ImVec2(X(b.step), Y(o + b.mean)), with_alpha(run, 0.9f), px(1.5f));
}
for (size_t i = 1; i < v.history.size(); ++i) {
const auto& a = v.history[i - 1];
const auto& b = v.history[i];
dl->AddLine(ImVec2(X(a.step), Y(a.ours + a.others)),
ImVec2(X(b.step), Y(b.ours + b.others)),
ImGui::GetColorU32(run), px(2.0f));
}
if (!v.history.empty())
dl->AddLine(ImVec2(X(last_step), r0.y), ImVec2(X(last_step), r1.y), grid);
if (v.total_bytes > 0.0)
dl->AddLine(ImVec2(r0.x, Y(v.total_bytes)), ImVec2(r1.x, Y(v.total_bytes)),
ImGui::GetColorU32(kErr), px(1.5f));
dl->AddRect(r0, r1, grid);
char s[32];
dl->AddText(ImVec2(r0.x, r1.y + st.ItemInnerSpacing.y), text_dim, "0");
std::snprintf(s, sizeof s, "%d", total_steps);
dl->AddText(ImVec2(r1.x - ImGui::CalcTextSize(s).x, r1.y + st.ItemInnerSpacing.y),
text_dim, s);
swatch(ImGui::GetColorU32(run));
ui::TextDisabled(msg::vram_legend_run);
ImGui::SameLine();
swatch(with_alpha(run, 0.35f));
ui::TextDisabled(msg::vram_legend_projected);
ImGui::SameLine();
swatch(others_fill);
ui::TextDisabled(msg::vram_legend_others);
ImGui::SameLine();
swatch(ImGui::GetColorU32(kErr));
ui::TextDisabled(msg::vram_legend_capacity);
}
void breakdown(const VramForecast& v) {
struct Row { const spirula::i18n::Msg* label; double now; double peak; ImVec4 color; };
const ImVec4 bar = ImGui::GetStyleColorVec4(ImGuiCol_PlotHistogram);
const auto cat = [&](VramCategory c) { return v.category[(int)c]; };
const double sxi = cat(VramCategory::SplatXImg);
const double sxi_peak = std::max(sxi, v.grow_peak_mean - v.scratch);
const Row rows[] = {
{&msg::vram_cat_splat, cat(VramCategory::Splat), 0.0, bar},
{&msg::vram_cat_splat_x_img, sxi, v.valid ? sxi_peak : 0.0, bar},
{&msg::vram_cat_image, cat(VramCategory::Image), 0.0, bar},
{&msg::vram_cat_appearance, cat(VramCategory::Appearance), 0.0, bar},
{&msg::vram_cat_viewer, cat(VramCategory::Viewer), 0.0, bar},
{&msg::vram_cat_other, cat(VramCategory::Other), 0.0, bar},
{&msg::vram_cat_scratch, v.scratch, 0.0, bar},
{&msg::vram_cat_unpooled, v.unpooled, 0.0, bar},
{&msg::vram_legend_others, v.others_bytes, 0.0, kOthers},
};
const double shown = 1024.0 * 1024.0; // below 1 MiB a row is noise
double scale = 1.0;
float label_w = 0.0f;
for (const Row& r : rows) {
if (std::max(r.now, r.peak) < shown) continue;
scale = std::max(scale, std::max(r.now, r.peak));
label_w = std::max(label_w, ImGui::CalcTextSize(r.label->get()).x);
}
ImDrawList* dl = ImGui::GetWindowDrawList();
const ImGuiStyle& st = ImGui::GetStyle();
const float bar_w = px(220.0f);
const float h = ImGui::GetTextLineHeight();
bool faded = false;
for (const Row& r : rows) {
if (std::max(r.now, r.peak) < shown) continue;
ui::Text(*r.label);
ImGui::SameLine(label_w + st.ItemSpacing.x * 2.0f);
const ImVec2 p = ImGui::GetCursorScreenPos();
const float w_now = bar_w * (float)(r.now / scale);
const float w_peak = bar_w * (float)(r.peak / scale);
dl->AddRectFilled(p, ImVec2(p.x + bar_w, p.y + h),
ImGui::GetColorU32(ImGuiCol_FrameBg));
if (w_peak > w_now) {
dl->AddRectFilled(ImVec2(p.x + w_now, p.y), ImVec2(p.x + w_peak, p.y + h),
with_alpha(r.color, 0.3f));
faded = true;
}
dl->AddRectFilled(p, ImVec2(p.x + w_now, p.y + h), ImGui::GetColorU32(r.color));
ImGui::Dummy(ImVec2(bar_w, h));
ImGui::SameLine();
if (r.peak > r.now)
ui::TextRaw(gib(r.now) + " -> " + gib(r.peak));
else
ui::TextRaw(gib(r.now));
}
if (faded) ui::TextDisabled(msg::vram_breakdown_growth);
}
} // namespace
const spirula::i18n::Msg* oom_risk_label(OomRisk r) {
switch (r) {
case OomRisk::Low: return &msg::oom_risk_low;
case OomRisk::Medium: return &msg::oom_risk_medium;
case OomRisk::High: return &msg::oom_risk_high;
default: return nullptr;
}
}
ImVec4 oom_risk_color(OomRisk r) {
return r == OomRisk::High ? kErr : r == OomRisk::Medium ? kWarn : kOk;
}
void vram_forecast_card(const VramForecast& v, int total_steps) {
ui::Text(msg::vram_chart_title);
chart(v, total_steps);
if (v.valid && !v.projection.empty() && v.total_bytes > 0.0) {
char pct[16];
std::snprintf(pct, sizeof pct, "%.0f", v.p_oom * 100.0);
ui::TextColoredWrapped(oom_risk_color(v.risk), msg::vram_chart_peak,
{gib(v.peak_mean), gib(v.peak_sigma),
gib(0.99 * v.total_bytes - v.others_bytes),
std::string(pct)});
if (v.provisional) ui::TextDisabledWrapped(msg::vram_chart_provisional);
} else if (!v.valid) {
ui::TextDisabledWrapped(msg::vram_chart_waiting);
}
ImGui::Spacing();
ui::Text(msg::vram_breakdown_title);
breakdown(v);
}
} // namespace gui
+19
View File
@@ -0,0 +1,19 @@
#pragma once
// The trainer's VRAM forecast on screen: the out-of-memory risk tag beside the
// status strip's VRAM bar, and the card that bar shows on hover.
#include "app/TrainForecast.h"
#include "app/gui/Ui.h"
namespace gui {
// nullptr while the risk is unknown.
const spirula::i18n::Msg* oom_risk_label(spirula::OomRisk r);
ImVec4 oom_risk_color(spirula::OomRisk r);
// Memory over the run with its projection, then this run's usage by category.
// Call inside a tooltip.
void vram_forecast_card(const spirula::VramForecast& v, int total_steps);
} // namespace gui
+6 -4
View File
@@ -22,13 +22,15 @@ namespace backend {
// counter/table are shared across all translation units. Allocation is pooled
// upstream (Tensor.h), so this map is touched rarely, not per frame.
namespace detail {
// Leaked on purpose: DevicePool::global() is constructed before the first
// device_malloc, so it is destroyed after these and frees through them.
inline std::mutex& alloc_mutex() {
static std::mutex m;
return m;
static std::mutex* m = new std::mutex();
return *m;
}
inline std::unordered_map<void*, size_t>& alloc_sizes() {
static std::unordered_map<void*, size_t> m;
return m;
static auto* m = new std::unordered_map<void*, size_t>();
return *m;
}
inline std::atomic<uint64_t>& device_bytes() {
static std::atomic<uint64_t> v{0};
+197
View File
@@ -0,0 +1,197 @@
// TrainForecast against synthetic runs whose true time and VRAM are known.
#include "app/TrainForecast.h"
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <random>
using namespace spirula;
static int g_fail = 0;
#define CHECK(cond, ...) \
do { \
if (!(cond)) { \
std::printf("FAIL %s:%d: ", __func__, __LINE__); \
std::printf(__VA_ARGS__); \
std::printf("\n"); \
++g_fail; \
} \
} while (0)
static DensifyConfig densify(int start, int stop, int every, float growth) {
DensifyConfig d;
d.refine_start_iter = start;
d.refine_stop_iter = stop;
d.refine_stop_num_iter = 1 << 20; // leaves refine_stop_iter in charge
d.refine_every = every;
d.growth_factor = growth;
return d;
}
// The replay must agree with stepping densify_grows_at / densify_target.
static void schedule_matches_engine_rule() {
const DensifyConfig d = densify(500, 15000, 100, 1.05f);
const int T = 30000;
const int64_t cap = 1000000;
const SplatSchedule sch(d, T, cap);
for (int from : {0, 499, 501, 7300, 20000}) {
int64_t n = 200000;
for (int s = 0; s < from; ++s)
if (densify_grows_at(d, s, T)) n = densify_target(d, n, cap);
std::vector<int64_t> want(T - from);
int64_t m = n;
for (int s = from; s < T; ++s) {
want[s - from] = m;
if (densify_grows_at(d, s, T)) m = densify_target(d, m, cap);
}
int covered = from;
bool ok = true;
sch.segments(from, n, [&](int f, int e, int64_t k) {
ok = ok && f == covered;
for (int s = f; s < e; ++s) ok = ok && want[s - from] == k;
covered = e;
});
CHECK(ok && covered == T, "segments diverge from the engine rule, from=%d", from);
CHECK(sch.final_count(from, n) == m, "final count %lld vs %lld",
(long long)sch.final_count(from, n), (long long)m);
}
CHECK(sch.first_densify(0) == 600, "first densify %d", sch.first_densify(0));
CHECK(sch.first_densify(15000) == -1, "densify past the stop");
}
// Step time = 20 ms + (40 ms + 3 ms per SH coefficient) per Msplat: the
// naive mean underestimates badly while the count and the SH degree are still
// growing, the model must not.
static void eta_tracks_growth() {
const DensifyConfig d = densify(500, 15000, 100, 1.05f);
const int T = 30000;
const int64_t cap = 3000000;
const SplatSchedule sch(d, T, cap);
ForecastSetup fs;
fs.schedule = sch;
fs.sh_degree = 3;
fs.sh_degree_every = 1000;
TrainForecast fc;
fc.reset(fs);
std::mt19937 rng(1);
std::normal_distribution<double> noise(1.0, 0.1);
const double a = 0.020, b = 0.040, b_coeff = 0.003;
auto coeffs = [](int s) {
const int d = std::min(s / 1000, 3);
return (d + 1) * (d + 1) - 1;
};
std::vector<int64_t> n_at(T);
sch.segments(0, 300000, [&](int f, int e, int64_t n) {
for (int s = f; s < e; ++s) n_at[s] = n;
});
std::vector<double> t_at(T);
for (int s = 0; s < T; ++s)
t_at[s] = a + (b + b_coeff * coeffs(s)) * n_at[s] / 1e6;
auto remaining = [&](int from) {
double sum = 0.0;
for (int s = from; s < T; ++s) sum += t_at[s];
return sum;
};
double recent = 0.0;
for (int s = 0; s < T; ++s) {
const double wall = t_at[s] * noise(rng);
const double gpu = s % 10 == 0 ? (t_at[s] - a) * noise(rng) : -1.0;
fc.add_step(s, wall, n_at[s], gpu);
recent = s < 100 ? wall : recent + (wall - recent) / 100.0;
if (s == 400 || s == 1500 || s == 2500 || s == 8000) {
const double truth = remaining(s + 1);
const EtaForecast e = fc.eta(s + 1, n_at[s + 1]);
const double naive = recent * (T - s - 1);
std::printf(" eta @%d: model %.0f s (sigma %.0f), naive %.0f s, truth %.0f s\n",
s, e.seconds, e.sigma, naive, truth);
// Before the count moves only the prior speaks, so all it can be
// held to is its own sigma -- and beating the naive estimate.
const bool grown = s > 1000;
// Past ~5300 the count sits at the cap and naive is exact too.
if (s < 5000)
CHECK(std::fabs(e.seconds - truth) < std::fabs(naive - truth),
"eta at step %d no better than naive", s);
CHECK(std::fabs(e.seconds - truth) <
(grown ? std::max(0.1 * truth, 2.0 * e.sigma) : 3.0 * e.sigma),
"eta at step %d off by %.1f%%", s, 100.0 * (e.seconds / truth - 1.0));
}
}
}
// splat x img follows 60 bytes per splat with per-step spread; the rest is 1 GiB.
static VramForecast run_vram(double others_gib, int stop_at) {
const DensifyConfig d = densify(500, 15000, 100, 1.05f);
const int T = 30000;
const int64_t cap = 3000000;
const SplatSchedule sch(d, T, cap);
ForecastSetup fs;
fs.schedule = sch;
fs.distinct_batches = 200;
TrainForecast fc;
fc.reset(fs);
std::mt19937 rng(7);
std::vector<double> view_ratio(200);
std::lognormal_distribution<double> spread(0.0, 0.2);
for (double& r : view_ratio) r = 60.0 * spread(rng);
std::uniform_int_distribution<int> pick(0, 199);
const double gib = 1024.0 * 1024.0 * 1024.0;
std::vector<int64_t> n_at(T + 1);
sch.segments(0, 300000, [&](int f, int e, int64_t n) {
for (int s = f; s < e; ++s) n_at[s] = n;
});
n_at[T] = n_at[T - 1];
size_t hw = 0;
VramForecast at_stop;
for (int s = 0; s < T; ++s) {
const size_t used = (size_t)(n_at[s] * view_ratio[pick(rng)]);
hw = std::max(hw, used);
MemorySample m;
m.step = s;
m.splats_ran = n_at[s];
m.splats_next = n_at[s + 1];
m.pool_used[(int)VramCategory::SplatXImg] = used;
m.pool_cap[(int)VramCategory::SplatXImg] = hw;
m.pool_cap[(int)VramCategory::Splat] = (size_t)gib;
m.has_process = m.has_used = m.has_total = true;
m.process_bytes = (uint64_t)gib + hw;
m.used_bytes = m.process_bytes + (uint64_t)(others_gib * gib);
m.total_bytes = (uint64_t)(8.0 * gib);
fc.add_memory(m);
if (s == stop_at) at_stop = fc.vram();
}
const double truth = gib + (double)hw;
std::printf(" vram @%d: peak %.3f +- %.3f GiB, truth %.3f GiB, p_oom %.3f\n",
stop_at, at_stop.peak_mean / gib, at_stop.peak_sigma / gib,
truth / gib, at_stop.p_oom);
CHECK(at_stop.valid, "no forecast at step %d", stop_at);
CHECK(std::fabs(at_stop.peak_mean - truth) < 3.0 * at_stop.peak_sigma + 0.02 * truth,
"peak off: %.3f vs %.3f GiB", at_stop.peak_mean / gib, truth / gib);
return at_stop;
}
static void vram_predicts_peak() {
const VramForecast early = run_vram(1.0, 1000);
CHECK(early.risk == OomRisk::Low, "risk %d with 6 GiB spare", (int)early.risk);
const VramForecast late = run_vram(1.0, 10000);
CHECK(late.peak_sigma <= early.peak_sigma, "sigma grew with more data");
// 8 GiB device, 6.8 GiB elsewhere: 1.2 GiB for a ~1.2 GiB peak.
const VramForecast tight = run_vram(6.8, 1000);
CHECK(tight.risk >= OomRisk::Medium, "risk %d on a full device", (int)tight.risk);
}
int main() {
schedule_matches_engine_rule();
eta_tracks_growth();
vram_predicts_peak();
if (g_fail) {
std::printf("train_forecast_test: %d failure(s)\n", g_fail);
return 1;
}
std::printf("train_forecast_test: OK\n");
return 0;
}
+83 -19
View File
@@ -50,9 +50,9 @@
// other.
enum class VramCategory : uint8_t {
Splat = 0, // per-splat params, gradients, optimizer state, densify aux
// Scratch sized by (camera, gaussian), visible or splat-tile PAIRS, so it
// grows with the batch too. A buffer sized [N] is Splat however late it is
// allocated -- docs/notes/vram-splat-x-img.md.
// Scratch sized by (camera, gaussian), visible or splat-tile PAIRS: the
// one category that grows during training. Splat is sized for cap_max
// (POOL_LIVE_SPLAT_TABLE) -- docs/notes/vram-splat-x-img.md.
SplatXImg,
Image, // per-image buffers: render outputs, GT, loss maps, img grads
Appearance, // bilagrid / background-SH / PPISP / color-space / color-shift
@@ -316,27 +316,27 @@ enum class SaveClass : uint8_t {
/* ---- densify scratch ---- */ \
X(DensifyQuantileTemp , "densify_quantile_temp", Other , Never) \
X(DensifyMapNorm , "densify_map_norm", Other , Never) \
X(DensifyScoreGather , "densify_score_gather", Other , Never) \
X(DensifyScoreGather , "densify_score_gather", Splat , Never) \
X(DensifyScoreClip , "densify_score_clip", Other , Never) \
X(DensifyClipScale , "densify_clip_scale", Other , Never) \
X(DensifyUpdateWeight , "densify_update_weight", Other , Never) \
X(DensifyWswrSortingValues , "densify_wswr_sorting_values", Other , Never) \
X(DensifyWswrOutIdx , "densify_wswr_out_idx", Other , Never) \
X(DensifyWswrKeysOut , "densify_wswr_keys_out", Other , Never) \
X(DensifyWswrIndicesIn , "densify_wswr_indices_in", Other , Never) \
X(DensifyWswrIndicesOut , "densify_wswr_indices_out", Other , Never) \
X(DensifyWswrSortingValues , "densify_wswr_sorting_values", Splat , Never) \
X(DensifyWswrOutIdx , "densify_wswr_out_idx", Splat , Never) \
X(DensifyWswrKeysOut , "densify_wswr_keys_out", Splat , Never) \
X(DensifyWswrIndicesIn , "densify_wswr_indices_in", Splat , Never) \
X(DensifyWswrIndicesOut , "densify_wswr_indices_out", Splat , Never) \
X(DensifyWswrEligible , "densify_wswr_eligible", Other , Never) \
X(DensifyRelocMask , "densify_reloc_mask", Other , Never) \
X(DensifyRelocMask , "densify_reloc_mask", Splat , Never) \
X(DensifyRelocCount , "densify_reloc_count", Other , Never) \
X(DensifyRelocDstIndices , "densify_reloc_dst_indices", Other , Never) \
X(DensifyMcmcSampleProbs , "densify_mcmc_sample_probs", Other , Never) \
X(DensifyMcmcSampleProbsCumsum , "densify_mcmc_sample_probs_cumsum", Other , Never) \
X(DensifyMcmcIndexMap , "densify_mcmc_index_map", Other , Never) \
X(DensifyMcmcNIdxBuffer , "densify_mcmc_n_idx_buffer", Other , Never) \
X(DensifyMcmcAddSampleProbs , "densify_mcmc_add_sample_probs", Other , Never) \
X(DensifyMcmcAddSampleProbsCumsum, "densify_mcmc_add_sample_probs_cumsum",Other , Never) \
X(DensifyMcmcAddIndexMap , "densify_mcmc_add_index_map", Other , Never) \
X(DensifyMcmcAddNIdxBuffer , "densify_mcmc_add_n_idx_buffer", Other , Never) \
X(DensifyRelocDstIndices , "densify_reloc_dst_indices", Splat , Never) \
X(DensifyMcmcSampleProbs , "densify_mcmc_sample_probs", Splat , Never) \
X(DensifyMcmcSampleProbsCumsum , "densify_mcmc_sample_probs_cumsum", Splat , Never) \
X(DensifyMcmcIndexMap , "densify_mcmc_index_map", Splat , Never) \
X(DensifyMcmcNIdxBuffer , "densify_mcmc_n_idx_buffer", Splat , Never) \
X(DensifyMcmcAddSampleProbs , "densify_mcmc_add_sample_probs", Splat , Never) \
X(DensifyMcmcAddSampleProbsCumsum, "densify_mcmc_add_sample_probs_cumsum",Splat , Never) \
X(DensifyMcmcAddIndexMap , "densify_mcmc_add_index_map", Splat , Never) \
X(DensifyMcmcAddNIdxBuffer , "densify_mcmc_add_n_idx_buffer", Splat , Never) \
X(DensifyRobustResid , "densify_robust_resid", Other , Never) \
X(DensifyTukeyC , "densify_tukey_c", Other , Never) \
/* ---- viewer cache/scratch ---- */ \
@@ -510,6 +510,70 @@ constexpr bool ce_alias_rows_unique() {
static_assert(ce_alias_rows_unique(),
"POOL_ALIAS_TABLE: a slot is listed twice");
// ---- Slots sized by the live splat count ----------------------------------
// Requested at a size proportional to the LIVE count, so the pool allocates
// them for cap_max on first use: Splat then stops growing as the model
// densifies, which the training VRAM forecast assumes.
#define POOL_LIVE_SPLAT_TABLE(X) \
X(EngSubbatchAccumWeightSum) X(EngDensifyOversizeWeight) \
X(RasterBwdAccumWeight) X(RasterBwdVWorld) \
X(FusedProjBwdCamBounds) X(DensifyScoreGather) \
X(DensifyWswrSortingValues) X(DensifyWswrKeysOut) \
X(DensifyWswrIndicesIn) X(DensifyWswrIndicesOut) \
X(DensifyRelocMask) X(DensifyRelocDstIndices) \
X(DensifyMcmcSampleProbs) X(DensifyMcmcSampleProbsCumsum) \
X(DensifyMcmcIndexMap) X(DensifyMcmcNIdxBuffer) \
X(DensifyMcmcAddSampleProbs) X(DensifyMcmcAddSampleProbsCumsum) \
X(DensifyMcmcAddNIdxBuffer)
// One int32 per drawn splat: the draw's size varies from call to call and is
// bounded only by the live count, so these get cap_max elements outright.
#define POOL_SPLAT_DRAW_TABLE(X) \
X(DensifyWswrOutIdx) X(DensifyMcmcAddIndexMap)
enum class LiveSizing : uint8_t { None, Proportional, Draw };
inline constexpr PoolSlot kLiveSplatSlots[] = {
#define X(name) PoolSlot::name,
POOL_LIVE_SPLAT_TABLE(X)
#undef X
};
inline constexpr PoolSlot kSplatDrawSlots[] = {
#define X(name) PoolSlot::name,
POOL_SPLAT_DRAW_TABLE(X)
#undef X
};
constexpr LiveSizing slot_live_sizing(PoolSlot s) {
for (PoolSlot r : kLiveSplatSlots)
if (r == s) return LiveSizing::Proportional;
for (PoolSlot r : kSplatDrawSlots)
if (r == s) return LiveSizing::Draw;
return LiveSizing::None;
}
constexpr bool ce_live_splat_rows_are_splat() {
for (PoolSlot r : kLiveSplatSlots)
if (slot_category(r) != VramCategory::Splat || slot_phase(r) != PoolPhase::None)
return false;
for (PoolSlot r : kSplatDrawSlots)
if (slot_category(r) != VramCategory::Splat || slot_phase(r) != PoolPhase::None)
return false;
return true;
}
static_assert(ce_live_splat_rows_are_splat(),
"POOL_LIVE_SPLAT_TABLE: rows must be Splat and own their memory");
// DevicePool::category_bytes() books the whole arena to SplatXImg.
constexpr bool ce_alias_rows_grow() {
for (const AliasRow& r : kAliasRows)
if (slot_category(r.slot) != VramCategory::SplatXImg) return false;
return true;
}
static_assert(ce_alias_rows_grow(),
"POOL_ALIAS_TABLE: an arena-backed slot must be SplatXImg");
constexpr const char* to_string(PoolPhase p) {
switch (p) {
case PoolPhase::None: return "none";
+51 -4
View File
@@ -8,6 +8,7 @@
#include "backend/api/BackendRuntime.h"
#include "core/Env.h"
#include <algorithm>
#include <mutex>
#include <tuple>
#include <type_traits>
@@ -181,6 +182,8 @@ class DevicePool {
Arena _arena;
PoolPhase _phase = PoolPhase::None;
uint32_t _epoch = 0; // bumped per begin_phase; 0 never matches a Slot
int64_t _live_splats = 0;
int64_t _cap_splats = 0;
// Guards _slots / _dyn / _arena against concurrent acquire. It does NOT
// make the phase protocol thread-safe: two threads in different phases
// would fight over the arena, which the engine mutex is what prevents.
@@ -219,8 +222,9 @@ class DevicePool {
}
// Grow-if-needed on a Slot already selected by the caller (mutex held).
// `reserve` > n allocates room for that many elements up front.
template<typename T>
static T* _acquire_into(Slot& slot, size_t n) {
static T* _acquire_into(Slot& slot, size_t n, size_t reserve = 0) {
size_t bytes = n * sizeof(T);
// An arena slice is not capacity this slot may reuse; drop it first.
if (!slot.owns) { slot.ptr = nullptr; slot.cap_bytes = 0; slot.owns = true; }
@@ -230,12 +234,13 @@ class DevicePool {
// in a consistent empty state rather than {null ptr, stale cap}.
slot.ptr = nullptr;
slot.cap_bytes = 0;
slot.ptr = backend::device_malloc_checked(bytes);
slot.cap_bytes = bytes;
const size_t alloc = std::max(bytes, reserve * sizeof(T));
slot.ptr = backend::device_malloc_checked(alloc);
slot.cap_bytes = alloc;
// SS_POISON_POOL=1: fresh device memory holds whatever the
// driver left, so a read-before-write is silent where that is
// zero and catastrophic where it is not. NaN makes it show.
if (_poison_pool()) backend::memset_sync(slot.ptr, 0xff, bytes);
if (_poison_pool()) backend::memset_sync(slot.ptr, 0xff, alloc);
}
slot.owns = true;
slot.used_bytes = bytes;
@@ -333,10 +338,27 @@ public:
T* acquire(PoolKey key, size_t n) {
std::lock_guard<std::mutex> lock(_mu);
const PoolSlot slot = pool_key_slot(key);
const LiveSizing live = slot_live_sizing(slot);
if (live != LiveSizing::None && _live_splats > 0 &&
_cap_splats > _live_splats && n > 0) {
const size_t reserve = live == LiveSizing::Draw
? (size_t)_cap_splats
: (size_t)((double)n * (double)_cap_splats / (double)_live_splats + 1.0);
return _acquire_into<T>(_slots[key], n, reserve);
}
return _acquire_checked<T>(_slots[key], n, slot_phase(slot),
slot_name(slot));
}
// The engine's live and cap splat counts, which size the slots in
// POOL_LIVE_SPLAT_TABLE and POOL_SPLAT_DRAW_TABLE; the engine updates
// them wherever it changes either. 0 disables the reservation.
void set_splat_counts(int64_t live, int64_t cap) {
std::lock_guard<std::mutex> lock(_mu);
_live_splats = live;
_cap_splats = cap;
}
// Convenience: acquire a (slot, sub) buffer. sub defaults to the main slot.
template<typename T>
T* acquire(PoolSlot slot, size_t n, uint32_t sub = kSubMain) {
@@ -401,6 +423,7 @@ public:
if (_arena.ptr) backend::device_free(_arena.ptr);
_arena = Arena{};
_phase = PoolPhase::None;
_live_splats = _cap_splats = 0;
}
// Total bytes allocated (capacity, not logical size).
@@ -412,6 +435,30 @@ public:
return total;
}
// Per category, without building names: cheap enough to call every step.
struct CategoryBytes {
size_t used[(int)VramCategory::Count] = {};
size_t cap[(int)VramCategory::Count] = {};
};
CategoryBytes category_bytes() const {
std::lock_guard<std::mutex> lock(_mu);
CategoryBytes out;
for (uint32_t i = 0; i < _slots.size(); ++i) {
const Slot& s = _slots[i];
if (s.cap_bytes == 0 && s.used_bytes == 0) continue;
const int c = (int)slot_category(pool_key_slot(i));
out.used[c] += s.used_bytes;
out.cap[c] += s.cap_bytes;
}
for (const auto& kv : _dyn) {
const int c = (int)kv.second.cat;
out.used[c] += kv.second.slot.used_bytes;
out.cap[c] += kv.second.slot.cap_bytes;
}
out.cap[(int)VramCategory::SplatXImg] += _arena.cap;
return out;
}
// Per-slot breakdown: [(name, used_bytes, cap_bytes), ...]. Names are
// reconstructed byte-identically to the old string keys (slot_name + sub
// suffix), so existing consumers keep working.
+6
View File
@@ -679,6 +679,12 @@ std::vector<std::tuple<std::string, std::string, size_t, size_t>>
engine_get_pool_breakdown_categorized();
size_t engine_get_scratch_bytes();
// GPU seconds the next step spends in its splat stages (forward, raster and
// projection backward, optimizer, densify) for the trainer's ETA model. Arm
// before the step, read after; < 0 if not armed or the device has no timer.
void engine_step_timing_arm();
double engine_step_timing_read();
// Formatted per-category pool report (SS_PROFILE). Reads the pool's
// high-water capacities, so it is a peak, not an instantaneous figure.
std::string engine_vram_report();
+1
View File
@@ -431,6 +431,7 @@ int engine_load_checkpoint(std::string input_dir) {
// Install runtime scalars + optimizer layout from the checkpoint.
s.cur_num_splats = cur_n;
s.max_num_splats = max_n;
DevicePool::global().set_splat_counts(cur_n, max_n);
s.num_sh = num_sh;
s.sh_degree = sh_degree;
s.packed = (packed != 0);
+13
View File
@@ -9,7 +9,9 @@
#include "kernels/loss/PerPixelLoss.cuh" // LossWeightIndex
#include "kernels/pixelwise/PixelWise.cuh" // PPISPRegLossIndex
#include <algorithm>
#include <array>
#include <cstdint>
// Bundles the scalars engine_compute_loss_backward takes, for the call path
@@ -240,6 +242,17 @@ struct DensifyConfig {
int las_split_opacity_k_warmup = 4500;
};
// The schedule engine_densify_step grows the model on; the trainer's ETA and
// VRAM forecasts replay it.
inline bool densify_grows_at(const DensifyConfig& c, int step, int max_steps) {
return c.refine_every > 0 && step > c.refine_start_iter &&
step % c.refine_every == 0 &&
step < std::max(c.refine_stop_iter, max_steps - c.refine_stop_num_iter);
}
inline int64_t densify_target(const DensifyConfig& c, int64_t cur, int64_t cap) {
return std::max(cur, std::min(cap, (int64_t)(c.growth_factor * (float)cur)));
}
// Per-type Adam LR + TV regularization weight. lr <= 0 disables the channel
// for the current step (so a single config covers "enabled but skipped" too).
+4 -3
View File
@@ -90,7 +90,7 @@ int engine_densify_step(int step, int max_steps, const DensifyConfig& cfg) {
bool densify_ongoing =
(step < std::max(cfg.refine_stop_iter,
max_steps - cfg.refine_stop_num_iter));
bool do_densify = densify_ongoing && (step > cfg.refine_start_iter && step % cfg.refine_every == 0);
bool do_densify = densify_grows_at(cfg, step, max_steps);
float progress = ((float)step + 0.5f) / (float)max_steps;
// Use pool-backed DeviceVector/DeviceTensor from Buffers directly
@@ -340,7 +340,7 @@ int engine_densify_step(int step, int max_steps, const DensifyConfig& cfg) {
);
// Add more splats
int64_t n_target = std::min(max_num_splats, (int64_t)(cfg.growth_factor * cur_num_splats));
int64_t n_target = densify_target(cfg, cur_num_splats, max_num_splats);
num_added = (int)std::max((int64_t)0, n_target - cur_num_splats);
if (num_added > 0) {
// The oversize channel draws first, so its candidate set is the
@@ -406,7 +406,7 @@ int engine_densify_step(int step, int max_steps, const DensifyConfig& cfg) {
, engine().region.weight);
// MCMC sample add
int64_t n_target = std::min(max_num_splats, (int64_t)(cfg.growth_factor * cur_num_splats));
int64_t n_target = densify_target(cfg, cur_num_splats, max_num_splats);
num_added = (int)std::max((int64_t)0, n_target - cur_num_splats);
if (num_added > 0) {
add_splats_mcmc_tensor(
@@ -449,5 +449,6 @@ int engine_densify_step(int step, int max_steps, const DensifyConfig& cfg) {
}
engine().cur_num_splats = cur_num_splats + num_added;
DevicePool::global().set_splat_counts(engine().cur_num_splats, engine().max_num_splats);
return num_added;
}
+10
View File
@@ -185,3 +185,13 @@ void engine_ensure_optim_state(int sh_optim_bits, int sh_value_bits,
float* _engine_ppisp_reg_loss_into(
const std::array<float, (int)PPISPRegLossIndex::length>& loss_weights,
bool compute_grad);
// Brackets one of a step's splat stages while engine_step_timing_arm() is in
// force. Defined in EngineStepTiming.cpp.
struct SplatStageTimer {
SplatStageTimer();
~SplatStageTimer();
SplatStageTimer(const SplatStageTimer&) = delete;
SplatStageTimer& operator=(const SplatStageTimer&) = delete;
int _pair = -1;
};
+1
View File
@@ -146,6 +146,7 @@ static void _engine_raster_proj_backward(
TorchTensorView v_depth_dist = _tv_null(),
TorchTensorView v_normal_dist = _tv_null()
) {
SplatStageTimer stage_timer;
RenderOutput::TensorTuple v_render_outputs = std::make_tuple(
DeviceTensor3D<float3>(v_render_rgb),
DeviceTensor3D<float>(v_render_depth),
+1
View File
@@ -180,6 +180,7 @@ void engine_scene_activate(int slot) {
engine().world = s.world;
engine().cur_num_splats = s.cur_num_splats;
engine().max_num_splats = s.max_num_splats;
DevicePool::global().set_splat_counts(engine().cur_num_splats, engine().max_num_splats);
engine().num_sh = s.num_sh;
if (g_cs_slot != slot || g_cs_gen != s.cs_gen) {
engine_init_color_space(s.cs_enabled, s.cs_transfer, s.cs_is_linear,
+1
View File
@@ -29,6 +29,7 @@ void set_data_3dgs(
engine().cur_num_splats = num_splats;
engine().max_num_splats = max_num_splats;
DevicePool::global().set_splat_counts(engine().cur_num_splats, engine().max_num_splats);
auto sh_shape = std::get<2>(features_sh);
engine().num_sh = (sh_shape.size() >= 2) ? (int)sh_shape[1] : 0;
+59
View File
@@ -0,0 +1,59 @@
// GPU time of a step's splat-proportional stages, for the trainer's ETA model.
#include "engine/Engine.h"
#include "engine/EngineInternal.h"
#include "backend/api/BackendRuntime.h"
#include <utility>
#include <vector>
namespace {
struct StageTiming {
bool armed = false;
// Reused across steps; one pair per bracketed stage of the armed step.
std::vector<std::pair<backend::Event*, backend::Event*>> pairs;
size_t used = 0;
};
StageTiming& timing() {
static StageTiming t;
return t;
}
} // namespace
SplatStageTimer::SplatStageTimer() {
StageTiming& t = timing();
if (!t.armed) return;
if (t.used == t.pairs.size())
t.pairs.emplace_back(backend::event_create(true), backend::event_create(true));
_pair = (int)t.used++;
backend::event_record(t.pairs[_pair].first);
}
SplatStageTimer::~SplatStageTimer() {
if (_pair < 0) return;
backend::event_record(timing().pairs[_pair].second);
}
void engine_step_timing_arm() {
StageTiming& t = timing();
t.armed = true;
t.used = 0;
}
double engine_step_timing_read() {
StageTiming& t = timing();
if (!t.armed) return -1.0;
t.armed = false;
if (t.used == 0) return -1.0;
backend::event_synchronize(t.pairs[t.used - 1].second);
double ms = 0.0;
for (size_t i = 0; i < t.used; ++i)
ms += backend::event_elapsed_ms(t.pairs[i].first, t.pairs[i].second);
t.used = 0;
// 0 means the device has no timestamps, not that the stages were free.
return ms > 0.0 ? ms * 1e-3 : -1.0;
}
+13 -3
View File
@@ -139,7 +139,10 @@ static std::map<std::string, float> _engine_step_fwd_bwd_only(
engine().ppisp.enabled && engine().ppisp.cur_run_before_color_space;
engine().background.match_luma_pending = cfg.background.match_luminance;
forward_3dgs(primitive, sh_degree, packed, /*output_median=*/false, (int)dist_type);
{
SplatStageTimer stage_timer;
forward_3dgs(primitive, sh_degree, packed, /*output_median=*/false, (int)dist_type);
}
// PPISP already ran inside the forward in the before-color-space order.
const bool ppisp_after = engine().ppisp.enabled &&
@@ -186,7 +189,10 @@ static void _engine_step_optim_and_densify(
const EngineStepConfig& cfg,
std::map<std::string, float>& loss_dict
) {
engine_optim_step(step, cfg.optim);
{
SplatStageTimer stage_timer;
engine_optim_step(step, cfg.optim);
}
if (engine().background.enabled) {
engine_background_optim_step(step, cfg.background);
@@ -226,7 +232,11 @@ static void _engine_step_optim_and_densify(
ppisp_reg_readout.issue(losses_buf);
}
int num_added = engine_densify_step(step, max_steps, cfg.densify);
int num_added = 0;
{
SplatStageTimer stage_timer;
num_added = engine_densify_step(step, max_steps, cfg.densify);
}
loss_dict["num_added"] = (float)num_added;
loss_dict["cur_num_splats"] = (float)engine().cur_num_splats;
+178
View File
@@ -2207,6 +2207,184 @@ SS_MSG(vram_help,
"/ aygıtın kapasitesi. “?”, arka ucun o değeri sorgulayamadığı "
"anlamına gelir."));
// The VRAM forecast: the risk tag beside the bar, and its hover card.
SS_MSG(oom_risk_low,
EN("OOM risk: low"), JA("メモリ不足リスク: 低"), ZH_HANS("显存不足风险:低"),
ZH_HANT("顯示記憶體不足風險:低"), KO("메모리 부족 위험: 낮음"),
DE("OOM-Risiko: gering"), FR("Risque de saturation : faible"),
ES("Riesgo de falta de memoria: bajo"), PT("Risco de falta de memória: baixo"),
IT("Rischio memoria esaurita: basso"), NL("Risico geheugentekort: laag"),
RU("Риск нехватки памяти: низкий"), TR("Bellek yetmeme riski: düşük"));
SS_MSG(oom_risk_medium,
EN("OOM risk: medium"), JA("メモリ不足リスク: 中"), ZH_HANS("显存不足风险:中"),
ZH_HANT("顯示記憶體不足風險:中"), KO("메모리 부족 위험: 보통"),
DE("OOM-Risiko: mittel"), FR("Risque de saturation : moyen"),
ES("Riesgo de falta de memoria: medio"), PT("Risco de falta de memória: médio"),
IT("Rischio memoria esaurita: medio"), NL("Risico geheugentekort: middel"),
RU("Риск нехватки памяти: средний"), TR("Bellek yetmeme riski: orta"));
SS_MSG(oom_risk_high,
EN("OOM risk: high"), JA("メモリ不足リスク: 高"), ZH_HANS("显存不足风险:高"),
ZH_HANT("顯示記憶體不足風險:高"), KO("메모리 부족 위험: 높음"),
DE("OOM-Risiko: hoch"), FR("Risque de saturation : élevé"),
ES("Riesgo de falta de memoria: alto"), PT("Risco de falta de memória: alto"),
IT("Rischio memoria esaurita: alto"), NL("Risico geheugentekort: hoog"),
RU("Риск нехватки памяти: высокий"), TR("Bellek yetmeme riski: yüksek"));
SS_MSG(vram_chart_title,
EN("GPU memory over the run (GiB)"), JA("学習中の GPU メモリ(GiB)"),
ZH_HANS("训练过程中的显存(GiB)"), ZH_HANT("訓練過程中的顯示記憶體(GiB)"),
KO("학습 중 GPU 메모리(GiB)"), DE("Grafikspeicher im Verlauf (GiB)"),
FR("Mémoire GPU au fil de l'entraînement (Gio)"),
ES("Memoria de GPU durante el entrenamiento (GiB)"),
PT("Memória da GPU ao longo do treinamento (GiB)"),
IT("Memoria GPU durante l'addestramento (GiB)"),
NL("GPU-geheugen tijdens de training (GiB)"),
RU("Видеопамять по ходу обучения (ГиБ)"), TR("Eğitim boyunca GPU belleği (GiB)"));
SS_MSG(vram_legend_run,
EN("this run"), JA("この学習"), ZH_HANS("本次训练"), ZH_HANT("本次訓練"),
KO("이 학습"), DE("dieses Training"), FR("cet entraînement"),
ES("este entrenamiento"), PT("este treinamento"), IT("questo addestramento"),
NL("deze training"), RU("это обучение"), TR("bu eğitim"));
SS_MSG(vram_legend_projected,
EN("projected (95% band)"), JA("予測(95% 範囲)"), ZH_HANS("预测(95% 区间)"),
ZH_HANT("預測(95% 區間)"), KO("예측(95% 범위)"), DE("Prognose (95-%-Band)"),
FR("prévision (bande à 95 %)"), ES("previsión (banda del 95 %)"),
PT("previsão (faixa de 95%)"), IT("previsione (banda al 95%)"),
NL("prognose (95%-band)"), RU("прогноз (полоса 95 %)"), TR("tahmin (%95 aralığı)"));
SS_MSG(vram_legend_others,
EN("other programs"), JA("他のプログラム"), ZH_HANS("其他程序"), ZH_HANT("其他程式"),
KO("다른 프로그램"), DE("andere Programme"), FR("autres programmes"),
ES("otros programas"), PT("outros programas"), IT("altri programmi"),
NL("andere programma's"), RU("другие программы"), TR("diğer programlar"));
SS_MSG(vram_legend_capacity,
EN("device capacity"), JA("デバイスの容量"), ZH_HANS("设备容量"), ZH_HANT("裝置容量"),
KO("장치 용량"), DE("Kapazität des Geräts"), FR("capacité du périphérique"),
ES("capacidad del dispositivo"), PT("capacidade do dispositivo"),
IT("capacità del dispositivo"), NL("capaciteit van het apparaat"),
RU("объём устройства"), TR("aygıt kapasitesi"));
SS_MSG(vram_chart_peak,
EN("Projected peak: {0} ± {1} GiB free for training: {2} GiB chance of running out: {3}%"),
JA("予測ピーク: {0} ± {1} GiB 学習に使える量: {2} GiB 不足する確率: {3}%"),
ZH_HANS("预计峰值:{0} ± {1} GiB 可供训练:{2} GiB 耗尽的概率:{3}%"),
ZH_HANT("預計峰值:{0} ± {1} GiB 可供訓練:{2} GiB 耗盡的機率:{3}%"),
KO("예상 최대치: {0} ± {1} GiB 학습에 쓸 수 있는 양: {2} GiB 부족할 확률: {3}%"),
DE("Erwartete Spitze: {0} ± {1} GiB für das Training frei: {2} GiB Wahrscheinlichkeit, dass er ausgeht: {3} %"),
FR("Pic prévu : {0} ± {1} Gio disponible pour l'entraînement : {2} Gio probabilité de saturation : {3} %"),
ES("Pico previsto: {0} ± {1} GiB libre para entrenar: {2} GiB probabilidad de quedarse sin memoria: {3} %"),
PT("Pico previsto: {0} ± {1} GiB livre para o treinamento: {2} GiB chance de faltar memória: {3}%"),
IT("Picco previsto: {0} ± {1} GiB libera per l'addestramento: {2} GiB probabilità di esaurirla: {3}%"),
NL("Verwachte piek: {0} ± {1} GiB vrij voor training: {2} GiB kans op tekort: {3}%"),
RU("Ожидаемый пик: {0} ± {1} ГиБ доступно для обучения: {2} ГиБ вероятность нехватки: {3} %"),
TR("Beklenen tepe: {0} ± {1} GiB eğitim için boş: {2} GiB yetmeme olasılığı: %{3}"));
SS_MSG(vram_chart_provisional,
EN("The estimate firms up once the first densification step has run."),
JA("最初の高密度化ステップが済むと、推定の精度が上がります。"),
ZH_HANS("第一次加密步骤运行后,估计会更准确。"),
ZH_HANT("第一次加密步驟執行後,估計會更準確。"),
KO("첫 번째 밀집화 단계가 끝나면 추정이 더 정확해집니다."),
DE("Die Schätzung wird genauer, sobald der erste Verdichtungsschritt gelaufen ist."),
FR("L'estimation se précise après la première étape de densification."),
ES("La estimación se afina en cuanto se ejecuta el primer paso de densificación."),
PT("A estimativa fica mais precisa depois do primeiro passo de densificação."),
IT("La stima si affina dopo il primo passo di densificazione."),
NL("De schatting wordt nauwkeuriger zodra de eerste verdichtingsstap is uitgevoerd."),
RU("Оценка уточнится после первого шага уплотнения."),
TR("İlk yoğunlaştırma adımı çalıştıktan sonra tahmin netleşir."));
SS_MSG(vram_chart_waiting,
EN("The projection appears after the first steps have been measured."),
JA("最初のステップを計測すると予測が表示されます。"),
ZH_HANS("测量完最初的若干步后会显示预测。"),
ZH_HANT("量測完最初的若干步後會顯示預測。"),
KO("처음 몇 단계를 측정하면 예측이 표시됩니다."),
DE("Die Prognose erscheint, sobald die ersten Schritte gemessen sind."),
FR("La prévision apparaît une fois les premières étapes mesurées."),
ES("La previsión aparece cuando se han medido los primeros pasos."),
PT("A previsão aparece depois que os primeiros passos são medidos."),
IT("La previsione compare dopo che i primi passi sono stati misurati."),
NL("De prognose verschijnt zodra de eerste stappen gemeten zijn."),
RU("Прогноз появится, когда будут измерены первые шаги."),
TR("Tahmin, ilk adımlar ölçüldükten sonra görünür."));
SS_MSG(vram_breakdown_title,
EN("This run by category (GiB)"), JA("この学習の内訳(GiB)"),
ZH_HANS("本次训练按类别(GiB)"), ZH_HANT("本次訓練按類別(GiB)"),
KO("이 학습의 항목별 사용량(GiB)"), DE("Dieses Training nach Kategorie (GiB)"),
FR("Cet entraînement par catégorie (Gio)"), ES("Este entrenamiento por categoría (GiB)"),
PT("Este treinamento por categoria (GiB)"), IT("Questo addestramento per categoria (GiB)"),
NL("Deze training per categorie (GiB)"), RU("Это обучение по категориям (ГиБ)"),
TR("Bu eğitim, kategoriye göre (GiB)"));
SS_MSG(vram_breakdown_growth,
EN("Faded: growth still to come, up to the projected peak."),
JA("薄い部分: 予測ピークまでにこれから増える分。"),
ZH_HANS("浅色部分:到预计峰值前还会增加的量。"),
ZH_HANT("淺色部分:到預計峰值前還會增加的量。"),
KO("흐린 부분: 예상 최대치까지 앞으로 늘어날 양."),
DE("Blass: der Zuwachs, der bis zur erwarteten Spitze noch kommt."),
FR("En pâle : la croissance encore à venir, jusqu'au pic prévu."),
ES("Atenuado: el crecimiento que aún falta hasta el pico previsto."),
PT("Esmaecido: o crescimento que ainda virá, até o pico previsto."),
IT("Sbiadito: la crescita ancora da venire, fino al picco previsto."),
NL("Vaag: de groei die nog komt, tot de verwachte piek."),
RU("Бледным: рост, который ещё впереди, до ожидаемого пика."),
TR("Soluk: beklenen tepeye kadar daha gelecek artış."));
SS_MSG(vram_cat_splat,
EN("Splats"), JA("スプラット"), ZH_HANS("泼溅"), ZH_HANT("潑濺"), KO("스플랫"),
DE("Splats"), FR("Splats"), ES("Splats"), PT("Splats"), IT("Splat"),
NL("Splats"), RU("Сплаты"), TR("Splat'ler"));
SS_MSG(vram_cat_splat_x_img,
EN("Splats × images"), JA("スプラット × 画像"), ZH_HANS("泼溅 × 图像"),
ZH_HANT("潑濺 × 影像"), KO("스플랫 × 이미지"), DE("Splats × Bilder"),
FR("Splats × images"), ES("Splats × imágenes"), PT("Splats × imagens"),
IT("Splat × immagini"), NL("Splats × beelden"), RU("Сплаты × изображения"),
TR("Splat × görüntü"));
SS_MSG(vram_cat_image,
EN("Images"), JA("画像"), ZH_HANS("图像"), ZH_HANT("影像"), KO("이미지"),
DE("Bilder"), FR("Images"), ES("Imágenes"), PT("Imagens"), IT("Immagini"),
NL("Beelden"), RU("Изображения"), TR("Görüntüler"));
SS_MSG(vram_cat_appearance,
EN("Appearance"), JA("外観補正"), ZH_HANS("外观校正"), ZH_HANT("外觀校正"),
KO("외관 보정"), DE("Erscheinungsbild"), FR("Apparence"), ES("Apariencia"),
PT("Aparência"), IT("Aspetto"), NL("Uiterlijk"), RU("Внешний вид"),
TR("Görünüm"));
SS_MSG(vram_cat_viewer,
EN("Viewer"), JA("ビューア"), ZH_HANS("查看器"), ZH_HANT("檢視器"), KO("뷰어"),
DE("Betrachter"), FR("Visionneuse"), ES("Visor"), PT("Visualizador"),
IT("Visualizzatore"), NL("Viewer"), RU("Просмотр"), TR("Görüntüleyici"));
SS_MSG(vram_cat_other,
EN("Other"), JA("その他"), ZH_HANS("其他"), ZH_HANT("其他"), KO("기타"),
DE("Sonstiges"), FR("Autre"), ES("Otros"), PT("Outros"), IT("Altro"),
NL("Overig"), RU("Прочее"), TR("Diğer"));
SS_MSG(vram_cat_scratch,
EN("Sort scratch"), JA("ソート用の作業領域"), ZH_HANS("排序临时缓冲"),
ZH_HANT("排序暫存緩衝"), KO("정렬 작업 공간"), DE("Sortierpuffer"),
FR("Tampon de tri"), ES("Búfer de ordenación"), PT("Buffer de ordenação"),
IT("Buffer di ordinamento"), NL("Sorteerbuffer"), RU("Буфер сортировки"),
TR("Sıralama tamponu"));
SS_MSG(vram_cat_unpooled,
EN("Backend and staging"), JA("バックエンドと転送用"), ZH_HANS("后端与中转"),
ZH_HANT("後端與中轉"), KO("백엔드와 전송용"), DE("Backend und Staging"),
FR("Backend et transfert"), ES("Backend y transferencia"),
PT("Backend e transferência"), IT("Backend e trasferimento"),
NL("Backend en staging"), RU("Бэкенд и передача"), TR("Arka uç ve aktarım"));
// ===========================================================================
// Log panel
// ===========================================================================
+39
View File
@@ -1725,6 +1725,45 @@ SS_MSG(train_finished,
RU("Обучение завершено. Шагов: {0} Время: {1}"),
TR("Eğitim tamamlandı. Adım: {0} Süre: {1}"));
SS_MSG(vram_forecast_warn,
EN("Warning: training may run out of GPU memory. Projected peak: {0} ± {1} GiB "
"free for training: {2} GiB chance of running out: {3}%. Lower --cap-max, "
"or close other programs using the GPU."),
JA("警告: 学習中に GPU メモリが不足する可能性があります。予測ピーク: {0} ± {1} GiB "
"学習に使える量: {2} GiB 不足する確率: {3}%。--cap-max を下げるか、GPU を"
"使っている他のプログラムを閉じてください。"),
ZH_HANS("警告:训练可能会耗尽显存。预计峰值:{0} ± {1} GiB 可供训练:{2} GiB "
"耗尽的概率:{3}%。请调低 --cap-max,或关闭其他占用 GPU 的程序。"),
ZH_HANT("警告:訓練可能會耗盡顯示記憶體。預計峰值:{0} ± {1} GiB 可供訓練:{2} GiB "
"耗盡的機率:{3}%。請調低 --cap-max,或關閉其他佔用 GPU 的程式。"),
KO("경고: 학습 중 GPU 메모리가 부족할 수 있습니다. 예상 최대치: {0} ± {1} GiB "
"학습에 쓸 수 있는 양: {2} GiB 부족할 확률: {3}%. --cap-max를 낮추거나 GPU를 "
"쓰는 다른 프로그램을 닫으세요."),
DE("Warnung: Dem Training kann der Grafikspeicher ausgehen. Erwartete Spitze: "
"{0} ± {1} GiB für das Training frei: {2} GiB Wahrscheinlichkeit: {3} %. "
"Senken Sie --cap-max oder schließen Sie andere Programme, die die GPU nutzen."),
FR("Avertissement : l'entraînement risque de manquer de mémoire GPU. Pic prévu : "
"{0} ± {1} Gio disponible pour l'entraînement : {2} Gio probabilité : {3} %. "
"Réduisez --cap-max ou fermez les autres programmes qui utilisent le GPU."),
ES("Aviso: el entrenamiento puede quedarse sin memoria de GPU. Pico previsto: "
"{0} ± {1} GiB libre para entrenar: {2} GiB probabilidad: {3} %. Reduzca "
"--cap-max o cierre otros programas que usen la GPU."),
PT("Aviso: o treinamento pode ficar sem memória de GPU. Pico previsto: {0} ± {1} GiB "
"livre para o treinamento: {2} GiB probabilidade: {3}%. Reduza --cap-max ou "
"feche outros programas que usam a GPU."),
IT("Attenzione: l'addestramento potrebbe esaurire la memoria GPU. Picco previsto: "
"{0} ± {1} GiB libera per l'addestramento: {2} GiB probabilità: {3}%. Riduci "
"--cap-max o chiudi gli altri programmi che usano la GPU."),
NL("Waarschuwing: de training kan zonder GPU-geheugen komen te zitten. Verwachte piek: "
"{0} ± {1} GiB vrij voor training: {2} GiB kans: {3}%. Verlaag --cap-max of "
"sluit andere programma's die de GPU gebruiken."),
RU("Предупреждение: обучению может не хватить видеопамяти. Ожидаемый пик: "
"{0} ± {1} ГиБ доступно для обучения: {2} ГиБ вероятность нехватки: {3} %. "
"Уменьшите --cap-max или закройте другие программы, использующие GPU."),
TR("Uyarı: eğitimin GPU belleği yetmeyebilir. Beklenen tepe: {0} ± {1} GiB "
"eğitim için boş: {2} GiB yetmeme olasılığı: %{3}. --cap-max değerini düşürün "
"ya da GPU kullanan diğer programları kapatın."));
SS_MSG(partition_applied,
EN("Partition part {0}: cameras {1} (core {2}, ring {3}), seed points {4}"),
JA("分割パート {0}: カメラ {1}(コア {2}、リング {3})、初期点 {4}"),