← Back to DevBytes

Error Recovery Patterns with llama.cpp: Complete Guide

Introduction to Error Recovery in llama.cpp

llama.cpp is a popular C/C++ inference engine for running large language models locally. While it is fast and lightweight, real-world deployments face a variety of failure modes: malformed input, context overflow, OOM conditions, model loading failures, and network timeouts when streaming from remote backends. Building robust applications requires explicit error recovery patterns that detect failures, degrade gracefully, and resume operation where possible.

This tutorial walks through the most important error recovery patterns for llama.cpp-based applications, with practical C++ examples you can adapt to your own projects.

Why Error Recovery Matters

Unlike managed cloud APIs, llama.cpp runs on heterogeneous hardware with unpredictable memory constraints. A prompt that works on a 32 GB workstation may crash on a laptop with 8 GB of RAM. Additionally, llama.cpp exposes low-level primitives — tokenizers, context objects, sampling state — that require careful lifecycle management. Without recovery patterns, a single failure can leave the process in an inconsistent state, leak memory, or produce corrupted output.

Core Failure Modes in llama.cpp

Before writing recovery code, it helps to categorize the failures you will encounter:

Pattern 1: Guarded Resource Initialization

The first line of defense is checking every allocation. llama.cpp uses C-style APIs that return null pointers or error codes rather than throwing exceptions. Wrap these calls in guard functions.

#include "llama.h"
#include <stdexcept>
#include <string>

struct llama_model_ptr {
    llama_model* ptr;
    explicit llama_model_ptr(llama_model* p) : ptr(p) {}
    ~llama_model_ptr() { if (ptr) llama_model_free(ptr); }
    llama_model_ptr(const llama_model_ptr&) = delete;
    llama_model_ptr& operator=(const llama_model_ptr&) = delete;
};

llama_model_ptr load_model_or_throw(const std::string& path) {
    llama_model_params params = llama_model_default_params();
    params.n_gpu_layers = 99; // attempt full offload

    llama_model* model = llama_model_load_from_file(path.c_str(), params);
    if (!model) {
        throw std::runtime_error("Failed to load model: " + path);
    }
    return llama_model_ptr(model);
}

This RAII wrapper ensures the model is freed even if an exception propagates upward. The same pattern applies to llama_context.

Pattern 2: Context Allocation Fallback

When GPU memory is insufficient, llama_new_context_with_model may fail. A robust loader retries with progressively smaller configurations.

llama_context* create_context_with_fallback(llama_model* model, int target_ctx) {
    struct Attempt {
        int n_ctx;
        int n_gpu_layers;
        int n_batch;
    };

    std::vector<Attempt> attempts = {
        {target_ctx, 99, 512},
        {target_ctx, 0,  512},  // CPU fallback
        {target_ctx / 2, 0, 256}, // shrink context
        {1024, 0, 128},           // minimal viable
    };

    for (const auto& a : attempts) {
        llama_context_params ctx_params = llama_context_default_params();
        ctx_params.n_ctx     = a.n_ctx;
        ctx_params.n_batch   = a.n_batch;
        ctx_params.n_gpu_layers = a.n_gpu_layers;

        llama_context* ctx = llama_new_context_with_model(model, ctx_params);
        if (ctx) {
            fprintf(stderr, "Context created: n_ctx=%d, gpu_layers=%d\n",
                    a.n_ctx, a.n_gpu_layers);
            return ctx;
        }
        fprintf(stderr, "Context attempt failed: n_ctx=%d, gpu_layers=%d\n",
                a.n_ctx, a.n_gpu_layers);
    }
    return nullptr;
}

This pattern lets your application start in a degraded mode rather than refusing to run at all.

Pattern 3: Context Overflow Recovery

One of the most common runtime errors is exceeding n_ctx. The recovery strategy is to truncate the prompt or implement a sliding window. The safest approach is to check token counts before decoding.

