Modules
08/30
QLoRA

Contents

QLoRA in LLMs

How QLoRA makes large language models easier to fine-tune when GPU memory is limited.

A pretrained language model can already write and answer questions, but it may not behave the way a particular product or task requires. Fine-tuning teaches it that new behavior using examples, such as support conversations, medical text, or instructions written in a preferred style.

The difficulty is memory. A large model contains billions of learned numbers called weights, and those weights must be available while the model processes each training example. LoRA lowers the cost by learning a small set of additional weights instead of changing the whole model. QLoRA goes one step further: it also stores the unchanged model in a more compact form while those small LoRA weights are trained.

The Problem QLoRA Solves

LoRA keeps the original model weights frozen. Here, frozen only means that training does not change them. The weights do not disappear, because the model still needs them to understand the input and produce an answer. They must remain in memory during every training step.

Consider a model with 7 billion weights. When each weight uses 16 bits, or 2 bytes, the original model weights alone need about 14 GB of memory. This estimate does not yet include the LoRA adapter, training data being processed, or other temporary memory. QLoRA focuses on making this large frozen part smaller.

Why QLoRA saves memory

Compare the size of the frozen model

Choose a model size to see how much memory its weights need when each one is stored with 16 bits or 4 bits. This first comparison covers the base weights only.

Base model used with LoRA
each frozen weight uses 16 bits
14 GB
16 bits per weight
Base model used with QLoRA
each frozen weight uses a 4-bit code
3.5 GB
4 bits per weight

For 7 billion weights, the 4-bit codes use about 10.5 GB less raw storage than 16-bit values. Later sections add the smaller memory costs that this simple comparison leaves out.

QLoRA in one sentence

QLoRA stores the large, unchanged model with fewer bits and trains only the small LoRA adapter.

1. Bits, Bytes, and Number Formats

A computer stores numbers as patterns of zeros and ones. Each position in that pattern is a bit, and eight bits together make one byte. A number stored with 8 bits occupies 1 byte; a number stored with 16 bits occupies 2 bytes; a number stored with 32 bits occupies 4 bytes.

00101101
Each box holds one bit. All eight boxes occupy one byte.

Each extra bit doubles the number of possible patterns. One bit gives us two patterns, two bits give us four, and eight bits give us 256. A number format determines what those patterns mean, including whether they represent whole numbers or values with a fractional part.

Integers: INT8 and UINT8

An integer is a whole number, such as 12, 0, or -45. The name INT8 means an integer stored using eight bits. Signed INT8 can represent every whole number from -128 to 127. The unsigned version, UINT8, uses the same amount of space but represents values from 0 to 255.

Both formats have 256 possible values. The difference is how they interpret the bits: UINT8 uses all of its range for nonnegative numbers, while INT8 includes negative numbers. For example, the pattern 11111111 means 255 as UINT8 and -1 as INT8.

Signed integers commonly use two's complement. To represent -45 in eight bits, start with the binary pattern for +45, flip every zero and one, then add one. This representation lets the same binary addition rules work for positive and negative integers.

Start with +4500101101
Flip each bit11010010
Add 1 to get -4511010011
In signed INT8, the leftmost bit has a value of -128. The other positions have values of 64, 32, 16, 8, 4, 2, and 1.

Integer formats also come in larger sizes, including INT16 and INT32. More bits allow a wider range of whole numbers, but an integer by itself still cannot express a weight such as 0.127. To store fractional values directly, we use floating point.

Floating point: sign, exponent, and mantissa

Scientific notation separates a number into significant digits and a power that sets its size. For example, 625 can be written as 6.25 × 10². Binary floating point uses the same principle with powers of two, which allows a fixed number of bits to represent both small fractions and large values.

Each floating-point number has three fields. The sign uses one bit: 0 for positive and 1 for negative. The exponent sets the power of two and controls the numerical range. The fraction field, often called the mantissa, stores the detail within that range.

For a normal binary floating-point number, the significant digits begin with 1. That leading 1 is implied, so only the digits after the binary point need to be stored. The leading 1 together with those fraction bits is the significand. More fraction bits let the format distinguish values that are closer together.

