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 —
|
||||
# the digest, the cache, the run detail — sees one spelling.
|
||||
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
|
||||
# parameter should be told now, not by a run that fails in a minute.
|
||||
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"
|
||||
|
||||
|
||||
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(
|
||||
client, superuser_token_headers, monkeypatch
|
||||
):
|
||||
|
||||
Reference in New Issue
Block a user