Parameter-Efficient Fine-Tuning (PEFT)¶
The taktiny.takt package provides adapters that transform existing models by injecting low-rank or parameter-efficient modules while automatically freezing the pre-existing base weights.
Adapters in Taktiny are designed with JAX’s SPMD paradigm in mind. They correctly inherit logical axis names and partition specifications from the base layers they wrap, support quantized base weights via Qwix, and naturally separate trainable and frozen states for checkpointing.
Injecting an Adapter¶
In Taktiny, adapters are not standalone modules; they are applied dynamically to an existing model using Takt.apply_adapter().
from taktiny import nn
from taktiny.takt import Takt, LoRAAdapter
# 1. Start with an existing base model
model = nn.Sequential([
nn.Linear(64, 128, rngs=nn.Rngs(0)),
nn.Linear(128, 10, rngs=nn.Rngs(1)),
])
# 2. Configure the adapter
# "targets" can be a single regex string or a list of regex patterns.
adapter = LoRAAdapter(
targets=".*",
rank=8,
alpha=16.0,
rngs=nn.Rngs(42),
)
# 3. Apply the adapter
adapted_model = Takt.apply_adapter(model, adapter)
When an adapter is applied, apply_adapter:
Searches the model for target layers (e.g.,
nn.Linear) that match the regex pattern.Replaces those layers with specialized wrappers (like
LoRALinear) that encapsulate the original frozen layer.Automatically sets
trainable = Falseon all pre-existing parameters across the entire model.Leaves the newly injected parameters as
trainable = True.
Quantization and Qwix Support¶
Taktiny adapters fully support mixed-precision and quantized fine-tuning (QLoRA/QPEFT).
Because the specialized wrappers separate the base weight computation from the adapter update, the base Linear module can be quantized using Qwix. When you run a forward pass, the base layer will dequantize its weights and compute its contribution, while the trainable adapter parameters remain in pure floating point (e.g., bfloat16 or float32).
You do not need special configuration for QPEFT: simply quantize the model before or after applying the adapter.
Supported Adapters¶
Taktiny includes several state-of-the-art PEFT methods. They all inherit from AdapterBase.
Adapter |
Mechanism |
Best For |
|---|---|---|
|
|
Standard fine-tuning tasks. |
|
Decouples magnitude and direction of the weights. Adapts direction via LoRA and learns a separate output magnitude parameter. |
High-performance fine-tuning matching full-rank updates. |
|
An SVD-style adapter: |
Strict parameter budgets. |
|
Hadamard product adapter with |
Computer vision and stable diffusion. |
|
Kronecker product weight adapter: |
Extremely high-dimensional target layers. |
|
Uses frozen random projections shared by all targets, learning only diagonal rank/output scales. |
Maximum parameter efficiency. |
Post-Step Updates (AdaLoRA)¶
A few adapters require periodic maintenance during training. AdaLoRAAdapter dynamically masks out less important singular values based on importance scores, and it requires an orthogonal regularization penalty.
To support this, Takt retains the applied adapter objects as static metadata on the model (model._takt_adapters). Your training loop or callback can invoke Takt.update_adapters() after an optimizer step to execute these hooks:
# During your training loop, after optax.apply_updates:
Takt.update_adapters(adapted_model, step=current_step)
You can compute custom penalties by iterating over the model tree. For example, to add AdaLoRA’s orthogonal loss:
from taktiny.nn.modules.peft import AdaLoRALinear
adapter_loss = 0.0
for layer in adapted_model.flat_children():
if isinstance(layer, AdaLoRALinear):
adapter_loss += layer.orthogonal_loss()
Checkpointing and Resuming¶
Taktiny’s trainer separates parameters into trainable_params and frozen_params internally. Because Takt.apply_adapter sets the base parameters to trainable = False, you can trivially extract just the adapter weights for saving:
# To save only the adapter parameters:
adapter_params = {
path: param
for path, param in adapted_model.flat_parameter_dict().items()
if param.trainable
}
When resuming, load the base model checkpoint first, apply the adapter architecture, and then inject the loaded adapter parameters.
A complete training example¶
This snippet demonstrates creating a model, applying LoRA, and using the Taktiny Trainer to fine-tune it. The trainer natively respects trainable flags and will only optimize the adapter.
import jax.numpy as jnp
import optax
from taktiny import nn
from taktiny.takt import Takt, LoRAAdapter
from taktiny.trainer import Trainer, TrainingConfig, DatasetConfig
class MLP(nn.Module):
def __init__(self, *, rngs: nn.Rngs):
self.dense1 = nn.Linear(32, 64, rngs=rngs)
self.dense2 = nn.Linear(64, 4, rngs=rngs)
def __call__(self, x):
return self.dense2(jax.nn.relu(self.dense1(x)))
# 1. Initialize the base model
model = MLP(rngs=nn.Rngs(0))
# 2. Inject the adapter
adapter = LoRAAdapter(
targets="dense.*",
rank=4,
rngs=nn.Rngs(1),
)
model = Takt.apply_adapter(model, adapter)
# 3. Dummy dataset and loss
def dummy_loss(model_params, batch, **kwargs):
x, y = batch["x"], batch["y"]
# Taktiny models are PyTrees! The trainer passes the updated model
# as the first argument, so you just call it directly.
logits = model_params(x)
return jnp.mean((logits - y) ** 2)
dummy_data = [{"x": jnp.ones(32), "y": jnp.zeros(4)} for _ in range(10)]
# 4. Train only the adapter
trainer = Trainer(
model=model,
loss_fn=dummy_loss,
training_config=TrainingConfig(
max_steps=5,
optimizer=optax.adamw(learning_rate=1e-3),
),
dataset_config=DatasetConfig(dummy_data, batch_size=2),
)
trainer.train()