
How to Optimize Language Model Size for Deployment.
Image by Author | Ideogram
The rise of language models, and more specifically large language models (LLMs), has been of such a magnitude that it has permeated every aspect of modern AI applications — from chatbots and search engines to enterprise automation and coding assistants. Notwithstanding, this boom has not been exempt from challenges, particularly when it comes to deploying these models in such a way that their size is optimized to strike the right, yet delicate balance between performance, accessibility, energy, and computing resource consumption.
Because LLM size matters, this article provides an exploratory discussion on conceptual and practical strategies for model size optimization.
Architectural Approaches for LLM Simplification
To have a precise idea of how exponentially language models have grown, the evolutionary journey from GPT-2 to GPT-4 entailed scaling from 1.5B to over 175B parameters. That’s an absurdly large rate of growth.
While this upscaling helped unlock extraordinary text generation, reasoning, and interaction capabilities, obvious challenges arose when attempting to deploy such models efficiently in on-device, cloud-based, and real-time settings. Beyond the cost of training or fine-tuning these models, inference speed is also important since latency directly impacts user experience, especially in interactive applications. Meanwhile, quantization tradeoffs are another issue worth looking at, as they can significantly reduce model size and computing requirements — yet this often happens at the cost of precision or robustness.
Model size can be optimized architecture-wise based on various strategies, such as distillation, pruning, layer reduction, and modular or adapted-based architectures.
Model distillation (also referred to as knowledge distillation) applies a teacher-student paradigm to train a smaller model, such that the “student” (the smaller of the two models) learns by observing the teacher-generated outputs, namely the iterative next-word prediction outcomes and the probability distributions of most likely words upon which the next generated word is generated. The latter is an indicator of the teacher’s confidence in generating every single output. Applying model distillation often entails the decision of striking the right balance between accuracy and compactness. A loss function can be used to strike this balance and guide the process of training a simplified model.
output = teacher_model(input) loss = distillation_loss(student_model(input), output) |
Model pruning inherits the idea underlying pruning decision trees to reduce their complexity and size. This technique consists of removing weights whose contribution to the model’s output is smallest; in other words, those with the lowest values. Dynamic sparsity techniques during training allow the model to gradually learn which weights — connections among layers — to maintain or discard. At inference time, sparser models resulting from pruning can reduce memory usage and potentially boost computation. The following example shows a simplified glimpse of what this strategy would look like.
import torch
def prune_small_weights(model, threshold=1e–3): with torch.no_grad(): for name, param in model.named_parameters(): if “weight” in name: mask = param.abs() > threshold param.mul_(mask) # zero out small weights |
Meanwhile, layer reduction, as its name suggests, attempts to make the overall neural network components within the LLM architecture shallower, concretely by reducing the number of layers where nonlinear transformations of the encoded linguistic information are progressively applied throughout the encoder and decoder of the LLM’s underlying transformer architecture. When might fewer layers mean a better LLM? In short, when the language task doesn’t necessitate profound contextual reasoning, or latency and resource constraints outweigh the marginal benefits of having extra depth.
Layer reduction can be applied at a higher level, not necessarily at single-layer level, to remove some of the replicated encoder or decoder layers, as follows:
from transformers import BertModel
model = BertModel.from_pretrained(“bert-base-uncased”)
# Keeping only the first 6 encoder layers (this Bert Model has 12) model.encoder.layer = model.encoder.layer[:6] |
Lastly, there are advanced plug-in modular approaches, such as LoRA (Low-Rank Adaptation), which simplify model adaptation by injecting lightweight, trainable components into a pre-trained model with frozen weights. These methods are particularly effective in resource-constrained and multi-task environments, as they reduce the need to fine-tune or deploy multiple full-size models for each task the system is expected to handle.
Weight Level Optimization
Unlike architectural approaches for model size optimization, which change the model’s structure by reducing layers or pruning entire neuron weights, level optimization doesn’t focus on a hard simplification of the components or weight elimination, but instead tries to compress or numerically approximate the weights to yield a more efficient, production-ready model. These methods, which include quantization, weight-sharing, and compression codecs, reduce memory footprint and improve inference speed, often with minimal impact on accuracy.
Quantization is a very popular approach to enable faster fine-tuning and inference in otherwise heavy models, especially in edge and constrained machines. Weight sharing uses tensor factorization to approximate large weight matrices with smaller components, reducing redundant values. Compression codecs are an algorithmic approach to compress or decompress weights in certain operation stages of the model, typically model storage and loading. Unlike model quantization, they do not remove part of the precision representation of weights, and they can later be decompressed in full.
Let’s finalize by looking at some simple yet illustrative examples of applying these weight-level optimization techniques.
Quantization to reduce weights precision from 32-bit to 8-bit:
import torch
model.qconfig = torch.quantization.get_default_qconfig(‘fbgemm’) quantized_model = torch.quantization.prepare(model, inplace=False) quantized_model = torch.quantization.convert(quantized_model, inplace=False) |
Weight sharing based on tensor factorization:
import torch.nn as nn
original = nn.Linear(512, 512) factorized = nn.Sequential(nn.Linear(512, 64), nn.Linear(64, 512)) |
Compression-aware weight storage:
import torch torch.save(model.state_dict(), “model.pt”)
import zipfile with zipfile.ZipFile(“model.zip”, ‘w’) as zf: zf.write(“model.pt”) |
Conclusion
Throughout this article, we took a tour and discussion of the most prominent techniques and strategies to reduce language model size: a key aspect to ensure an efficient operation in production environments.