@@ -71,6 +71,94 @@ def compute_trajectory_nll(
7171 return turn_nll
7272
7373
74+ def _tool_output_ranges (
75+ segments : list [dict ],
76+ step_segment_indices : list [int ],
77+ ) -> dict [int , tuple [int , int ]]:
78+ """Return {turn_k: (start_token, end_token)} for each tool output segment."""
79+ ranges : dict [int , tuple [int , int ]] = {}
80+ for k , asst_seg_idx in enumerate (step_segment_indices ):
81+ if k == 0 or asst_seg_idx == 0 :
82+ continue
83+ tool_seg = segments [asst_seg_idx - 1 ]
84+ if tool_seg .get ("role" ) != "user" :
85+ continue
86+ start , end = tool_seg ["start_token" ], tool_seg ["end_token" ]
87+ if end > start and start > 0 :
88+ ranges [k ] = (start , end )
89+ return ranges
90+
91+
92+ def compute_trajectory_nll_chunked (
93+ token_ids : list [int ],
94+ segments : list [dict ],
95+ step_segment_indices : list [int ],
96+ model ,
97+ device ,
98+ chunk_size : int ,
99+ ) -> dict [int , float ]:
100+ """KV-cache chunked version of compute_trajectory_nll.
101+
102+ Identical semantics to compute_trajectory_nll but processes the sequence in
103+ chunks of chunk_size tokens (with accumulated KV cache), so peak GPU memory is
104+ O(chunk_size × vocab_size) rather than O(seq_len × vocab_size). Handles
105+ arbitrarily long sequences without OOM.
106+ """
107+ turn_ranges = _tool_output_ranges (segments , step_segment_indices )
108+ if not turn_ranges :
109+ return {}
110+
111+ seq_len = len (token_ids )
112+ input_ids = torch .tensor ([token_ids ], dtype = torch .long , device = device )
113+ # Accumulate per-token NLL values keyed by turn
114+ turn_nll_values : dict [int , list [float ]] = {k : [] for k in turn_ranges }
115+ past_kv = None
116+
117+ for chunk_start in range (0 , seq_len , chunk_size ):
118+ chunk_end = min (chunk_start + chunk_size , seq_len )
119+ chunk_input = input_ids [:, chunk_start :chunk_end ]
120+ # Attention mask must span past (cached) + current tokens
121+ chunk_attn = torch .ones (1 , chunk_end , dtype = torch .long , device = device )
122+
123+ with torch .no_grad ():
124+ out = model (
125+ input_ids = chunk_input ,
126+ attention_mask = chunk_attn ,
127+ past_key_values = past_kv ,
128+ use_cache = True ,
129+ output_hidden_states = False ,
130+ )
131+
132+ chunk_logits = out .logits [0 ] # [chunk_len, vocab_size], on GPU
133+
134+ for k , (tok_start , tok_end ) in turn_ranges .items ():
135+ # logits[j-1] predicts token[j]: need logit positions [tok_start-1, tok_end-1)
136+ logit_start = tok_start - 1
137+ logit_end = tok_end - 1
138+ # Intersect with the current chunk's logit positions [chunk_start, chunk_end)
139+ overlap_start = max (logit_start , chunk_start )
140+ overlap_end = min (logit_end , chunk_end )
141+ if overlap_start >= overlap_end :
142+ continue
143+ local_start = overlap_start - chunk_start
144+ local_end = overlap_end - chunk_start
145+ logit_slice = chunk_logits [local_start :local_end ] # [n, vocab]
146+ # Tokens predicted: token positions [overlap_start+1, overlap_end+1)
147+ actual = torch .tensor (
148+ token_ids [overlap_start + 1 : overlap_end + 1 ],
149+ dtype = torch .long , device = device ,
150+ )
151+ n = actual .shape [0 ]
152+ log_probs = torch .log_softmax (logit_slice .float (), dim = - 1 )
153+ nll_vals = - log_probs [torch .arange (n , device = device ), actual ]
154+ turn_nll_values [k ].extend (nll_vals .tolist ())
155+
156+ past_kv = out .past_key_values
157+ del out , chunk_logits
158+
159+ return {k : sum (v ) / len (v ) for k , v in turn_nll_values .items () if v }
160+
161+
74162def _load_model_adapter (adapter_name : str ):
75163 if adapter_name == "laguna" :
76164 from src .models .laguna import LagunaAdapter
@@ -93,6 +181,9 @@ def main() -> None:
93181 parser .add_argument ("--generation-config" , required = True )
94182 parser .add_argument ("--traj-dir" , required = True , help = "Directory of trajectory JSON files" )
95183 parser .add_argument ("--output" , required = True , help = "Output .pt path" )
184+ parser .add_argument ("--chunk-size" , type = int , default = 4096 ,
185+ help = "Tokens per forward-pass chunk (KV-cache chunking); "
186+ "bounds peak GPU memory to O(chunk_size × vocab_size)" )
96187 parser .add_argument ("--shard-rank" , type = int , default = 0 )
97188 parser .add_argument ("--num-shards" , type = int , default = 1 )
98189 args = parser .parse_args ()
@@ -101,15 +192,14 @@ def main() -> None:
101192 gen_config : GenerationConfig = load_config (args .generation_config , GenerationConfig )
102193
103194 out_path = Path (args .output )
104- # If sharded, accumulate into a shard-specific temp file; merge manually afterwards.
105- # For simplicity we write the whole shard's results to the output path (caller merges).
106195 if args .num_shards > 1 :
107196 out_path = out_path .with_suffix (f".shard{ args .shard_rank } .pt" )
108197
109198 print (f"Loading trajectories from { args .traj_dir } ..." )
110199 all_trajs = load_trajectories (args .traj_dir )
111200 trajs = all_trajs [args .shard_rank ::args .num_shards ]
112- print (f" { len (trajs )} trajectories (shard { args .shard_rank } /{ args .num_shards } )" )
201+ print (f" { len (trajs )} trajectories (shard { args .shard_rank } /{ args .num_shards } ), "
202+ f"chunk_size={ args .chunk_size } " )
113203
114204 model_adapter = _load_model_adapter (model_config .adapter )
115205 model_adapter .load_for_extraction (model_config , gen_config )
@@ -118,25 +208,16 @@ def main() -> None:
118208 index : dict [str , dict [int , float ]] = {}
119209
120210 for i , traj in enumerate (trajs ):
121- print (f" [{ i + 1 } /{ len (trajs )} ] { traj .sample_id } ({ len (traj .token_ids )} tokens)..." , flush = True )
122- input_ids = torch .tensor ([traj .token_ids ], dtype = torch .long , device = device )
123- try :
124- with torch .no_grad ():
125- out = model_adapter ._model (input_ids = input_ids , output_hidden_states = False )
126- logits = out .logits [0 ].cpu () # [seq_len, vocab_size] — move to CPU immediately
127- del out
128- torch .cuda .empty_cache ()
129- except torch .cuda .OutOfMemoryError :
130- torch .cuda .empty_cache ()
131- print (f" OOM — skipping { traj .sample_id } " )
132- continue
133-
134- turn_nll = compute_trajectory_nll (
135- traj .token_ids , traj .segments , traj .step_segment_indices , logits
211+ n_chunks = (len (traj .token_ids ) + args .chunk_size - 1 ) // args .chunk_size
212+ print (f" [{ i + 1 } /{ len (trajs )} ] { traj .sample_id } "
213+ f"({ len (traj .token_ids )} tokens, { n_chunks } chunks)..." , flush = True )
214+ turn_nll = compute_trajectory_nll_chunked (
215+ traj .token_ids , traj .segments , traj .step_segment_indices ,
216+ model_adapter ._model , device , args .chunk_size ,
136217 )
137- del logits
138218 index [traj .sample_id ] = turn_nll
139- print (f" { len (turn_nll )} turns with tool NLL (out of { len (traj .step_segment_indices )} total turns)" )
219+ print (f" { len (turn_nll )} turns with NLL "
220+ f"(out of { len (traj .step_segment_indices )} total turns)" )
140221
141222 out_path .parent .mkdir (parents = True , exist_ok = True )
142223 torch .save (index , out_path )
0 commit comments