@@ -29,6 +29,15 @@ class SamplerVersion:
2929 snapshot_path : str
3030
3131
32+ @dataclass (frozen = True )
33+ class PromotableCheckpoint :
34+ """Inference checkpoint persisted for later model promotion."""
35+
36+ snapshot_path : str
37+ checkpoint_resource : str | None
38+ checkpoint_type : str | None
39+
40+
3241@dataclass (frozen = True )
3342class FireworksInferenceEndpoint :
3443 """Native OpenAI-compatible endpoint for the managed deployment."""
@@ -72,6 +81,7 @@ def __init__(
7281 self .tokenizer = tokenizer
7382 self .config = config
7483 self ._state_lock = threading .Condition ()
84+ self ._provider_operation_lock = threading .RLock ()
7585 self ._publish_lock = asyncio .Lock ()
7686 self ._sampler : Any | None = None
7787 self ._sampler_identity : SamplerVersion | None = None
@@ -238,6 +248,91 @@ def _save() -> str:
238248 self ._next_version = version + 1
239249 return identity
240250
251+ def save_promotable_checkpoint (
252+ self ,
253+ checkpoint_name : str ,
254+ * ,
255+ appear_timeout_s : float = 90.0 ,
256+ poll_s : float = 3.0 ,
257+ ) -> PromotableCheckpoint :
258+ with self ._provider_operation_lock :
259+ with self ._state_lock :
260+ if self ._closed :
261+ raise RuntimeError ("Fireworks runtime is closed" )
262+
263+ result = self .training_client .save_weights_for_sampler (
264+ checkpoint_name ,
265+ checkpoint_type = "base" ,
266+ ).result (timeout = self .config .request_timeout_s )
267+ snapshot_path = str (getattr (result , "path" , "" ) or "" )
268+ if not snapshot_path :
269+ raise RuntimeError (f"Fireworks save_weights_for_sampler({ checkpoint_name !r} ) returned no path" )
270+
271+ snapshot_id = snapshot_path .rstrip ("/" ).rsplit ("/" , 1 )[- 1 ]
272+ deadline = time .monotonic () + appear_timeout_s
273+ matches : list [dict ] = []
274+ while time .monotonic () < deadline :
275+ try :
276+ rows = self .service .list_checkpoints (self .trainer_job_id )
277+ except Exception :
278+ time .sleep (poll_s )
279+ continue
280+ matches = [row for row in rows if row .get ("promotable" ) and _checkpoint_short_name (row ) == snapshot_id ]
281+ if matches :
282+ break
283+ time .sleep (poll_s )
284+ if len (matches ) > 1 :
285+ raise RuntimeError (
286+ f"Expected at most one promotable Fireworks checkpoint for { checkpoint_name !r} , "
287+ f"got { len (matches )} "
288+ )
289+ if not matches :
290+ warnings .warn (
291+ f"Promotable Fireworks checkpoint { checkpoint_name !r} was saved but did not "
292+ "surface on the control plane before the visibility timeout" ,
293+ RuntimeWarning ,
294+ )
295+ return PromotableCheckpoint (
296+ snapshot_path = snapshot_path ,
297+ checkpoint_resource = None ,
298+ checkpoint_type = None ,
299+ )
300+ checkpoint = matches [0 ]
301+ return PromotableCheckpoint (
302+ snapshot_path = snapshot_path ,
303+ checkpoint_resource = checkpoint ["name" ],
304+ checkpoint_type = checkpoint .get ("checkpointType" ),
305+ )
306+
307+ async def promote_checkpoint_resource (
308+ self ,
309+ * ,
310+ checkpoint : PromotableCheckpoint ,
311+ output_model_id : str ,
312+ ) -> dict [str , Any ]:
313+ if not checkpoint .checkpoint_resource :
314+ raise ValueError ("Promotable checkpoint has no control-plane resource" )
315+
316+ def _promote () -> dict :
317+ with self ._provider_operation_lock :
318+ with self ._state_lock :
319+ if self ._closed :
320+ raise RuntimeError ("Fireworks runtime is closed" )
321+ return self .service .promote_checkpoint (
322+ name = checkpoint .checkpoint_resource ,
323+ output_model_id = output_model_id ,
324+ base_model = self .config .base_model ,
325+ )
326+
327+ model = await asyncio .to_thread (_promote )
328+ return {
329+ "sampler_path" : checkpoint .snapshot_path ,
330+ "checkpoint_resource" : checkpoint .checkpoint_resource ,
331+ "checkpoint_type" : checkpoint .checkpoint_type ,
332+ "output_model_id" : output_model_id ,
333+ "model" : model ,
334+ }
335+
241336 async def promote_final_model (
242337 self ,
243338 * ,
@@ -397,4 +492,9 @@ def _wait_for_samples() -> tuple[Any | None, int]:
397492 )
398493 if sampler is not None :
399494 await asyncio .to_thread (_close_quietly , sampler )
400- await asyncio .to_thread (_close_quietly , self .service )
495+
496+ def _close_service () -> None :
497+ with self ._provider_operation_lock :
498+ _close_quietly (self .service )
499+
500+ await asyncio .to_thread (_close_service )
0 commit comments