Using Hugging Face Inference Endpoints
Source:vignettes/hugging_face_inference.Rmd
hugging_face_inference.RmdThis vignette shows how to embed and classify text with EndpointR using Hugging Face’s inference services.
Setup
library(EndpointR)
library(dplyr)
library(httr2)
library(tibble)
library(arrow)
my_data <- tibble(
id = 1:3,
text = c(
"Machine learning is fascinating",
"I love working with embeddings",
"Natural language processing is powerful"
),
category = c("ML", "embeddings", "NLP")
)Follow Hugging Face’s docs to generate a Hugging Face token, and then register it with EndpointR:
set_api_key("HF_TEST_API_KEY")Choosing Your Service
Hugging Face offers two inference options:
- Inference API: Free, good for testing
- Dedicated Endpoints: Paid, reliable, fast
For this vignette, we’ll use the Inference API. To switch to dedicated endpoints, just change the URL.
Getting Started
Go to Hugging Face’s models hub and fetch the Inference API’s URL for the model you want to embed your data with. Not all models are available via the Hugging Face Inference API, if you need to use a model that is not available you may need to deploy a Dedicated Inference Endpoint.
Which engine does my endpoint run?
Hugging Face endpoints run one of two inference engines, and the two engines need different request formats. EndpointR checks which engine an endpoint runs, so in most cases you don’t need to do anything. It still helps to know the differences, because they change how texts are cut, how fast a job runs, and whether you get the same scores twice.
- Text Embeddings Inference (TEI) is a server for embedding models and classifiers. Hugging Face picks it by default for most embedding models, and you can choose it for classifiers.
- The default Hugging Face Inference Toolkit runs a
transformerspipeline. Endpoints that don’t use TEI, or another named container, run the toolkit.
| Default toolkit | TEI | |
|---|---|---|
| Batch of texts, embeddings | {"inputs": ["a", "b"]} |
{"inputs": ["a", "b"], "truncate": true} |
| Batch of texts, classification | {"inputs": ["a", "b"], "parameters": {...}} |
{"inputs": [["a"], ["b"]], "truncate": true, "raw_scores": true} |
| Batching on the GPU | Only if the request sets parameters$batch_size, which
EndpointR does. The default is 1 text at a time. |
Always. TEI combines texts from all waiting requests into one batch. |
| Texts per request | No fixed limit |
--max-client-batch-size, 32 by default. Larger requests
are rejected. |
| Truncation |
parameters$truncation and
parameters$max_length
|
Top-level truncate, which cuts at the model’s own limit
(8,192 tokens for ModernBERT and BGE M3). TEI ignores
max_length. |
| Sorting texts by length | Helps, because the pipeline pads each batch to its longest text (about 20% faster) | No effect, because TEI does not pad |
| More requests in flight | No gain above about 8 | Helps up to about 32 |
| Precision | fp32 | fp16 |
| Repeatable results | Yes | No, see Repeatable results |
GET /info route |
None | Returns the model and its limits |
How EndpointR detects the engine
Every hf_embed_*() and hf_classify_*()
function has an engine argument. The default,
"auto", sends GET {endpoint_url}/info once per
endpoint URL in each R session and keeps the answer. TEI answers with
JSON that includes max_client_batch_size, and the toolkit
has no /info route, so it answers with an error such as
404. The call retries 503 responses for about 2 minutes, because an
endpoint that has scaled to zero returns 503 while it starts.
You can skip the check with engine = "tei" or
engine = "toolkit", e.g. when a proxy blocks
/info. To set it for a whole session, use an option:
options(EndpointR.hf_engine = "tei")To see what an endpoint reports, call
hf_get_endpoint_info(). It returns the /info
JSON on TEI, and NULL with a message on the toolkit:
info <- hf_get_endpoint_info(
endpoint_url = "https://your-endpoint.endpoints.huggingface.cloud",
key_name = "HF_API_KEY"
)
info$max_client_batch_size # texts allowed per request
info$max_input_length # tokens per text before TEI cuts it
info$versionIf you have the hf
command line tool, hf endpoints describe <name>
shows the engine too. An "image": {"tei": ...} entry means
TEI, and an "image": {"huggingface": {}} entry means the
toolkit.
EndpointR records the engine, and on TEI the /info
fields, in the metadata.json file of every
_chunks() and _df() run.
Understanding the Function Hierarchy
EndpointR provides four levels of functions for working with Hugging Face endpoints.
KEY FEATURE: The
*_df()and*_chunks()functions preserve your original column names. If you pass a data frame with columns namedreview_idandreview_text, those exact names will appear in the output and in the saved.parquetfiles. This makes it easy to join results back to your original data.
Single Text Functions
-
hf_embed_text()- Embed a single text -
hf_classify_text()- Classify a single text
Use these for one-off requests or testing.
Batch Functions
-
hf_embed_batch()- Embed multiple texts in memory -
hf_classify_batch()- Classify multiple texts in memory
Use these for small to medium datasets (<5000 texts) that fit in memory. Results are returned as a single data frame.
Chunk Functions (NEW in v0.1.2)
-
hf_embed_chunks()- Process large volumes with incremental file writing -
hf_classify_chunks()- Process large volumes with incremental file writing
Use these for large datasets (>5000 texts). Results are written
incrementally as .parquet files to avoid memory issues and
provide safety against crashes.
Data Frame Functions
-
hf_embed_df()- Convenience wrapper that callshf_embed_chunks() -
hf_classify_df()- Convenience wrapper that callshf_classify_chunks()
Most users will use these. They handle extraction from data frames and call the chunk functions internally.
Choosing the Right Function
Use this decision tree:
# Single text? Use _text functions
if (n_texts == 1) {
result <- hf_embed_text(text, endpoint_url, key_name)
# or
result <- hf_classify_text(text, endpoint_url, key_name)
}
# Small batch (<5000 texts) and want results in memory only?
if (n_texts < 5000 && !need_file_output) {
results <- hf_embed_batch(texts, endpoint_url, key_name,
batch_size = 32, concurrent_requests = 16)
# or
results <- hf_classify_batch(texts, endpoint_url, key_name,
batch_size = 32, concurrent_requests = 16)
}
# Large dataset or want file output for safety?
# Use _df functions (they call _chunks internally)
if (n_texts >= 5000 || need_safety) {
results <- hf_embed_df(df, text, id, endpoint_url, key_name,
chunk_size = 5000, output_dir = "my_results",
batch_size = 32, concurrent_requests = 16)
# or
results <- hf_classify_df(df, text, id, endpoint_url, key_name,
chunk_size = 5000, output_dir = "my_results",
batch_size = 32, concurrent_requests = 16,
max_length = 512)
}Recommendation: For most production use cases, use
_dffunctions even for smaller datasets. The safety of incremental file writing is worth it.
How the batch, chunk and data frame functions send texts
The _batch(), _chunks() and
_df() functions send batch_size texts in each
request (32 by default) and keep concurrent_requests
requests in flight (16 by default). Before 0.3.0, the
_chunks() and _df() functions sent one text
per request and _df() sent one request at a time, which
left the GPU mostly idle. In our tests, the new defaults were more than
100 times faster on TEI. The Improving Performance vignette has
the numbers.
The functions also do the following:
- They don’t send empty or missing texts. Those rows come back with
.error = TRUEand the message “Empty or missing text, not sent”, so the output has one row per input. - On the toolkit, they sort texts by length before they make batches. Results always come back in the order of your input.
- When a batch fails with HTTP 400, 413, 422 or 424, a network error, or the wrong number of results, they split it in half and send each half again, down to single texts. Only the text at fault fails.
- When a request gets HTTP 429, 502, 503 or 504, they wait and send it
again unchanged, up to
max_retriestimes. - On TEI, when
batch_sizeis above the endpoint’smax_client_batch_size, they lowerbatch_sizeto that value and warn you.
Key Differences: Embeddings vs Classification
The request EndpointR sends depends on the task and on the engine. EndpointR handles the differences, but they explain some of the arguments and results.
Text Truncation Handling
Embeddings (hf_embed_*):
- There is no
max_lengthargument. - On TEI, EndpointR sends
truncate: trueat the top level of the request, so TEI cuts each text at the model’s own limit (e.g. 8,192 tokens for BGE M3). You no longer needAUTO_TRUNCATE=trueon the endpoint, although it does no harm. - On the toolkit, truncation depends on the model’s pipeline. None of our embedding endpoints run the toolkit, so this path is untested.
Classification (hf_classify_*):
-
max_length(default512L) sets the maximum number of tokens per text. - On the toolkit, EndpointR sends
truncation: trueandmax_lengthto the endpoint, and the pipeline cuts each text. - On TEI, EndpointR cuts texts in R before it sends them, because TEI
ignores
max_lengthand only cuts at the model’s own limit.
Why EndpointR cuts classifier texts in R on TEI
Long texts give NaN scores on some TEI classifiers. TEI runs models in fp16 (half precision), and our ModernBERT spam classifier returned NaN scores for many long texts. The same texts failed when sent on their own, so the cause is length, not batching.
| Text length | Texts with a NaN score |
|---|---|
| Up to 2,048 tokens | 0 of 10,877 |
| 2,049 to 4,096 tokens | 9 of 683 |
| 4,097 to 8,192 tokens | 53 of 143 |
| Above 8,192 tokens | 54 of 54 |
A NaN score also crashed TEI 1.8.2 when the request asked TEI for
probabilities. TEI’s softmax failed, the endpoint returned 503, and it
then stayed in a failed state until someone restarted it. EndpointR
avoids the crash because it sends raw_scores: true and
applies the softmax in R (see
tidy_tei_classification_response()). With raw scores, TEI
returns 424 {"error": "score is NaN"} for the request and
keeps running, and EndpointR then splits the batch so that only the long
text fails.
EndpointR cuts texts in one of two ways:
- If the
tokpackage is installed and EndpointR knows the model id, it downloads the model’stokenizer.jsonfrom the Hugging Face Hub (with yourkey_name, so private models work). It then cuts each text longer thanmax_lengthtokens to the text covered by its firstmax_lengthtokens. Dedicated endpoints report their model id as/repository, so pass the id with thetokenizerargument. - Otherwise, it cuts each text to
max_charscharacters (2,000 by default) and prints a message. The tokeniser works on bytes, so some scripts produce more than one token per character, and 2,000 characters is not a guaranteed limit of 512 tokens.
install.packages("tok")
hf_classify_df(
df = my_data,
text_var = text,
id_var = id,
endpoint_url = classify_url,
key_name = "HF_API_KEY",
max_length = 512,
tokenizer = "org/model-name" # the model id on the Hugging Face Hub
)Set max_length = NULL to send texts uncut.
metadata.json records the method in
truncation_method ("tok",
"max_chars", "none", or
"endpoint" on the toolkit) and the number of texts that
were cut in n_texts_cut. The output’s text column always
holds your original texts.
Inference Parameters Sent to API
The request body depends on the engine and the task. EndpointR always sends the texts as a JSON array, even for a batch of one text.
Embeddings on TEI:
Classification on TEI:
Each text is wrapped in its own list, because TEI reads a flat list of exactly 2 texts as one sentence pair. It returns one result with no error, so 2 texts would get 1 score. TEI rejects a flat list of 3 or more texts.
Classification on the toolkit:
{
"inputs": ["first text", "second text"],
"parameters": {
"return_all_scores": true,
"truncation": true,
"max_length": 512,
"batch_size": 2
}
}batch_size is set to the number of texts in the request.
Without it, the toolkit pipeline runs one text at a time on the GPU,
even when a request holds many texts.
The parameters argument of the hf_*
functions adds to or overrides these defaults. On TEI, EndpointR adds
the entries to the top level of the body, because TEI ignores a
parameters field. Check metadata.json (see
below) to see what was sent.
Embeddings
Single Text
Embed one piece of text:
# inference api url for embeddings
embed_url <- "https://router.huggingface.co/hf-inference/models/sentence-transformers/all-mpnet-base-v2/pipeline/feature-extraction"
result <- hf_embed_text(
text = "This is a sample text to embed",
endpoint_url = embed_url,
key_name = "HF_API_KEY"
)The result is a tibble with one row and 384 columns (V1 to V384). Each column is an embedding dimension.
Note: The number of columns depends on your model. Check the model’s Hugging Face page for its embedding size.
List of Texts
Embed multiple texts at once using batching:
texts <- c(
"First text to embed",
"Second text to embed",
"Third text to embed"
)
batch_result <- hf_embed_batch(
texts,
endpoint_url = embed_url,
key_name = "HF_API_KEY",
batch_size = 32, # texts per request
concurrent_requests = 16 # requests in flight at once
)The result includes:
-
text: your original text -
.error: TRUE if something went wrong -
.error_msg: what went wrong (if anything) -
.status: the HTTP status code of a failed request -
V1toV384: the embedding values
Processing Data Frames with Chunk Writing
Most commonly, you’ll want to embed a column in a data frame. The
hf_embed_df() function processes data in chunks and writes
intermediate results to disk.
Understanding output_dir
Both hf_embed_df() and hf_classify_df()
write intermediate results to disk as .parquet files. This
provides:
- Safety: If your job crashes, you don’t lose all progress
- Memory efficiency: Large datasets don’t overwhelm your RAM
- Reproducibility: Metadata tracks exactly what parameters you used
# Basic usage - auto-generates output directory
embedding_result <- hf_embed_df(
df = my_data,
text_var = text, # column with your text
id_var = id, # column with unique ids
endpoint_url = embed_url,
key_name = "HF_API_KEY",
output_dir = "auto", # Creates "hf_embeddings_batch_TIMESTAMP"
chunk_size = 5000, # Writes every 5000 rows
batch_size = 32, # Texts per request
concurrent_requests = 16
)
# Custom output directory
embedding_result <- hf_embed_df(
df = my_data,
text_var = text,
id_var = id,
endpoint_url = embed_url,
key_name = "HF_API_KEY",
output_dir = "my_embeddings_v1", # Your custom directory name
chunk_size = 5000
)Output Directory Structure
After running hf_embed_df() or
hf_classify_df(), you’ll have:
my_embeddings_v1/
├── chunk_001.parquet
├── chunk_002.parquet
├── chunk_003.parquet
└── metadata.json
IMPORTANT: Add your output directories to
.gitignore! These files contain API responses and can be
large.
Reading Results from Disk
If your R session crashes or you want to reload results later:
# List all parquet files (excludes metadata.json automatically)
parquet_files <- list.files("my_embeddings_v1",
pattern = "\\.parquet$",
full.names = TRUE)
# Read all chunks into a single data frame
results <- arrow::open_dataset(parquet_files, format = "parquet") |>
dplyr::collect()
# Check for any errors
results |> count(.error)
# Extract only successful embeddings
successful <- results |> filter(.error == FALSE)Understanding metadata.json
The metadata file records everything about your processing job:
metadata <- jsonlite::read_json("my_embeddings_v1/metadata.json")
# Check which endpoint was used
metadata$endpoint_url
# Check which engine the endpoint runs, and its limits on TEI
metadata$engine
metadata$max_client_batch_size
metadata$version
# See processing parameters
metadata$chunk_size
metadata$batch_size
metadata$concurrent_requests
metadata$timeout
# See how many texts were empty, and how many batches were split
metadata$n_empty_texts
metadata$n_split_batches
# For classification, see the request body without the texts, and how texts were cut
metadata$inference_parameters
metadata$truncation_method
metadata$n_texts_cut
# Check when the job ran
metadata$timestampThis metadata is invaluable for:
- Debugging why a job failed
- Reproducing results with identical settings
- Tracking which model/endpoint version was used
- Understanding performance characteristics
Check for Errors
Always verify your results:
embedding_result |> count(.error)
# View any failures (column names match your original data frame)
failures <- embedding_result |>
filter(.error == TRUE) |>
select(id, .error_msg)
# Extract just the embeddings for successful rows
embeddings_only <- embedding_result |>
filter(.error == FALSE) |>
select(starts_with("V"))Classification
Classification works similarly to embeddings, but with a different
URL, output format, and the additional max_length,
max_chars and tokenizer arguments for
controlling text truncation.
Single Text
classify_url <- "https://router.huggingface.co/hf-inference/models/distilbert/distilbert-base-uncased-finetuned-sst-2-english"
sentiment <- hf_classify_text(
text = "I love this package!",
endpoint_url = classify_url,
key_name = "HF_API_KEY"
)Processing Data Frames
classification_result <- hf_classify_df(
df = my_data,
text_var = text,
id_var = id,
endpoint_url = classify_url,
key_name = "HF_API_KEY",
max_length = 512, # Truncate texts longer than 512 tokens
output_dir = "my_classification_v1",
chunk_size = 5000,
batch_size = 32,
concurrent_requests = 16,
timeout = 120
)The result includes:
- Your original ID and text columns (with their original names preserved)
- One column per classification label (e.g., POSITIVE, NEGATIVE), holding that label’s probability
- Error tracking columns (
.error,.error_msg,.status) - Chunk tracking (
.chunk)
NOTE: Classification labels are model and task specific. Check the model card on Hugging Face for label mappings.
NOTE: On TEI, EndpointR asks for raw scores (logits) and turns them into probabilities in R with
tidy_tei_classification_response(). On the toolkit, it usestidy_batch_classification_response(). If you pass your owntidy_func, it receives the response for one batch and must return one row per text.
IMPORTANT: The function preserves your original column names. If your data frame has
review_idandreview_text, those names will appear in the output, not genericidandtext.
Renaming Classification Labels
Many classification models use generic labels like
LABEL_0, LABEL_1. You can rename these:
# Create a mapping function
labelid_2class <- function() {
return(list(
negative = "LABEL_0",
neutral = "LABEL_1",
positive = "LABEL_2"
))
}
# Apply the mapping
classification_result <- hf_classify_df(
df = my_data,
text_var = text,
id_var = id,
endpoint_url = classify_url,
key_name = "HF_API_KEY",
max_length = 512
) |>
dplyr::rename(!!!labelid_2class())Utility Functions
EndpointR provides utility functions to help you work with Hugging Face endpoints.
Get Model Token Limits
Find out the maximum token length for a model:
# Get the model's max token length from Hugging Face
max_tokens <- hf_get_model_max_length(
model_name = "cardiffnlp/twitter-roberta-base-sentiment",
api_key = "HF_API_KEY"
)
# Use this to set max_length for classification
hf_classify_df(
df = my_data,
text_var = text,
id_var = id,
endpoint_url = classify_url,
key_name = "HF_API_KEY",
max_length = max_tokens # Use the model's actual limit
)This is especially useful when working with different models that have varying token limits (e.g., 512, 1024, 2048).
Get Endpoint Information
Retrieve detailed information about a Dedicated Inference Endpoint
that runs TEI. For endpoints that run the toolkit,
hf_get_endpoint_info() returns NULL with a
message, because the toolkit has no /info route:
endpoint_info <- hf_get_endpoint_info(
endpoint_url = "https://your-endpoint.endpoints.huggingface.cloud",
key_name = "HF_API_KEY"
)
# Check endpoint configuration
endpoint_infoThis is useful for:
- Checking which engine an endpoint runs
- Checking
max_client_batch_sizebefore you raisebatch_size - Checking the TEI version and the model’s token limit
Using Dedicated Endpoints
To use dedicated endpoints instead of the Inference API:
- Deploy your model to a dedicated endpoint (see Hugging Face docs)
- Get your endpoint URL
- Replace the URL in any function:
# just change this line
dedicated_url <- "https://your-endpoint-name.endpoints.huggingface.cloud"
# everything else stays the same
result <- hf_embed_text(
text = "Sample text",
endpoint_url = dedicated_url, # <- only change
key_name = "HF_API_KEY"
)Note: A dedicated endpoint that has scaled to zero returns 503 for about 1.5 to 2 minutes while it starts. With
engine = "auto", EndpointR’s/infocheck waits for up to about 2 minutes, so the endpoint is usually ready before the first batch goes out.
Setting AUTO_TRUNCATE for Embedding Endpoints
Since 0.3.0, EndpointR sends truncate: true with every
embedding request to a TEI endpoint, so TEI cuts long texts at the
model’s limit. Earlier versions sent truncate inside
parameters, which TEI ignores, and relied on the endpoint
setting.
You can still set the environment variable
AUTO_TRUNCATE=true in your endpoint settings on Hugging
Face, so that requests from other tools are cut too.
Tips and Best Practices
Performance Tuning
-
Use the defaults:
batch_size = 32andconcurrent_requests = 16work on every TEI endpoint, because 32 is TEI’s defaultmax_client_batch_size -
Raise
batch_sizeon TEI if the endpoint allows it: checkmax_client_batch_sizewithhf_get_endpoint_info(). On our spam classifier, set to 128, 128 texts per request and 32 requests in flight gave about 4,300 texts per second - Don’t go above 64 texts per request on the toolkit: 64 was the best setting in our tests, and larger batches were slower
-
Watch your rate limits:
- Inference API: Shared limits, reduce concurrency if you hit errors
- Dedicated Endpoints: Limited by hardware, not API rate limits
Memory Management
- Use
chunk_sizeto control memory usage - Smaller chunks = more frequent disk writes = less memory needed
- For very large datasets (>100k rows), use
chunk_size = 1000-2500
# For very large datasets
hf_embed_df(
df = large_data,
text_var = text,
id_var = id,
endpoint_url = embed_url,
key_name = "HF_API_KEY",
chunk_size = 1000 # Smaller chunks for memory efficiency
)Truncation Strategy
For Embeddings:
- On TEI, EndpointR sends
truncate: true, so TEI cuts texts at the model’s limit - For Inference API, truncation is handled automatically by most models
- Consider preprocessing very long texts before embedding (e.g., take first N characters)
For Classification:
- Use
hf_get_model_max_length()to check the model’s token limit - Set
max_lengthappropriately (default 512 works for most models) - On TEI, install
tokand pass the model id astokenizer, so texts are cut at exactlymax_lengthtokens - For documents longer than
max_length, consider:- Chunking documents and classifying each chunk
- Summarization before classification
- Using models with longer context windows
# Get model's actual max length
model_limit <- hf_get_model_max_length(
model_name = "distilbert/distilbert-base-uncased-finetuned-sst-2-english",
api_key = "HF_API_KEY"
)
# Use 90% of the limit to be safe
safe_limit <- as.integer(model_limit * 0.9)
hf_classify_df(
df = my_data,
text_var = text,
id_var = id,
endpoint_url = classify_url,
key_name = "HF_API_KEY",
max_length = safe_limit
)Error Recovery
Always check for errors and consider retrying failures:
# Check results for errors
results |> count(.error)
# Identify failed texts (column names match your input data frame)
failed <- results |> filter(.error == TRUE)
# Note: Column names below will match your original data frame
# If you used review_id and review_text, use those names instead
failed |> select(id, .error_msg)
# EndpointR has already split failed batches down to single texts, so a text
# that still fails usually fails on its own (e.g. a NaN score, HTTP 424).
# Texts that failed with a timeout or 5xx may succeed on a second attempt.
# Access text column by its actual name from your data
retry_results <- hf_embed_batch(
texts = failed$text, # Use your actual column name
endpoint_url = embed_url,
key_name = "HF_API_KEY",
timeout = 300, # Longer timeout
max_retries = 10 # More retries
)Production Recommendations
- Always use output_dir: Never rely solely on in-memory results for large jobs
-
Monitor metadata: Check
metadata.jsonto verify your settings - Add to .gitignore: Keep API responses out of version control
- Use Dedicated Endpoints: For production workloads, avoid the free Inference API
- Set appropriate timeouts: The default of 120 seconds allows for batches of long texts
- Test with small samples: Before processing 1M rows, test with 100 rows
- Monitor costs: Track your Dedicated Endpoint usage on Hugging Face
Common Issues
“Payload too large” Errors
For Embeddings:
-
Dedicated Endpoints on TEI: EndpointR sends
truncate: true, so long texts are cut. If you still see the error, lowerbatch_size, because the whole request may be too large - Inference API: Preprocess and truncate texts before sending
# Preprocessing approach for Inference API
my_data <- my_data |>
mutate(text = substr(text, 1, 5000)) # Limit to ~5000 charactersFor Classification:
- Reduce the
max_lengthparameter
hf_classify_df(
df = my_data,
text_var = text,
id_var = id,
endpoint_url = classify_url,
key_name = "HF_API_KEY",
max_length = 256 # Reduce from default 512
)Timeouts
Batches of long texts can take longer than the default timeout of 120 seconds. When a request times out, EndpointR splits the batch and sends the halves again. If many batches time out, increase the timeout:
hf_classify_df(
df = my_data,
text_var = text,
id_var = id,
endpoint_url = classify_url,
key_name = "HF_API_KEY",
timeout = 300, # Increase from default 120
max_retries = 10
)Dedicated Endpoint Cold Starts
A dedicated endpoint that has scaled to zero returns 503 for about
1.5 to 2 minutes while it starts. With engine = "auto",
EndpointR’s /info check retries for about 2 minutes, so it
usually waits until the endpoint is ready. If you set
engine yourself, the batches are retried instead, with
waits of 2, 4, 8, 16 and 30 seconds by default. Raise
max_retries to wait longer:
hf_embed_df(
df = my_data,
text_var = text,
id_var = id,
endpoint_url = dedicated_url,
key_name = "HF_API_KEY",
engine = "tei",
max_retries = 10 # waits up to about 4 minutes in total
)Out of Memory Errors
Reduce chunk_size:
# Instead of default 5000
hf_embed_df(
df = large_data,
text_var = text,
id_var = id,
endpoint_url = embed_url,
key_name = "HF_API_KEY",
chunk_size = 1000 # Smaller chunks
)Rate Limit Errors
For Inference API:
- Reduce
concurrent_requests, e.g. to 2 - EndpointR waits and retries requests that get 429
hf_embed_df(
df = my_data,
text_var = text,
id_var = id,
endpoint_url = embed_url,
key_name = "HF_API_KEY",
concurrent_requests = 2,
max_retries = 10 # More retries with backoff
)For Dedicated Endpoints:
- Not typically rate-limited
- TEI returns 429 when its queue is full, and EndpointR retries those requests
- If you see many errors, reduce
concurrent_requestsor upgrade your endpoint hardware
Model Not Available
Not all models work with the Inference API. Check the model page on Hugging Face. If the model isn’t available via Inference API, you’ll need to:
- Deploy a Dedicated Inference Endpoint
- Use a different model that is available via Inference API
- Run the model locally (outside of EndpointR)
Problems we have seen with TEI and the toolkit
We found the following problems when we tested our endpoints. EndpointR handles each of them, but they help explain errors you may see from other tools.
- A flat list of exactly 2 texts sent to a TEI classifier is read as
one sentence pair, and TEI returns one result with no error. A flat list
of 3 or more texts is rejected with
Failed to deserialize the JSON body ... expected a string, a pair of strings [string, string] or a batch of mixed strings and pairs. EndpointR sends[["a"], ["b"]]to TEI classifiers. - A batch of 1 must still be sent as a list. In R,
as.list(texts)gives["a"], but a character vector of length 1 gives"a". - TEI ignores
parameters$max_length, and long texts can give NaN scores in fp16. See Why EndpointR cuts classifier texts in R on TEI. - A NaN score crashes TEI 1.8.2 when
raw_scoresis false, and the endpoint stays in a failed state until someone restarts it. Withraw_scores: true, TEI returns 424 for the request and keeps running. - One empty text makes the toolkit reject the whole batch with
You need to specify either text or text_target. EndpointR does not send empty texts. - TEI rejects requests with more texts than
max_client_batch_size. EndpointR lowersbatch_sizeto that value. - A scaled-to-zero endpoint returns 503 for about 1.5 to 2 minutes while it starts.
- Some fine-tuned classifiers need a change to their
config.jsonbefore they run on TEI. For our spam classifier, thelabel2idvalues had to be numbers, not strings, and we had to add"num_labels": 2.
Repeatable results
TEI does not give exactly the same results each time you run the same texts. TEI runs models in fp16 and combines texts from all waiting requests into one batch, so the numbers depend on which texts share a batch, and that depends on timing. This happens whatever the client sends, including one text per request. The toolkit runs in fp32 and gave the same results each time in our tests.
In our tests, the differences were small:
- When we ran the same 100,000 texts through our spam classifier on TEI twice, 18% of the scores changed slightly and 4 labels changed. The largest change in a score was 0.071.
- 99.95% of spam labels from TEI matched the labels from the same model on the toolkit. The 50 labels that changed all had toolkit scores between 0.494 and 0.557.
- BGE M3 embeddings of 50,000 messages, made on TEI with 32 texts per request, matched embeddings of the same messages made a year earlier with 1 text per request. The median cosine similarity was 0.9999993 and the lowest was 0.99907.
The embedding differences change a UMAP and HDBSCAN clustering about as much as a new UMAP seed does. The adjusted Rand index against the original clustering was 0.59 to 0.66, compared with 0.65 for a new seed. PCA followed by k-means was stable, at 0.96 to 1.00.
To make an analysis repeatable, save the embeddings and scores once,
e.g. in the .parquet files that the _df()
functions write, and reuse them. Don’t embed or classify the same texts
again and expect identical numbers.
Improving Performance
For detailed performance optimization strategies, visit the Improving Performance vignette.
Quick tips:
- Start with the defaults,
batch_size = 32andconcurrent_requests = 16 - On TEI, raise
batch_sizeup to the endpoint’smax_client_batch_size, andconcurrent_requestsup to about 32 - On the toolkit, use up to 64 texts per request and about 8 requests in flight
- Use larger
chunk_sizevalues for faster processing (if memory allows) - For Dedicated Endpoints, upgrade hardware for better throughput
- Use batch functions (
hf_embed_batch(),hf_classify_batch()) for small datasets to avoid file I/O overhead
Appendix
Comparison of Inference API vs Dedicated Inference Endpoints
| Feature | Inference API | Dedicated Inference Endpoints |
|---|---|---|
| Accessibility | Public, shared service | Private, dedicated hardware |
| Cost | Free (with paid tiers) | Paid service - rent specific hardware |
| Hardware | Shared computing resources | Dedicated hardware allocation |
| Wait Times | Variable, unknowable in advance | Predictable, ~30s for cold start |
| Production Ready | Not recommended for production | Recommended for production use |
| Use Case | Casual usage, testing, prototyping | Production applications |
| Scalability | Limited by shared resources | Scales with dedicated allocation |
| Availability | Subject to shared infrastructure limits | Guaranteed availability during rental |
| Model Coverage | Commonly-used models, models selected by HF | Virtually all models on the Hub |
| Truncation Control | Limited (model-dependent) | Full control via environment variables |
| Engine | Chosen by Hugging Face | TEI or the toolkit, chosen when you deploy |