Parameter Profiles and Reactive Granularity in Marimo: Dev vs Production Tension #8530
Replies: 2 comments 9 replies
-
|
Figured I'd share my thoughts/pattern. I usually rely on pydantic to define some model parameters for training, something like: from pydantic import computed_field, BaseModel, Field
class ModelParams(BaseModel):
epochs: int = Field(default=25, description="Number of training epochs.")
batch_size: int = Field(default=32, description="Training batch size.")
learning_rate: float = Field(default=1e-4, description="Learning rate for AdamW.")
wandb_project: str = Field(default="batch-sizes", description="W&B project name (empty to skip).")These params can be generated with a marimo form, or from the command line. params_form = mo.md("""
## Model parameters
{epochs}
{batch_size}
{learning_rate}
""").batch(
epochs=mo.ui.slider(10, 50, value=50, step=1, label="epochs"),
batch_size=mo.ui.slider(8, 512, value=32, step=8, label="batch size"),
learning_rate=mo.ui.slider(1e-5, 5e-4, value=1e-4, step=1e-5, label="learning rate"),
).form()I usually merge that via something like: if mo.app_meta().mode == "script":
model_params = ModelParams(
**{k.replace("-", "_"): v for k, v in mo.cli_args().items()}
)
else:
model_params = ModelParams(**(params_form.value or {}))The main thinking is that Tool vs. PatternIn my mind this is much more of a pattern and not so much a feature that needs to be added. This is also why I added this example as part of the marimo batch job skill (here). LLMs can help fill in the blanks, assuming that you've gotten a good I am coming at this from the perspective of a ML/data science batch job though. If I'm missing a use-case with this approach I'll gladly hear it. |
Beta Was this translation helpful? Give feedback.
-
|
I think I came up with a pretty neat pattern that let's you load a profile from a file (eg yaml), let's you modify the values using marimo ui elements, and then let's you save it to the same or a new profile file. Upsides:
Downsides:
Here's an example notebook that demonstrates this pattern: # /// script
# dependencies = [
# "marimo",
# ]
# requires-python = ">=3.12"
# ///
import marimo
__generated_with = "0.20.4"
app = marimo.App(width="medium")
with app.setup:
import marimo as mo
import yaml
from pathlib import Path
@app.cell(hide_code=True)
def _():
mo.md(r"""
# Load a profile
By default the paramters will start out blank.
If you choose a profile here, these will be used as the basis.
""")
return
@app.cell(hide_code=True)
def _():
profiles_dir = Path("./profiles/")
profiles_dir.mkdir(exist_ok=True)
file_browser = mo.ui.file_browser(initial_path=profiles_dir, multiple=False, filetypes=["yaml"], restrict_navigation=True)
file_browser
return file_browser, profiles_dir
@app.cell(hide_code=True)
def _(file_browser):
loaded_profile = {}
# Load profile from yaml. file_browser is prioritized over CLI given --profile_path
yaml_path = mo.cli_args().get("profile_path")
if file_browser.value:
yaml_path = file_browser.value[0].path
if yaml_path:
with open(file_browser.value[0].path) as f:
loaded_profile = yaml.load(f, Loader=yaml.FullLoader)
loaded_profile.update(mo.cli_args()) # Load individual parameters from CLI
return (loaded_profile,)
@app.cell(hide_code=True)
def _():
mo.md(r"""
# Define the parameters
This can be done all over the notebook
""")
return
@app.cell(hide_code=True)
def _(loaded_profile):
threshold_field = mo.ui.number(label="Threshold", value=loaded_profile.get("threshold"))
threshold_field
return (threshold_field,)
@app.cell(hide_code=True)
def _(loaded_profile):
epochs_field = mo.ui.number(label="Epochs", value=loaded_profile.get("epochs"))
epochs_field
return (epochs_field,)
@app.cell(hide_code=True)
def _():
mo.md(r"""
The priority order in defining parameters looks like this (high to low):
1. UI fields
2. Individual CLI arguments (--threshold and --epochs)
3. File-browser profile file
4. CLI profile file (--profile_path)
""")
return
@app.cell(hide_code=True)
def _():
mo.md(r"""
# Use the parameters
""")
return
@app.cell
def _(threshold_field):
threshold_field.value
return
@app.cell
def _(epochs_field):
epochs_field.value
return
@app.cell(hide_code=True)
def _():
mo.md(r"""
# Save the profile
By default save it to the same profile it was opened from, but you can also save it to a new profile.
""")
return
@app.cell
def _(epochs_field, threshold_field):
# When adding new parameters, don't forget to add them here
profile = {
"threshold": threshold_field.value,
"epochs": epochs_field.value
}
return (profile,)
@app.cell(hide_code=True)
def _(file_browser, profile, profiles_dir):
output_filename_field = mo.ui.text(value=file_browser.value[0].path.stem, label="Profile name")
def save_profile(_):
filename = output_filename_field.value
if filename:
if not filename.endswith(".yaml"):
filename += ".yaml"
with open(profiles_dir / filename, "w") as f:
yaml.dump(profile, f)
print(f"Wrote succesfully to {filename}")
else:
print("Invalid profile name")
save_button = mo.ui.button(on_click=save_profile, label="Save profile")
mo.vstack([output_filename_field, save_button])
return
if __name__ == "__main__":
app.run() |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
I’ve been using marimo notebooks both as an interactive development environment and as production scripts, and I’ve run into some friction around parameter management. I’m curious how others are handling this.
Context
I work with multiple clients, each with different parameter profiles, for example:
and read them in the notebook like this:
During development and debugging, I often need to:
Steps 1 and 3 feel repetitive and error-prone.
Problems
1. Reactivity Granularity
Using standard argument parsers (e.g.
argparse,simple-parsing,cyclopts) means parsing all arguments in a single cell.Because marimo tracks dependencies at the cell level, changing any argument marks all downstream cells as stale — even those that don’t depend on the modified argument.
Defining each parameter in its own cell with
mo.cli_args()improves reactive granularity. However, this leads to other tradeoffs (see below).2. No Central
--helpSupportBecause
mo.cli_args()is called lazily per-cell, there is no central declaration of:This makes implementing
--helpdifficult or impossible in a clean way.With traditional CLI tools, argument definitions are centralized and automatically documented.
Possible Approaches
Here are a few ideas I’ve considered:
Option 1: External Shell Scripts
Use shell scripts to import/export profiles.
Downside:
Upside:
Option 2: Config File Support (JSON/YAML)
Marimo could optionally read from a config file.
Each key would map to a reactive variable. If a single value changes, only cells depending on that variable are marked stale.
Benefits:
Option 3: A “Configuration Cell” Type
Introduce a special cell type (similar to the setup cell) with strict key-value syntax, e.g.:
Features could include:
--helpgenerationThis would preserve:
Questions for the Community
Beta Was this translation helpful? Give feedback.
All reactions