linearly

Lecture 30

The Adapter Field Guide

Every adapter in the PEFT library, drawn: forty-six methods, one per figure, each verified against the source and sorted by the shape of its change.

The idea in one sentence

Lecture 29 asked one question, what shape is ΔW\Delta W, and this guide answers it forty-six times: every method in the PEFT library’s tuner folder, one drawing each, sorted into families by shape.

How to read this guide

Every drawing uses the same grammar, so the figures teach by contrast. Ink outlines are frozen structure. Green is what trains. Cyan is frozen random material. Yellow marks chosen cells, purple marks prompt vectors, and blue marks a rotation or an action. When a figure shows a number, it was measured in Lecture 29’s run on this machine; when it quotes a claim, the source is named. Every mechanism below was checked against the library’s source code (peft 0.20.0) before its figure was drawn; the drawing shows what the code does, and where a method’s finer points go beyond what could be verified, the entry says only what could.

Each entry closes with a few lines of pseudo-code: the forward pass, with what trains named in the comments. They are shapes, not runnable programs; Lecture 29 has the runnable one, and any method here drops into its sixty-step run by changing one config line.

This is a reference chapter, built to be skimmed, stepped through figure by figure, and come back to. Read one family when you need it. The families:

  1. Add a thin product, LoRA’s line.
  2. A structured product.
  3. A few coefficients in a fixed basis.
  4. Rotations.
  5. Surgery on the weight’s own subspaces.
  6. Cheap vectors.
  7. Leave the weights alone, work on the input.
  8. Routers and mixtures.

Add a thin product

The founding family. Everything here writes ΔW=CR\Delta W = CR or a close relative, and everything inherits the two facts Lecture 29 measured: born at zero, merges away.

LoRA is the reference specimen, the method the whole of Lecture 29 walks through: a thin CC times a wide RR, the waist first drawn in Lecture 6, 0.22 percent of the model in training on this machine’s run.

201102311001C: 6×2×120130012012R: 2×6=10ΔW = CR, computedone entry, checked by hand:row 4 of C is (3, 1)column 5 of R is (3, 1)3×3 + 1×1 = 10every column of ΔW mixes C’s two columns: rank 2 here, 16 in the runW+C = 0×R randomday one: ΔW = 0, output = base modeltrain,mergeW + CR: one matrixno extra latencythe bill, per 896×896 matrixstore ΔW dense:802,816store C and R instead:28,672: twenty-eight times smallermeasured on the run: 1,081,344 of 495,114,112 numbers train, 0.22%
Fig. 1 

LoRA, worked end to end: the product computed cell by cell, one entry checked by hand, born at zero, merged away, and the bill drawn to scale. The rest of the guide is variations on this stage.

python
h = W @ x                        # frozen
h += (alpha / r) * C @ (R @ x)   # C starts at 0: day one is the base

AdaLoRA notices that a fixed rr everywhere is a guess. It scores each direction’s importance while training and moves rank away from layers that waste it, so the same budget ends up wide where the task needs width and starved where it does not.

one budget, three layers: who deserves the rank?layer 1+starved: r = 4importance, same scalelayer 2+fed: r = 22layer 3+enough: r = 1042210the budget:36 ranks, split by needsame total bill as LoRA; the split is learned, layer by layer
Fig. 2 

AdaLoRA. The budget is the same; its allocation is learned.

python
h = W @ x + C @ (R @ x)          # the stage, unchanged
rank[layer] = budget(importance) # the split is learned while training

DoRA splits each output channel of the adapted weight into a direction and a length, and trains the lengths as their own small dial. The decomposition costs one number per channel and lets training adjust how strongly a channel speaks separately from where it points.

one column of the weight, split: (3, 4) = 5 × (0.6, 0.8)(3, 4)length 5, direction, tangledsplit(0.6, 0.8)direction: length exactly 1length: its own dial5one number per channelthe split costs one number per channel: 896 dials for q here
Fig. 3 

