@@ -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