Choose where computation happens
hf_embed() and hf_classify() still use
hosted inference. Nothing in this workflow changes their defaults or
initializes Python when you load the package.
The explicit local functions run embeddings and text classification on your computer. After the initial model download, they can work offline without sending your input text to an inference provider. There are no hosted inference charges, but you supply the disk space, memory, and compute. Public models in this example do not require an API token. Private or gated models may require a read token and acceptance of the model’s terms.
This first local workflow supports standard Transformers classifiers
and Sentence Transformers embedding models with
safetensors weights. It does not run arbitrary
repository Python code, support every Hub model, or manage a local chat
server. Embedding snapshots must include modules.json with
a root Transformer followed by built-in Pooling or Normalize modules. A
transformer-only cache is rejected rather than silently changing the
model’s pooling behavior. Adapter and custom-module repositories are
unsupported.
Set up the optional Python stack
Install reticulate once if it is not already
installed:
install.packages("reticulate")Setup declares the Python requirements; the first local operation may download a compatible Python interpreter and packages, including PyTorch, Transformers, and Sentence Transformers. This can take several minutes and substantial disk space. The default device is the CPU.
If you already select a Python environment with
reticulate, that choice is respected. It must contain
compatible dependencies; installing packages into a different
environment will not fix the selected one. See reticulate’s
environment documentation. API-only users do not need this
setup.
Use matching R and Python architectures. In particular, x64 R emulation on Windows ARM can crash when loading Python-related R packages; successful-looking console output does not establish that the process finished successfully. The automated real-model runs for this workflow use native Linux CPU runners.
Download reproducible model snapshots
Pin a full commit revision rather than relying on a moving
main branch. The download helper uses Hugging Face’s
version-aware cache and preserves the repository’s subdirectories. It
retrieves safetensors weights and standard configuration/tokenizer
files, not every alternative model format.
cache_dir <- Sys.getenv(
"HF_LOCAL_CACHE_DIR",
unset = tools::R_user_dir("huggingfaceR", "cache")
)
embedding_id <- "sentence-transformers/all-MiniLM-L6-v2"
embedding_revision <- "1110a243fdf4706b3f48f1d95db1a4f5529b4d41"
classifier_id <- "distilbert/distilbert-base-uncased-finetuned-sst-2-english"
classifier_revision <- "714eb0fa89d2f80546fda750413ed43d93601a13"
embedding_path <- hf_download_model(
embedding_id, revision = embedding_revision, cache_dir = cache_dir
)
classifier_path <- hf_download_model(
classifier_id, revision = classifier_revision, cache_dir = cache_dir
)
data.frame(
task = c("embed", "classify"),
snapshot = c(basename(embedding_path), basename(classifier_path))
)
#> task snapshot
#> 1 embed 1110a243fdf4706b3f48f1d95db1a4f5529b4d41
#> 2 classify 714eb0fa89d2f80546fda750413ed43d93601a13Downloading again reuses the cache. A snapshot is a directory containing the model’s files, not a single weight file. For example, the embedding model also needs its pooling configuration:
list.files(embedding_path, pattern = "config.json$", recursive = TRUE)
#> [1] "1_Pooling/config.json" "config.json"
#> [3] "data_config.json" "sentence_bert_config.json"
#> [5] "tokenizer_config.json"Load once, embed many texts
Loading from a downloaded directory uses local files only. Tokenizer and model components come from the same snapshot.
embedding_model <- hf_load_local_model(
embedding_path, task = "embed", local_files_only = TRUE
)
embedding_model
#> <hf_local_model> embed
#> Model: /home/runner/work/_temp/hf-model-cache/models--sentence-transformers--all-MiniLM-L6-v2/snapshots/1110a243fdf4706b3f48f1d95db1a4f5529b4d41 (local)
#> Revision: 1110a243fdf4706b3f48f1d95db1a4f5529b4d41
#> Path: /home/runner/work/_temp/hf-model-cache/models--sentence-transformers--all-MiniLM-L6-v2/snapshots/1110a243fdf4706b3f48f1d95db1a4f5529b4d41
#> Device: cpu
sentences <- c(
"The cat sat on the mat.",
"A feline is resting on a rug.",
"The database server needs an update."
)
embeddings <- hf_embed_local(
sentences, embedding_model, batch_size = 2L, normalize = TRUE
)
embeddings
#> # A tibble: 3 × 3
#> text embedding n_dims
#> <chr> <list> <int>
#> 1 The cat sat on the mat. <dbl [384]> 384
#> 2 A feline is resting on a rug. <dbl [384]> 384
#> 3 The database server needs an update. <dbl [384]> 384
# A small view of the actual numeric output
round(do.call(rbind, embeddings$embedding)[, 1:8], 4)
#> [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8]
#> [1,] 0.1302 -0.0158 -0.0367 0.0580 -0.0598 0.0331 0.0301 0.0289
#> [2,] 0.0827 0.0186 0.0380 0.1138 -0.0026 0.0736 -0.0191 0.0106
#> [3,] 0.0451 -0.1290 -0.0505 0.0119 -0.0449 -0.0745 -0.0271 -0.1096The output has the same text, embedding,
and n_dims columns as hf_embed(). Each
embedding is a numeric vector in a list-column.
similarities <- hf_similarity(embeddings)
similarities
#> # A tibble: 3 × 3
#> text_1 text_2 similarity
#> <chr> <chr> <dbl>
#> 1 The cat sat on the mat. A feline is resting on a rug. 0.563
#> 2 The cat sat on the mat. The database server needs an update. 0.0392
#> 3 A feline is resting on a rug. The database server needs an update. 0.0373
stopifnot(
all(embeddings$n_dims == 384L),
similarities$similarity[1] > similarities$similarity[2]
)These values describe this model’s representation of these sentences. They are not a general accuracy benchmark. Other embedding models may require query or document prefixes; follow the model card when using them for retrieval.
Classify text locally
classifier <- hf_load_local_model(
classifier_path, task = "classify", local_files_only = TRUE
)
reviews <- c(
"I loved this movie. The acting was wonderful!",
"I hated this movie. It was a complete waste of time."
)
classification <- hf_classify_local(reviews, classifier, batch_size = 2L)
classification
#> # A tibble: 2 × 3
#> text label score
#> <chr> <chr> <dbl>
#> 1 I loved this movie. The acting was wonderful! POSITIVE 1.000
#> 2 I hated this movie. It was a complete waste of time. NEGATIVE 1.000
stopifnot(
identical(classification$label, c("POSITIVE", "NEGATIVE")),
all(classification$score > 0.9)
)This English movie-review sentiment model returns one highest-scoring
label per text, with the same text, label, and
score columns as hf_classify(). Scores are
model outputs, not a guarantee of calibrated confidence or suitability
for a new domain.
Long inputs are truncated to the classifier’s tokenizer limit by
default. Set truncation = FALSE to disable that behavior;
an over-length input can then fail rather than being silently shortened.
These helpers do not split long documents into meaning-preserving
chunks.
Missing values, repeated text, and reuse
hf_classify_local(c(reviews[1], NA_character_, reviews[1]), classifier)
#> # A tibble: 3 × 3
#> text label score
#> <chr> <chr> <dbl>
#> 1 I loved this movie. The acting was wonderful! POSITIVE 1.000
#> 2 <NA> <NA> NA
#> 3 I loved this movie. The acting was wonderful! POSITIVE 1.000
hf_embed_local(c(sentences[1], NA_character_, sentences[1]), embedding_model)
#> # A tibble: 3 × 3
#> text embedding n_dims
#> <chr> <list> <int>
#> 1 The cat sat on the mat. <dbl [384]> 384
#> 2 <NA> <NULL> NA
#> 3 The cat sat on the mat. <dbl [384]> 384Input order and duplicates are preserved. Missing text produces missing output, and empty or entirely missing vectors do not invoke the prediction backend. Classification also treats empty strings as missing text. Embedding models accept empty strings and produce their model-specific representation.
Keep a loaded model handle for repeated calls in the same R session.
A handle contains Python objects: do not use
saveRDS() to move it between sessions. Save the
model ID and revision, or the snapshot directory, and load a new handle
in the next session.
Work offline in another R session
Once both the Python dependencies and model snapshots are present, start a new R session with the Hub offline flag set before initializing Python:
Sys.setenv(HF_HUB_OFFLINE = "1", TRANSFORMERS_OFFLINE = "1")
library(huggingfaceR)
cache_dir <- Sys.getenv(
"HF_LOCAL_CACHE_DIR",
unset = tools::R_user_dir("huggingfaceR", "cache")
)
path <- hf_download_model(
"sentence-transformers/all-MiniLM-L6-v2",
revision = "1110a243fdf4706b3f48f1d95db1a4f5529b4d41",
cache_dir = cache_dir,
local_files_only = TRUE
)
model <- hf_load_local_model(path, task = "embed", local_files_only = TRUE)
hf_embed_local("This text stays on this computer.", model)An incomplete or missing cache fails explicitly; local prediction never falls back to paid hosted inference. Cached files must remain available at the same path. Offline model loading does not install missing Python dependencies.
Batch size and memory
batch_size controls the model’s inference batches, not
the size of the complete result returned to R. Embedding output is first
transferred as a matrix and then placed in a tibble list-column. At
100,000 texts and 384 dimensions, each double-precision representation
needs about 293 MiB, before model memory and other allocations.
For larger corpora, reuse the loaded handle but process smaller R-side chunks. Write each result to disk or pass it to a downstream consumer instead of retaining every embedding in memory. This small example only records row counts:
Profiling the local workflow
The developer profiler compares an installed baseline with the refactored package on the same runner, using the same cached weights, Python environment, and two PyTorch CPU threads. It warms the models before measurement, records seven inference repetitions for batches of 1, 32, and 256 texts, and checks that the public API, prediction code, and output values agree.
The Python-native timing excludes transfer to R and tidy result
construction. R allocation measurements do not include Python/PyTorch
memory and are not peak process memory. Shared-runner
timings are descriptive, not a promise of performance on another machine
or evidence of a statistically established speedup. The profiling
artifacts also contain R-only result-shaping measurements and
Rprof() call summaries.
profile_dir <- Sys.getenv("HF_LOCAL_PROFILE_DIR")
if (nzchar(profile_dir)) {
timing <- read.csv(file.path(profile_dir, "comparison.csv"))
timing <- timing[timing$method %in% c("wrapper", "cached_hub_load"), ]
data.frame(
task = timing$task,
operation = timing$method,
texts = timing$n_texts,
baseline_ms = round(1000 * timing$median_s_baseline, 2),
refactored_ms = round(1000 * timing$median_s_refactored, 2)
)
}
#> task operation texts baseline_ms refactored_ms
#> 1 classify cached_hub_load 0 80.62 70.94
#> 2 classify wrapper 1 22.68 22.72
#> 3 classify wrapper 256 2140.16 2214.27
#> 4 classify wrapper 32 273.54 275.09
#> 5 embed cached_hub_load 0 75.88 68.04
#> 6 embed wrapper 1 11.46 11.22
#> 7 embed wrapper 256 517.78 517.74
#> 8 embed wrapper 32 81.46 82.70scripts/profile-local-models.R writes the measurements,
environment metadata, and profiles.
scripts/compare-local-profiles.R checks the two runs and
produces the comparison table. The CI workflow pins the baseline commit
explicitly; change that reference deliberately when adopting a new
performance baseline.
Reproduce this document
Rendering is opt-in so ordinary package checks do not install Python
or download models. From an installation containing these functions, set
HF_RUN_LOCAL_EXAMPLES=true and render this source:
Sys.setenv(HF_RUN_LOCAL_EXAMPLES = "true")
rmarkdown::render("vignettes/local-models.Rmd")The repository’s scripts/test-local-models.R
independently exercises downloads, real predictions, output shapes,
missing values, and cached offline reuse in a fresh R process. It also
rejects an incomplete embedding snapshot. Its online and offline modes
compare numeric outputs.
metadata <- reticulate::import("importlib.metadata")
python_packages <- c(
"torch", "transformers", "sentence-transformers", "huggingface-hub", "numpy"
)
data.frame(
component = c("R", "huggingfaceR", "reticulate", python_packages),
version = c(
as.character(getRversion()),
as.character(packageVersion("huggingfaceR")),
as.character(packageVersion("reticulate")),
vapply(python_packages, metadata$version, character(1))
)
)
#> component version
#> 1 R 4.6.1
#> 2 huggingfaceR 2.3.0
#> 3 reticulate 1.47.0
#> 4 torch 2.6.0+cpu
#> 5 transformers 4.57.6
#> 6 sentence-transformers 5.7.0
#> 7 huggingface-hub 0.36.2
#> 8 numpy 2.5.2
cat(sprintf(
"Executed: %d embeddings with %d dimensions; %d classifications.\n",
nrow(embeddings), embeddings$n_dims[1], nrow(classification)
))
#> Executed: 3 embeddings with 384 dimensions; 2 classifications.