DoRA. Length and direction, decoupled, the way Lecture 13 separated size from angle.

python
V = W + C @ R                    # the adapted weight
h = m * unit_columns(V) @ x      # direction pinned; the length m trains

HiRA wants a high-rank change at low-rank cost, so it multiplies where LoRA adds: the update is W(CR)W \circ (CR), the frozen weight rescaled entry by entry through a low-rank pattern. Because WW itself has full rank to lend, the product can move far more directions than rr.

W, frozen: rank 4entrywise14233241P = c times rrank one=ΔW, entry by entrythe same ΔW, rearranged:1234××4321rescaled rows and columns: rank stays 4LoRA’s bill, W’s rank: the frozen matrix does the spreading
Fig. 4 

HiRA. Multiply the frozen map instead of adding to it, and rank stops being the ceiling.

python
h = (W + W * (C @ R)) @ x        # entrywise: W is rescaled, not added to

DeLoRA puts the update on a leash: the change is kept at a controlled size, with a learned bound λ\lambda capping how far the adapted weights can drift from where they started. The circle from Lecture 29’s rotation figure returns, now as a budget.

W+×CRthe leash: radius λpassespulled backλ trains toothe model cannot drift further than λ, and λ itself is learned
Fig. 5 

DeLoRA. However training pulls, the update ends on or inside the circle.

python
delta = C @ R                    # a thin update
h = (W + cap(delta, lam)) @ x    # drift capped at lam, and lam trains

GLoRA refuses to pick one place to adapt. It gives the layer five slots, the weight becoming W+WA+BW + W \circ A + B with three more slots for the bias, and each slot can be a low-rank pair, a single vector, a scalar, or off, so one method covers scaling, shifting, and bias editing at whatever budget each slot deserves.

xW, frozen+yA: a vector, rescales W row by row×CRB: a pair, its own thin paththe bias b:×+D: one numberE: a vectorC: W feeds the biasfive sockets; each is a pair, a vector, one number, or off
Fig. 6 

GLoRA. One adapter, five slots, each as cheap or as rich as you configure it.

python
h = (W + W * A + B) @ x          # A rescales W, B adds a path
b2 = b * D + E                   # and the bias gets its own sockets

GraLoRA cuts the matrix into a k×kk \times k grid of blocks first and gives every block its own small waist. The parameter count matches plain LoRA at the same rr, but the update is assembled from local pieces, so its total rank can reach kk times further.

W, cut 3×3nine local waistsΔW, stitched: see the seamsreach, same bill:LoRA: rank 16up to 48LoRA’s exact bill, and the block ranks add: k times the reach
Fig. 7 

GraLoRA. Local waists; the blocks add their ranks.

python
for i, j in rooms(W, k):         # k by k rooms
    dW[i, j] = C[i, j] @ R[i, j] # each room, its own waist

Lily attacks depth. Neighboring layers share one thin factor, and a small global pool of wide factors serves the whole model through a learned router, so the bill stops growing linearly with the number of layers.

layer 1layer 2layer 3layer 4C, shared by bothC, shared by bothR1R2R3the poolR1R2R3R1R2R3R1R2R3R1R2R3four layers: two C stamps, one pool of R, four blends
Fig. 8 

Lily. Depth stops multiplying the cost: factors are shared across layers and routed.

python
z = C_shared @ x                 # neighbors share the thin factor
h += blend(router, pool_R) @ z   # a learned mix over few wide factors

PEANuT gives the update a hidden layer. Instead of the linear CRCR, the adapter runs its input through a thin projection, a small nonlinear stack, and a wide projection out, so the weight update it expresses can bend in ways no single low-rank product can.

xAthe bendBthe updatelinear: −x mirrors xrelu: −1 goes silentevery other update in this guide is linear; this one can bend
Fig. 9 

PEANuT. A nonlinearity between the thin factors; the update becomes a small network.

