Record the inputs a run actually starts from, not only the ones passed
A run submitted without explicit inputs recorded `params = {}`: Port
initials filled the values at node level and were never written back, so an
exported row had a blank `param.*` cell and the runs listing could not tell
a run that took every default from one submitted with those same numbers.
Declared initials are now folded in at submit, explicit values winning, and
the run-level seed still wins over a declared one.
`params_digest` is computed over the resolved values, so it changes shape
once: a run recorded before this does not dedupe against a newer identical
submit, and its stage-cache entries miss once.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Hra4ndWMCLU5F3KjUuVAc
This commit is contained in:
@@ -846,6 +846,20 @@ class RunService:
|
|||||||
# reference a python caller would have passed and every later reader —
|
# reference a python caller would have passed and every later reader —
|
||||||
# the digest, the cache, the run detail — sees one spelling.
|
# the digest, the cache, the run detail — sees one spelling.
|
||||||
params = resolve_references(flow, params, self._artifacts)
|
params = resolve_references(flow, params, self._artifacts)
|
||||||
|
# What the run actually starts from, not only what was passed: an input
|
||||||
|
# left out takes its declared value, and a row that records `{}` cannot
|
||||||
|
# say which. Folded literally — an initial is a value from the
|
||||||
|
# definition, never a reference to resolve. The run-level seed still
|
||||||
|
# wins over a declared one, the way `seed_values` has it.
|
||||||
|
params = {
|
||||||
|
**{
|
||||||
|
declared.spec.name: declared.initial
|
||||||
|
for declared in flow.inputs
|
||||||
|
if declared.initial is not None
|
||||||
|
and not (declared.spec.name == "seed" and seed is not None)
|
||||||
|
},
|
||||||
|
**params,
|
||||||
|
}
|
||||||
# Checked here rather than in the driver: a caller who mistyped a
|
# Checked here rather than in the driver: a caller who mistyped a
|
||||||
# parameter should be told now, not by a run that fails in a minute.
|
# parameter should be told now, not by a run that fails in a minute.
|
||||||
seed_values(flow, params, seed)
|
seed_values(flow, params, seed)
|
||||||
|
|||||||
@@ -400,6 +400,63 @@ def test_a_repeated_submit_returns_the_run_it_already_made():
|
|||||||
assert again.id == "dedup-1"
|
assert again.id == "dedup-1"
|
||||||
|
|
||||||
|
|
||||||
|
class _OneFlow:
|
||||||
|
"""A controller that has exactly one flow and no engine behind it."""
|
||||||
|
|
||||||
|
def __init__(self, flow):
|
||||||
|
self.store = self
|
||||||
|
self._flow = flow
|
||||||
|
|
||||||
|
def read_flow(self, name, draft=False):
|
||||||
|
return self._flow
|
||||||
|
|
||||||
|
def head(self):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
class _Collect:
|
||||||
|
def __init__(self):
|
||||||
|
self.items = []
|
||||||
|
|
||||||
|
def add(self, item):
|
||||||
|
self.items.append(item)
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_run_records_the_inputs_it_actually_starts_from():
|
||||||
|
"""An input left out takes its declared value, and the row says so.
|
||||||
|
|
||||||
|
`params = {}` could not tell a run that took every default from one
|
||||||
|
submitted with those same numbers spelled out — and an export of the
|
||||||
|
first had a blank cell where its `lr` should be.
|
||||||
|
"""
|
||||||
|
flow = FlowDef(
|
||||||
|
name="study",
|
||||||
|
mode="batch",
|
||||||
|
inputs=[
|
||||||
|
FlowInput(
|
||||||
|
spec=MessageSpec(name="lr", dtype=DType.FLOAT), initial=0.01
|
||||||
|
),
|
||||||
|
FlowInput(spec=MessageSpec(name="epochs", dtype=DType.INT)),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
service = RunService(controller=_OneFlow(flow), queue=_Collect())
|
||||||
|
made = []
|
||||||
|
try:
|
||||||
|
defaulted = service.submit("study", {"epochs": 5})
|
||||||
|
made.append(defaulted.id)
|
||||||
|
assert defaulted.params == {"lr": 0.01, "epochs": 5}
|
||||||
|
|
||||||
|
# Spelling out the declared value is the same run, and now reads as it.
|
||||||
|
spelled = service.submit("study", {"lr": 0.01, "epochs": 5})
|
||||||
|
made.append(spelled.id)
|
||||||
|
assert spelled.params_digest == defaulted.params_digest
|
||||||
|
finally:
|
||||||
|
with Session(db_engine) as session:
|
||||||
|
for run in session.exec(select(Run).where(col(Run.id).in_(made))).all():
|
||||||
|
session.delete(run)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
|
||||||
def test_a_key_nobody_used_submits_normally(
|
def test_a_key_nobody_used_submits_normally(
|
||||||
client, superuser_token_headers, monkeypatch
|
client, superuser_token_headers, monkeypatch
|
||||||
):
|
):
|
||||||
|
|||||||
Reference in New Issue
Block a user