@@ -286,5 +286,198 @@ async def test_missing_reply_has_deadline(self):
286286 )
287287
288288
289+ mllog_lib = load ("mllog_utils_under_test" , ROOT / "tunix/utils/mllog_utils.py" )
290+
291+
292+ class OfflineEvalRcpTest (unittest .IsolatedAsyncioTestCase ):
293+
294+ def test_compute_val_start_step (self ):
295+ self .assertEqual (mllog_lib .compute_val_start_step (256 ), 18 )
296+ self .assertEqual (mllog_lib .compute_val_start_step (128 ), 33 )
297+ self .assertEqual (mllog_lib .compute_val_start_step (256 , 5 ), 5 )
298+ with self .assertRaises (ValueError ):
299+ mllog_lib .compute_val_start_step (0 )
300+
301+ def test_checkpoint_manifest_read_write_and_contiguity (self ):
302+ with tempfile .TemporaryDirectory () as tmpdir :
303+ manifest_path = str (Path (tmpdir ) / "eval_checkpoints.jsonl" )
304+ mllog_lib .append_checkpoint_manifest (
305+ manifest_path ,
306+ {
307+ "step" : 19 ,
308+ "samples_count" : 4864 ,
309+ "checkpoint_timestamp_ms" : 2000 ,
310+ "checkpoint_path" : "gs://ckpt/19/model_params" ,
311+ "val_start_at" : 18 ,
312+ "max_steps" : 50 ,
313+ },
314+ )
315+ mllog_lib .append_checkpoint_manifest (
316+ manifest_path ,
317+ {
318+ "step" : 18 ,
319+ "samples_count" : 4608 ,
320+ "checkpoint_timestamp_ms" : 1000 ,
321+ "checkpoint_path" : "gs://ckpt/18/model_params" ,
322+ "val_start_at" : 18 ,
323+ "max_steps" : 50 ,
324+ },
325+ )
326+ records = mllog_lib .load_checkpoint_manifest (
327+ manifest_path , check_contiguous = True
328+ )
329+ self .assertEqual ([r ["step" ] for r in records ], [18 , 19 ])
330+
331+ # Non-contiguous step should raise ValueError
332+ mllog_lib .append_checkpoint_manifest (
333+ manifest_path ,
334+ {
335+ "step" : 21 ,
336+ "samples_count" : 5376 ,
337+ "checkpoint_timestamp_ms" : 4000 ,
338+ "checkpoint_path" : "gs://ckpt/21/model_params" ,
339+ "val_start_at" : 18 ,
340+ "max_steps" : 50 ,
341+ },
342+ )
343+ with self .assertRaisesRegex (ValueError , "Non-contiguous" ):
344+ mllog_lib .load_checkpoint_manifest (
345+ manifest_path , check_contiguous = True
346+ )
347+
348+ def test_summarize_includes_pass_at_4 (self ):
349+ rows = [
350+ {"instance_id" : "inst_1" , "attempt" : i , "resolved" : i == 0 , "reward" : 1.0 if i == 0 else 0.0 , "status" : "ok" }
351+ for i in range (4 )
352+ ]
353+ summary = eval_lib .summarize (rows , ["inst_1" ], 4 )
354+ self .assertIn ("4" , summary ["pass_at_k" ])
355+ self .assertAlmostEqual (summary ["pass_at_k" ]["4" ], 1.0 )
356+ self .assertAlmostEqual (summary ["mean_reward" ], 0.25 )
357+
358+ def test_offline_eval_rcp_sequence_converged_and_aborted (self ):
359+ fake_mllogger = mock .MagicMock ()
360+ with (
361+ mock .patch .object (mllog_lib , "mllogger" , fake_mllogger ),
362+ mock .patch .object (mllog_lib , "_is_master_process" , return_value = True ),
363+ mock .patch .object (mllog_lib , "_flush_to_gcs_if_needed" ),
364+ ):
365+ args = types .SimpleNamespace (
366+ batch_size = 16 , num_generations = 16 , max_steps = 50
367+ )
368+ mllog_lib .train_stop (args , step = 50 , time_ms = 5000 )
369+ fake_mllogger .end .assert_called_once_with (
370+ key = "block_stop" ,
371+ metadata = {"step" : 50 , "samples_count" : 12800 },
372+ time_ms = 5000 ,
373+ )
374+ fake_mllogger .reset_mock ()
375+
376+ # Step 18: accuracy 0.68 < 0.69 -> no run_stop
377+ passed_18 = mllog_lib .log_offline_eval_step (
378+ step = 18 ,
379+ samples_count = 4608 ,
380+ eval_accuracy = 0.68 ,
381+ target_accuracy = 0.69 ,
382+ checkpoint_timestamp_ms = 1000 ,
383+ is_last_checkpoint = False ,
384+ validation_time = 12.5 ,
385+ )
386+ self .assertFalse (passed_18 )
387+ end_keys_18 = [c .kwargs ["key" ] for c in fake_mllogger .end .call_args_list ]
388+ self .assertEqual (end_keys_18 , ["eval_stop" ])
389+ fake_mllogger .reset_mock ()
390+
391+ # Step 19: accuracy 0.71 >= 0.69 -> backdated run_stop(time_ms=2000, status="success") + train_samples=4864
392+ passed_19 = mllog_lib .log_offline_eval_step (
393+ step = 19 ,
394+ samples_count = 4864 ,
395+ eval_accuracy = 0.71 ,
396+ target_accuracy = 0.69 ,
397+ checkpoint_timestamp_ms = 2000 ,
398+ is_last_checkpoint = False ,
399+ validation_time = 14.0 ,
400+ )
401+ self .assertTrue (passed_19 )
402+ fake_mllogger .end .assert_any_call (
403+ key = "run_stop" ,
404+ metadata = {"status" : "success" , "samples_count" : 4864 },
405+ time_ms = 2000 ,
406+ )
407+ fake_mllogger .event .assert_any_call (
408+ key = "train_samples" ,
409+ value = 4864 ,
410+ )
411+ fake_mllogger .reset_mock ()
412+
413+ # Step 50 (final): accuracy 0.68 < 0.69 -> backdated run_stop(time_ms=5000, status="aborted")
414+ passed_50 = mllog_lib .log_offline_eval_step (
415+ step = 50 ,
416+ samples_count = 12800 ,
417+ eval_accuracy = 0.68 ,
418+ target_accuracy = 0.69 ,
419+ checkpoint_timestamp_ms = 5000 ,
420+ is_last_checkpoint = True ,
421+ validation_time = 15.0 ,
422+ )
423+ self .assertFalse (passed_50 )
424+ fake_mllogger .end .assert_any_call (
425+ key = "run_stop" ,
426+ metadata = {"status" : "aborted" , "samples_count" : 12800 },
427+ time_ms = 5000 ,
428+ )
429+
430+ async def test_controller_manifest_loop_stops_on_convergence (self ):
431+ with tempfile .TemporaryDirectory () as tmpdir :
432+ manifest_path = str (Path (tmpdir ) / "eval_checkpoints.jsonl" )
433+ for step , ts_ms in ((18 , 1000 ), (19 , 2000 ), (20 , 3000 )):
434+ mllog_lib .append_checkpoint_manifest (
435+ manifest_path ,
436+ {
437+ "step" : step ,
438+ "samples_count" : step * 256 ,
439+ "checkpoint_timestamp_ms" : ts_ms ,
440+ "checkpoint_path" : f"gs://ckpt/{ step } /model_params" ,
441+ "val_start_at" : 18 ,
442+ "max_steps" : 20 ,
443+ },
444+ )
445+
446+ evaluated_steps = []
447+
448+ async def fake_single_controller (step_args ):
449+ evaluated_steps .append (step_args .checkpoint_step )
450+ reached = step_args .checkpoint_step == 19
451+ return {
452+ "checkpoint_step" : step_args .checkpoint_step ,
453+ "checkpoint_timestamp_ms" : step_args .checkpoint_timestamp_ms ,
454+ "samples_count" : step_args .samples_count ,
455+ "target_reached" : reached ,
456+ "rcp_logged" : True ,
457+ }
458+
459+ a = eval_lib .parse_args ([
460+ "--checkpoint_manifest_file" ,
461+ manifest_path ,
462+ "--output_dir" ,
463+ str (Path (tmpdir ) / "results" ),
464+ ])
465+ with (
466+ mock .patch .dict (
467+ sys .modules ,
468+ {"tunix.utils" : types .SimpleNamespace (mllog_utils = mllog_lib )},
469+ ),
470+ mock .patch .object (
471+ eval_lib , "_run_single_controller" , side_effect = fake_single_controller
472+ ),
473+ ):
474+ summary = await eval_lib .run_controller (a )
475+
476+ self .assertEqual (evaluated_steps , [18 , 19 ])
477+ self .assertTrue (summary ["target_reached" ])
478+ self .assertEqual (summary ["checkpoint_step" ], 19 )
479+ self .assertEqual (summary ["checkpoint_timestamp_ms" ], 2000 )
480+
481+
289482if __name__ == "__main__" :
290483 unittest .main ()
0 commit comments