Quota Preflight & Advisor¶
quota
¶
Read the meter before asking for the hardware.
capacity is what the system does after a provision fails. This module is what it does
before one is attempted: ask the project what it is actually allowed in the region it is about
to try, compare that against what the profiler sized, and either proceed, lower the ceiling, or
stop with a reason. A HARD_CEILING discovered by attempting a create costs ~12 minutes on the Ray
path; discovered here it costs one HTTP GET.
Four parts, in dependency order:
- The vocabulary (
QuotaMetricand the*_metricfunctions under it) — which meter a given piece of infra is actually billed to. This is the part that had been wrong. - The reader (
read_limits) — liveeffectiveLimitper metric per region from Service Usage. It never raises and never blocks a run on its own failure. - The reconciler (
reconcile) — pure. Turns a limit and an ask into a verdict and, where the ask is too big, a lowered set of bounds. - The advisor (
advise) — pure. The "your quota is costing you this much wall clock, and a raise would buy you this much back" report.
Why the vocabulary is a module and not a constant. On 2026-09-04 a Ray GPU pool provisioned
seven T4s in a project documented — in three separate places — as having four. Nothing was broken:
Dataproc's GPUs are billed to Compute Engine's nvidia_t4_gpus and Ray's to Vertex's
custom_model_training_nvidia_t4_gpus, and in us-central1 those are 4 and 12. The same
conflation had also been made for vCPUs (200 vs 2,200). One wrong number in prose is a typo; the
same wrong number reached for three times is a missing abstraction, so the mapping now lives in one
table that both the docs and the preflight read.
Three things learned from the live API that shape the code:
- Limits are returned as strings, and an absent
effectiveLimitis ambiguous — the proto means "unlimited", butus-east4reads absent for T4 whileus-east1reads2, which is plainly "not offered here" rather than "infinite". Neither reading is safe to act on, so absent isQUOTA_UNKNOWN: reported, never enforced. Only a literal zero blocks. - Vertex CPU quota is machine-family-specific.
custom_model_training_cpuscovers N1/E2 only;n2-,g2-,a2-,c2-,m1-,n4-anda3-machines each have their own metric. Checking the N1 meter for a G2 pool reads a number that has nothing to do with the run. - Dataproc has no capacity meter. Every
dataproc.googleapis.comquota is an API request rate; a Serverless batch's vCPUs are billed to Compute Enginecpusin the region. So the Serverless path checks a Compute Engine meter, which looks wrong and is right.
The clamp only ever lowers, and it only ever lowers what it has to. min_units is the
profiler's answer to "how much hardware does this load need" and quota is not evidence about load,
so a large allowance never raises a minimum: an eight-node quota does not mean a two-node job wants
eight nodes. Quota is a ceiling, and the only case in which it touches the floor is when the
floor is above it — a pool that cannot start is a pool that fails at create.
Nothing here may enter run_id. A quota reading is an observation of the world at a moment,
not an input the author chose; folding one into identity would mean the same config resolved to a
different run every time someone's allowance changed, and two runs of genuinely the same work would
stop being comparable. The same rule BatchInfra follows for SF_SERVERLESS_DEPS. The reconciler
therefore adjusts a plan, never a config, and every function here is either pure or a read.
QuotaAdvice
dataclass
¶
QuotaAdvice(
pool,
region,
metric,
limit,
ceiling_units,
saturating_units,
n_cells,
slots_per_unit,
seconds_per_cell,
projections=(),
density_note=None,
)
Why a run will take as long as it will, and what a quota increase would change (pure).
The user-facing half of this module, and the reason it is not merely a guard. A clamp that silently succeeds leaves an operator with a run that is four times slower than it needed to be and no way to know that quota was the cause — the wall clock looks like the model's fault. This turns the throttle into a number, and then into a number someone can put on a quota request.
density_note
class-attribute
instance-attribute
¶
fleet.RuntimeResourcePlan.density_note, carried here so it prints where it matters.
The note explains where slots_per_unit came from when the answer is not the obvious one —
memory holding a pool below what its cores allow, or a measured memory bound that the runtime
will not actually enforce. It used to be logged only by the engine, minutes into a run and
nowhere near the node count it qualifies. But the person reading the saturating width is
sizing a quota request from it, and that number is n_cells / slots_per_unit — so the
sentence explaining slots_per_unit belongs beside it, not in a different log.
throttled
property
¶
True when the fleet cannot grow wide enough to run every cell at once.
render
¶
Human-readable lines — what gets logged at plan time and on a clamp.
Source code in src/scale_forecasting/quota.py
to_dict
¶
JSON-safe dict for telemetry.
Source code in src/scale_forecasting/quota.py
QuotaDemand
dataclass
¶
What one pool wants from one meter in one region (pure).
per_unit is the metric's units consumed by a single node — accelerators per node for a
device metric, vCPUs per node for a CPU metric. fixed is the units-independent share: the
head node's vCPUs, which are spent whether or not a worker ever starts.
min_units / max_units are the pool's autoscaling bounds as the profiler sized them,
before any quota is consulted. The reconciler returns adjusted copies; these stay as the record
of what was wanted.
fixed_only
property
¶
True when the ask does not scale with a node count.
The shape a whole-cluster vCPU total takes: a Ray cluster's head node, CPU pool and GPU pool
all bill to the same Vertex meter at different rates, so the total is a single number rather
than a per-unit rate, and there is no units for which asking "how many fit" means
anything. reconcile answers such a demand with a straight does-it-fit.
amount
¶
to_dict
¶
JSON-safe dict for telemetry.
Source code in src/scale_forecasting/quota.py
units_within
¶
The largest node count whose consumption fits inside limit (pure, floored at 0).
Meaningless for a fixed_only demand, which reconcile handles before reaching here.
Source code in src/scale_forecasting/quota.py
QuotaMetric
dataclass
¶
One meter: the service that owns it, its id, and what it counts.
scope is the part that decides whether a shortfall may be acted on or only reported:
"pool"— the metric belongs to exactly one worker pool (a device count). Clamping it has an unambiguous meaning: make that pool smaller."shared"— several pools and the head node bill to it (vCPUs). Dividing a shortfall between them needs a policy nobody has asked for, and in practice it never binds: a Ray run is limited by 12 T4s long before it is limited by 2,200 vCPUs. So a shared metric reports, and blocks when even the minimum will not fit, but never silently reshapes a fleet.
QuotaOutcome
dataclass
¶
QuotaOutcome(
demand,
reading,
status,
min_units,
max_units,
autoscale_viable,
detail,
advisory=False,
)
One demand judged against one reading, with the bounds it leaves behind (pure).
min_units / max_units are the bounds to use; demand still carries what was asked
for, so the difference between the two is legible without reconstructing it.
autoscale_viable is False in the one case that has no autoscaling expression at all: an
allowance that permits exactly one node. Vertex requires min < max strictly, so a ceiling of
one cannot be written as a range and the pool has to be created fixed-size instead. Discovered
the hard way — a spec with min == max is rejected outright, and the resulting
InvalidArgument was being retried as though the cloud were merely busy.
advisory marks the one outcome that is QUOTA_OK and still worth saying out loud: a
shared meter that will not cover the full fleet. Nothing is reshaped — that is what shared
scope means — but "this region has 24 vCPUs and your cluster wants 68" is precisely the
sentence an operator needs, and without this flag QuotaPreflight.render would drop it for
being OK.
clamped
property
¶
True when the usable ceiling came out below the one the profiler asked for.
noteworthy
property
¶
True when this outcome has something to tell a reader — anything but a clean pass.
to_dict
¶
JSON-safe dict — part of one job_telemetry.$.capacity.preflight entry.
Source code in src/scale_forecasting/quota.py
QuotaPreflight
dataclass
¶
Every outcome for one region, plus the one-line answer: may this region be attempted?
blocked is what turns a preflight into a saved 12 minutes. A region that answers True here
is recorded as a capacity.HARD_CEILING and dropped from the candidate walk without a create
attempt being spent on it — the same conclusion the walk would eventually reach, reached
before the money.
block_reason
property
¶
Every blocking detail joined — the message the HARD_CEILING ledger entry carries.
for_pool
¶
The pool-scoped outcome for pool, or None when no meter covered it.
Pool-scoped only: a shared vCPU outcome names a pool too, but it never carries adjusted
bounds (see QuotaMetric.scope), so returning one to a caller asking "what should this
pool's bounds be" would hand back the unclamped numbers as though they had been checked.
Source code in src/scale_forecasting/quota.py
render
¶
Human-readable lines for the plan output and the launch log.
Source code in src/scale_forecasting/quota.py
to_dict
¶
JSON-safe dict — one entry under job_telemetry.$.capacity.preflight.
It rides in capacity.CapacityLedger rather than a column of its own because it answers
the same question the attempt list answers — why this run landed where it landed — and a
reader should not have to join two places for the two halves of that answer.
Source code in src/scale_forecasting/quota.py
QuotaReading
dataclass
¶
What the meter said, and how confidently (pure data).
limit is None for every kind of "we do not know": the API was unreachable, the caller
lacks serviceusage.services.get, the metric has no bucket for this region, or the bucket
came back with an absent effectiveLimit. They are one value because they warrant one
behaviour — report it, change nothing — and distinguishing them in the type would invite a
caller to act on a distinction that does not survive contact with the API.
detail keeps them apart for a human, which is where the difference actually matters.
to_dict
¶
advise
¶
advise(
outcome,
*,
n_cells,
slots_per_unit,
saturating_units,
seconds_per_cell,
density_note=None,
)
Price the current ceiling against the alternatives it is being compared to (pure).
The projection set is the current ceiling, then _RAISE_MULTIPLES of it, then — when the run is
throttled and the width that would un-throttle it is a number anyone could actually be granted
(_MAX_PROJECTION_MULTIPLE) — that width too. It is the one worth putting on a quota request,
which is why it is singled out; it is also the one that goes absurd fastest on a large fan-out,
which is why it is bounded.
Source code in src/scale_forecasting/quota.py
apply_to_ray_plan
¶
A copy of plan with each pool's bounds lowered to what the region will grant (pure).
Only the plan changes. The config is untouched and so is run_id: the same authored config
still resolves to the same run in a region that grants less, which is what makes two runs of the
same work comparable across a quota change. The adjustment is recorded in telemetry instead.
autoscale is switched off for the whole cluster when a pool comes back with a one-node
ceiling, because Vertex requires min < max strictly and a single node has no such range. The
switch is cluster-wide rather than per-pool only because RayClusterPlan.autoscale is one flag;
the alternative — a fixed pool beside an autoscaling one — is a shape the plan cannot express,
and inventing it to avoid a rare degenerate case is not worth the surface.
[cpu|gpu]_node_count is lowered alongside the ceiling. It is the fixed-size-equivalent, so
leaving it above a ceiling that was just lowered would mean the fixed path provisions a fleet
the region has already said no to.
Source code in src/scale_forecasting/quota.py
attachment_region
¶
The region a network-attachment resource name lives in (pure; unparseable → None).
Source code in src/scale_forecasting/quota.py
clamp_worker_count
¶
The worker count this region will actually grant, never above the one asked for (pure).
Only the device meter moves this number — the vCPU meter is shared and reports rather than
reshapes. Floored at one, because a zero-worker cluster is not a smaller cluster, it is a
different thing; a region that cannot seat even one worker comes back QuotaPreflight.blocked
and is dropped from the walk before this is ever consulted.
Source code in src/scale_forecasting/quota.py
cluster_demands
¶
cluster_demands(
region,
*,
master_machine_type,
worker_machine_type,
worker_count,
gpu_type=None,
accelerators_per_worker=0,
)
The meters a Dataproc cluster of this shape will draw on, in one region (pure).
Two demands, and both are Compute Engine's. A Dataproc cluster is a set of GCE VMs and is
billed as one; nothing under dataproc.googleapis.com is a capacity meter, so there is no
third, Dataproc-specific number to go looking for.
- the GPU device meter, pool-scoped, so a shortfall lowers the worker count. Note this is
compute_gpu_metricand notvertex_gpu_metric— the same T4 reads 4 here and 12 on the Ray path, and reaching for the wrong one is the mistake this module exists to make impossible. - the cluster's vCPUs, master plus workers, against
compute_cpu_metric. Shared scope, so a shortfall is reported rather than acted on (seeQuotaMetric.scope): those vCPUs are the same pool every other VM in the project draws from, and shrinking this cluster because something else is using the region is a judgement call belonging to a person. The one case it does stop a run is when even a single worker will not fit, which is not a judgement call.
min_units is 1, not the worker count, and that difference is the whole behaviour. A
Dataproc cluster does not autoscale, so its "floor" is not a shape requirement the way a Ray
pool's is — it is just the smallest thing that is still a cluster. An allowance that seats two
of the eight workers asked for should build a two-worker cluster and say so, not refuse to
build anything.
No wall-clock advice comes back from this path, and the omission is deliberate rather than
unfinished: advise prices a ceiling in cells-per-slot, and a cluster is sized against tasks
— buckets of compute.bucket_target_cells cells that run sequentially inside one frame (see
dataproc_cluster.cluster_sizing). Quoting the Ray arithmetic over a different unit would put a
confidently wrong number in front of an operator.
Source code in src/scale_forecasting/quota.py
1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 | |
compute_cpu_metric
¶
The Compute Engine regional vCPU meter.
Billed by Dataproc clusters and by Dataproc Serverless batches — Serverless has no meter of
its own (every dataproc.googleapis.com quota is an API request rate), so a batch's vCPUs
land here.
Source code in src/scale_forecasting/quota.py
compute_gpu_metric
¶
The Compute Engine meter for an accelerator, or None for one not tabulated (pure).
What a Dataproc cluster's GPUs are billed to. Deliberately a different function from
vertex_gpu_metric rather than a flag on one: the two are separate allowances with separate
numbers, and a signature that made them a parameter is a signature that invites passing the
wrong one.
Source code in src/scale_forecasting/quota.py
estimate_wall_seconds
¶
Wall clock for n_cells at a given fleet width (pure; unmeasured → None).
ceil(n_cells / concurrency) x seconds_per_cell. Deliberately the crudest possible model: it
ignores stragglers, scheduling gaps and the cluster create, all of which make it an
under-estimate of the absolute time. That is fine, because the number it exists to produce is
a ratio — halve the fleet and the estimate doubles — and every one of those omissions is
roughly constant across the widths being compared.
Source code in src/scale_forecasting/quota.py
format_duration
¶
4512.0 → 1h15m (pure). Coarse on purpose — these are estimates, not measurements.
Source code in src/scale_forecasting/quota.py
gce_demands
¶
The meters a Compute Engine single-VM job of this shape will draw on, in one region (pure).
Draws on Compute Engine meters (compute_gpu_metric and compute_cpu_metric) with fixed=0
and min_units=max_units=1: a single VM has zero head/master node tax and requires 1 unit.
Source code in src/scale_forecasting/quota.py
gke_demands
¶
The meters a GKE job's node pool will draw on, in one region (pure).
GKE node pools provision Compute Engine VMs underneath, so they draw on the regional Compute
Engine device (compute_gpu_metric) and vCPU (compute_cpu_metric) meters with fixed=0
(GKE's control plane is Google-managed and consumes zero project vCPU quota).
Source code in src/scale_forecasting/quota.py
gpu_type_from_accelerator
¶
Vertex's accelerator enum back to the config's short name — NVIDIA_TESLA_T4 → T4.
The plan carries the enum (that is what the SDK wants) while the quota table is keyed on the short name (that is what a person types and what the metric id echoes). Deriving one from the other here means the preflight needs nothing threaded down from the config.
Pure. None for a name whose remainder is not in _GPU_METRIC_SUFFIX — an accelerator we
have no meter for is one the preflight declines to judge, not one it guesses about.
Source code in src/scale_forecasting/quota.py
machine_family
¶
The GCE family prefix of a machine type — n1-standard-8 → n1 (pure).
Returns "" for a name with no prefix, which routes to the default metric rather than to a
lookup miss.
Source code in src/scale_forecasting/quota.py
parse_buckets
¶
Pull one region's effectiveLimit out of a consumerQuotaMetrics GET (pure).
The response holds one or more consumerQuotaLimits, each with a quotaBuckets list whose
entries carry a dimensions map. The regional bucket is the one whose dimensions.region
matches; the bucket with no dimensions is the project-wide default and is used only as a
fallback when no regional bucket exists.
Every not-found path returns an unknown reading rather than raising — a preflight that can fail is a preflight that has to be wrapped in a try by every caller, and one of them will forget.
Source code in src/scale_forecasting/quota.py
preflight_cluster
¶
preflight_cluster(
regions,
project_id,
*,
master_machine_type,
worker_machine_type,
worker_count,
gpu_type=None,
accelerators_per_worker=0,
fetch=None,
)
Read each candidate region's Compute Engine meters and judge this cluster shape against them.
The Dataproc analog of preflight_ray, and the same economics: one GET per metric returns every
regional bucket, so judging three candidate regions costs exactly what judging one does. Returns
one QuotaPreflight per region, in the order given.
The Dataproc candidate list is zonal (compute_fallback.Candidate) while these meters are
regional, so several candidates share a verdict. Callers group by region before calling.
Source code in src/scale_forecasting/quota.py
preflight_gce
¶
Read each candidate region's Compute Engine meters and judge plan against them.
Source code in src/scale_forecasting/quota.py
preflight_gke
¶
Read each candidate region's Compute Engine meters and judge a GKE plan against them.
Source code in src/scale_forecasting/quota.py
preflight_ray
¶
preflight_ray(
plan,
regions,
project_id,
*,
fetch=None,
cpu_seconds_per_cell=None,
gpu_seconds_per_cell=None,
)
Read every candidate region's meters and judge this cluster shape against each.
Returns one QuotaPreflight per region, in the order given. Regions are judged all at once
rather than lazily per attempt because the reading is one GET per metric for every region at
once — so knowing that two of three candidates are hopeless costs exactly as much as knowing it
about one, and the walk can then be handed only the candidates worth spending a create on.
*_seconds_per_cell come from the measured profile when there is one; without them the advice
still reports the ceiling and the throttle ratio, just not a wall clock.
Source code in src/scale_forecasting/quota.py
preflight_vertex
¶
Read each candidate region's Vertex training meters and judge plan against them.
Source code in src/scale_forecasting/quota.py
ray_demands
¶
The meters a Vertex Ray cluster of this shape will draw on, in one region (pure).
Three demands at most, and the third is the one the old documentation missed entirely:
- the GPU device meter, pool-scoped, so a shortfall lowers the GPU pool's ceiling;
- the CPU pool against Vertex's training-vCPU meter for its machine family, shared-scope;
- the whole cluster's vCPU total — head node plus both pools at full scale — as a
QuotaDemand.fixed_onlyask, because those three bill to the same meter at three different rates and no per-node figure describes the sum.
A pool with no planned nodes contributes nothing: a run with no deep-learning work should not be blocked by an accelerator allowance it was never going to spend.
Note both vCPU demands are Vertex's, not Compute Engine's. A Ray worker is a Vertex training
node and is billed as one; compute.googleapis.com/cpus has nothing to say about it.
Source code in src/scale_forecasting/quota.py
read_limits
¶
Read every (metric, region) pair, keyed by (metric.metric, region).
One HTTP GET per metric — the response already carries every region's bucket, so asking per region would multiply the calls for no extra information.
fetch(service, metric) -> dict is injected by tests; the default builds an
AuthorizedSession from ADC, the same way identity does. Nothing here raises. A
preflight is a diagnostic: when it cannot run, the run proceeds exactly as it did before this
module existed, which is also what makes it safe to deploy under a service account that has not
yet been granted serviceusage.services.get.
Source code in src/scale_forecasting/quota.py
reconcile
¶
Judge one ask against one allowance and return the bounds to actually use (pure).
The rules, and the asymmetry between them is the whole design:
- Unknown limit →
QUOTA_UNKNOWN, bounds untouched. Never guess. - Zero →
QUOTA_BLOCKED. The region cannot host this pool at all. - A shared metric that cannot seat the minimum →
QUOTA_BLOCKED. Reported, not reshaped: seeQuotaMetric.scopefor why a shared shortfall is not divided up automatically. - Fewer units allowed than the ceiling asks →
QUOTA_CLAMPED, ceiling lowered to what fits. - Fewer units allowed than the floor asks → the floor comes down too, because a pool whose
minimum exceeds quota does not start. This is the only circumstance in which a quota reading
touches
min_units: a generous allowance never raises a floor, since quota is evidence about permission andmin_unitsis evidence about load. An eight-node allowance does not mean a two-node job wants eight nodes. - Otherwise →
QUOTA_OK.
Finally min < max is re-established, because lowering a ceiling onto a floor produces
exactly the malformed spec Vertex rejects. Where the ceiling has room the floor steps back one;
where the ceiling is one node there is nowhere to step, so autoscale_viable goes False and
the caller creates the pool fixed-size.
Source code in src/scale_forecasting/quota.py
511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 | |
regions_without_attachment
¶
Candidate regions a PSC-I cluster cannot be created in, mapped to why (pure).
A network attachment is a regional resource, and Terraform builds exactly one. So a config
listing three ray_regions while RayInfra.network_attachment names an attachment in
us-central1 has, in fact, one usable region — the other two 404 on the attachment, and they
do it before quota is ever consulted, which is why no amount of quota-reading finds them.
Found live 2026-09-04, where a single-candidate walk against a region with no attachment churned for 35 minutes on a config that could never have succeeded. The check costs a regex.
Returns {} when no attachment is configured (public or VPC-peered clusters have no such
constraint) or when the name does not parse — an unreadable prerequisite is not evidence of a
missing one, and blocking a launch on a failed parse would be the worst kind of false
negative.
Source code in src/scale_forecasting/quota.py
report_for_run
¶
The whole quota picture for a config, as lines to print. Reads only; changes nothing.
This is the --quota verb. Everything under it already runs inside a launch — the launch path
reads the same meters and clamps the same plans — so the report exists for the one thing the
launch path cannot do, which is tell you before you spend anything. An operator deciding
whether to file a quota increase should not have to start a run to find out what it would buy.
One block per metered family node in the DAG (dag.dag_nodes), because the ask genuinely
differs between them: a deep_learning job on Vertex T4s and a statistical job on a
Dataproc cluster draw on different services' meters for the same card, with different
allowances. Three shapes come back:
- Ray — the Vertex pools, their ceilings, and what a raise would buy in wall clock.
- Vertex CustomJob — the Vertex training-CPU and GPU meters with zero head-node tax.
- Compute Engine single VM — the Compute Engine device and vCPU meters with zero head-node tax.
- Google Kubernetes Engine — the Compute Engine device and vCPU meters with zero control-plane vCPU tax.
- Spark on an ephemeral cluster — the Compute Engine device and vCPU meters, and the worker count the region will actually grant.
- Spark on Serverless, BigQuery, and a reused cluster — named, not metered. A reused cluster
already exists so nothing is being asked for; a Serverless batch has no fixed allocation to
pre-read (its vCPUs bill to Compute Engine as they are used); BigQuery has no capacity meter
at all (
UNMETERED_SERVICES).
Best-effort throughout: an unresolvable environment or an unreadable meter produces a line saying so, never an exception. A reporting verb that can fail is one an operator stops running.
Source code in src/scale_forecasting/quota.py
1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 | |
vertex_cpu_metric
¶
The Vertex training-CPU meter for a machine type (pure; untabulated family → N1/E2).
Source code in src/scale_forecasting/quota.py
vertex_demands
¶
The meters a Vertex AI CustomJob of this shape will draw on, in one region (pure).
Draws on the same Vertex custom-model-training meters as Ray on Vertex (vertex_gpu_metric and
vertex_cpu_metric), but with fixed=0 because a CustomJob has zero head-node tax:
every replica in worker_pool_specs is an active training worker.
Source code in src/scale_forecasting/quota.py
vertex_gpu_metric
¶
The Vertex training meter for an accelerator, or None for one not tabulated (pure).