bool safe_tokenize(llama_model* model,
                   const std::string& text,
                   std::vector<llama_token>& out_tokens,
                   int max_tokens,
                   bool add_bos) {
    out_tokens.resize(max_tokens);
    int n = llama_tokenize(model, text.c_str(), text.size(),
                           out_tokens.data(), max_tokens, add_bos, true);
    if (n < 0) {
        // Buffer too small — retry with exact required size
        out_tokens.resize(-n);
        n = llama_tokenize(model, text.c_str(), text.size(),
                           out_tokens.data(), -n, add_bos, true);
    }
    if (n < 0) return false;

    out_tokens.resize(n);

    int n_ctx = llama_n_ctx_from_model(model);
    if ((int)out_tokens.size() > n_ctx - 4) {
        // Reserve space for generation; truncate from the front
        int keep = n_ctx - 4;
        out_tokens.erase(out_tokens.begin(),
                         out_tokens.begin() + (out_tokens.size() - keep));
        fprintf(stderr, "Warning: prompt truncated to %d tokens\n", keep);
    }
    return true;
}

Truncating from the front preserves the most recent context, which is usually more relevant for conversational applications. For document QA, you may prefer to truncate from the back or split into chunks.

Pattern 4: Decode Error Handling

llama_decode returns a status code. A return value of 1 indicates the KV cache is full; 2 indicates a dimension mismatch. Both are recoverable if handled promptly.

enum class DecodeResult { OK, KV_FULL, ERROR };

DecodeResult safe_decode(llama_context* ctx, llama_batch& batch) {
    int rc = llama_decode(ctx, batch);
    if (rc == 0) return DecodeResult::OK;
    if (rc == 1) {
        fprintf(stderr, "KV cache full — triggering context shift\n");
        // Shift context: remove oldest tokens
        int n_ctx = llama_n_ctx(ctx);
        int n_keep = 4; // preserve system prompt
        int n_shift = n_ctx / 2;
        llama_kv_self_seq_rm(ctx, 0, n_keep, n_keep + n_shift);
        llama_kv_self_seq_add(ctx, 0, n_keep + n_shift, n_ctx, -n_shift);
        return DecodeResult::KV_FULL;
    }
    fprintf(stderr, "Decode error: rc=%d\n", rc);
    return DecodeResult::ERROR;
}

After a KV cache shift, you must also adjust your internal token tracking so future batches reference the correct positions. The shift effectively "forgets" the oldest half of the conversation.

Pattern 5: Retry with Exponential Backoff

For transient failures — particularly when llama.cpp is wrapped behind an HTTP server or used with mmap'd models on network filesystems — a retry loop with exponential backoff is essential.

#include <chrono>
#include <thread>

template <typename Fn>
auto retry_with_backoff(Fn fn, int max_attempts, int initial_delay_ms)
    -> decltype(fn()) {
    int delay = initial_delay_ms;
    for (int attempt = 1; attempt <= max_attempts; ++attempt) {
        try {
            return fn();
        } catch (const std::exception& e) {
            fprintf(stderr, "Attempt %d/%d failed: %s\n",
                    attempt, max_attempts, e.what());
            if (attempt == max_attempts) throw;
            std::this_thread::sleep_for(
                std::chrono::milliseconds(delay));
            delay *= 2;
        }
    }
    throw std::runtime_error("retry_with_backoff: unreachable");
}

Use this wrapper around model loading or any operation that touches external resources. Avoid retrying decode errors, since those indicate state corruption that retrying will not fix.

Pattern 6: Sampling Error Recovery

Grammar-constrained sampling can fail when the grammar is incompatible with the current token distribution. A fallback to unconstrained sampling prevents hard failures.

llama_token safe_sample(llama_context* ctx,
                        llama_sampler* constrained,
                        llama_sampler* fallback) {
    llama_token token = llama_sampler_sample(constrained, ctx, -1);

    // Detect invalid token (grammar rejection exhausted all candidates)
    if (token == LLAMA_TOKEN_NULL) {
        fprintf(stderr, "Constrained sampling failed — using fallback\n");
        token = llama_sampler_sample(fallback, ctx, -1);
    }
    return token;
}