python
h = W @ x + B @ phi(A @ x)       # phi bends; a linear update cannot

TinyLoRA is the family’s extremist. It keeps a frozen SVD of the weight, routes a handful of trainable numbers through fixed random projections, and steers the model with a parameter count you can say out loud: its paper is titled “Learning to Reason in 13 Parameters.”

12345678910111213v: the trainable part, 13 numbersrandom, frozen,from a seedW’s own singular ladder, frozen(the SVD, Lecture 9’s spirit)the title is the receipt: Learning to Reason in 13 Parameters
Fig. 10 

TinyLoRA. The frozen structure does almost everything; the trainable part fits in a sentence.

python
v = params(13)                   # the whole trainable state
dW = frozen_svd @ (frozen_rand @ v)   # structure does the spreading

UniLoRA makes the budget literal: one shared pool of numbers θ\theta for the whole model, and every entry of every adapter factor is a lookup into it through frozen random wiring. Training moves the pool; the wiring never changes.

472951836θ: the whole trainable budgetCR7537move the 7 in θ,both copies moveevery factor entry is a lookup into θ; θ is the checkpoint
Fig. 11 

UniLoRA. One pool, frozen wiring; the model’s entire adaptation lives in a single short vector.

python
C[i, j] = theta[wire_C[i, j]]    # every entry: a lookup into theta
R[i, j] = theta[wire_R[i, j]]    # the wiring is random and frozen

VB-LoRA does the same with structure: a bank of small vectors, shared model-wide, and every row of every factor is a mixture of the bank’s top few entries, chosen and weighted by training. The factors never own their numbers; they borrow them.

v1v2v3v4v5the bank: five vectors, shared model-widethe row’s scores; yellow = the two winnersone row of a factor =0.62 × v2 + 0.38 × v4computedthe factors never own their numbers; they cite the bank
Fig. 12 

VB-LoRA. Rows are mixtures over a shared bank; the checkpoint is the bank plus the recipes.

python
winners, w = top2(logits[row])   # scores choose two bank vectors
row = w[0] * bank[winners[0]] + w[1] * bank[winners[1]]

The family also carries dials that change LoRA’s birth rather than its shape. rsLoRA scales the update by α/r\alpha/\sqrt{r} so large ranks stay stable. PiSSA initializes the factors from WW‘s own principal directions; OLoRA starts them orthonormal via QR, the factorization from Lecture 13; EVA starts from the directions the task’s data actually uses; LoftQ starts where quantization error is smallest, for QLoRA-style training; CorDA builds its start context-first. Same shape, different first step.

the dials on LoRA itselfnone change the shape; all change where training beginsrsLoRAscale by α/√r: big ranks stay stablePiSSAstart from W’s principal directionsOLoRAstart orthonormal, via QR (Lecture 13)EVAstart from the data’s own directionsLoftQstart where 4-bit rounding hurts leastCorDAstart context-aware, task firstand none of it shows in the first step: Lecture 29 proved it matches at every rank
Fig. 13 

The dials on LoRA itself. None of these change what trains; they change where training begins.

python
C, R = init(method)              # PiSSA, OLoRA, EVA, LoftQ, CorDA...
h = W @ x + scale * C @ (R @ x)  # the forward pass never changes

Three field notes for this family, all measured by Schulman and colleagues in LoRA Without Regret and unpacked in Lecture 29. Attach adapters to every matrix, with the MLPs first, because attention-only adapters learn slower even at matched parameter counts. Port hyperparameters by the ten-times rule: LoRA’s best learning rate sits near ten times full fine-tuning’s. And size the rank by the bit budget: when the adapter’s capacity exceeds what the dataset can teach, these methods match full fine-tuning step for step, which for reinforcement learning happens already at rank one.

A structured product

The second family builds ΔW\Delta W from small pieces with algebraic structure, so a few numbers reach a large matrix.