The exponent also has a storage convention called a bias, an offset added before storing it. FP32 uses a bias of 127, so an actual exponent of 2 is stored as 129. Subtracting the bias when reading the number recovers the actual exponent and allows the field to cover both positive and negative powers.

-6.25 = -1 × 1.5625 ×
Sign: negative
The sign bit is 1.
Exponent: 2
The significand is multiplied by 4.
Significand: 1.5625
In binary this is 1.1001, so the fraction field starts with 1001.
This example describes a normal number. Formats also reserve patterns for zero, very small subnormal values, and special values.

FP32, FP16, BF16, and FP8

These formats divide their available bits differently. FP32 has room for a wide range and fine detail. FP16 uses half as much storage, with fewer bits for both the exponent and fraction. BF16 also uses 16 bits, but allocates more of them to the exponent.

FP3232 bits: 1 sign + 8 exponent + 23 fraction
FP1616 bits: 1 sign + 5 exponent + 10 fraction
BF1616 bits: 1 sign + 8 exponent + 7 fraction
FP8 E4M38 bits: 1 sign + 4 exponent + 3 fraction
FP8 E5M28 bits: 1 sign + 5 exponent + 2 fraction
SignExponent: rangeFraction: precisionEach cell represents one stored bit.

FP16 keeps 10 fraction bits, compared with 7 in BF16, so it can preserve finer differences between nearby values within its range. BF16 has a wider range because its exponent uses 8 bits instead of 5. During training, that range helps accommodate calculations that produce very large or very small numbers.

FP8 takes the same idea down to one byte. E4M3 has 4 exponent bits and 3 fraction bits, while E5M2 uses 5 and 2. FP8 and INT8 occupy the same space, but interpret it differently: FP8 has an exponent and a fraction, while INT8 represents whole numbers.

The storage cost adds up quickly across a model. Seven billion weights require about 28 GB in FP32 or 14 GB in FP16 or BF16, before any other memory is counted. Representing those weights with one byte each would reduce their raw storage to 7 GB. The next step is to understand how an integer can represent an approximate fractional weight.

2. How Quantization Works

A weight such as 0.314 fits naturally in a floating-point format. INT8 only stores whole numbers, so rounding it directly to 0 would lose too much information. Quantization first changes the scale of the numbers, allowing the available integers to represent much smaller steps.

For example, if one integer step represents 0.01, the stored integer 31 stands for a weight of 0.31. The integer takes one byte, and the scale tells us how to interpret it. This is how integer quantization can represent fractional weights while reducing their storage.

Choose a scale, then round

Suppose a group of weights ranges from -1.27 to 1.27. We can map that range to the integers -127 through 127, making each integer step worth 1.27 ÷ 127 = 0.01. This value is the scale, shared by all the weights in the group.

To store a weight, divide it by 0.01 and round to the nearest integer. To read it back, multiply the stored integer by 0.01. Follow a weight of -0.826 through those operations:

Original weight
-0.826
The value before quantization.
Divide by the scale
-0.826 ÷ 0.01 = -82.6
Express the weight in steps of 0.01.
Round and store
-83
Save this integer in INT8.
Multiply to read it back
-83 × 0.01 = -0.83
Recover an approximate weight.
The integer weights share a scale, so the scale adds a small amount of storage for the group.

Multiplying the integer by its scale is called dequantization. The recovered value is -0.83, which differs from -0.826 by 0.004. That difference is the quantization error introduced when we rounded -82.6 to -83.

This example uses symmetric quantization: the chosen range extends equally in both directions from zero. INT8 supports -128 too, but this symmetric mapping leaves that value unused. The scale is the largest absolute weight divided by 127.

An asymmetric mapping also uses an integer offset called a zero point, which specifies where a weight of zero lands. This can use the integer range more effectively when the weights are unevenly distributed around zero. Both approaches share the same idea: a scale and a rounding step connect the original weights to the stored integers.

What determines the rounding error?

