Burn-LM AI Engine

repository·main·Indexed 42 days ago

https://github.com/tracel-ai/burn-lm

An AI engine project designed to be embedded into applications or deployed as a standalone server. It provides a framework for implementing model architectures using the Burn library, importing weights from external sources like PyTorch, and managing models via the burn-lm-cli tool. Features include a model registry, support for multiple backends (WebGPU, CUDA, ROCm, Metal, Vulkan, ndarray, LibTorch), and a command-line interface for chatting with models and managing data types (f32, f16, bf16). Note: This project has been archived and is no longer maintained.

Tokens
26.7K
Snippets
101
Records
122
Agent score
54%

What's inside Burn-LM

  1. Configure model variants using the Config trait

    main

    Use the #[derive(Config)] macro to define how model components and variants (e.g., different parameter sizes) are initialized.

    Key Features of Config:

    • Default values: Use #[config(default = ...)] to specify defaults.
    • Automatic constructors: Provides new(...) and with_ methods for fields.
    • Serialization: Enables easy saving and loading of configuration files.

    Pattern:

    1. Define a Config struct for each module.
    2. Implement an init method on the config struct that takes a &Device<B> and returns the initialized module.
    3. For complex models, create a top-level composite config that delegates initialization to the sub-module configs.
    #[derive(Config, Debug)]
    pub struct FeedForwardConfig {
        pub d_model: usize,
        pub hidden_size: usize,
    }
    
    impl FeedForwardConfig {
        pub fn init<B: Backend>(&self, device: &Device<B>) -> FeedForward<B> {
            let swiglu = SwiGluConfig::new(self.d_model, self.hidden_size)
                .with_bias(false)
                .init(device);
            let w2 = LinearConfig::new(self.hidden_size, self.d_model)
                .with_bias(false)
                .init(device);
    
            FeedForward { swiglu, w2 }
        }
    }
  2. Define a Burn model module

    main

    To implement a model architecture, define custom modules using structs or enums that derive the Module trait.

    Best Practice: While not mandatory, closely following the architecture and naming structure of the original source model (e.g., PyTorch) makes importing weights significantly easier by reducing the amount of remapping logic required.

    Implementation Steps:

    1. Define the struct with fields representing the model's components (e.g., Linear, Embedding, RmsNorm).
    2. Implement the forward method (or a similarly named method) to define the data flow through the module.

    Note: Pay special attention to tokenization and positional encoding, as discrepancies here can lead to subtle bugs.

    /// Feed-forward transformation network.
    #[derive(Module, Debug)]
    pub struct FeedForward<B: Backend> {
        swiglu: SwiGlu<B>,
        w2: Linear<B>,
    }
    
    impl<B: Backend> FeedForward<B> {
        pub fn forward(&self, input: Tensor<B, 3>) -> Tensor<B, 3> {
            self.w2.forward(self.swiglu.forward(input))
        }
    }
  3. Importing weights from external models

    main

    To support a new model, you must map external weights (e.g., from PyTorch) to your Burn-native implementation. The process follows three main steps:

    1. Define the model with Burn: Implement the architecture using Module.
    2. Add configs for each variant: Use Config to handle different model sizes/parameters.
    3. Import the weights: Map the external model definition to the Burn structure and convert the weights.

    Recommendation: Once weights are successfully loaded, save them in Burn's native format to avoid repeated conversion and deserialization overhead.

  4. Manage KV cache with TransformerCache

    main

    The TransformerCache manages the Key-Value states for all layers to enable efficient autoregressive generation.

    • Initialization: Use TransformerCache::new(config, max_batch_size, device).
    • Preparation: Call prepare(seq_len) before a forward pass. This updates the current sequence length and returns an optional attention mask (Option<Tensor<4, Bool>>).
    • Error Handling: prepare returns Err(GenerationError::MaxSequenceLengthExceeded) if the sequence length exceeds the configured max_seq_len.
    • Resetting: Call reset() to clear all cached states and set curr_seq_len to 0.
    let mut cache = TransformerCache::new(&config, batch_size, &device);
    
    // Prepare for a new sequence of length 'n'
    match cache.prepare(n) {
        Ok(Some(mask)) => { /* use mask in transformer.forward */ },
        Ok(None) => { /* no mask needed (e.g. seq_len <= 1) */ },
        Err(e) => { /* handle MaxSequenceLengthExceeded */ },
    }
    
    // Clear cache for a new session
    cache.reset();
  5. Configure CacheStrategy for overflow management

    main

    When the autoregressive cache exceeds its capacity, you can use CacheStrategy to decide how to make room for new tokens via the prepare(num_tokens) method.

    Strategies

    • CacheStrategy::Shift (Default): Shifts the remaining tokens to the start of the existing buffer in-place, overwriting the oldest tokens. This is useful for sliding window attention.
    • CacheStrategy::Shrink: Shrinks the cache by copying the remaining tokens into a new buffer, effectively removing the oldest tokens beyond the context limit.

    Use .with_strategy(strategy) during cache initialization to set the behavior.

    // Using the default Shift strategy
    let cache = AutoregressiveCache::<3>::new(shape, seq_dim, &device);
    
    // Explicitly using the Shrink strategy
    let cache = AutoregressiveCache::<3>::new(shape, seq_dim, &device)
        .with_strategy(CacheStrategy::Shrink);
  6. Streaming response format for chat_completions

    main

    When stream: true is requested, the server responds with text/event-stream. The stream emits ChatCompletionChunkSchema objects wrapped in SSE events.

    Key features of the stream:

    • Model Lifecycle Feedback: If a model is being loaded or an old model is being unloaded, the stream emits text chunks formatted as Markdown code blocks (e.g., ```Burn LM\nloading model '...'...) to provide real-time status to the client.
    • Reply Marker: A specific chunk containing ##### Model Reply is sent before the actual content begins.
    • Stats Chunk: After the text generation is complete, a final chunk containing generation statistics (via stats.display_stats()) is sent.
    • Termination: The stream concludes with a StreamingChunk::Done event.
  7. Start a chat session with a model via the `chat` command

    main

    The chat command initiates an interactive shell session with a specific model. The available subcommands for chat are dynamically generated based on the models registered in the Registry.

    To start a session, you must provide the name of the model as a subcommand. Note that models must be downloaded before they can be used in a chat session. If a model is not downloaded, the CLI will suggest running the download <model_name> command first.

    # Example usage (assuming a model named 'llama' is registered)
    # The exact subcommand name depends on the plugin's `model_cli_param_name()`
    $ burn-lm chat llama --some-plugin-flag
    
    Chat session started! (press CTRL+D or type /exit to close session)
    (backend-dtype) chat|llama>
  8. Load pre-trained Llama models

    main

    The LlamaConfig provides high-level helper functions to initialize a model and load weights from a checkpoint in one step. These functions return an inference::Llama<T> instance.

    Llama-3 Series (requires llama3 feature)

    These methods use the Tiktoken tokenizer:

    • load_llama3_2_3b(checkpoint, tokenizer_path, max_seq_len, device)
    • load_llama3_2_1b(checkpoint, tokenizer_path, max_seq_len, device)
    • load_llama3_1_8b(checkpoint, tokenizer_path, max_seq_len, device)
    • load_llama3_8b(checkpoint, tokenizer_path, max_seq_len, device)

    TinyLlama (requires tiny feature)

    This method uses the SentencePieceTokenizer:

    • load_tiny_llama(checkpoint, tokenizer_path, max_seq_len, device)
    // Example: Loading a pre-trained Llama 3.2 1B model
    let llama = LlamaConfig::load_llama3_2_1b(
        "path/to/checkpoint",
        "path/to/tokenizer",
        2048,
        &device
    ).expect("Failed to load model");
  9. Manage inference jobs with InferenceJob and JobHandle

    main

    An InferenceJob represents a unit of work to be performed by the inference server. To run a job, use InferenceJob::create, which returns both the InferenceJob (used to emit generated items) and a JobHandle (used to wait for completion and retrieve results).

    Workflow

    1. Create: Call InferenceJob::create(task, listener).
    2. Execute: Perform the inference logic in a separate thread/process. As tokens or text are generated, call job.emitter.completed(GeneratedItem::Text(text)) to stream them to the listener.
    3. Finish: Call job_handle.join() to block until the job is complete and retrieve the final CompletedItem produced by your listener.
    // Example workflow
    let task = InferenceTask::Prompt(Prompt::new("Hello!"));
    let listener = TextGenerationListener::default();
    let (job, handle) = InferenceJob::create(task, listener);
    
    // In your inference loop:
    job.emitter.completed(GeneratedItem::Text("Hello".to_string()));
    job.emitter.completed(GeneratedItem::Text(" world!".to_string()));
    
    // Wait for result:
    let final_text = handle.join();
    println!("Result: {}", final_text);
  10. Configure Multi-Head Attention with MultiHeadAttentionConfig

    main

    Use MultiHeadAttentionConfig to define the parameters for a multi-head attention module. This configuration supports Grouped Query Attention (GQA) by allowing the number of key-value heads (n_kv_heads) to differ from the number of query heads (n_heads).

    Config Fields:

    • d_model (usize): The total dimension of the model.
    • n_heads (usize): The number of attention heads.
    • n_kv_heads (usize): The number of key-value heads (used for GQA).
    let config = MultiHeadAttentionConfig::new(
        d_model, // e.g., 512
        n_heads, // e.g., 8
        n_kv_heads, // e.g., 2 for GQA
    );