CertSafari
    Snowflake SnowPro Advanced: MLOps Engineer (MLA-B01)· Lessons

    Domain 2 · Lesson 8/17

    Snowflake ML HPO, CustomModel and Dataset Version Lineage

    Track experiments and metadata.

    14 min read
    8% of exam
    4 sources
    Published 5 Oct 2026
    Docs as of 4 Oct 2026

    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.

    Search algorithms in snowflake.ml.modeling.tune.search
    ClassHow it explores
    GridSearchTries every combination in a grid you define
    BayesOptUses a probabilistic model and earlier trial results to choose the next configuration
    RandomSearchSamples randomly; works well for large or mixed continuous/discrete spaces
    Search space sampling functions
    FunctionSamplesTypical use
    uniform(lower, upper)Continuous value, uniform between boundsDropout rate, regularization strength
    loguniform(lower, upper)Value uniform in log spaceLearning rates spanning orders of magnitude
    randint(lower, upper)Integer, lower inclusive, upper exclusiveNumber of layers, epochs, estimators
    choice(options)One value from a listCategorical 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?

    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.

    TunerConfig maximizing accuracy with Bayesian optimization over five trialspython
    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.

    A training function that reads its trial config and reports resultspython
    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. 1.Choose the search algorithm
    2. 2.Configure the tuner with TunerConfig
    3. 3.Initiate training with Tuner(...).run(dataset_map=...)
    4. 4.Ingest the data into a dataset_map
    5. 5.Define how hyperparameters are sampled (search space)
    6. 6.Read TunerResults for the best configuration and model

    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.

    A CustomModel that reads its wrapped model from the ModelContextpython
    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?

    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.

    Logging the custom model with dependencies and sample inputpython
    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?

    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?

    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?

    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.

    Materializing a DataFrame into a named Dataset versionpython
    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.

    Linking an externally trained model to its source datapython
    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.

    Answering lineage questions with the Snowpark ML lineage API
    QuestionCall
    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?

    Checkpoint 8 of 9· Check yourself

    When does Snowflake record the lineage edge between a dataset and a model?

    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?

    Sources34

    Exam traps

    Each one states something that sounds right. Open it to see what is actually true.

    1. 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. 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. 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. 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. 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. 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. 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. 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. 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. 2.
      “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. 3.
      “Each version holds a materialized snapshot of your data with guaranteed immutability”
      ↩︎ Tying models to dataset versions for lineage and reproducibility
    4. 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

    Ready to test yourself?

    Practise the 29 questions on this subdomain.

    Spotted a mistake, or was something unclear? Tell us.