This is especially useful when using GBNF grammars for structured output. A malformed grammar rule can silently produce no valid tokens; the fallback ensures generation continues, even if the output is no longer strictly structured.

Pattern 7: Circuit Breaker for Repeated Failures

In a service context, repeated failures may indicate a systemic problem (corrupted model, exhausted memory). A circuit breaker prevents cascading failures by short-circuiting requests after a threshold is reached.

class CircuitBreaker {
    int failure_count = 0;
    int threshold;
    std::chrono::steady_clock::time_point open_until;
    std::chrono::seconds cooldown;

public:
    CircuitBreaker(int thresh, std::chrono::seconds cd)
        : threshold(thresh), cooldown(cd) {}

    bool allow() {
        if (failure_count < threshold) return true;
        return std::chrono::steady_clock::now() >= open_until;
    }

    void record_failure() {
        if (++failure_count >= threshold) {
            open_until = std::chrono::steady_clock::now() + cooldown;
            fprintf(stderr, "Circuit opened for %ld seconds\n",
                    cooldown.count());
        }
    }

    void record_success() {
        failure_count = 0;
    }
};

Integrate the circuit breaker at the request boundary. When open, return a 503 response or a cached fallback answer instead of attempting inference.

Putting It All Together

The following example combines several patterns into a single resilient generation function:

struct GenerationResult {
    std::string text;
    bool truncated;
    bool degraded;
    std::string warning;
};

GenerationResult resilient_generate(llama_model* model,
                                    llama_context* ctx,
                                    const std::string& prompt,
                                    int max_new_tokens) {
    GenerationResult result;

    // Pattern 3: safe tokenization with overflow recovery
    std::vector<llama_token> tokens;
    if (!safe_tokenize(model, prompt, tokens, llama_n_ctx(ctx), true)) {
        result.warning = "Tokenization failed";
        return result;
    }
    if ((int)tokens.size() > (int)prompt.size() / 2) {
        result.truncated = true;
    }

    llama_batch batch = llama_batch_get_one(tokens.data(), tokens.size());

    // Pattern 4: decode with KV recovery
    auto dr = safe_decode(ctx, batch);
    if (dr == DecodeResult::ERROR) {
        result.warning = "Initial decode failed";
        return result;
    }
    if (dr == DecodeResult::KV_FULL) {
        result.degraded = true;
    }

    // Generation loop
    llama_sampler* sampler = llama_sampler_chain_init(
        llama_sampler_chain_default_params());
    llama_sampler_chain_add(sampler,
        llama_sampler_init_temp(0.8f));
    llama_sampler_chain_add(sampler,
        llama_sampler_init_dist(0));

    for (int i = 0; i < max_new_tokens; ++i) {
        llama_token tok = llama_sampler_sample(sampler, ctx, -1);
        if (llama_token_is_eog(model, tok)) break;

        char buf[16];
        int n = llama_token_to_piece(model, tok, buf, sizeof(buf), 0, true);
        if (n > 0) result.text.append(buf, n);

        batch = llama_batch_get_one(&tok, 1);
        dr = safe_decode(ctx, batch);
        if (dr == DecodeResult::ERROR) {
            result.warning = "Decode error during generation";
            break;
        }
        if (dr == DecodeResult::KV_FULL) {
            result.degraded = true;
        }
    }

    llama_sampler_free(sampler);
    return result;
}

The caller receives not only the generated text but also metadata about whether the response was truncated or degraded, enabling transparent reporting to end users.

Best Practices

Conclusion

Error recovery in llama.cpp is not optional for production deployments — it is the difference between a demo and a reliable product. By combining RAII resource management, context overflow detection, decode error handling, retry logic, sampling fallbacks, and circuit breakers, you can build applications that survive the messy realities of local LLM inference. The patterns in this guide are composable: start with guarded initialization and safe tokenization, then layer in decode recovery and circuit breaking as your reliability requirements grow. With these foundations in place, your llama.cpp applications will degrade gracefully under stress and recover automatically when conditions improve.

— Ad —

Google AdSense will appear here after approval

← Back to all articles