The scale sets the distance between values that can be recovered. With a scale of 0.01, the integers 30, 31, and 32 represent 0.30, 0.31, and 0.32. Values between those points must be rounded. A smaller step preserves more detail, provided the largest and smallest weights still fit within the available integer range.

A few unusually large weights can make that difficult. If the largest absolute weight grows from 1.27 to 12.7, the scale becomes 0.1, and the spacing between recoverable values becomes ten times larger. That loses more detail among the small weights. Such unusually large values are called outliers.

One way to limit that effect is to divide the weights into smaller groups and give each group its own scale. A large value then affects fewer weights. This balances two costs: smaller groups can reduce rounding error, while more groups require more scales to be stored. These choices become especially important as we reduce the number of bits further.

3. How LoRA and Quantization Fit Together

QLoRA applies quantization directly to the pretrained base model using four bits per quantized weight. Two codes fit into one byte, reducing raw weight storage to half that of INT8 and one quarter that of FP16. This is why four bits matters for QLoRA: it lets the frozen base model occupy substantially less memory during fine-tuning.

With only sixteen possible codes, the choice of values becomes more important. QLoRA uses a format called NormalFloat 4, or NF4. Each code selects a value from a shared list of sixteen entries called a codebook. A weight matrix is divided into small groups called blocks, and a scale for each block adjusts those values to the local weight range. Section 5 explains how NF4 chooses its sixteen entries.

The base weights stay frozen in this compact form. When a layer needs them for a calculation, they are dequantized into a format such as BF16. Alongside this base calculation, LoRA trains a small adapter that learns from the fine-tuning examples. The layer adds the base result and the adapter result together.

Large part
Pretrained model

Stored as compact 4-bit codes and left unchanged during training.

Temporary step
Base calculation

Approximate base weights are reconstructed only when a calculation needs them.

Small part
LoRA adapter

Stored in a wider format and updated as the model learns from examples.

For an input, the base model produces its usual result using the reconstructed weights. The LoRA adapter produces a small learned correction, and the layer adds the two results together. Only the adapter is changed by training.

h=dequant(Wq)x+αrB(Ax)h = \operatorname{dequant}(W_q)x + \frac{\alpha}{r}B(Ax)

Here, xx is the input to the layer and WqW_q is the quantized base weight. The matrices AA and BB form the trainable LoRA adapter, while α/r\alpha / r controls the strength of its correction.

The first term is the frozen base calculation and the second term is the trainable LoRA correction. This is the meaning of Quantized Low-Rank Adaptation: quantization reduces the memory used by the base model, while low-rank adaptation keeps the trainable part small.

4. One QLoRA Training Step, End to End

A training example still passes through the entire Transformer and produces the usual task loss. Inside each adapted linear layer, the frozen base path and the trainable adapter path handle the same hidden state differently. Use the two views below to follow the forward and backward calculations.

One adapted linear layer

Stored small, computed wider

Follow the quantized base path and the trainable LoRA path through one training step.

Showing the QLoRA forward pass.

Frozen base path
stored in 4-bit
input x
4-bit weight block
codes + scale
BF16 compute tile
dequantize for matmul
Trainable LoRA path
adapter matrices keep a wider precision
input x
A
down projection
B
up projection
base result + LoRA correction = layer output

In the forward pass, a quantized base block is reconstructed only for computation. The resulting base projection is added to the LoRA correction. In the backward pass, gradients travel through the calculation so the adapter can learn, but the optimizer holds and updates state only for parameters marked as trainable.

The quantized base model is therefore part of the differentiable computation without becoming a trainable 4-bit model. QLoRA does not repeatedly round newly updated base weights because those base weights never receive optimizer updates.

5. NF4: A 4-Bit Format Designed for Model Weights

Four bits provide sixteen codes, but a quantizer still has to decide which numerical values those codes represent. Uniform 4-bit quantization spreads them evenly. This is not always the best use of a small codebook because pretrained weights within a normalized block are often concentrated near zero, with fewer values in the tails.

NormalFloat 4, usually shortened to NF4, places its representable values according to quantiles of a standard normal distribution. Under the normally distributed weight assumption in the QLoRA paper, this gives the dense part of the distribution more resolution and is information-theoretically optimal for the stated setting.