LoKr uses the Kronecker product. Two small trained factors stamp a full-size update, every entry of the first scaling a whole tile shaped like the second; Lecture 29 measured a 28×2828 \times 28 and a 32×3232 \times 32 standing in for the full 896×896896 \times 896, 1,808 numbers for 802,816 slots.

312131213the factorkronitself=the shading is the real Kronecker productthe outlined tile:3 × the whole factor,corner entry 3×3 = 9802,816slots1,808 storedmeasured: 1,808 numbers stood in for 802,816 slots, to scale above
Fig. 14 

LoKr. Each small entry stamps a tile; structure does the spreading.

python
dW = kron(F, G)                  # each entry of F stamps a tile of G

LoHa multiplies two thin products entry by entry, (C1R1)(C2R2)(C_1 R_1) \circ (C_2 R_2). The bill is exactly twice LoRA’s, 2,162,688 against 1,081,344 in the measured table, and the reward is rank: an entrywise product of two rank-rr matrices can reach rank r2r^2.

×rank 2×rank 2=rank 4pair one2pair two2product4measured: 2,162,688 numbers, exactly twice LoRA’s bill
Fig. 15 

LoHa. Two waists, multiplied; the ranks multiply too.

python
dW = (C1 @ R1) * (C2 @ R2)       # entrywise: the ranks multiply

MiSS reshapes the weight into equal tiles and trains one small block that updates every tile at once, part multiplied in, part added, so a single r×rr \times r block moves the whole matrix.

2103the blockW, cut into eight tilesthe same block,every tilean r by r block, broadcast: the tiling is the compression
Fig. 16 

MiSS. One block, tiled across the matrix by construction.

python
for tile in tiles(W):            # equal tiles
    dW[tile] = block             # one r by r block, broadcast

C3A trains one row per block and lets rotation write the rest: each block of ΔW\Delta W is circulant, every row a shifted copy of the first. A circulant matrix is a convolution in disguise, which is why the method can apply it fast with Fourier transforms, a trick Part VI will build properly when it reaches convolution.

3102the first row, trained231002311023the block: circulant, computeda convolution in disguise:apply it by FFTone row per block; the shift structure writes the rest
Fig. 17 

C3A. One row learns; the shift structure fills the block.

python
row = params(n)                  # one row per block trains
dW_block = circulant(row)        # shifts write the rest; apply by FFT

A few coefficients in a fixed basis

The third family stores no matrix shape at all. Pick a basis once, train a few coordinates in it, and let the transform build the dense update.

FourierFT trains chosen positions in the frequency plane, 1,000 per matrix in the measured run, 0.0097 percent of the model, and an inverse Fourier transform turns them into a full-size, generally full-rank ΔW\Delta W.

the spectrum: what trainsinverseFourierΔW: this shading is the real inverse FFTa full-size, full-rankchange, bought withfour numbersmeasured: 1,000 coefficients per matrix, 48,000 in all
Fig. 18 

FourierFT. The JPEG bet, made about weight updates: sparse somewhere, if you pick the right somewhere.

python
spectrum[chosen] = coeffs        # 1,000 trained positions
dW = ifft2(spectrum)             # the fixed basis builds the update

WaveFT makes the same bet in a wavelet basis, whose coefficients carry scale and position at once, so a coefficient can say “here, and this fine.”

coefficients at two scalesinversewaveletΔW: place and scale, both visiblethe coarse square paintsthe wash; the fine squareplaces the spotlike Fourier, plus an address: wavelets know where
Fig. 19 

WaveFT. Wavelets know where as well as how fine.

python
dW = inverse_wavelet(coeffs)     # each coefficient: a scale and a place

SHiRA picks the simplest basis of all, the entries themselves: a fixed sparse mask of positions, r(m+n)r(m+n) of them per matrix, each trained directly. No factors, no transform, and because the scatter is spread across the whole matrix, the update is high-rank from the start.

