22  Model Registry

Log, deploy, and serve R models

Keywords

snowflake, R, RStudio, Posit, VS Code, workspace notebooks, snowflakeR, RSnowflake, mlops

22.1 Overview

Training a model in R is only half the MLOps story. Production needs versioned artifacts, metadata, reproducible deployment, and governed inference — the same discipline application teams apply with git and CI/CD, not a shared drive of model_v3_final.RData.

The Snowflake Model Registry is the in-account system of record for ML models. Each version is an immutable snapshot: serialized model files, schema contracts, metrics, lineage to training data, and the configuration needed to run inference. Python teams use snowflake-ml-python directly; snowflakeR lets R teams register fits from lm, tidymodels, forecast, lme4, and custom objects via sfr_log_model() without hand-writing Python serving code.

The important architectural fact: Snowflake’s serving runtime is Python-first. snowflakeR bridges that gap at registration time by uploading your .rds and a generated CustomModel wrapper. At inference, SPCS runs Python, which calls back into R through rpy2 to execute predict() — you stay in R for training; the platform handles the container contract.

Context: MLOps on Snowflake (lifecycle and Posit interplay), Feature Store (datasets and lineage inputs).

22.2 Learning Objectives

  • Explain registry concepts and the train → log → deploy → infer lifecycle
  • Describe the CustomModel + rpy2 serving path in plain language
  • Log models with schema contracts and optional Feature Store lineage
  • Choose inference patterns (local, in-account, REST, batch, SQL)
  • Anticipate deploy failures (conda deps, schema mismatch, alias promotion)

22.3 End-to-end flow (mtcars example)

The sections below follow one linear story you can run in a notebook or RStudio. The MTCARS_MPG model is intentionally small so you can see every phase without domain noise.

Step Section What happens
1 Setup Connect; open registry in ML_DB.MODELS
2 Train & validate Fit lm(); sfr_predict_local() on holdout rows
3 Log sfr_log_model()V1 on stage + registry
4 Metrics & V2 Attach RMSE; retrain with disp; log V2
5 Aliases Point @production at the version you trust
6 Deploy sfr_deploy_model() → SPCS inference service
7 Infer sfr_predict() on warehouse rows

A separate path in Lineage shows Feature Store → CHURN_MODEL when training data is a governed dataset. Custom predict shows forecast when predict(model, newdata) is not enough.


22.4 The problem the Registry solves

Without a registry, R teams often ship models as opaque files, ad hoc REST services, or notebook outputs. That breaks down when auditors ask which data trained this model, when ops must roll back a bad deploy, or when Python and R teams need to share the same production catalog.

Without registry With registry
model_v3_final.RData on a laptop Immutable V1, V2, V3 in Snowflake
Unknown training data Lineage to datasets and feature views
Hand-built Plumber/Docker per model SPCS deployment from the same API
Metrics in a slide deck Metrics stored per version
Production = “whatever was saved last” Aliases (@champion) for controlled promotion

The Registry holds promoted models ready to serve and monitor. Exploratory work and hyperparameter search belong in Experiments first; the registry is where the winner lands.


22.5 Core concepts

Think of the registry as a versioned catalog inside your Snowflake account, not a filesystem on your laptop.

Concept Definition What it means in practice
Registry Schema/database holding models (sfr_model_registry()) Where model names live
Model name Logical identifier (CHURN_MODEL) Container for versions
Version Immutable snapshot (V1, V2, …) Never overwrite — register anew
ModelVersion R handle from sfr_log_model() Use for deploy and metadata
CustomModel Python class Snowflake runs in SPCS Loads .rds via rpy2
Stage artifacts .rds + generated wrapper Uploaded at log time
input_cols / output_cols Schema contract Enforced at inference
Alias Named pointer (production) Promote without renaming pipelines
Deployment SPCS inference service (HTTPS) Elastic scoring endpoint
Lineage Upstream/downstream links Snowsight governance graph
Batch inference sfr_run_batch() / SQL Score at scale without REST

22.6 Lifecycle overview

A complete R MLOps loop on Snowflake usually follows six phases. You can stop after log if you only need governance and batch SQL; you need deploy when external apps or low-latency REST call a service.

flowchart LR
  subgraph dev [Development]
    TR[Train in R]
    LOC[sfr_predict_local]
  end

  subgraph reg [Registry]
    LOG[sfr_log_model]
    MET[Metrics lineage]
    VER[V1 V2 versions]
  end

  subgraph prod [Production]
    DEP[sfr_deploy_model]
    SVC[SPCS service]
    INF[sfr_predict / REST / SQL]
    MON[Monitoring]
  end

  TR --> LOC --> LOG --> MET --> VER
  VER --> DEP --> SVC --> INF --> MON

