mirror of
https://github.com/luxfi/torus-ml.git
synced 2026-10-30 15:15:23 +00:00
The distribution called itself torus-ml while shipping a package directory named concrete, so the name a user installs and the name they import disagreed — and most of the tree already said torus.ml, which could not resolve against src/concrete. Moved src/concrete to src/torus and pointed the include at it. The dependency was Zama's concrete-python from their index. It is torus-fhe from ours, which is what luxfhe/torus already ships: that frontend declares torus-fhe and ships a torus/fhe module, so every 'from torus.fhe' in this tree was already naming our compiler and only the dependency lagged. Two distributions share one namespace, which is the shape the upstream this forked from used and the reason the split works: torus-fhe gives torus.fhe, torus-ml gives torus.ml. Swept every other spelling — identifiers, enum members, workflow variables, filenames, notebooks. LICENSE and NOTICE keep the upstream attribution a fork owes; the figures carry the old name in JPEG metadata a text sweep cannot reach.
1.5 MiB
1.5 MiB
In [1]:
import shutil
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
import torch
from peft import LoraConfig, get_peft_model
from sklearn.datasets import make_circles, make_moons
from torch import nn, optim
from torch.utils.data import DataLoader, TensorDataset
from torus.ml.torch.lora import LoraTrainer
# Set random seed for reproducibility
SEED = 42
np.random.seed(SEED)
torch.manual_seed(SEED);In [2]:
# Task 1: Two interleaving half circles
X_task1, y_task1 = make_moons(n_samples=500, noise=0.1)
# Task 2: Two concentric circles
X_task2, y_task2 = make_circles(n_samples=500, noise=0.2, factor=0.5)
def plot_datasets_and_boundaries(X_task1, y_task1, X_task2, y_task2, model=None, titles=None):
_, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 6))
if titles is None:
titles = ["Task 1 Dataset", "Task 2 Dataset"]
for ax, X, y, title in zip([ax1, ax2], [X_task1, X_task2], [y_task1, y_task2], titles):
ax.scatter(X[:, 0], X[:, 1], c=y, cmap="viridis", edgecolor="k")
ax.set_title(title)
if model is not None:
x_min, x_max = X[:, 0].min() - 0.5, X[:, 0].max() + 0.5
y_min, y_max = X[:, 1].min() - 0.5, X[:, 1].max() + 0.5
h = 0.1 # step size in the mesh
xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h))
grid = torch.FloatTensor(np.c_[xx.ravel(), yy.ravel()])
with torch.no_grad():
Z = model(grid)
probabilities = torch.softmax(Z, dim=1)
Z = probabilities[:, 1].numpy().reshape(xx.shape)
ax.contourf(xx, yy, Z, cmap="viridis", alpha=0.3)
plt.tight_layout()
plt.show()
# Plot datasets
plot_datasets_and_boundaries(X_task1, y_task1, X_task2, y_task2)In [3]:
# Convert datasets to PyTorch tensors
X_task1 = torch.FloatTensor(X_task1)
y_task1 = torch.LongTensor(y_task1)
X_task2 = torch.FloatTensor(X_task2)
y_task2 = torch.LongTensor(y_task2)
# Create DataLoaders
batch_size = 32
train_loader_task1 = DataLoader(
TensorDataset(X_task1, y_task1), batch_size=batch_size, shuffle=True
)
train_loader_task2 = DataLoader(
TensorDataset(X_task2, y_task2), batch_size=batch_size, shuffle=True
)
# Define an MLP model without LoRA layers
class SimpleMLP(nn.Module):
"""Simple MLP model without LoRA layers."""
def __init__(self, input_size=2, hidden_size=128, num_classes=2):
super().__init__()
self.fc1 = nn.Linear(input_size, hidden_size)
self.relu = nn.ReLU()
self.fc2 = nn.Linear(hidden_size, num_classes)
def forward(self, x, labels=None): # pylint: disable=unused-argument
"""Forward pass of the MLP."""
out = self.fc1(x)
out = self.relu(out)
out = self.fc2(out)
return out
# Instantiate the model
model = SimpleMLP()
# Training loop for Task 1
def train_model(model, train_loader, num_epochs=100):
"""Train the model.
Args:
model (nn.Module): The model to train.
train_loader (DataLoader): DataLoader for training data.
num_epochs (int): Number of epochs to train.
"""
device = torch.device("cpu")
model.to(device)
model.train()
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.01)
for epoch in range(num_epochs):
total_loss = 0
for x_batch, y_batch in train_loader:
x_batch = x_batch.to(device)
y_batch = y_batch.to(device)
optimizer.zero_grad()
outputs = model(x_batch)
loss = criterion(outputs, y_batch)
loss.backward()
optimizer.step()
total_loss += loss.item()
# Print loss every 20 epochs
if (epoch + 1) % 20 == 0:
print(f"Epoch [{epoch+1}/{num_epochs}], Loss: {total_loss/len(train_loader):.4f}")
# Train the model on Task 1
print("Training on Task 1 without LoRA:")
train_model(model, train_loader_task1, num_epochs=20)
# Plot datasets with decision boundaries
plot_datasets_and_boundaries(
X_task1.numpy(),
y_task1.numpy(),
X_task2.numpy(),
y_task2.numpy(),
model=model,
titles=["Task 1 after Training", "Task 2 after Training"],
)Training on Task 1 without LoRA:
Epoch [20/20], Loss: 0.0036
In [4]:
# Apply LoRA to the model using peft
lora_config = LoraConfig(
r=1, lora_alpha=1, lora_dropout=0.01, target_modules=["fc1", "fc2"], bias="none"
)
peft_model = get_peft_model(model, lora_config)In [5]:
# Update training parameters, including loss function
optimizer = optim.Adam(filter(lambda p: p.requires_grad, peft_model.parameters()), lr=0.01)
loss_fn = nn.CrossEntropyLoss()
training_args = {"gradient_accumulation_steps": 1}
# Set up LoRA training
lora_trainer = LoraTrainer(
peft_model, optimizer=optimizer, loss_fn=loss_fn, training_args=training_args
)
# Prepare input data for calibration
batch_size_per_task = batch_size // 2
inputset = (
torch.cat([X_task1[:batch_size_per_task], X_task2[:batch_size_per_task]]),
torch.cat([y_task1[:batch_size_per_task], y_task2[:batch_size_per_task]]),
)
# Compile the model
lora_trainer.compile(inputset, n_bits=8)LoRA layers detected in the model.
Compiling FHE layers: 0%| | 0/3 [00:00<?, ?it/s]
In [6]:
# Fine-tune the model on Task 2 using LoRA
lora_trainer.train(train_loader_task2, num_epochs=10, fhe="execute")Training: 0%| | 0/10 [00:00<?, ?epoch/s]
Training completed. Final Avg Loss: 0.2978, FHE Mode: execute
In [7]:
# Enable LoRA adapters (already enabled by default)
peft_model.enable_adapter_layers()
# Plot datasets with decision boundaries after fine-tuning
plot_datasets_and_boundaries(
X_task1.numpy(),
y_task1.numpy(),
X_task2.numpy(),
y_task2.numpy(),
model=peft_model,
titles=["Task 1 after Fine-tuning", "Task 2 after Fine-tuning"],
)In [8]:
# Disable LoRA adapters
peft_model.disable_adapter_layers()
# Plot datasets with decision boundaries after fine-tuning
plot_datasets_and_boundaries(
X_task1.numpy(),
y_task1.numpy(),
X_task2.numpy(),
y_task2.numpy(),
model=peft_model,
titles=["Task 1 after Fine-tuning", "Task 2 after Fine-tuning"],
)In [9]:
# Enable LoRA adapters (already enabled by default)
peft_model.enable_adapter_layers()
# Print trainable (lora) parameters
peft_model.print_trainable_parameters()
# Save the model and remove all layers that will be done on the server
path = Path("lora_mlp")
if path.is_dir() and any(path.iterdir()):
shutil.rmtree(path)
lora_trainer.save_and_clear_private_info(path)
# At this point, the client's model only contains the trainable parameters of the LoRA layers.
peft_model.print_trainable_parameters()trainable params: 260 || all params: 902 || trainable%: 28.8248 trainable params: 260 || all params: 260 || trainable%: 100.0000