no factors, no transform: the entries themselvesthe count, same scale:548 trainr(m+n) entries, placed once by a seed, high rank from birth
Fig. 20 

SHiRA. FourierFT in the identity basis: choose entries, train them, done.

python
dW[mask] = values                # the entries themselves train
                                 # the mask is placed once, by a seed

Rotations

The orthogonal family from Lecture 29, expanded. Everything here multiplies WW by an orthogonal RR instead of adding to it, and inherits the guarantee Lecture 13 proved: lengths cannot change.

OFT keeps RR block-diagonal and stores only skew-symmetric entries, which the Cayley transform turns into an orthogonal block. The rotation is an algebraic certainty; training cannot break it even by accident.

R: block-diagonal0a−a0skew: a trainsCayleybeforeaftersame length, by construction×W, turned, never stretchedmeasured: 14 blocks of 64, 2,016 skew numbers each
Fig. 21 

OFT. Rotation by construction; Lecture 29 measured its blocks at 14 by 64 per matrix.

python
R = cayley(skew)                 # skew in, orthogonal out, always
h = (R @ W) @ x                  # turned, never stretched

BOFT builds the same promise the way an FFT builds a dense transform: from stages of sparse butterfly rotations that compose into a dense one, reaching every coordinate in a few crossing layers.

a rotation built like an FFTstage 1: neighbor turnsstage 2: across-pair turnsevery crossing:one small turnlog-many sparse stages let every pair of coordinates meet
Fig. 22 

BOFT. The FFT’s wiring diagram, reused for rotations.

python
R = stage2 @ stage1              # sparse butterfly turns compose
h = (R @ W) @ x

HRA composes mirrors. Each factor is a Householder reflection, the I2uuTI - 2uu^{\T} from QR’s engine room, and only the mirror normals train; a chain of rr reflections turns WW without ever being able to stretch it.

wmirror 1one flipmirror 2two flipsevery arrow has length 170.9, by construction; only the normals train
Fig. 23 

HRA. Two computed reflections; every vector in the figure has the same length, by construction.

python
for u in normals:                # r mirror normals train
    W = W - 2 * u @ (u.T @ W)    # each reflection keeps every length

RoAd is the cheapest turn in the library: take the output coordinates two at a time and give each pair its own 2D rotation angle and scale. Its smallest variant stores about one number per output channel.

the output, taken two at a timebeforeafterone pair, worked:turn 40 degrees,scale by 1.15an angle and a scale per pair; the smallest variant is one number a channel
Fig. 24 

RoAd. Rotations at the smallest possible size: two coordinates at a time.

python
for i, j in pairs(h):            # two coordinates at a time
    h[i], h[j] = turn(h[i], h[j], angle, scale)

PSOFT rotates inside the waist. It freezes CC and RR from the weight’s own SVD and trains only a small r×rr \times r orthogonal transformation between them, so adaptation happens strictly inside the principal subspace.

WSVDtop rthe strongest directionsB×R − I×Athe factors stay put;the turn trainsthe principal subspace is the room; training only turns inside it
Fig. 25 

PSOFT. The factors come from the SVD and stay put; the turn between them trains.

python
B, A = svd_top(W, r)             # fixed from W's own SVD
h = W @ x + B @ ((R - I) @ (A @ x))   # only the turn R trains

Surgery on the weight’s own subspaces

Three methods start by asking what WW already contains, using the subspace language of Lecture 9, and operate on the answer.

OSF splits WW by its own SVD, freezes the top directions where old knowledge lives, and trains only in the leftover subspace, so a new task cannot trample what the model already does. It is built for learning tasks in sequence.

W, split by its own SVDtop σ: frozenthe tail: free to trainold knowledgethe new tasknew tasks cannot trample the directions that matter most
Fig. 26 

OSF. The strong directions are locked; training lives in what is left.

python
top, tail = split_svd(W)         # strong directions, and the rest
W = top + train(tail)            # old knowledge stays locked

