> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.nvidia.com/cuvs/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.nvidia.com/cuvs/_mcp/server.

# Gaussian Mixture Model

A Gaussian Mixture Model (GMM) is a GPU-accelerated probabilistic clustering and density-estimation algorithm. It models a dataset as a weighted sum of `n_components` Gaussian components, learning a weight, mean, and covariance for each one with the Expectation-Maximization (EM) algorithm.

Use a GMM when you want soft cluster assignments (a probability that each row belongs to each component), a generative density model you can score new points against, or clusters that are elliptical rather than spherical. Unlike K-Means, which assigns every row to exactly one centroid, a GMM returns a full responsibility distribution over components and captures per-component shape through its covariance. Its primary outputs are the component weights, means, covariances (and their Cholesky-factored precisions), per-row labels, and the converged log-likelihood lower bound.

## Example API Usage

[C++ API](/api-reference/cpp-api-cluster-gmm)

### Fitting a mixture

Fitting learns the component weights, means, and covariances from a dataset on the device. The covariance-shaped outputs (`covariances`, `precisions_chol`, `precisions`) have a layout that depends on `covariance_type`; see the Covariance types section below for the exact shapes.

```cpp
#include <cuvs/cluster/gmm.hpp>

#include <raft/core/device_mdarray.hpp>
#include <raft/core/resources.hpp>

using namespace cuvs::cluster;

raft::device_resources res;
raft::device_matrix_view<const float, int64_t> dataset = load_dataset();

gmm::params params;
params.n_components = 1024;
params.cov_type = gmm::covariance_type::FULL;
params.max_iter = 100;
params.tol = 1e-3;

// Output buffers; covariance-shaped extents follow params.cov_type.
auto [weights, means, covariances, precisions_chol, precisions, labels] =
    allocate_outputs(res, params, dataset);

float lower_bound;
int n_iter;
bool converged;

gmm::fit(res,
         params,
         dataset,
         weights.view(),
         means.view(),
         covariances.view(),
         precisions_chol.view(),
         precisions.view(),
         labels.view(),
         raft::make_host_scalar_view(&lower_bound),
         raft::make_host_scalar_view(&n_iter),
         raft::make_host_scalar_view(&converged));
```

### Assigning labels and scoring

After fitting, reuse the learned `weights`, `means`, and `precisions_chol` to assign hard labels (`predict`), produce per-component responsibilities (`predict_proba`), or evaluate the per-row log-likelihood of new data (`score_samples`).

```cpp
int64_t n_samples = dataset.extent(0);

// Hard label per row (argmax responsibility).
auto labels = raft::make_device_vector<int, int64_t>(res, n_samples);
gmm::predict(
  res, params, dataset, weights.view(), means.view(), precisions_chol.view(), labels.view());

// (n_samples, n_components) responsibility matrix.
auto resp = raft::make_device_matrix<float, int64_t>(res, n_samples, params.n_components);
gmm::predict_proba(
  res, params, dataset, weights.view(), means.view(), precisions_chol.view(), resp.view());

// Per-row log-likelihood under the fitted mixture.
auto log_prob = raft::make_device_vector<float, int64_t>(res, n_samples);
gmm::score_samples(
  res, params, dataset, weights.view(), means.view(), precisions_chol.view(), log_prob.view());
```

## How GMM works

EM alternates between two steps until the average log-likelihood stops improving:

1. **E-step**: given the current parameters, compute the responsibility of each component for each row — the posterior probability that the row was generated by that component.
2. **M-step**: given the responsibilities, update each component's weight, mean, and covariance to the responsibility-weighted statistics of the data.

The algorithm repeats until it reaches `max_iter` or the per-sample average log-likelihood changes by less than `tol`. Because both steps reduce to dense linear algebra over many rows and components, the GPU is well suited to the work.

## Covariance types

`covariance_type` controls how much shape each component can express, trading flexibility for parameters and cost. The covariance-shaped buffers (`covariances`, `precisions_chol`, `precisions`) are passed as flat device vectors because their logical shape depends on the covariance type. With `K = n_components` and `d = n_features` the expected lengths are (row-major):

| Type | Description | Buffer length | Logical shape |
| --- | --- | --- | --- |
| `full` | Each component has its own full covariance matrix. Most flexible, most expensive. | `K * d * d` | `(K, d, d)` |
| `tied` | All components share a single full covariance matrix. | `d * d` | `(d, d)` |
| `diag` | Each component has its own diagonal covariance (axis-aligned ellipsoids). | `K * d` | `(K, d)` |
| `spherical` | Each component has a single variance (isotropic). Fewest parameters, fastest. | `K` | `(K,)` |

For `full`/`tied`, `precisions_chol` holds the upper-triangular factor `U` of each precision matrix (precision `= U @ Uᵀ`); for `diag`/`spherical` it holds reciprocal standard deviations. These conventions match scikit-learn's `GaussianMixture`.

## When to use

Use a GMM when soft, probabilistic assignments matter, when components are elliptical or have different shapes, or when you need a density model to score or compare new points. Prefer `full` or `diag` covariances when component shape is informative, and `spherical` or `tied` when data is limited or speed matters more than per-component shape. If you only need hard, roughly spherical partitions, K-Means is simpler and faster.

## Configuration parameters

| Parameter | Default | Description |
| --- | --- | --- |
| `n_components` | `1` | Number of mixture components (at most 65535). Larger values fit finer structure but increase work and parameter memory. |
| `covariance_type` | `full` | Covariance parameterization (`full`, `tied`, `diag`, `spherical`). |
| `tol` | `1e-3` | Convergence threshold on the change of the per-sample average log-likelihood. |
| `reg_covar` | `1e-6` | Non-negative regularization added to the covariance diagonal for numerical stability. |
| `max_iter` | `100` | Maximum number of EM iterations for one run. |
| `n_init` | `1` | Number of independent runs with different seeds; the best result is kept. |
| `init_method` | `kmeans` | Responsibility initialization: `kmeans`, `k-means++`, `random`, or `random_from_data`. Note: `k-means++` assigns every row to its nearest k-means++ seed, whereas scikit-learn one-hots only the seed rows themselves — the two libraries start EM from different responsibilities for this init. |
| `seed` | `0` | Seed for the random number generator. |

## Tuning

Start with `n_components` and `covariance_type`. More components and richer covariances capture more structure but cost more memory and time, and can overfit when data is limited; raise `reg_covar` if covariances become ill-conditioned. Use `kmeans` initialization for robust default seeding, and increase `n_init` when different seeds produce noticeably different log-likelihoods. Tune `max_iter` and `tol` together: if `n_iter` regularly reaches `max_iter`, increase `max_iter` or relax `tol`.

## Memory footprint

Fitting streams the E and M steps over tiles of rows, so it never materializes the full `(n_samples, n_components)` responsibility matrix — peak device memory stays bounded by the input data, the model parameters, and one responsibility tile, independent of `n_samples`. `predict` and `score_samples` likewise avoid the full responsibility matrix; only `predict_proba` materializes the `(n_samples, n_components)` output because that matrix is its result.