Phase What you do Compute
Train Fit on Feature Store dataset or table R in notebook / local
Validate sfr_predict_local() — same logic as serve R only
Log sfr_log_model() — upload artifacts + metadata Snowflake ML API
Deploy sfr_deploy_model() — create/update SPCS service SPCS compute pool
Infer sfr_predict(), REST, SQL, batch SPCS and/or warehouse
Monitor Inference log + drift jobs Warehouse + registry API

Promotion is a governance step: register V2, compare metrics, move alias @production from V1 to V2, keep V1 for rollback if needed.


22.7 How logging and serving work

Understanding the two runtimes — your R session at log time vs the SPCS container at predict time — prevents confusion when debugging deploy or latency.

sequenceDiagram
  participant R as R train session
  participant SFR as snowflakeR
  participant ST as Internal stage
  participant REG as Model Registry
  participant SPCS as SPCS inference container

  R->>SFR: sfr_log_model(model, input_cols, ...)
  SFR->>SFR: saveRDS model
  SFR->>SFR: Generate Python CustomModel plus rpy2 handler
  SFR->>ST: Upload artifacts
  SFR->>REG: Register ModelVersion V1

  Note over REG,SPCS: sfr_deploy_model()
  REG->>SPCS: Create inference service

  Note over SPCS: Predict request arrives
  SPCS->>SPCS: Python CustomModel.load_context()
  SPCS->>SPCS: rpy2 loads R plus readRDS
  SPCS->>SPCS: predict(model, input_df)
  SPCS-->>SPCS: Return scores as DataFrame

At log time (R session):

  1. Serialize model to .rds
  2. Generate CustomModel Python module (you do not write this)
  3. Upload to stage; register ModelVersion with schema and metadata

At inference time (SPCS container):

  1. Python handler receives a batch of input rows
  2. rpy2 embeds R, loads .rds, calls your predict path (or custom predict_body)
  3. Results return as a columnar structure Snowflake serves to callers
rpy2 vs reticulate
  • reticulate (R→Python): sfr_log_model() calls Snowflake ML Python SDK from R
  • rpy2 (Python→R): SPCS CustomModel calls back into R at predict time

Same bridges as Architecture, reversed at serve time.


22.8 Setup

Start every registry workflow by connecting and pinning the catalog location. Explicit sfr_model_registry() avoids ambiguity when your notebook session database/schema differ from where models should live.

library(snowflakeR)
conn <- sfr_connect()
conn <- sfr_load_notebook_config(conn)

reg <- sfr_model_registry(conn, database = "ML_DB", schema = "MODELS")

Pass reg or conn to registry functions; the registry object records database/schema for model names.


22.9 Train and validate locally

Always run predict logic locally before registration. sfr_predict_local() executes pure R — no Python bridge — and should match what the CustomModel will call inside the container (modulo conda-forge package versions at serve time).

In the end-to-end example, train on mtcars, build a small scoring frame with the same column names you will declare in input_cols, and confirm predictions look sensible:

model <- lm(mpg ~ wt + hp + cyl, data = mtcars)

test_data <- data.frame(
  wt  = c(2.5, 3.0),
  hp  = c(110, 150),
  cyl = c(4, 6)
)

preds <- sfr_predict_local(model, test_data)

If this fails or column names differ from your production scoring table, fix it here — SPCS deploy/debug cycles are much slower.

tidymodels: pass the fitted workflow object to sfr_log_model(); snowflakeR extracts the parsnip fit and predict interface.


22.10 Log the model

Once local predict works, sfr_log_model() uploads artifacts and creates V1 (or the version you name). This is step 3 in the end-to-end table.

mv <- sfr_log_model(
  reg,
  model       = model,
  model_name  = "MTCARS_MPG",
  input_cols  = list(wt = "double", hp = "double", cyl = "integer"),
  output_cols = list(prediction = "double"),
  comment     = "LM predicting MPG"
)

The return value mv is a ModelVersion handle — keep it for deploy helpers or inspect in Snowsight.

22.10.1 Schema contract (input_cols / output_cols)

Inference validates incoming rows against input_cols — names and types must match scoring data. These are enforced contracts, not comments. Define them from the same frame you used in sfr_predict_local().

Registry type R column type
"double" numeric
"integer" integer
"string" character
"boolean" logical

Mismatch at deploy or predict time is one of the most common production failures.

22.10.2 Serving dependencies