DEFT is the surgeon: it learns a projection that erases a chosen slice of what WW says, the projection algebra of Lecture 11, and writes new content in the erased slice, with the whole operation equal to the identity at birth.

the chosen sliceat birth: erases nothingerased:a projection,Lecture 11the refillsurgery on a subspace: remove a behavior, install another
Fig. 27 

DEFT. Projection as a scalpel: remove a subspace’s worth of behavior, then replace it.

python
h = ((I - P) @ W + Q) @ x        # P erases a slice, Q refills it
                                 # at birth P = 0 and Q = 0: identity

AdaMSS decomposes the weight by SVD, clusters the directions into a few groups, and gives each cluster its own small trainable update, so the adapter’s structure follows the weight’s own geography.

W’s directions, clustered by angleupdate 1update 2update 3the adapter’s structure follows the weight’s own geography
Fig. 28 

AdaMSS. Cluster the directions, then adapt cluster by cluster.

python
groups = cluster(svd_dirs(W))    # directions, grouped by likeness
dW = sum(update[g] for g in groups)

Cheap vectors

The family that refuses to buy matrices. Everything here trains vectors, and the measured rows in Lecture 29’s table are the smallest for exactly that reason.

IA3 trains one number per channel and multiplies the activations by it: 141,312 numbers on the measured model, 0.0286 percent, and no matrix anywhere.

x2×0.5=11×2=23×1=3activationsdialsrescaledyno matrix anywhere:a dial per channelmeasured: 141,312 = 5,888 dials a layer, 24 layers
Fig. 29 

IA3. The layer’s voice, remixed channel by channel.

python
h = W @ x                        # frozen
h = h * ell                      # one trained dial per channel

BEFT is the humblest entry in the guide: it trains only additive bias vectors and leaves every matrix alone. Move the intercepts, keep the slopes.

W, frozenx2+1=3-1+1=00+-1=-1Wxbyshift by bmove the intercepts, keep every slopeone vector per layer; the matrices never hear about it
Fig. 30 

BEFT. Bias-only tuning; sometimes the intercepts are enough.

python
h = W @ x + b                    # only b trains: intercepts, not slopes

LN tuning trains only the normalization layers, the small gain-and-shift vectors that sit between the big frozen blocks, retuning the thermostat while leaving the machine.

attentionnormMLPnormraw: off-center, unevennormalized: centered, unit spreadgain gthe shift,at the centera few numbers per block steer the statistics everything runs on
Fig. 31 

LN tuning. A few vectors per block steer the statistics every block runs on.

python
z = (x - mean(x)) / std(x)       # built into the norm
h = g * z + shift                # only g and the shift train

VeRA is Lecture 29’s shared-pair trick, here in its guide form: one frozen random pair for the whole model, two small trained vectors per layer, 27,648 numbers in the measured run.

seedA, randomB, randomfrozen, never trained, rebuilt from the seedlayer 1×××dblayer 2×××layer 3×××d is 256 numbers, b is 896,the strips drawn to that ratiomeasured: 27,648 numbers; the pair ships as a seed
Fig. 32 

VeRA. The pair can be rebuilt from a seed; the vectors are the checkpoint.

python
A, B = rand(seed)                # one frozen pair, model-wide
h = W @ x + b * (B @ (d * (A @ x)))   # d and b train, per layer

PVeRA keeps VeRA’s silhouette, one shared pair and two small trainable vectors per layer, and refines how the shared pair is built and applied.

VeRA:seedd and b, trainedPVeRA:seedprepared and b, trainedeverything else is identical, stamp for stampsame trainable vectors; the pair is prepared before use
Fig. 33 

PVeRA. Same idea, sharper projections; the family resemblance is the point.

python
A, B = prepare(rand(seed))       # the pair, prepared before use
h = W @ x + b * (B @ (d * (A @ x)))   # the same vectors train

