Clustering & Graph Partitioning
Clustering & Graph Partitioning
Located at:
netcl.cluster(Algorithms:kmeans.py,spectral.py,gmm.py,dbscan.py, Kernels:ops.py)
GPU-accelerated clustering serves a critical engineering purpose: partitioning and analyzing large sets of high-dimensional representations (e.g. 512-dimensional image embeddings from a ResNet or dense text embeddings) directly within GPU device memory, completely bypassing CPU memory bandwidth bottlenecks.
Traditional libraries like Scikit-learn allocate intermediate matrices in host RAM, hitting severe PCIe bandwidth limits. netcl.cluster executes distance calculations, neighbor searches, and assignments directly within optimized OpenCL GPU kernels.
[!TIP] Theory & Curriculum Link: For the first-principles geometric derivations of Voronoi partitions, fused distance-argmin execution, and Graph Laplacians, see Curriculum: Clustering & Geometry and Curriculum: Spectral Graph Theory.
Algorithm Selection Guide
| Use Case | Recommended Algorithm | Distance Metric / Mechanism | Core Advantage |
|---|---|---|---|
| Standard Clustering with known cluster count $K$ | KMeans |
Euclidean (L2) | Ultra-fast with k-means++ seeding and fused argmin |
| Dataset exceeds GPU VRAM capacity | MiniBatchKMeans |
Euclidean (Streaming mini-batches) | Clusters millions of samples in chunks |
| Feature vectors from SSL or NLP | SphericalKMeans |
Cosine Distance (L2-normalized) | Measures direction and orientation, ignoring magnitude |
| Generating pseudo-labels for training (DeepCluster / SwAV) | BalancedKMeans |
Sinkhorn Equipartition | Prevents cluster collapse where one cluster swallows all points |
| Clusters have complex, non-convex shapes (rings, manifolds) | SpectralClustering |
k-NN Affinity Graph + LOBPCG | Maps graph topology into linearly separable eigenspaces |
| Unknown $K$ and dataset contains noise and outliers | DBSCAN |
Density-based ($\epsilon$-neighborhoods) | Marks noise as -1 and scales with linear $O(N)$ VRAM |
| Requiring soft posterior probabilities per cluster | GaussianMixture |
EM with diagonal covariance | Provides calibrated probabilistic cluster memberships |
1. Quick Example: GPU KMeans
import numpy as np
from netcl import cluster
from netcl.core.device import manager
from netcl.core.tensor import Tensor
q = manager.default("auto").queue
# 1. Prepare data (e.g. 10,000 embeddings of dimension 128)
X_np = np.random.randn(10000, 128).astype(np.float32)
X = Tensor.from_host(q, X_np)
# 2. Configure and fit KMeans
km = cluster.KMeans(
n_clusters=10,
init="k-means++",
max_iter=50,
seed=42,
).fit(X)
# 3. Retrieve results
labels = km.labels_ # NumPy int32 array of cluster assignments (0 to 9)
centers = km.cluster_centers_ # Tensor of shape (10, 128)
inertia = km.inertia_ # Sum of squared distances to closest centroid
# 4. Predict cluster assignments for new data points
new_samples = Tensor.from_host(q, np.random.randn(5, 128).astype(np.float32))
new_labels = km.predict(new_samples)
print("Assigned clusters for new samples:", new_labels)
Why NetCL is Memory-Efficient
Classical KMeans minimizes the Within-Cluster Sum of Squares (WCSS):
$$\min_{\mathcal{C} = \{\mathbf{c}_1, \dots, \mathbf{c}_K\}} \sum_{i=1}^N \min_{k=1, \dots, K} \|\mathbf{x}_i - \mathbf{c}_k\|_2^2$$
Standard implementations materialize an $(N \times K)$ distance matrix in VRAM. For $N = 100,000$ points and $K = 1,000$ clusters, that consumes 400 MB of VRAM for temporary distance values. At $N = 1,000,000$, it consumes 4 GB.
netcl.cluster.ops employs a Fused Distance-Argmin Kernel:
- Each GPU thread loads sample $\mathbf{x}_i \in \mathbb{R}^D$, streams through centroids $\mathbf{c}_k$, and tracks the running minimum distance directly inside private GPU registers.
- The $(N \times K)$ matrix is never written to global VRAM.
- Centroids are accumulated via a single atomic reduction directly in device buffers.
2. Spectral Clustering & Graph Manifolds
When clusters form nested spirals, rings, or continuous manifolds, KMeans fails because it cannot draw non-linear boundaries. SpectralClustering constructs a neighborhood affinity graph and embeds data into the eigenvectors of the Graph Laplacian.
# Spectral clustering over a k-NN affinity graph
sc = cluster.SpectralClustering(
n_clusters=3,
n_neighbors=15, # Connect each point to its 15 nearest neighbors
affinity="knn", # k-NN graph with Zelnik-Manor local density scaling
seed=42,
).fit(X)
cluster_labels = sc.labels_
embedding = sc.embedding_ # (N, n_clusters) spectral coordinates
The Spectral Pipeline
- k-NN Graph with Local Density Scaling: Rather than using a fixed global bandwidth, edge weights adapt to local neighborhood density:
$$\mathbf{W}_{ij} = \exp\left(-\frac{\|\mathbf{x}_i - \mathbf{x}_j\|_2^2}{\sigma_i \cdot \sigma_j}\right)$$
Where $\sigma_i$ is the Euclidean distance from $\mathbf{x}_i$ to its $k$-th nearest neighbor. Edges are symmetrized via $\mathbf{W} = \max(\mathbf{W}, \mathbf{W}^T)$.
- Normalized Symmetric Laplacian:
$$\mathbf{L}_{\text{sym}} = \mathbf{I} - \mathbf{D}^{-1/2} \mathbf{W} \mathbf{D}^{-1/2}$$
Where $\mathbf{D}_{ii} = \sum_j \mathbf{W}_{ij}$ is the degree matrix.
-
GPU LOBPCG Eigensolver: Standard power iteration requires thousands of steps when eigenvalues cluster tightly. NetCL employs LOBPCG (Locally Optimal Block Preconditioned Conjugate Gradient):
- All compute-heavy sparse matrix-vector multiplications run on the GPU.
- Only the tiny $(3K \times 3K)$ Rayleigh-Ritz subproblem is solved on the host.
- Typically converges in fewer than 150 iterations even on poorly conditioned graphs.
3. Balanced K-Means (Optimal Transport Equipartition)
When clustering representations in self-supervised pipelines (e.g. DeepCluster), standard KMeans often suffers from cluster collapse: 90% of data points fall into a single dominant cluster while others become empty.
BalancedKMeans enforces an equipartition constraint via the Sinkhorn-Knopp algorithm, solving an entropy-regularized optimal transport problem:
$$\min_{\mathbf{P} \in \mathcal{U}(\mathbf{r}, \mathbf{c})} \langle \mathbf{P}, \mathbf{M} \rangle - \varepsilon H(\mathbf{P})$$
Where $\mathbf{M}_{ik} = \|\mathbf{x}_i - \mathbf{c}_k\|_2^2$ is the cost matrix, and marginal constraints $\mathbf{r} = \frac{1}{N} \mathbf{1}_N$ and $\mathbf{c} = \frac{1}{K} \mathbf{1}_K$ guarantee that every cluster receives exactly $\frac{N}{K}$ samples.
# Guarantees that every cluster receives approximately N / K samples
bkm = cluster.BalancedKMeans(
n_clusters=10,
max_iter=50,
eps=0.05,
seed=42,
).fit(X)
pseudo_labels = bkm.labels_
4. Density-Based Clustering: DBSCAN
When cluster count $K$ is unknown or the dataset contains noisy outliers:
db = cluster.DBSCAN(
eps=0.4, # Neighborhood search radius
min_samples=5, # Minimum neighbors to qualify as a core point
metric="euclidean", # Or "cosine"
).fit(X)
labels = db.labels_ # -1 designates noise / outlier points
n_clusters = db.n_clusters_
print(f"Identified clusters: {n_clusters}, Noise points: {(labels == -1).sum()}")
5. Evaluating Cluster Quality
# Intrinsic evaluation (without ground-truth labels)
sil = cluster.silhouette_score(X, km.labels_)
db_score = cluster.davies_bouldin_score(X, km.labels_)
print(f"Silhouette Score: {sil:.3f} | Davies-Bouldin Index: {db_score:.3f}")
# Supervised evaluation against validation targets
report = cluster.cluster_report(y_true, km.labels_, X)
print(report)
The Silhouette coefficient $s(i)$ for sample $\mathbf{x}_i$ measures the ratio of intra-cluster distance $a(i)$ to nearest inter-cluster distance $b(i)$:
$$s(i) = \frac{b(i) - a(i)}{\max(a(i), b(i))} \in [-1, +1]$$
cluster_report computes Normalized Mutual Information (NMI), Adjusted Rand Index (ARI), Purity, and Hungarian-matched unsupervised classification accuracy.
Related Documentation
- Cluster API Reference: Signatures for all clustering classes and scoring functions.
- Curriculum: Clustering & Geometry: Theoretical derivation of Voronoi partitions.
- Curriculum: Spectral Graph Theory: Deep dive into Graph Laplacians and eigensolvers.