SPCS inference images install R packages from conda-forge (plus r-base, rpy2). Declare anything your predict path needs at serve time:

mv <- sfr_log_model(
  reg,
  model        = arima_fit,
  model_name   = "SALES_FORECAST",
  predict_pkgs = c("forecast"),
  conda_deps   = c("r-forecast"),
  input_cols   = ...,
  output_cols  = ...
)
Warning

CRAN-only packages with no conda-forge recipe may fail at deploy. Options: custom inference image, conda_deps, or rewrite predict with base R — see Appendix C.

22.10.3 Custom predict bodies

Standard predict(model, newdata) works for many models. forecast, bsts, and multi-column outputs need custom code — write an R function, test it locally, wrap with sfr_predict_body(), and align output_cols with the returned data.frame:

my_forecast <- function(model, input) {
  pred <- forecast::forecast(model, h = nrow(input))
  data.frame(
    point_forecast = as.numeric(pred$mean),
    lower_95       = as.numeric(pred$lower[, 2]),
    upper_95       = as.numeric(pred$upper[, 2])
  )
}

mv <- sfr_log_model(
  reg,
  model        = arima_model,
  model_name   = "SALES_FORECAST",
  predict_pkgs = c("forecast"),
  predict_body = sfr_predict_body(my_forecast),
  input_cols   = list(period = "integer"),
  output_cols  = list(
    point_forecast = "double",
    lower_95       = "double",
    upper_95       = "double"
  )
)

See workspace_forecasting_demo.ipynb for a runnable forecast path.


22.11 Lineage from Feature Store

When training data comes from a Feature Store Dataset, pass training_dataset to sfr_log_model() so Snowsight shows:

Source table → Feature View → Dataset → Model

This is the governed alternative to the mtcars walkthrough — same log API, richer upstream metadata.

training <- sfr_generate_dataset(
  fs,
  name     = "CHURN_TRAINING",
  spine    = "SELECT customer_id, label FROM labels",
  features = list(list(name = "CUSTOMER_FEATURES", version = "v1")),
  version  = "v1",
  spine_label_cols = "label"
)

model <- glm(label ~ ., data = training, family = binomial)

mv <- sfr_log_model(
  reg,
  model            = model,
  model_name       = "CHURN_MODEL",
  input_cols       = sfr_input_cols(training, exclude = "label"),
  output_cols      = list(prediction = "double"),
  training_dataset = training
)

Inspect lineage after logging:

sfr_model_lineage(reg, "CHURN_MODEL", "V1", direction = "upstream")

Details on datasets and spines: Feature Store.


22.12 Metrics and version management

After V1 is registered, attach metrics that justify promotion decisions. Metrics are evaluative (“how good”); lineage is structural (“what data”) — both belong on production versions.

sfr_set_model_metric(reg, "MTCARS_MPG", "V1", "rmse", 2.45)
sfr_set_model_metric(reg, "MTCARS_MPG", "V1", "r_squared", 0.87)
sfr_show_model_metrics(reg, "MTCARS_MPG", "V1")

sfr_show_models(reg)
sfr_show_model_versions(reg, "MTCARS_MPG")

Retraining never overwrites a version — register V2 with an expanded formula (end-to-end step 4):

model_v2 <- lm(mpg ~ wt + hp + cyl + disp, data = mtcars)

mv2 <- sfr_log_model(
  reg,
  model        = model_v2,
  model_name   = "MTCARS_MPG",
  version_name = "V2",
  input_cols   = list(wt = "double", hp = "double", cyl = "integer", disp = "double"),
  output_cols  = list(prediction = "double"),
  comment      = "V2: added displacement"
)

sfr_set_default_model_version(reg, "MTCARS_MPG", "V2")

Compare V1 vs V2 metrics before pointing production aliases at the challenger.


22.13 Aliases and promotion

Aliases decouple production callers from version numbers. Register V3 as a challenger while @production still references V2; promote by moving the alias, not rewriting SQL and REST clients.

sfr_set_model_alias(reg, "MTCARS_MPG", "V2", "production")
sfr_set_model_alias(reg, "MTCARS_MPG", "V1", "staging")

sfr_unset_model_alias(reg, "MTCARS_MPG", "production")

Downstream SQL inference and REST clients can target @production while you validate a new version under @staging.


22.14 Deploy to SPCS

Deploy (end-to-end step 6) materializes an inference service on a compute pool. The registry version supplies artifacts and the CustomModel entrypoint; your image repository must include conda R, rpy2, and packages from predict_pkgs.

