@@ -1847,3 +1847,89 @@ def test_every_convertible_parameter_has_a_map(self):
18471847 assert not missing , (f"these parameters are placed by a name category but carry no affine map, "
18481848 f"so conversion still depends on the category: { sorted (missing )} " )
18491849 engine .destroy ()
1850+
1851+
1852+ def _uneven_vocab_engine (tp_size , load_universal = False ):
1853+ """A vocabulary head split 101 ways over `tp_size` ranks, so the shards are uneven."""
1854+ torch .manual_seed (42 )
1855+ model = UnevenVocabLmHeadModel (12 , 101 )
1856+ optimizer = torch .optim .Adam (model .parameters (), lr = 1e-3 )
1857+ config = {
1858+ "train_micro_batch_size_per_gpu" : 1 ,
1859+ "zero_allow_untested_optimizer" : True ,
1860+ "zero_optimization" : {
1861+ "stage" : 1
1862+ },
1863+ "checkpoint" : {
1864+ "load_universal" : load_universal
1865+ },
1866+ }
1867+ if tp_size > 1 :
1868+ config ["tensor_parallel" ] = {
1869+ "autotp_size" : tp_size ,
1870+ "partition_config" : {
1871+ "use_default_specs" :
1872+ False ,
1873+ "layer_specs" : [{
1874+ "patterns" : [r".*lm_head\.weight$" ],
1875+ "partition_type" : "column" ,
1876+ "gather_output" : True ,
1877+ }],
1878+ },
1879+ }
1880+ engine , _ , _ , _ = deepspeed .initialize (model = model , optimizer = optimizer , config = config )
1881+ return engine
1882+
1883+
1884+ class uneven_vocab_checkpoint (DistributedFixture ):
1885+ world_size = 2
1886+
1887+ def run (self , tmpdir ):
1888+ from deepspeed .checkpoint .constants import AFFINE_MAP , AFFINE_MAP_PARAMS
1889+ from deepspeed .module_inject .layers import collect_autotp_universal_checkpoint_info
1890+
1891+ engine = _uneven_vocab_engine (self .world_size )
1892+ _train_steps (engine , hidden_dim = 12 )
1893+
1894+ # Pin that the checkpoint below is converted through the map rather than the
1895+ # vocabulary category, and that the map carries the real 51/50 split.
1896+ maps = collect_autotp_universal_checkpoint_info (engine .module )[AFFINE_MAP ][AFFINE_MAP_PARAMS ]
1897+ head = maps [r"^lm_head\.weight$" ]
1898+ assert [head ["ranks" ][rank ]["shard_shape" ][0 ] for rank in sorted (head ["ranks" ])] == [51 , 50 ]
1899+
1900+ tp_group = groups .get_tensor_model_parallel_group ()
1901+ weight = _all_gather_cat_dim0 (engine .module .lm_head .weight .detach (), tp_group ).cpu ()
1902+ bias = _all_gather_cat_dim0 (engine .module .lm_head .bias .detach ().view (- 1 , 1 ), tp_group ).view (- 1 ).cpu ()
1903+ if dist .get_rank () == 0 :
1904+ torch .save ({"weight" : weight , "bias" : bias }, os .path .join (tmpdir , "uneven_vocab_reference.pt" ))
1905+
1906+ _save_and_convert (engine , tmpdir )
1907+ engine .destroy ()
1908+
1909+
1910+ @pytest .mark .parametrize ("world_size" , [1 , 2 ], ids = ["tp1" , "tp2" ])
1911+ class TestUnevenVocabCrossTpResume (DistributedTest ):
1912+ """A vocabulary head saved at TP2 must restore at a different TP degree.
1913+
1914+ 101 rows over two ranks gives shards of 51 and 50, so the map has to carry the per-rank
1915+ extents rather than assume an even split. Restoring at TP1 then merges two unequal
1916+ shards into one tensor, which is where an even-split assumption would show up.
1917+ """
1918+
1919+ def test_resume_from_tp2 (self , uneven_vocab_checkpoint , tmpdir , world_size ):
1920+ tp_size = dist .get_world_size ()
1921+ engine = _uneven_vocab_engine (tp_size , load_universal = True )
1922+ engine .load_checkpoint (tmpdir , tag = UNIVERSAL_TAG , load_module_only = True )
1923+
1924+ reference = torch .load (os .path .join (tmpdir , "uneven_vocab_reference.pt" ))
1925+ if tp_size > 1 :
1926+ tp_group = groups .get_tensor_model_parallel_group ()
1927+ weight = _all_gather_cat_dim0 (engine .module .lm_head .weight .detach (), tp_group ).cpu ()
1928+ bias = _all_gather_cat_dim0 (engine .module .lm_head .bias .detach ().view (- 1 , 1 ), tp_group ).view (- 1 ).cpu ()
1929+ else :
1930+ weight = engine .module .lm_head .weight .detach ().cpu ()
1931+ bias = engine .module .lm_head .bias .detach ().cpu ()
1932+
1933+ torch .testing .assert_close (weight , reference ["weight" ])
1934+ torch .testing .assert_close (bias , reference ["bias" ])
1935+ engine .destroy ()
0 commit comments