RandLoRA stacks several frozen random bases and trains small diagonal mixes that combine them; summed low-rank pieces can reach full rank, at vector prices.

×1+×2+×1random and frozen, rank one each=the sum: full rank,one rank per piecerandomness supplies directions; training supplies amounts
Fig. 34 

RandLoRA. Randomness supplies the directions; training supplies the amounts.

python
dW = sum(lam[k] * basis[k])      # frozen pieces, trained amounts

FRoD freezes two random patterns per matrix, one dense low-rank piece and one sparse one, and trains only the small vectors that blend them, covering both smooth and spiky updates from a fixed random menu.

dense, smooth×2+sparse, spiky×1=ΔW: smooth wash, sharp spikesthe patterns never train;the two amounts dothe patterns come from a seed; the blend is the learning
Fig. 35 

FRoD. The patterns are random and frozen; only the blend is learned.

python
dW = lam1 * dense + lam2 * sparse    # both patterns from a seed

Trainable tokens goes surgical on the embedding table from Lecture 1: train the rows of a few chosen tokens, touch nothing else. Teach the model two new words for the price of two vectors.

theofNEWandNEWisthe embedding table (Lecture 1)teach two new words,touch nothing elsea word is a row; two rows is the entire training run
Fig. 36 

Trainable tokens. New vocabulary without touching the model that reads it.

python
E[chosen_rows] = trained         # a few embedding rows
E[everything_else] = frozen

Leave the weights alone

The soft-prompt family from Lecture 29, completed. Nothing here touches WW at all; everything edits the vectors the frozen model reads.

Prompt tuning prepends kk learned vectors to the input, 14,336 numbers in the measured run, the smallest row of the whole table.

the model, every weight frozenthe sentencelearned, the only trainingattention reachesback to the promptmeasured: 16 vectors × 896 numbers = 14,336, the smallest row
Fig. 37 

Prompt tuning. The guide’s smallest method: the input grows, the map never moves.

python
tokens = concat(P, embed(text))  # P: k learned vectors; W never moves

Prefix tuning walks the prompts in deeper: learned key and value vectors join the attention of every layer, so the steering happens at depth instead of only at the entrance.

layer 1layer 2layer 3K over Vlearnedthe sentencequeriesone attention score table, zoomedprompts at depth,past the front doorthe steering happens inside attention, layer by layer
Fig. 38 

Prefix tuning. Every layer gets its own learned visitors.

python
K = concat(K_learned, K_text)    # every layer's attention
V = concat(V_learned, V_text)    # gets its learned visitors

P-tuning adds a writer: a small MLP or LSTM produces the prompt vectors during training, smoothing their optimization, and is thrown away once the vectors are learned.

training:MLP orLSTMmodel, frozenserving:gonemodel, frozenthe writer smooths training;the vectors are the productscaffolding for optimization; only the building survives
Fig. 39 

P-tuning. Scaffolding for training; only the prompts survive.

python
P = writer(z)                    # an MLP or LSTM writes the prompt
serve(P)                         # afterward, the writer is discarded

CPT mixes learned prompt tokens into real context tokens and keeps their updates on a leash, projecting each step back toward where the token began, so the prompt stays close to language instead of drifting into arbitrary directions.

texttasktasktexttextprompts, embedded in real textthe allowed circletoo farkeptthe leash is a projection: Lecture 11, used as a guardrail
Fig. 40 

CPT. Prompts embedded in context, constrained by projection, the algebra of Lecture 11 again.

python
P = P + step                     # the update wants to wander
P = pull_back(P, start, radius)  # Lecture 11, used as a guardrail

Multitask prompt tuning learns one shared prompt for all tasks plus a thin per-task tweak of it, multiplied in entrywise, so a new task costs a whisper instead of a full prompt.

123the shared prompttask A×211=223task B×102=106a zero in the tweakmutes that channeltasks share the prompt and rescale it entrywise, cheaply
Fig. 41 

