What you will be able to do
- Run a hyperparameter search with the Tuner, TunerConfig and TunerContext APIs
- Wrap an external or custom model in CustomModel with a ModelContext and log it with its dependencies and code
- Pin training to an immutable Dataset version and trace a model version back to its data with ML Lineage
1.Hyperparameter optimization with the Tuner API
The Snowflake ML Hyperparameter Optimization (HPO) API runs tuning trials in parallel and works with any model. Your training code can use any open-source framework or the Snowflake ML APIs. You run it from a Snowflake Notebook that uses Container Runtime. The trials execute in Snowpark Container Services on CPU or GPU, spread across the cores of one node in the compute pool. The API also brings in data efficiently through Snowflake Datasets or DataFrames, and it captures ML lineage automatically.
You pass data to the tuner as a dataset_map, a dictionary that maps names such as "train" and "test" to DataConnector objects. Next you choose a search algorithm and describe how each hyperparameter is sampled.
| Class | How it explores |
|---|---|
| GridSearch | Tries every combination in a grid you define |
| BayesOpt | Uses a probabilistic model and earlier trial results to choose the next configuration |
| RandomSearch | Samples randomly; works well for large or mixed continuous/discrete spaces |
| Function | Samples | Typical use |
|---|---|---|
| uniform(lower, upper) | Continuous value, uniform between bounds | Dropout rate, regularization strength |
| loguniform(lower, upper) | Value uniform in log space | Learning rates spanning orders of magnitude |
| randint(lower, upper) | Integer, lower inclusive, upper exclusive | Number of layers, epochs, estimators |
| choice(options) | One value from a list | Categorical options such as activation functions |
Checkpoint 1 of 9· Check yourself
You want to search learning rates between 1e-5 and 1e-1. Which sampling function should you use?
loguniform samples evenly in log space. A uniform draw over this range would put almost every sample near the upper end.
“Samples a value in logarithmic space, ideal for parameters that span several orders of magnitude (for example, learning rates).”Source: docs.snowflake.com
TunerConfig defines the objective and how the search runs: the metric to optimize, the mode ("max" or "min"), the search algorithm, the number of trials and the maximum number of concurrent trials.
from snowflake.ml.modeling import tune
tuner_config = tune.TunerConfig(
metric="accuracy",
mode="max",
search_alg=BayesOpt(
utility_kwargs={"kind": "ucb", "kappa": 2.5, "xi": 0.0}
),
num_trials=5,
max_concurrent_trials=1,
)Your training function gets the current trial's hyperparameters and data from the TunerContext. It must send its results back with report, because the optimizer uses each trial's metrics to decide what to try next.
def train_func():
tuner_context = get_tuner_context()
config = tuner_context.get_hyper_params()
dm = tuner_context.get_dataset_map()
...
tuner_context.report(metrics={"accuracy": accuracy}, model=model)Finally, tune.Tuner(train_func, search_space, tuner_config).run(dataset_map=dataset_map) distributes the trials and returns a TunerResults. Its results attribute is a pandas DataFrame with every trial. best_result is the row for the best trial, and best_model is that trial's model.
Checkpoint 2 of 9· Put it in order
Put the HPO workflow steps in order
- 1.Choose the search algorithm
- 2.Configure the tuner with TunerConfig
- 3.Initiate training with Tuner(...).run(dataset_map=...)
- 4.Ingest the data into a dataset_map
- 5.Define how hyperparameters are sampled (search space)
- 6.Read TunerResults for the best configuration and model
The Tuner takes the training function, search space and config together, so all three must exist before run() is called. The results exist only after the trials finish.
“Use the Tuner object to initiate the training job. The Tuner object takes the training function, search space, and tuner configuration as arguments.”Source: docs.snowflake.com
Sources1
2.Bringing external models in with CustomModel
The Model Registry can log built-in model types directly. For anything else, such as a model trained with an external tool, an open-source model, or a custom algorithm, subclass snowflake.ml.model.custom_model.CustomModel. A ModelContext carries what the model needs at inference time. You create it with your own keyword arguments, and each value is either an in-memory model object of a supported type or a string file path, such as a pickle or a JSON config. Everything in the context is packaged with the model.
model_context = custom_model.ModelContext(
my_model=my_model,
)
# Define a custom model class that utilizes the context
class ExampleBringYourOwnModel(custom_model.CustomModel):
def __init__(self, context: custom_model.ModelContext) -> None:
super().__init__(context)
@custom_model.inference_api
def predict(self, input: pd.DataFrame) -> pd.DataFrame:
# Use the model with key 'my_model' from the context to make predictions
model_output = self.context['my_model'].predict(input)
return pd.DataFrame({'output': model_output})Three rules matter here. First, always go through the context, as in self.context['my_model']. If you assign an outside model object directly, a second copy is captured in a closure and the serialized model gets much larger. Second, if you combine a supported model type such as XGBoost with unsupported parts, put the supported model object straight into the context and let Snowflake serialize it. Third, write every @custom_model.inference_api method to handle multi-row DataFrames. Server-side batching can merge single-record requests into one DataFrame.
An inference method can also take tuning knobs such as temperature or max_tokens. Each one must be keyword-only (declared after *), have a type annotation and have a default value. These parameters become part of the model signature when the model is logged, so callers can override them.
Checkpoint 3 of 9· Check yourself
Which inference-parameter declaration on an @inference_api predict method follows the documented rules?
The parameter must come after *, have a type annotation and have a default value. Each of the other options misses at least one of these.
“They must be keyword-only (defined after * in the method signature).”Source: docs.snowflake.com
After testing the model locally, log it to the registry. Pass conda_dependencies or pip_requirements for the libraries your class imports. Pass sample_input_data so the input signature can be inferred, or supply a signature yourself. Use code_paths to package your own Python code, such as helper modules, utilities and config files, so it can be imported in Snowflake just as it is locally.
reg = Registry(session=session, database_name="ML", schema_name="REGISTRY")
mv = reg.log_model(my_model,
model_name="my_custom_model",
version_name="v1",
conda_dependencies=["scikit-learn"],
comment="My Custom ML Model",
sample_input_data=train_features)
output_df = mv.run(input_df)Checkpoint 4 of 9· Check yourself
In __init__, a reviewer sees self.model = model, where model is a notebook-level XGBoost object. What change does the documentation recommend?
Assigning the outside object directly captures a second copy in a closure, which inflates the serialized model. Reading it from the context avoids that copy.
“In your custom model class, always access model objects through the model context.”Source: docs.snowflake.com
Checkpoint 5 of 9· Exam question
A team trains an XGBoost model in a Container Runtime notebook and wants every epoch's validation loss stored so that the loss curve can be plotted per run in Snowsight. Which call pattern achieves this?
Correct answer: A — Call `log_metric("val_loss", value, step=epoch)` inside the loop so each epoch is a separate point on that metric
- A. Correct: `log_metric` accepts a `step` argument, so one metric name holds a series of points that Snowsight plots as a curve.
- B. Incorrect: a single `log_metric` call stores one value at the default step; Snowflake does not rebuild history from the event table.
- C. Incorrect: artifacts are stored files and are not parsed into charts; the metrics view only reads logged metrics.
- D. Incorrect: parameters are constant training inputs and are not step-indexed, so they cannot represent a time series.
Checkpoint 6 of 9· Exam question
Which privileges does a role need to create an experiment and write runs into a given schema using Experiment Tracking?
Correct answer: C — `CREATE EXPERIMENT` on the target schema together with `USAGE` on the parent database and schema that contain it
- A. Incorrect: experiment artifacts are stored by Snowflake with the experiment, so no user-managed stage is needed.
- B. Incorrect: `CREATE DATASET` governs Dataset objects; it does not authorize creating experiments.
- C. Correct: experiments are schema-level objects, so `CREATE EXPERIMENT` plus `USAGE` on the database and schema is required.
- D. Incorrect: `CREATE MODEL` governs Model Registry objects, not experiments.
Sources2
3.Tying models to dataset versions for lineage and reproducibility
A source table that changes daily cannot reproduce last quarter's training run. Snowflake Datasets solve this. A Dataset is a schema-level object made of versions, and each version is an immutable, point-in-time snapshot stored as Parquet files. Datasets are meant for reproducible training and testing and for tracking which data produced a model. You create a version from a Snowpark DataFrame. Reading from a Dataset always reads its selected version, and create_from_dataframe selects the version it creates.
ds1 = dataset.create_from_dataframe(
session,
"my_dataset",
"version1",
input_dataframe=df)ds1.read.to_snowpark_dataframe() returns a DataFrame that points at the materialized version, not at the original query. A model trained on it is therefore trained on frozen rows. When that model is logged to the Model Registry, ML Lineage records the chain from source data through feature views and datasets to the model. Lineage for a model is recorded when it is logged to the registry, not when training starts. Training on a Snowpark DataFrame produces lineage automatically. If you trained on pandas, outside Snowpark ML, or outside Snowflake, pass a Snowpark DataFrame backed by the source object as sample_input_data to log_model.
registry.log_model(...,
sample_input_data=df_backed_by_source_table)To explore lineage from Python APIs, you need the VIEW LINEAGE privilege. ACCOUNTADMIN has it and can grant it to other roles. FeatureView, ModelVersion and Dataset objects have a lineage() method. It takes a direction (DOWNSTREAM by default) and an optional domain_filter. Two gaps are documented. Tables created from model predictions do not link back to the model, and lineage is not replicated. Also, only objects created after ML Lineage is enabled carry lineage.
| Question | Call |
|---|---|
| Where did this model version's training data come from? | model_version.lineage(direction="upstream") |
| Which feature views does this dataset depend on? | my_dataset.lineage(direction="upstream", domain_filter=["feature_view"]) |
| Which models were trained on this dataset? | my_dataset.lineage(direction="downstream", domain_filter=["model"]) |
Checkpoint 7 of 9· Check yourself
A model was trained outside Snowflake on a pandas DataFrame. How do you link it to its source table in ML Lineage?
Lineage is recorded at log time. A sample_input_data DataFrame backed by the source object creates the link. VIEW LINEAGE and lineage() only read lineage that already exists.
“associate the source data object and the trained model by passing a Snowpark DataFrame backed by the source data object”Source: docs.snowflake.com
Checkpoint 8 of 9· Check yourself
When does Snowflake record the lineage edge between a dataset and a model?
Lineage is generally recorded when objects are created. For a model, that happens when it is logged to the registry.
“Lineage for models is captured when the model is logged to the Model Registry.”Source: docs.snowflake.com
Checkpoint 9 of 9· Exam question
A team runs nightly retraining and each night adds a new run to the same experiment. After about a year, `start_run` begins failing. Which documented limit is the most likely cause?
Correct answer: D — The experiment has reached the cap of 500 runs, so older runs must be dropped before new runs can be created
- A. Incorrect: the 200-metric limit is per run, not per schema, and it does not block creating runs.
- B. Incorrect: 1,000 versions is a Model Registry limit per model and is unrelated to experiment runs.
- C. Incorrect: Snowflake does not document a per-role concurrent-run cap; the run count limit applies per experiment.
- D. Correct: an experiment holds at most 500 runs; clean up with `ALTER EXPERIMENT ... DROP RUN` or move to a new experiment.
Exam traps
Each one states something that sounds right. Open it to see what is actually true.
1.Assigning the notebook's model object to self.model in a CustomModel is harmless.Why is that wrong?
Reading the model object directly captures a second copy in a closure and makes the serialized model much larger. Read it from self.context instead.
Covered in Bringing external models in with CustomModel
2.A real-time inference_api method can assume each call carries one row.Why is that wrong?
Server-side batching can merge requests from several callers into one DataFrame, so the method must handle many rows.
Covered in Bringing external models in with CustomModel
3.Prediction tables written by a model show up as downstream of that model in ML Lineage.Why is that wrong?
This is a documented gap. Tables and views built from predictions do not link back to the model.
Covered in Tying models to dataset versions for lineage and reproducibility
Practise it for real
Run a small Bayesian HPO sweep in a Container Runtime notebook and retrieve the best model
1.Build dataset_map = {"train": DataConnector.from_dataframe(...), "test": DataConnector.from_dataframe(...)} from your training and test splits.
Why: The Tuner passes this map to every trial, and the trial reads it with get_dataset_map().
You should see: A dictionary with two DataConnector values
2.Define search_space with tune.uniform for n_estimators, max_depth and learning_rate.
Why: Sampling functions tell the search algorithm what range and type each hyperparameter has.
You should see: A dict of three sampled hyperparameters
3.Create tune.TunerConfig(metric="accuracy", mode="max", search_alg=BayesOpt(), num_trials=5, max_concurrent_trials=1).
Why: The config sets the objective and how many trials run and how many at once.
You should see: A TunerConfig object
4.Write train_func that calls get_tuner_context(), trains with get_hyper_params(), and ends with tuner_context.report(metrics={"accuracy": accuracy}, model=model).
Why: Without reported metrics, Bayesian optimization has nothing to learn from.
You should see: A function that takes no arguments
5.Run tuner_results = tune.Tuner(train_func, search_space, tuner_config).run(dataset_map=dataset_map) and print tuner_results.best_result.
Why: TunerResults combines all trials and identifies the best one.
You should see: A results DataFrame with five trial rows, plus a best_result row and best_model
Stuck? Get a nudge
If every trial reports the same accuracy, check that train_func actually uses the values from get_hyper_params().
Sources
Every claim above is drawn from one of these pages, quoted as it was written on the date shown.
- 1.
“The workload scales out to the CPU or GPU cores that are available on a single node in the SPCS compute pool.”
↩︎ Hyperparameter optimization with the Tuner API“There are 8 (2 * 2 * 2) possible combinations of hyperparameters.”
↩︎ Prediction“Samples a value in logarithmic space, ideal for parameters that span several orders of magnitude (for example, learning rates).”
↩︎ Checkpoint“Use the Tuner object to initiate the training job. The Tuner object takes the training function, search space, and tuner configuration as arguments.”
↩︎ Checkpoint - 2.https://docs.snowflake.com/en/developer-guide/snowflake-ml/model-registry/bring-your-own-model-typesOfficial docs
“Use the code_paths parameter in Registry.log_model to package Python code, such as helper modules, utilities, and configuration files with your model.”
↩︎ Bringing external models in with CustomModel“Accessing the model directly captures a second copy of the model in a closure”
↩︎ Exam trap 1“Don’t assume that the input DataFrame will always contain a single row.”
↩︎ Exam trap 2“They must be keyword-only (defined after * in the method signature).”
↩︎ Checkpoint“In your custom model class, always access model objects through the model context.”
↩︎ Checkpoint - 3.
“Each version holds a materialized snapshot of your data with guaranteed immutability”
↩︎ Tying models to dataset versions for lineage and reproducibility - 4.
“Users need the VIEW LINEAGE privilege to explore lineage from Python APIs.”
↩︎ Tying models to dataset versions for lineage and reproducibility“Tables and views created from model predictions do not currently capture the lineage relationship back to the model.”
↩︎ Exam trap 3“associate the source data object and the trained model by passing a Snowpark DataFrame backed by the source data object”
↩︎ Checkpoint“Lineage for models is captured when the model is logged to the Model Registry.”
↩︎ Checkpoint