11import json
22import logging
3+ import time
34import warnings
45from pathlib import Path
56from typing import Literal , NamedTuple
3738root_logger = logging .getLogger ("speculators" )
3839metric_logger = logging .getLogger ("speculators.metrics" )
3940
41+
42+ class _StepTimer :
43+ # Each mark()/now() forces a cuda.synchronize to capture true GPU time.
44+ # This serialises the CUDA pipeline, so profiled steps are slower; keep
45+ # log_freq > 1 in perf-sensitive runs.
46+ def __init__ (self , enabled : bool = False ):
47+ self .enabled = enabled
48+ self ._marks : dict [str , float ] = {}
49+
50+ def reset (self , enabled : bool ) -> None :
51+ self .enabled = enabled
52+ self ._marks .clear ()
53+
54+ def mark (self , name : str ) -> None :
55+ if self .enabled :
56+ torch .cuda .synchronize ()
57+ self ._marks [name ] = time .perf_counter ()
58+
59+ def mark_value (self , name : str , value : float ) -> None :
60+ if self .enabled :
61+ self ._marks [name ] = value
62+
63+ def now (self ) -> float | None :
64+ if not self .enabled :
65+ return None
66+ torch .cuda .synchronize ()
67+ return time .perf_counter ()
68+
69+ def profile (self , num_tokens : int ) -> dict [str , float ] | None :
70+ if not self .enabled :
71+ return None
72+ m = self ._marks
73+ has_start = "start" in m
74+ fwd_ms = (m ["fwd" ] - m ["fetch" ]) * 1000
75+ bwd_ms = (m ["bwd" ] - m ["fwd" ]) * 1000
76+ opt_ms = (m ["opt" ] - m ["bwd" ]) * 1000
77+ fetch_ms = (m ["fetch" ] - m ["start" ]) * 1000 if has_start else 0.0
78+ step_ms = (m ["opt" ] - m ["start" ]) * 1000 if has_start else 0.0
79+ tokens_per_s = num_tokens / (step_ms / 1000 ) if step_ms > 0 else 0.0
80+ fetch_frac = fetch_ms / step_ms if step_ms > 0 else 0.0
81+ return {
82+ "fetch_ms" : fetch_ms ,
83+ "fwd_ms" : fwd_ms ,
84+ "bwd_ms" : bwd_ms ,
85+ "opt_ms" : opt_ms ,
86+ "step_ms" : step_ms ,
87+ "tokens_per_s" : tokens_per_s ,
88+ "fetch_frac" : fetch_frac ,
89+ }
90+
91+
4092warnings .filterwarnings ("ignore" , category = TqdmExperimentalWarning )
4193MIN_STEP_PCT = 0.25
4294
@@ -326,31 +378,45 @@ def train_epoch(self, epoch: int):
326378 if self .config .checkpoint_freq < 1
327379 else None
328380 )
381+ t_before_fetch = time .perf_counter ()
382+ timer = _StepTimer ()
329383 for local_step_rel , batch in enumerate (train_loader , 1 ):
330384 # local_step is 1-based index into the *full* epoch (not the slice).
331385 local_step = local_step_rel + skip_steps
386+ timer .reset (self .global_step % self .config .log_freq == 0 )
387+
388+ timer .mark_value ("start" , t_before_fetch )
332389 gpu_batch = {
333390 k : v .to (self .local_rank , non_blocking = True )
334391 if isinstance (v , torch .Tensor )
335392 else v
336393 for k , v in batch .items ()
337394 }
338395
396+ timer .mark ("fetch" )
339397 _draft_tokens , loss , metrics = self .model (
340398 ** gpu_batch , ** self .config .train_call_kwargs
341399 )
342400
401+ timer .mark ("fwd" )
343402 self ._optimizers_zero_grad ()
344403 loss .backward ()
345404 torch .nn .utils .clip_grad_norm_ (self .model .parameters (), 1.0 )
405+
406+ timer .mark ("bwd" )
346407 self ._optimizers_step ()
347408
348409 current_lrs = {
349410 type (opt ).__name__ : opt .param_groups [0 ]["lr" ] for opt in self .optimizers
350411 }
351412 self ._schedulers_step ()
413+ timer .mark ("opt" )
414+ t_before_fetch = timer .now () or time .perf_counter ()
352415
353- if self .global_step % self .config .log_freq == 0 :
416+ profile = None
417+ if timer .enabled :
418+ num_tokens = int ((gpu_batch ["document_ids" ] != - 1 ).sum ().item ())
419+ profile = timer .profile (num_tokens )
354420 if self .is_distributed :
355421 for v in metrics .values ():
356422 dist .reduce (v , dst = 0 , op = dist .ReduceOp .SUM )
@@ -366,6 +432,7 @@ def train_epoch(self, epoch: int):
366432 metric_logger .info (
367433 {
368434 "train" : metrics ,
435+ "profile" : profile ,
369436 "epoch" : epoch ,
370437 "lr" : lr_info ,
371438 "global_step" : self .global_step ,
0 commit comments