Multitask prompt tuning. The shared prompt carries the family; each task rescales it.

python
P_task = P_shared * tweak[task]  # entrywise; a whisper per task

Adaption prompt, LLaMA-Adapter in the literature, inserts learned prompt tokens into only the top few layers and guards them with a gate initialized at zero, so on day one the model is exactly the base model, the same promise LoRA makes with C=0C = 0.

layer 1layer 2layer 3layer 4prompts only at the toptraining stepsgateday one: silentat birth the output is the base model: LoRA’s promise, kept by a gate
Fig. 42 

Adaption prompt. Prompts for the top of the stack, behind a gate that opens from zero.

python
h_top = attn(x) + gate * attn(P) # top layers only
                                 # gate starts at exactly zero

CARTRIDGE stores the prompt after the model has read it: a trained key-value cache, kept as weights and loaded at serving time, so a whole corpus’s worth of context can be distilled once and reused forever.

attention, frozenK over V, learnedevery prompt, without it:and on…with the cartridge:the cache, loaded in one stepprefix tuning’s stored form: context, distilled to weights
Fig. 43 

CARTRIDGE. Prefix tuning’s stored form: the cache is the adapter.

python
cache = train_kv(corpus)         # read the corpus once, keep the cache
serve(load(cache))               # the cache is the adapter

Routers and mixtures

The last family manages adapters instead of inventing one.

Poly keeps an inventory of small LoRA-like skills shared across tasks, and each task learns only its own mixture over them, so skills transfer and the mixing is the task.

skill 1skill 2skill 3skill 4the shelf:task A0.50.00.40.1task B0.00.30.30.4skill 3 serves both;skill 1 serves onetasks own recipes, never ingredients; transfer comes free
Fig. 44 

Poly. A shared skill shelf; tasks are recipes over it.

python
dW_task = mix(recipe[task], skills)   # shared skills, task recipes

X-LoRA routes between finished adapters: several trained LoRAs are frozen, and a small router reads the hidden state and blends them token by token, a mixture of experts where the experts are adapters.

adapter 1adapter 2adapter 3routetoken 1a1a2a3token 2token 3the blend changes token by tokena mixture of experts, except the experts are adapters
Fig. 45 

X-LoRA. Training built the specialists; routing picks the committee.

python
w = router(hidden)               # per token
dW = sum(w[k] * adapter[k])      # a committee of finished LoRAs

Mixed is the library’s own closing move rather than a paper’s: it lets different adapter species run together in one model, a LoRA here, a LoHa there, an OFT beside them.

one block, five matrices, three speciesqkvupdownLoRALoHaOFTk and down: left alone, also a choicethe families in this guide compose; the config decides per matrix
Fig. 46 

Mixed adapters. The families in this guide are not exclusive; the library composes them.

python
config = per_matrix(q=LoRA, v=LoHa, up=OFT)   # species compose

Where this is going

Forty-six methods, and not one of them needed mathematics this course had yet to build. The thin products are Lecture 6’s factorization wearing training wheels; the rank arguments are Lecture 8; the rotations and mirrors are Lecture 13; the projections and erasures are Lecture 11; the subspace splits are Lecture 9; the input-side steering is Lecture 1. When the next batch of adapters arrives, and it will, read the shape of ΔW\Delta W first. It will land in one of these families, and you will already know its geometry. Lecture 29 has the running model where nine of these were measured; the run takes five seconds, and any entry in this guide can be swapped into it by changing one config line.

Here is the whole guide as one drawing.

xW, frozen+×y4rotate, never stretch5split W’s subspaces7steer the input1add a thin product2structure the product3spend a few coefficients6rescale the channels8route between adaptersone frozen stage, eight families of change: the whole guide
Fig. 47 

The families on one stage. Each numbered chip is a section of this guide, pointing at the one place it changes the stage. When the next method arrives, find its arrow first.