Implementing the LinearWithLoRA Layer
The replace_linear_with_lora function walks the model's children, swaps each Linear layer for a LinearWithLoRA layer built with rank and alpha, and recurses into every other child.
A Function That Rewrites a Model
This article begins with a function rather than a definition. The function is replace_linear_with_lora, and its job is to walk through an existing GPTModel and swap its Linear layers for LinearWithLoRA layers. LinearWithLoRA is the layer type that combines the behavior of a Linear layer with LoRA, and the replacement function is the mechanism that puts those new layers in place of the old ones throughout the model.
Keep the setting in mind. The GPTModel is the model you built in earlier chapters, and the goal is parameter-efficient fine-tuning. Read the function once in full before you run anything. Then try to answer the question the rest of this article is built around: when this function finishes, which objects in the model have changed, and which have not?
What the Function Changes in Place
The replace_linear_with_lora function has a summary you can hold onto: it walks the model's children, swaps each Linear layer for a LinearWithLoRA layer built with rank and alpha, and recurses into every other child. Each part of that summary corresponds to a concrete behavior of the function, and the rest of this article takes them one at a time.
The function modifies the model in place. It does this through setattr, which assigns a new object to a named attribute of a parent module. Because the change happens to the existing model object, the function does not need to return a new model. It returns nothing.
The rank and alpha values are the settings used to build each new LinearWithLoRA layer. The source names them as the construction inputs for the upgraded layer, and this article focuses on the traversal and placement logic that decides where those layers go. You do not need the internal math of LoRA to follow the replacement logic.
Two Decisions Made at Every Level
The recursion is easiest to understand as a sequence of decisions. The function looks at one child of the current module at a time. For each child, it asks one question: is this child a Linear layer? If it is, the function swaps it for a LinearWithLoRA layer. If it is not, the function does not give up on that child. It hands the child to another call of the same function, so that the child's own children get the same treatment.
The traversal finishes only when every child at every depth has been visited. This is the key property of the recursion. A Linear layer buried several levels down is still reached, because each non-Linear container is entered rather than skipped. The flowchart below follows a single child through this decision.
Notice that the function makes the decision based on what the child is at that moment. A Linear child is replaced, and that is the only branch that changes the model's layers. Everything else is treated as a container that might hold Linear layers of its own. This is why the function can work on a model whose structure it has never been told about in detail.
Tracing a Toy Model by Hand
The GPTModel is too large to trace line by line, so start with a small constructed model. This toy model is not part of the GPT model in the source. It uses the same rules as the function, and it shows the two things the function does at every level: it swaps any Linear layer it finds among the direct children, and it descends into everything else.
Suppose the toy model has three direct children. The first is fc_in, a Linear layer. The second is act, an activation module with no children of its own. The third is block, a container holding two children: fc_mid, a Linear layer, and drop, a dropout module. Before reading further, predict which of these nodes will be replaced after one call to the function.
What do you think happens?
After one call to replace_linear_with_lora on the toy model, which Linear layers become LinearWithLoRA layers?
Reveal answer
Both fc_in and fc_mid
The function swaps fc_in directly because it is a Linear child. For block, which is not a Linear layer, the function recurses into it, finds fc_mid as a Linear child, and swaps it. The act and drop modules are not Linear layers, so they are visited but not replaced.
Replacing the toy model's Linear layers
Given the toy model with direct children fc_in (Linear), act (activation), and block (container holding fc_mid (Linear) and drop (dropout)), determine which modules change type after one call to replace_linear_with_lora.
- 1Visit fc_in
fc_inis a direct child and is a Linear layer, so the function swaps it for a LinearWithLoRA layer. - 2Visit act
act is not a Linear layer, so the function recurses into it. It has no children, so nothing else happens and its type stays the same.
- 3Visit block
block is not a Linear layer, so the function recurses into it instead of swapping it.
- 4Visit fc_mid inside block
Inside the recursive call,
fc_midis a Linear child, so it is swapped for a LinearWithLoRA layer. - 5Visit drop inside block
drop is not a Linear layer and has no Linear children, so it is visited and left as it is.
- 6Check the result
fc_inandfc_midchanged type. act, block, and drop did not. The function returned nothing, so the caller inspects the same toy model object.
Two Linear layers were replaced: fc_in at the top level and fc_mid inside block. Every other module kept its type.
Where the Upgrade Lands in GPTModel
The function is generic, but the source applies it to a specific target. The intended upgrade covers the Linear layers found in the multihead attention, the feed-forward modules, and the output layer of the GPTModel. The diagram below maps the GPT architecture with those parts marked. Read it as a map of where the replacement is intended to take effect, not as a listing of every module in the model.
The upgraded parts are split into two groups, and it helps to hold them apart. The first group sits inside each transformer block. The second is the output layer, which is at the end of the model rather than inside a block. The sections below describe each group and what stays unchanged.
Mistakes When Predicting a Replacement
Common mistake Assigning the result of the function to a new variable
What it looks likeupdated = replace_linear_with_lora(model)Why it failsThe function modifies the model in place and returns nothing, so updated is None.
Do this insteadCall replace_linear_with_lora(model) and keep using the original model object.
Common mistake Assuming a Linear layer nested inside a container is missed
What it looks likeExpecting fc_mid inside block in the toy model to stay a plain Linear layer.
Why it failsEvery non-Linear child is recursed into, so the traversal reaches Linear layers at every depth.
Do this insteadTrace the recursion into each container before deciding a layer was skipped.
Common mistake Assuming every module in GPTModel is upgraded
What it looks likeExpecting the embedding layers or final LayerNorm to become LinearWithLoRA.
Why it failsThe source marks the upgrade for the Linear layers in multihead attention, feed-forward modules, and the output layer. Embeddings, dropout, and LayerNorm are not marked as upgraded.
Do this insteadCompare the final model against the upgraded set rather than against the full architecture.
Common mistake Confusing the decision with the module's name rather than its type
Predicting Results and Locating Surprises
When a replacement run gives an unexpected result, the source suggests a three-part check. Work through it in order rather than guessing. First, check whether the target layer was reached by the traversal. Second, check whether it was a Linear layer at the moment the function visited it. Third, check which object the code is now using. Each question narrows the search.
A colleague runs replace_linear_with_lora on a GPTModel and then prints the type of one feed-forward Linear layer, which still shows as Linear. Using the three-part check, list what you would verify first, second, and third, and say which of the three is most likely to reveal the problem if the caller had stored the return value in a new variable.
Hints
- Start by asking whether the traversal could reach that layer at all.
- Remember that the function returns nothing and modifies the model in place.
- Think about which object the caller is printing from.
Sketch a small model with two Linear layers at the top level and one container holding a third Linear layer. Predict which of the three will be replaced, then list each visit in order. Compare your list with the traversal rule: swap any Linear child, and recurse into everything else.
Hints
- Visit children in order, and recurse into the container when you reach it.
- Count the containers' children separately from the top-level children.
Key Takeaways
- The
replace_linear_with_lorafunction walks the model's children and swaps each Linear layer for a LinearWithLoRA layer built with rank and alpha. - Any child that is not a Linear layer is recursed into, so the traversal reaches Linear layers at every depth before finishing.
- The function modifies the model in place through setattr and returns nothing, so the caller must keep using the original model object.
- In the GPTModel, the upgrade targets the Linear layers in the multihead attention, feed-forward modules, and output layer, while embeddings, dropout, and final LayerNorm are not marked as upgraded.
- When a result is unexpected, check whether the target was reached, whether it was a Linear layer at that moment, and which object the code is now using.