Sixteen 4-bit codes

Where should the representable values go?

Both formats have sixteen choices. Their difference is how those choices are placed across the normalized range.

-1
0
1

Uniform quantization spaces all sixteen values evenly. The layout is simple, but it gives the dense center of the distribution no extra resolution.

NF4 does not add more codes. It uses the same sixteen possibilities more deliberately. The QLoRA experiments found NF4 more accurate than FP4 and INT4 for the studied 4-bit fine-tuning configurations, which is why NF4 is the usual bitsandbytes choice for QLoRA training.

6. Double Quantization Compresses the Scales

Blockwise quantization needs a scale for every block. The weight codes are small, but millions of blocks can make their scale values noticeable. If each 64-weight block stores one 32-bit scale, those scales add half a bit per model parameter.

First quantization
weights become 4-bit codes
then
Second quantization
their scales are quantized too

QLoRA calls this double quantization. The second stage quantizes the first stage's constants and stores a smaller set of higher-level constants for reconstruction. The original paper reported an average reduction from about 0.50 to 0.127 bits per parameter for the quantization constants, saving roughly 0.37 bits per parameter.

Double quantization is sometimes called nested quantization in libraries. It does not quantize the LoRA adapter twice, and it does not change the four-bit code assigned to each base weight. Its target is the metadata needed to interpret those codes.

7. Paged Optimizers Manage Temporary Memory Spikes

A training run can fit during ordinary steps and still fail when memory briefly rises. Long sequences, saved activations, and gradient checkpointing can create peaks that exceed the GPU's remaining capacity even though the average footprint is acceptable.

Memory across several training moments
ordinary stepstemporary peakordinary steps

Paged optimizers use NVIDIA unified memory so optimizer pages can move between GPU and CPU memory when pressure rises. This gives the runtime a way to survive short peaks instead of requiring the GPU to reserve enough space for the worst moment throughout the run.

Paging is a safety mechanism rather than free memory. Moving pages has a transfer cost, and sustained over-allocation can still make training slow or fail. The practical goal is to handle brief spikes after the base model and adapter state have already been reduced.

8. Account for QLoRA Memory Correctly

The phrase “4-bit training” can suggest that the complete training process occupies one quarter of a 16-bit run. QLoRA applies 4-bit storage to the frozen base weights, while several other allocations remain. A realistic estimate keeps these categories separate.

Memory category
Typical representation
Why it remains
Frozen base weights
4-bit codes
Every adapted layer still needs its pretrained transformation.
Quantization metadata
Scales and related constants
Codes cannot be reconstructed without their block metadata.
LoRA parameters
BF16, FP16, or FP32
The adapter must preserve useful trainable updates.
Adapter gradients and optimizer state
Usually wider than 4-bit
Optimization needs gradients, moments, and master state for trainable parameters.
Activations
Compute-dependent
Backpropagation needs intermediate values or must recompute them.
Temporary buffers
Kernel-dependent
Dequantization and matrix multiplication require working memory.

For PP parameters, raw 16-bit base weights occupy roughly 2P2P bytes. Raw 4-bit codes occupy roughly 0.5P0.5P bytes before quantization metadata. This is close to a fourfold reduction for the weight values, not a promise of fourfold lower end-to-end training memory.

Batch size, sequence length, model width, activation checkpointing, optimizer choice, attention implementation, and device placement can each change the final peak. Measure the actual configuration rather than choosing a model only from its parameter count.

9. Choosing the QLoRA Configuration

A QLoRA setup combines choices from quantization, LoRA, and the training system. Each control has a different job, so tuning becomes clearer when the controls are grouped by what they affect.

Quantization format

NF4 is the standard QLoRA choice for normally distributed model weights. FP4 uses a different 4-bit codebook.

Compute data type

BF16 is commonly used on supported hardware because it has a wider exponent range than FP16. Hardware support still matters.

Double quantization

Enable it when the additional reduction in scale metadata is useful for the memory budget.

Target modules