sfr_deploy_model(
  reg,
  model_name   = "MTCARS_MPG",
  version_name = "V2",
  service_name = "mpg_service",
  compute_pool = "ML_POOL",
  image_repo   = "ML_DB.MODELS.ML_IMAGES"
)

flowchart TB
  REG[Registry ModelVersion]
  IMG[Inference image conda plus rpy2]
  POOL[Compute pool]
  SVC[Inference service HTTPS]
  APP[Apps Shiny REST clients]

  REG --> SVC
  IMG --> SVC
  POOL --> SVC
  SVC --> APP

Operational notes:

  • Inference uses conda-forge R packages baked into the service image — align with predict_pkgs
  • Monitor via SYSTEM$GET_SERVICE_LOGS and service status SQL
  • Cycle compute pool after image updates (Production checklist)
  • Cold starts depend on pool sizing — consider min instances for latency-sensitive apps

List and manage services via registry helpers (sfr_show_services, etc. — see vignette).


22.15 Inference patterns

After deploy (or for warehouse-native models), pick a caller pattern that matches auth, latency, and who owns the client code (end-to-end step 7).

flowchart TD
  Q{Who calls predict?}
  Q -->|Same R session dev| LOC[sfr_predict_local]
  Q -->|R session in Snowflake| RREG[sfr_predict]
  Q -->|External app| REST[sfr_predict_rest]
  Q -->|SQL team| SQL[sfr_predict_sql warehouse models]
  Q -->|Batch millions| BATCH[sfr_run_batch many-model]

  LOC --> RONLY[R only no deploy]
  RREG --> SPCS1[SPCS or in-session bridge]
  REST --> SPCS2[SPCS HTTPS plus PAT]
  SQL --> WH[Warehouse]
  BATCH --> SPCS3[SPCS batch job]

Pattern API When
Development sfr_predict_local() Before log; no deploy
In-account R batch sfr_predict(reg, data) Scoring via Snowpark DataFrame in Snowflake
Deployed service sfr_predict(..., service_name=) After sfr_deploy_model()
REST (external) sfr_predict_rest() Posit Connect Shiny, apps outside Workspace
Warehouse SQL sfr_predict_sql() target_platforms = "WAREHOUSE" models
Many-model batch sfr_run_batch() Partition-keyed models — Many-Model

Score warehouse rows through the Snowpark session — typical after deploy in Workspace:

new_cars <- sfr_query(conn, "SELECT wt, hp, cyl FROM car_data LIMIT 100")
scores   <- sfr_predict(reg, "MTCARS_MPG", new_cars)

22.15.1 Workspace vs REST auth

  • sfr_predict() routes through the Snowpark session — no PAT in notebooks
  • sfr_predict_rest() hits SPCS ingress directly — typically requires PAT outside Workspace (R Cells — PAT)

22.15.2 Posit Connect

Publish Shiny or Quarto to Posit Connect; the app calls sfr_predict_rest() against the Snowflake endpoint. Registry stays authoritative; Connect is the UI layer (MLOps).

For models that orbital or SQL-native forms can express, warehouse SQL scoring may avoid SPCS entirely — complementary to CustomModel+R paths (MLOps — why Snowflake ML).


22.16 Many-model registration

Thousands of partition-specific models (one .rds per SKU) do not require thousands of registry entries. sfr_log_many_model() registers one aggregator CustomModel with a model_index.json mapping partition keys to stage paths — registration time stays ~constant. Batch scoring uses sfr_run_batch().

See Many-Model Patterns and Parallel doSnowflake.


22.17 Experiments and monitoring

Concern Tool
Hyperparameter search, run comparison Experiments — log before promoting to registry
Production drift / performance Model monitoring — attach to deployed version
Parallel retrain doSnowflake + log new version

Typical flow: many runs in Experiments → pick winner → sfr_log_model() → deploy → monitor → retrain and move @production when metrics slip.


22.18 Common pitfalls

Issue Mitigation
Deploy fails — missing R package predict_pkgs, conda_deps, custom image
Column name / type mismatch Match input_cols; test with sfr_predict_local()
Wrong output shape Use predict_body; forecast models need data.frame contract
Stale production version Aliases + sfr_set_default_model_version()
REST fails from notebook Use sfr_predict() in Workspace; PAT for external REST
High latency Warm service; right-size compute pool

22.19 Companion artifacts

Resource Content
Vignette model-registry Deploy params, SQL predict, export
workspace_model_registry.ipynb Workspace walkthrough
workspace_forecasting_demo.ipynb forecast + predict_body example

22.20 Next steps

Experiments · Model monitoring · End-to-end pipeline