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
22 Model Registry
Log, deploy, and serve R models
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.
| 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):
- Serialize model to
.rds - Generate CustomModel Python module (you do not write this)
- Upload to stage; register ModelVersion with schema and metadata
At inference time (SPCS container):
- Python handler receives a batch of input rows
- rpy2 embeds R, loads
.rds, calls yourpredictpath (or custompredict_body) - Results return as a columnar structure Snowflake serves to callers
- 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 = ...
)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:
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_LOGSand 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 |