Skip to content

Commit bb16d77

Browse files
committed
Keep palettizer _raw_lut computation on the same device as the weight
1 parent 92a206f commit bb16d77

2 files changed

Lines changed: 69 additions & 9 deletions

File tree

src/coreai_opt/palettization/kmeans/kmeans_fake_palettize.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,10 @@ def _initialize(self, weight: torch.Tensor) -> None:
192192
"""
193193
self.centroids, indices = self._cluster_to_centroids(weight, self._sensitivities)
194194
self.indices = indices.detach()
195+
196+
# Run self.quantize_lut to seed lut quantizer qparams
195197
self.quantize_lut(self._raw_lut(self.centroids))
198+
196199
self._centroids_initialized = True
197200
self._indices_stale = False
198201

@@ -391,9 +394,9 @@ def _combine_block_indices(
391394
@torch.no_grad()
392395
def _raw_lut(self, centroids: torch.Tensor) -> torch.Tensor:
393396
"""Reshape ``centroids`` (P, K, D) to the pre-quantization LUT shape
394-
``(P, K[, D])`` on CPU, detached.
397+
``(P, K[, D])``, detached.
395398
"""
396-
centroids = centroids.detach().cpu()
399+
centroids = centroids.detach()
397400
return centroids if self.cluster_dim > 1 else centroids.squeeze(-1)
398401

399402
def _palettize(
@@ -417,6 +420,8 @@ def _palettize(
417420
clustered_weight = None
418421
axis = self.granularity.axis if self.granularity.axis else 0
419422

423+
lut = lut.to(indices.device)
424+
420425
# Reshape indices back to 2D for block processing (reverse of
421426
# reshape_to_original in _assign_indices)
422427
indices = self.reshape_strategy.reshape_for_kmeans(indices, axis)

tests/palettization/test_kmeans_fake_palettize.py

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -474,9 +474,8 @@ def test_disabled_flag_behavior(self):
474474

475475
@pytest.mark.skipif(not torch.backends.mps.is_available(), reason="MPS not available")
476476
def test_mps_device_handling(self):
477-
"""
478-
Test that palettization preserves MPS device for weights while
479-
using CPU for computation.
477+
"""Palettization keeps the LUT on the weight's device while the weight-sized
478+
indices stay CPU-resident, and reconstruction returns on the original device.
480479
"""
481480
# Create weights on MPS device
482481
weight = torch.randn(4, 8, dtype=torch.float32, device="mps")
@@ -494,14 +493,15 @@ def test_mps_device_handling(self):
494493
enable_per_channel_scale=spec.enable_per_channel_scale,
495494
)
496495

497-
# Calculate centroids - this should move computation to CPU internally
496+
# Clustering runs on CPU internally, but centroids are placed on the weight's device.
498497
lut, indices = _initialize_and_get_lut_indices(palettizer, weight)
499498

500-
# Verify LUT and indices are on CPU (expected behavior)
501-
assert lut.device.type == "cpu"
499+
# The LUT follows the weight's device (kept on-device so training-time LUT
500+
# quantization stays on-device); the weight-sized indices stay CPU-resident.
501+
assert lut.device.type == "mps"
502502
assert indices.device.type == "cpu"
503503

504-
# Palettize the weights - this should return result on original device (MPS)
504+
# Reconstruction gathers against the CPU indices and returns on the original device (MPS).
505505
palettized_weight = palettizer._palettize(lut, indices, weight)
506506

507507
# Verify the palettized weights are back on MPS
@@ -1826,3 +1826,58 @@ def test_blocks_to_cluster_raises_on_indivisible_cluster_dim(self):
18261826
weight_2d = torch.randn(4, 16) # axis-0 size 4 not divisible by cluster_dim 3
18271827
with pytest.raises(_IncompatibleClusterDimError):
18281828
palettizer._blocks_to_cluster(weight_2d, axis=0)
1829+
1830+
1831+
def _accelerator_device() -> str | None:
1832+
"""Return an available accelerator device type ("cuda" or "mps"), else None."""
1833+
if torch.cuda.is_available():
1834+
return "cuda"
1835+
if torch.backends.mps.is_available():
1836+
return "mps"
1837+
return None
1838+
1839+
1840+
@pytest.mark.skipif(_accelerator_device() is None, reason="requires a CUDA or MPS accelerator")
1841+
def test_device_placement_on_accelerator():
1842+
"""LUT quantization and reconstruction device behavior on an accelerator.
1843+
1844+
Checks in one pass that:
1845+
- centroids follow the model device; ``indices`` stays on CPU (memory),
1846+
- ``_raw_lut`` and the ``lut`` property follow the device (not forced to CPU),
1847+
- ``quantize_lut`` (the training-side observe path) stays on the input device
1848+
with no forced CPU round-trip,
1849+
- ``hard_assign`` (eval) gathers against the CPU ``indices`` and returns on the
1850+
weight's device -- no accelerator/cpu device mismatch.
1851+
"""
1852+
device = _accelerator_device()
1853+
1854+
spec = PalettizationSpec(
1855+
n_bits=2,
1856+
granularity=PerTensorGranularity(),
1857+
cluster_dim=1,
1858+
lut_qspec=_make_lut_qspec(torch.int8),
1859+
)
1860+
palettizer = _KMeansFakePalettize(**spec.__dict__)
1861+
weight = torch.randn(8, 8, device=device)
1862+
1863+
# Clusters on CPU, places centroids on the weight's device, and seeds the LUT
1864+
# quantizer on-device via quantize_lut(_raw_lut(centroids)).
1865+
palettizer._initialize(weight)
1866+
1867+
# centroids follow the model device; the weight-sized indices stay CPU-resident.
1868+
assert palettizer.centroids.device.type == device
1869+
assert palettizer.indices.device.type == "cpu"
1870+
1871+
# _raw_lut and the lut property follow the centroids' device (not forced CPU).
1872+
raw_lut = palettizer._raw_lut(palettizer.centroids)
1873+
assert raw_lut.device.type == device
1874+
assert palettizer.lut.device.type == device
1875+
1876+
# Quantizing an on-device LUT stays on-device.
1877+
assert palettizer.quantize_lut(raw_lut).device.type == device
1878+
1879+
# Eval path: reconstruction gathers against the CPU indices and returns on the
1880+
# weight's device.
1881+
out = palettizer.hard_assign(weight)
1882+
assert out.device.type == device
1883+
assert out.shape == weight.shape

0 commit comments

Comments
 (0)