33
44from __future__ import annotations
55
6- import gc
76import json
87import logging
98import math
109import random
1110import time
1211from copy import deepcopy
12+ from dataclasses import dataclass
13+ from enum import Enum
1314from typing import TYPE_CHECKING , Any , cast
1415
1516import numpy as np
@@ -383,12 +384,10 @@ def logits(self, model: Any, test_controls: Any = None, return_ids: bool = False
383384
384385 if return_ids :
385386 del locs , test_ids
386- gc .collect ()
387387 return model (input_ids = ids , attention_mask = attn_mask ).logits , ids
388388 del locs , test_ids
389389 logits = model (input_ids = ids , attention_mask = attn_mask ).logits
390390 del ids
391- gc .collect ()
392391 return logits
393392
394393 def target_loss (self , logits : torch .Tensor , ids : torch .Tensor ) -> torch .Tensor :
@@ -598,7 +597,14 @@ def grad(self, model: Any) -> torch.Tensor:
598597 Returns:
599598 torch.Tensor: Aggregated prompt gradients.
600599 """
601- return torch .stack ([prompt .grad (model ) for prompt in self ._prompts ]).sum (dim = 0 )
600+ first_gradient = self ._prompts [0 ].grad (model )
601+ if len (self ._prompts ) == 1 :
602+ return first_gradient
603+ result_dtype = first_gradient .dtype
604+ gradient = first_gradient .float () if result_dtype in (torch .float16 , torch .bfloat16 ) else first_gradient .clone ()
605+ for prompt in self ._prompts [1 :]:
606+ gradient .add_ (prompt .grad (model ).to (dtype = gradient .dtype ))
607+ return gradient .to (dtype = result_dtype )
602608
603609 def logits (self , model : Any , test_controls : Any = None , return_ids : bool = False ) -> Any :
604610 """
@@ -888,7 +894,6 @@ def control_weight_fn(_: int) -> float:
888894
889895 steps += 1
890896 start = time .time ()
891- torch .cuda .empty_cache ()
892897 control , loss = self .step (
893898 batch_size = batch_size ,
894899 topk = topk ,
@@ -940,14 +945,14 @@ def test(
940945 Jailbreak, exact-match, and loss results.
941946 """
942947 for j , worker in enumerate (workers ):
943- worker (prompts [j ], "test" , worker . model )
948+ worker (prompts [j ], ModelWorkerOperation . TEST )
944949 model_tests = np .array ([worker .results .get () for worker in workers ])
945950 model_tests_jb = model_tests [..., 0 ].tolist ()
946951 model_tests_mb = model_tests [..., 1 ].tolist ()
947952 model_tests_loss : list [list [float ]] = []
948953 if include_loss :
949954 for j , worker in enumerate (workers ):
950- worker (prompts [j ], "test_loss" , worker . model )
955+ worker (prompts [j ], ModelWorkerOperation . TEST_LOSS )
951956 model_tests_loss = [worker .results .get () for worker in workers ]
952957
953958 return model_tests_jb , model_tests_mb , model_tests_loss
@@ -1781,6 +1786,33 @@ def run(
17811786 return total_jb , total_em , test_total_jb , test_total_em , total_outputs , test_total_outputs
17821787
17831788
1789+ class ModelWorkerOperation (str , Enum ):
1790+ """A model operation supported by ``ModelWorker``."""
1791+
1792+ GRAD = "grad"
1793+ LOGITS = "logits"
1794+ CONTRAST_LOGITS = "contrast_logits"
1795+ TEST = "test"
1796+ TEST_LOSS = "test_loss"
1797+
1798+
1799+ @dataclass (frozen = True )
1800+ class ModelWorkerTask :
1801+ """
1802+ A spawn-safe worker payload that excludes the worker-owned model.
1803+
1804+ Typed model operations receive the worker's persistent model during dispatch,
1805+ so each queued task serializes only the prompt payload and operation arguments
1806+ rather than serializing the full model again. ``obj`` is the prompt or prompt
1807+ manager that receives the operation.
1808+ """
1809+
1810+ obj : Any
1811+ operation : ModelWorkerOperation | Callable [..., Any ]
1812+ args : tuple [Any , ...]
1813+ kwargs : dict [str , Any ]
1814+
1815+
17841816class ModelWorker :
17851817 """Run model operations in a dedicated multiprocessing worker."""
17861818
@@ -1802,33 +1834,30 @@ def __init__(
18021834 move_to_device = cast ("Callable[[torch.device], PreTrainedModel]" , model .to )
18031835 self .model = move_to_device (torch .device (device )).eval ()
18041836 self .tokenizer = tokenizer
1805- self .tasks : mp .JoinableQueue [Any ] = mp .JoinableQueue ()
1837+ self .tasks : mp .JoinableQueue [ModelWorkerTask | None ] = mp .JoinableQueue ()
18061838 self .results : mp .JoinableQueue [Any ] = mp .JoinableQueue ()
18071839 self .process : mp .Process | None = None
18081840
18091841 @staticmethod
1810- def run (model : Any , tasks : mp .JoinableQueue [Any ], results : mp .JoinableQueue [Any ]) -> None :
1842+ def run (
1843+ model : Any ,
1844+ tasks : mp .JoinableQueue [ModelWorkerTask | None ],
1845+ results : mp .JoinableQueue [Any ],
1846+ ) -> None :
18111847 """Process queued model operations until a stop sentinel arrives."""
1848+ model .requires_grad_ (False )
1849+ model .zero_grad (set_to_none = True )
18121850 while True :
18131851 task = tasks .get ()
18141852 if task is None :
1853+ tasks .task_done ()
18151854 break
1816- ob , fn , args , kwargs = task
1817- if fn == "grad" :
1855+ if task .operation is ModelWorkerOperation .GRAD :
18181856 with torch .enable_grad (): # type: ignore[no-untyped-call, unused-ignore]
1819- results .put (ob . grad ( * args , ** kwargs ))
1857+ results .put (ModelWorker . _execute_task ( model = model , task = task ))
18201858 else :
18211859 with torch .no_grad ():
1822- if fn == "logits" :
1823- results .put (ob .logits (* args , ** kwargs ))
1824- elif fn == "contrast_logits" :
1825- results .put (ob .contrast_logits (* args , ** kwargs ))
1826- elif fn == "test" :
1827- results .put (ob .test (* args , ** kwargs ))
1828- elif fn == "test_loss" :
1829- results .put (ob .test_loss (* args , ** kwargs ))
1830- else :
1831- results .put (fn (* args , ** kwargs ))
1860+ results .put (ModelWorker ._execute_task (model = model , task = task ))
18321861 tasks .task_done ()
18331862
18341863 def start (self ) -> ModelWorker :
@@ -1856,16 +1885,35 @@ def stop(self) -> ModelWorker:
18561885 torch .cuda .empty_cache ()
18571886 return self
18581887
1859- def __call__ (self , ob : Any , fn : str , * args : Any , ** kwargs : Any ) -> ModelWorker :
1888+ def __call__ (
1889+ self ,
1890+ ob : Any ,
1891+ operation : ModelWorkerOperation | Callable [..., Any ],
1892+ * args : Any ,
1893+ ** kwargs : Any ,
1894+ ) -> ModelWorker :
18601895 """
18611896 Queue an operation for execution by this worker.
18621897
18631898 Returns:
18641899 ModelWorker: This worker.
18651900 """
1866- self .tasks .put (( deepcopy (ob ), fn , args , kwargs ))
1901+ self .tasks .put (ModelWorkerTask ( obj = deepcopy (ob ), operation = operation , args = args , kwargs = kwargs ))
18671902 return self
18681903
1904+ @staticmethod
1905+ def _execute_task (* , model : Any , task : ModelWorkerTask ) -> Any :
1906+ """
1907+ Execute a task with the persistent model when the operation requires one.
1908+
1909+ Returns:
1910+ Any: The operation result.
1911+ """
1912+ if isinstance (task .operation , ModelWorkerOperation ):
1913+ method = getattr (task .obj , task .operation .value )
1914+ return method (model , * task .args , ** task .kwargs )
1915+ return task .operation (* task .args , ** task .kwargs )
1916+
18691917
18701918def get_workers (params : Any , evaluation : bool = False ) -> tuple [list [ModelWorker ], list [ModelWorker ]]:
18711919 """
0 commit comments