QLoRA-style training commonly attaches LoRA to all Transformer linear layers, including attention and FFN projections.

Rank and alpha

Rank controls adapter capacity and parameter count. Alpha controls the scale of the adapter contribution.

Sequence and batch settings

These drive activation memory and can determine whether a run fits even after the base weights are compressed.

Target-module names are model-specific

Libraries can offer an all-linear shortcut, but exclusions, shared embeddings, output heads, and architecture-specific modules should still be checked for the selected model.

10. Quality, Memory, and Speed Are Separate Questions

QLoRA's direct benefit is a smaller memory footprint for the frozen base model. Quantization is lossy, so the base computation uses approximated weights. The trainable adapters can compensate for task-specific behavior, but they do not reconstruct every original weight exactly.

In the original paper's 7B-to-65B instruction-tuning comparison, NF4 with double quantization recovered the tested 16-bit LoRA performance. Separate experiments on smaller RoBERTa and T5 models found 4-bit adapter tuning matched the full 16-bit baseline. These are results for particular models, datasets, tasks, and evaluations rather than guarantees for every application.

Memory

Usually the clearest gain because the frozen base dominates many LoRA fine-tuning setups.

Quality

Depends on quantization error, adapter coverage, rank, data, optimization, and evaluation.

Speed

Depends on hardware and kernels because dequantization saves memory traffic but introduces additional work.

QLoRA also does not fix unrelated training problems. Poor examples, incorrect labels, unsuitable loss masking, unstable hyperparameters, or weak evaluation remain poor regardless of how efficiently the base model is stored.

11. Saving, Loading, and Merging QLoRA Adapters

A QLoRA training result is normally saved as a LoRA adapter rather than another full model. The checkpoint contains the learned adapter tensors and configuration, while the frozen base model remains a separate dependency.

Adapter tensors
The learned A and B matrices for every targeted linear layer.
LoRA configuration
Rank, alpha, dropout, target modules, bias policy, and task type.
Exact base identity
Model repository, revision, architecture, and any required custom code.
Quantization settings
NF4 or FP4, compute type, double-quantization choice, and compatible library behavior.
Tokenizer and prompt contract
Tokenizer revision, special tokens, chat template, and preprocessing assumptions.

Keeping the adapter separate allows several tasks to share one quantized base model. At load time, the same base and compatible quantization configuration must be reconstructed before attaching the adapter, otherwise the numerical reference that training used may differ.

Merging requires additional care. A common path dequantizes the base into a wider precision, adds the LoRA update, and optionally quantizes the merged result again. That process needs enough memory for the wider model and can introduce another round of quantization error, so merged and unmerged outputs should be compared before deployment.

12. Minimal QLoRA Setup with Hugging Face

This example shows the boundary between the four-bit base model and the trainable adapter. It uses the current Transformers quantization configuration with PEFT's k-bit preparation and all-linear target shortcut. The rank, alpha, dropout, model data type, and target coverage still need to be selected for the actual model and task.

import torch
from peft import (
    LoraConfig,
    get_peft_model,
    prepare_model_for_kbit_training,
)
from transformers import AutoModelForCausalLM, BitsAndBytesConfig

model_id = "your-base-model"

quantization_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_use_double_quant=True,
    bnb_4bit_compute_dtype=torch.bfloat16,
)

model = AutoModelForCausalLM.from_pretrained(
    model_id,
    quantization_config=quantization_config,
    dtype=torch.bfloat16,
)
model = prepare_model_for_kbit_training(model)

lora_config = LoraConfig(
    task_type="CAUSAL_LM",
    r=16,
    lora_alpha=32,
    lora_dropout=0.05,
    target_modules="all-linear",
    bias="none",
)

model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
Before training
  • Inspect which modules received adapters.
  • Confirm only intended parameters require gradients.
  • Measure the real memory footprint on the target hardware.
  • Verify BF16 support before selecting it as the compute type.
Before deployment
  • Save the adapter and its exact base-model revision.
  • Preserve tokenizer and chat-template settings.
  • Test loading in a fresh process.
  • Compare outputs if the adapter is merged or requantized.