A small adaptation job can run without errors and still train the wrong tokens, update the wrong weights, or cut off an adapter’s gradients. We will follow one example from a prompt–response pair to a loss, trace what “frozen” means in code, and build a resource budget that counts more than trainable parameters.
- Assigned reading
- This handout accompanies slides 1–17 of
16.adaptation.pdf. Slide 18 is bonus. No separate paper is required. - Time
- 50–65 minutes for §1–§5 and the practice in §6. Optional: 30–40 minutes for the CPU lab and upstream code reading in §7; 5 minutes for bonus serving in §8.
- By the end
- Construct response-only next-token targets; distinguish freezing from disabling autograd; derive LoRA counts and initialization gradients; compare memory components; design a fair adaptation experiment.
- Prerequisites
- Autograd (#6), attention/loss masks (#12), projection shapes and tying (#14), and optimizer memory and token batches (#16).
Slide-to-reading map
- Slides 2–4 → §1: turn a task into a training objective.
- Slides 5–7 → §2 and §4: freezing, backward paths, and resource costs.
- Slides 8–9 and 15 → §3: LoRA initialization, shapes, rank, and placement.
- Slides 10–12 and 16–17 → §5: interpret evidence and choose settings.
- Slides 13–14 → §4: training computation and memory accounting.
- Slide 18 (Bonus) → §8: shared-base serving.
1What exactly should the model learn?
1.1 One task, several possible objectives
Use the customer-support task from slide 3. Let $X$ contain the instruction and message, and let $Y$ be the desired response:
Instruction: Extract issue and product. Return JSON.
Message: My headphones won’t charge even after being plugged in overnight.
Response: {"issue": "charging_failure", "product": "headphones"}
Training should improve $p_\theta(Y\mid X)$. The response teaches both a semantic decision and an output format. A model can emit valid JSON with the wrong issue, or select the right issue in invalid JSON. Keep those failure modes separate when evaluating.
$$\mathcal L_{\mathrm{token}}=-\frac{1}{M}\sum_i\sum_{t=1}^{|Y_i|}\log p_\theta(y_{i,t}\mid X_i,y_{i,<t}),\qquad M=\sum_i |Y_i|.$$Here $Y_i$ includes the response’s stopping token. This token-mean objective gives longer responses more total weight. Averaging each example’s mean loss instead gives equal weight to examples; either is a choice, but they are not the same objective. With accumulation, normalize over the intended scored-token count, as in Handout #16.
1.2 Work through the boundary by hand
For a tiny illustration, pretend the response has just two tokens, y1 y2. These are symbolic tokens, not the actual tokenization of the JSON above. In a decoder-only model, the output at position $j$ predicts the token at $j+1$.
| Input position | Input token | Next target | Score it? |
|---|---|---|---|
| 0 | BOS | instruction | No |
| 1 | instruction | separator | No |
| 2 | separator | y1 | Yes: first response token |
| 3 | y1 | y2 | Yes |
| 4 | y2 | EOS | Yes: learn to stop |
| 5 | EOS | PAD | No |
Before shifting, labels match the input token IDs, except that prompt and padding positions are replaced by -100, PyTorch cross-entropy’s default ignored target. The separator’s input position produces the first scored prediction; do not accidentally mask that prediction just because it belongs to the prompt.
# From check_response_loss in the downloadable lab; logits are raw scores.
shifted_logits = logits[:, :-1, :].contiguous()
shifted_labels = labels[:, 1:].contiguous()
loss = F.cross_entropy(shifted_logits.view(-1, 8),
shifted_labels.view(-1), ignore_index=-100)
In a real implementation, apply the model’s chat template consistently during training and generation, then inspect the tokenized prompt/response boundary. Tokenizing two strings separately and concatenating them need not match tokenizing the combined string. Some causal-LM APIs shift labels internally: follow their contract and do not shift a second time. Reject or repair examples with zero scored response tokens after truncation.
An example has 100 prompt targets and 5 response targets. You train on all 105 targets. Which fraction directly trains response generation? Does a low aggregate loss necessarily mean good responses?
Predict, then reveal
Only $5/105\approx4.76\%$ of the target positions are response tokens. Easy prompt prediction can dominate the reported average. Measure response loss and task correctness separately; for the response-only objective above, mask prompt targets.
For the headphones message above, consider three generated responses:
{issue: charging_failure, product: headphones}{"issue": "pairing_failure", "product": "headphones"}- The correct JSON, followed by repeated copies until the token limit.
Which metric exposes each failure? For each, name one training-data or generation check. Can response loss alone distinguish them reliably?
Predict, then reveal
1: Format failure. JSON parsing fails. Inspect target serialization and prompt/template consistency; valid-JSON rate exposes it. 2: Semantic failure. Parsing succeeds, but issue accuracy and parsed-record exact match fail. Inspect labels, similar training examples, and performance on held-out paraphrases. 3: Stopping failure. Inspect stopping-token supervision, stopping-token IDs, and decoding settings. Report completion rate or token-limit hits, not just whether a correct JSON substring appears. These symptoms suggest checks, not unique causes. Teacher-forced response loss uses gold prefixes; generation uses the model’s own prefixes, so evaluate actual generations with fixed decoding settings too.
2Freeze parameters, not the gradient path
2.1 When a frozen backbone needs no backward pass
For head tuning, compute $H=f_{\mathrm{frozen}}(X)$ and logits $Z=HW_{\mathrm{head}}^\top$. If neither the backbone parameters nor its inputs require gradients, $H$ has no autograd history. Training the head needs $H$ but not the backbone’s internal activations.
requires_grad_(False) controls parameter gradients. eval() controls behavior such as dropout; it does not freeze weights. Choose dropout behavior deliberately and configure the optimizer after selecting trainable parameters. See the PyTorch autograd notes.
# Sketch: backbone and head are separate modules, with no shared parameters.
backbone.requires_grad_(False)
head.requires_grad_(True)
optimizer = torch.optim.AdamW(head.parameters(), lr=1e-4)
with torch.no_grad():
h = backbone(input_ids)
logits = head(h) # Keep this OUTSIDE no_grad().
If an input embedding and the vocabulary head share the same Parameter, changing its trainability affects both uses. Check parameter identity before claiming that only the output mapping changes. Here we assume separate parameters; untying them can itself require an additional copy of the weights.
2.2 A frozen layer can transmit gradients
Now put a trainable adapter before a frozen linear layer: $h=Wx$, where $W$ is frozen but $x$ depends on the adapter. Given upstream gradient $g=\partial\mathcal L/\partial h$, we still need
$$\frac{\partial\mathcal L}{\partial x}=W^\top g.$$We can skip storing/calculating the parameter gradient $\partial\mathcal L/\partial W$, but cannot skip the derivative with respect to $x$. A no_grad() block around this layer cuts that connection. The same problem occurs if you detach the activations between adapters.
Frozen backbone → trainable head → loss
Backward stops after the head. Retain its input features.
Early adapter → frozen layer → late adapter → loss
Backward crosses the frozen layer to reach the early adapter.
In a network with early and late adapters, someone wraps the frozen layer between them in no_grad(). Loss still decreases. Does that prove both adapters are learning? What would you inspect?
Predict, then reveal
No. The late adapter can learn while the early adapter is disconnected. After backward, inspect every intended trainable parameter’s .grad, then compare weights before and after an optimizer step. A missing gradient differs from a zero gradient: the early factor A can legitimately have a zero first-step gradient under standard LoRA initialization (§3). The lab reproduces this exact bug.
3A small update with a precise budget
3.1 Count the update, not the base matrix
For $W_0\in\mathbb R^{d_{\mathrm{out}}\times d_{\mathrm{in}}}$, LoRA uses
$$h=W_0x+sB(Ax),\quad A\in\mathbb R^{r\times d_{\mathrm{in}}},\quad B\in\mathbb R^{d_{\mathrm{out}}\times r},\quad s=\alpha/r.$$The trainable update has $r(d_{\mathrm{in}}+d_{\mathrm{out}})$ parameters and rank at most $r$. The base matrix remains stored and frozen. It need not be low rank: the constraint is on the update. During training, compute $A x$ then $B(Ax)$ without materializing the dense product $BA$. This is the formulation in Hu et al., LoRA.
For $d=4096$ and $r=8$, count the adapter parameters on one square projection. Extend this to Q/K/V/O in all 32 layers. What fraction of the corresponding dense projection parameters does this train?
Predict, then reveal
One adapter: $2dr=65{,}536$ versus $d^2=16{,}777{,}216$ dense parameters, a ratio $2r/d=1/256\approx0.391\%$. Across all $32\times4$ projections: $8{,}388{,}608$ adapter parameters. This percentage is relative to the targeted projections, not to all model parameters.
For the slide’s Llama-2-style example, $L=32$, $d=4096$, and $V=32{,}000$, a bias-free generation head has $dV=131{,}072{,}000$ trainable parameters. Q/K/V/O LoRA has $8Ldr=1{,}048{,}576r$.
| Configuration | Trainable parameters |
|---|---|
| Head only | 131,072,000 |
| Q/K/V/O LoRA, rank 1 | 1,048,576 |
| Q/K/V/O LoRA, rank 4 | 4,194,304 |
| Q/K/V/O LoRA, rank 16 | 16,777,216 |
At rank 125 these parameter counts are equal; this is a parameter-count crossover, not a total-memory crossover. These counts assume four square attention projections. Grouped-query attention can make K/V rectangular; use their actual shapes. For each of three gated-FFN projections with intermediate width $m$, the adapter count is $r(d+m)$.
Ignore biases. Compare a binary classification head with a generation head when $d=4096$ and $V=32{,}000$. Is low-rank LoRA necessarily smaller than either one?
Predict, then reveal
The classifier has $2d=8192$ parameters; the generation head has $dV=131{,}072{,}000$. Rank-16 Q/K/V/O LoRA has 16,777,216: much larger than this classifier and smaller than this generation head. Specify the task and head before making an efficiency claim.
3.2 Why initialize only one factor to zero?
Standard LoRA initializes $A$ randomly and $B=0$, so the adapter initially contributes zero. For one input and upstream gradient $g$,
$$\nabla_B\mathcal L=s\,g(Ax)^\top,\qquad \nabla_A\mathcal L=s\,B^\top g x^\top.$$Initially, the task gradient for $A$ is zero, but the gradient for $B$ can be nonzero. After $B$ changes, $A$ can receive a task gradient. If both factors start at zero, both task gradients are zero. This is a bilinear dead start, not a reason to initialize the entire adapter randomly and lose the base-model starting behavior.
Use scalars $x=1$, $W_0=1$, $A=2$, $B=0$, $s=1$, and $\mathcal L=\tfrac{1}{2}(h-3)^2$, where $h=W_0x+BAx$. With SGD learning rate 0.1 and no decay, find both factor gradients and the new output.
Predict, then reveal
Initially $h=1$, so $g=-2$. Then $\nabla_B=-4$ and $\nabla_A=0$. The simultaneous update gives $B=0.4$, $A=2$, and new $h=1.8$. Starting with both factors zero would leave both unchanged. Weight decay is excluded here; it can change a nonzero factor even when its task gradient is zero.
3.3 Rank and placement buy different things
A higher rank expands the possible update at each targeted projection. More target projections let updates enter at more points in the computation. At fixed adapter count, these choices compete: Q/V at rank 16 and Q/K/V/O at rank 8 cost the same under square-projection assumptions. Equal parameter budgets do not imply equal learned functions or runtime. Compare them on validation data.
Scaling $s$ matters too. Changing rank with fixed $\alpha$ also changes $\alpha/r$. Record rank, target modules, scaling convention, dropout, and learning rate together. Prefer the smallest configuration meeting the task requirement rather than assuming the largest fitting rank must generalize best.
4Budget training memory and computation separately
4.1 Fewer trainable parameters is only part of the story
Use four accounts: model weights, gradients, optimizer state, and activations retained for backward. Add temporary buffers and implementation overhead when measuring a real run. Low-rank adapters reduce parameter-related memory compared with full tuning; they do not remove the dense base weights or all activation storage.
| Method | Parameter gradients / Adam state | Backward activations |
|---|---|---|
| Full tuning | All trainable model parameters | Needed through the model |
| Head tuning | Head only | Retain head inputs; no internal frozen-backbone history under §2 assumptions |
| LoRA | Adapter parameters only, unless other parameters are explicitly trained | Backward traverses layers between adapters and the loss |
Increasing micro-batch size or sequence length can make activation memory dominate. Checkpointing trades recomputation for less activation storage. Gradient accumulation changes the effective batch without requiring all micro-batch graphs to remain live. Therefore a smaller adapter checkpoint does not establish a smaller peak training allocation.
4.2 A reversal you can calculate without the widget
At rank 16 with Q/K/V/O targets, the head needs 1.465 GiB for gradients and Adam moments. LoRA needs 0.219 GiB for adapter weights, gradients, and moments. Both share about 13.039 GiB of base weights under the widget’s assumptions.
Keep head activations at 0.25 GiB. First give LoRA 0.5 GiB of activations; then give it 4 GiB. Calculate both accounted totals and identify the winner. Does the adapter count change?
Predict, then reveal
Using unrounded component values, head: $13.039+1.465+0.25\approx14.753$ GiB. LoRA: $13.039+0.219+0.5\approx13.757$ GiB in the first case, and $17.257$ GiB in the second. The ranking reverses with no parameter-count change. Equality occurs at about 1.496 GiB of LoRA activations with the head fixed at 0.25 GiB. These sums explain a mechanism; they are not predictions of a particular GPU’s measured peak.
4.3 What a frozen matrix still computes
For $n$ token rows and a dense $d\times d$ projection, using two FLOPs per multiply–add, the matrix products cost approximately:
| Product | Full tuning | Frozen base inside LoRA |
|---|---|---|
| Forward: $XW^\top$ | $2nd^2$ | $2nd^2$ |
| Input gradient: $GW$ | $2nd^2$ when needed | $2nd^2$ when needed to reach earlier adapters |
| Weight gradient: $G^\top X$ | $2nd^2$ | Skipped for frozen $W$ |
The LoRA branch adds approximately $4ndr$ forward FLOPs for a square matrix, plus its backward work. Thus freezing the dense base can save weight-gradient computation, while retaining its forward and input-gradient work. This isolated projection is not an end-to-end Transformer speed model: attention, activations, optimizer work, checkpointing, and kernels also matter. Measure tokens/second and validation quality at a stated budget.
5Run an experiment that answers your question
5.1 Define success before selecting a method
For support-ticket extraction, report valid-JSON rate, correct field values, and exact match of parsed records. Compare against the same base model with a fixed prompt. Hold out paraphrases and split related messages by customer/thread or source so near-duplicates do not leak across splits. Use validation for rank, learning rate, and checkpoint selection; reserve the test set for the final comparison.
Also check a small set of general capabilities you want to preserve. Better target-task learning can accompany forgetting elsewhere. Biderman et al., LoRA Learns Less and Forgets Less studies this tradeoff on code and math; its results motivate measuring both, rather than assuming LoRA always matches full tuning or always preserves every capability.
5.2 Equal steps does not mean equal resources
Choose a comparison that matches your constraint: equal processed tokens for sample efficiency, equal training time for a time budget, or the best validation quality that fits one GPU. State which you use. Tune learning rates separately for full tuning and LoRA; a copied full-tuning rate can make a good adapter configuration look weak. “LoRA needs exactly 10× the learning rate” and “higher rank is always better” are not universal rules.
These are synthetic validation results after comparable training budgets. Which run would you deploy if parsed-record accuracy must be at least 89%, retained general-task accuracy at least 70%, and peak GPU allocation at most 20 GiB?
| Run | Record accuracy | General accuracy | Peak GiB |
|---|---|---|---|
| Full tuning | 92% | 66% | 28 |
| Head tuning | 85% | 73% | 16 |
| LoRA r=8 | 89% | 72% | 18 |
| LoRA r=64 | 90% | 69% | 21 |
Predict, then reveal
Rank-8 LoRA is the only listed run satisfying all three requirements. The example does not establish that rank 8 is generally optimal. Freeze the selected configuration, then evaluate it once on the held-out test set; validation percentages are not a final unbiased performance estimate.
6Core practice: diagnose before scaling up
Two toy input vectors are $x_1=(1,1)^\top$ and $x_2=(1,-1)^\top$, but they need different labels. The frozen backbone projects them with $W_0=[1\;0]$. Calculate both features. Can any head that sees only those features distinguish the inputs? Now apply the rank-one update $\Delta W=[0\;1]$ to that projection. Calculate the new features and give a separating threshold.
Predict, then reveal
Both original features equal 1, so any head depending only on that scalar produces identical outputs. The adapted projection is $[1\;1]$, giving features 2 and 0. A threshold at 1 separates them. This update changes the representation where the distinguishing second coordinate is still available. Putting an adapter after the original scalar projection cannot recover that discarded coordinate. A rank-one update can therefore make a distinction that head tuning cannot, even with a much larger head.
You merge a rank-8 adapter into $W_0$ for deployment. Must the dense projection now use fewer inference FLOPs? What changes if adapters remain separate?
Predict, then reveal
No. The merged matrix has the same dimensions as $W_0$, so its dense projection has the same arithmetic shape. Separate adapters add the small low-rank branch but permit sharing the unchanged base among tasks. Merging saves branch overhead, not the base projection.
model.eval() # “freeze the base”
optimizer = AdamW(model.parameters())
# Insert trainable adapters here, AFTER creating the optimizer.
with torch.no_grad():
logits = model(input_ids)
loss = cross_entropy(logits, targets)
loss.backward()Identify three independent reasons this is not a valid adapter-training setup.
Predict, then reveal
eval() does not freeze base weights. Adapters inserted after optimizer construction are absent from its parameter groups. Wrapping the whole forward in no_grad() removes the loss’s gradient path, so backward cannot train those adapters. Insert adapters, set and audit trainability, then build the optimizer over intended trainable parameters; keep the required graph enabled. Separately follow the model’s target-shifting and loss-mask contract.
An adapter checkpoint loads into a model with compatible tensor shapes, but outputs are wrong. Name three pieces of metadata you would check beyond the rank.
Predict, then reveal
Check the exact base checkpoint revision, tokenizer/chat template and vocabulary, targeted module names, adapter scaling convention, and any separately trained modules such as a head. Shape compatibility alone does not imply functional compatibility. Save this metadata alongside the adapter and verify a fresh-load output against a fixed input.
7Optional implementation lab and real-code reading
7.1 Run six small checks on CPU
Download 17_adaptation_checks.py and run python3 17_adaptation_checks.py in an environment with PyTorch. No model, dataset, or GPU download is needed. The checks were run with PyTorch 2.12.0 on CPU, using float64 for numerical comparisons. Random tiny networks test implementation mechanics; they are not pretrained LMs and do not compare adaptation quality.
check_response_loss: exactly three response targets, including EOS, receive loss; uniform logits give $\log 8\approx2.079442$.check_initialization_and_merge: the adapter starts as a no-op, only B has a nonzero first task gradient, the frozen base stays unchanged, and merged outputs match.check_frozen_bridge: both adapters receive gradients through a frozen intermediate layer; a deliberateno_grad()bug blocks only the early adapter.check_head_only: frozen-backbone features have no gradient history while the separate head receives gradients.check_counts: verify the model-wide parameter counts in §3.check_existing_layer: wrap an existing biased linear layer, preserve its initial output, update only the adapter, and verify that the base weight and bias stay unchanged.
The lab’s original teaching module is short enough to inspect completely:
class LoRALinear(nn.Module):
"""Teaching implementation; optional existing base, no adapter dropout/auto-merge."""
def __init__(self, din, dout, r=2, alpha=8, *, base=None):
super().__init__()
if r <= 0:
raise ValueError("rank must be positive")
self.base = nn.Linear(din, dout, bias=False) if base is None else base
self.base.requires_grad_(False)
weight = self.base.weight
self.A = nn.Parameter(torch.randn(r, din, device=weight.device,
dtype=weight.dtype) * 0.1)
self.B = nn.Parameter(weight.new_zeros(dout, r))
self.scale = alpha / r
@classmethod
def from_linear(cls, base, r=2, alpha=8):
# Reuse the exact layer, including its bias, dtype, device, and weights.
return cls(base.in_features, base.out_features, r, alpha, base=base)
def forward(self, x):
return self.base(x) + self.scale * F.linear(F.linear(x, self.A), self.B)
To adapt an existing model, reuse its actual layer instead of constructing a fresh random base. For example, replacing a model’s projection can look like this:
model.requires_grad_(False) # Freeze before injecting adapters.
original = model.proj # An existing nn.Linear from the loaded model.
x = example_features # Inputs to this projection, not raw token IDs.
with torch.no_grad():
expected = original(x).clone()
model.proj = LoRALinear.from_linear(original, r=2, alpha=8)
torch.testing.assert_close(model.proj(x), expected, rtol=0, atol=0)
optimizer = torch.optim.AdamW(
[p for p in model.parameters() if p.requires_grad], lr=1e-4)
This replacement retains the exact base weight, bias, dtype, and device, and freezes that layer’s base parameters. It does not freeze the rest of the model. For adapter-only training, freeze the full model before replacing the desired projections; then audit all trainable names before creating the optimizer. The lab’s check_existing_layer checks exact initial output parity, a nonzero adapter update, and unchanged base weights and bias.
The nested F.linear calls apply A and then B. PyTorch stores linear weights as [out_features, in_features]; the math uses column vectors while the implementation accepts batches of row vectors. The frozen base call remains outside no_grad() so the module works correctly when its input depends on an earlier adapter.
- In the merge check, replace
+ layer.scale * (layer.B @ layer.A)with- layer.scale * (layer.B @ layer.A). Predict which assertion fails, run it, then repair it. - In
TinyAdapterChain.forward, wrap the bridge inno_grad()for both branches. Predict which adapter loses its gradient and rerun. - Mask the EOS target in
check_response_loss. Change the expected target count to two and revise the EOS gradient assertion. Does the initial loss stay at $\log8$?
Predict, then reveal
The merge output-parity assertion fails after the nonzero B update; the correct run’s max absolute error is below $10^{-12}$. The bridge mutation makes good.early.B.grad missing, while the late B still has a gradient. With EOS masked there are two targets; its prediction-position gradient is zero, but the uniform-logit mean remains $\log8$. An unchanged scalar loss does not prove the same supervision.
7.2 Read the original LoRA linear layer
c4593f060e6a368d7bb5af5273b8e42810cdef90. Focus on Linear, lines 90–152, rather than the whole file. This is the original library, not a claim about every PEFT implementation. The excerpts below are verbatim and retain the MIT license.7.3 Find registration, freezing, and scaling
loralib/layers.py · Linear.__init__ · lines 109–114 (verbatim)
if r > 0:
self.lora_A = nn.Parameter(self.weight.new_zeros((r, in_features)))
self.lora_B = nn.Parameter(self.weight.new_zeros((out_features, r)))
self.scaling = self.lora_alpha / self.r
# Freezing the pre-trained weight matrix
self.weight.requires_grad = False
nn.Parameter registers A and B so the module exposes them to optimizers (§2). The shape calculation is exactly §3.1. Check that the optimizer is constructed after injection. Inspect the surrounding constructor: freezing weight here does not by itself freeze a possible bias. A whole-model training policy must decide which biases and other modules to train.
7.4 Locate the two forward branches
loralib/layers.py · Linear.forward · lines 147–152 (verbatim)
if self.r > 0 and not self.merged:
result = F.linear(x, T(self.weight), bias=self.bias)
result += (self.lora_dropout(x) @ self.lora_A.transpose(0, 1) @ self.lora_B.transpose(0, 1)) * self.scaling
return result
else:
return F.linear(x, T(self.weight), bias=self.bias)
When unmerged, the first call computes the base projection and the next line adds the low-rank path; this is §4.3 in code. Inspect lines 119–125: the executable code initializes A randomly and B to zero, despite a nearby comment describing them in reverse. Read what the operations do.
Read Linear.train, lines 127–142. In this particular library, switching to evaluation can merge the adapter into the base weights, and switching back subtracts it again. This is additional library-specific behavior, beyond ordinary eval() semantics. Do not add the low-rank branch a second time after merging.
In your own copy of pinned layers.py, change nn.init.zeros_(self.lora_B) in Linear.reset_parameters to nn.init.ones_(self.lora_B). Instantiate Linear(4, 3, r=2, lora_alpha=8, bias=False, merge_weights=False) after torch.set_default_dtype(torch.float64) and torch.manual_seed(17), with input torch.tensor([[1., 2., -1., 0.5]]). Compare its output with F.linear(x, layer.weight). Does initialization still preserve the base output? Restore the line, then repeat after calling eval() with merging enabled.
Predict, then reveal
The ones mutation loses no-op initialization. In the checked float64 run, the maximum output difference changes from 0 to approximately 4.386213. Repairing B to zero restores equality. For a meaningful merge check, then set a nonzero B, compare unmerged and merged outputs with dropout disabled, and verify that repeated eval() calls do not merge twice. Before changing initialization, predict A and B task-gradient behavior from §3.2.
8Bonus: one shared base, many tasks
Slide 18’s serving benefit follows from the same decomposition: requests share the base projection but can select different adapter branches. Keeping adapters separate supports switching and batching tasks; merging one adapter removes its branch overhead for a dedicated model. Those are different deployment choices.
Use the approximate 7B base, rank-16 Q/K/V/O adapters from §3, and two bytes per stored weight. Compare 20 full checkpoints with one base plus 20 adapter checkpoints. Ignore metadata and all training state.
Predict, then reveal
Full checkpoints: $20\times7\times10^9\times2=280$ GB. Shared base plus adapters: $14+20\times16{,}777{,}216\times2/10^9\approx14.671$ GB. These are decimal GB of checkpoint weights, not GPU-memory requirements or a claim that every server can batch all configurations efficiently.
Optional sources: LoRA for the factorization and merge; LoRA Learns Less and Forgets Less for controlled adaptation/retention experiments; Punica for multi-tenant serving; PEFT LoRA documentation for a broader implementation API. These extend the reading; model-specific recipes are not required memorization.