Codex Claude Opus 4.8 commited on
Commit
c1939e1
·
1 Parent(s): f4b9db0

ZeroGPU: move models to CUDA inside @gpu calls, not in the main process

Browse files

ZeroGPU forbids real CUDA init outside an @gpu allocation. load() now keeps
models on CPU (no .cuda(), no device_map=auto, no pipe.to('cuda')); the move to
CUDA happens inside _minicpm_generate/_nemotron_generate/_run_art_pipe.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Files changed (3) hide show
  1. art.py +2 -1
  2. local_llm.py +7 -3
  3. tests/test_local_llm.py +8 -0
art.py CHANGED
@@ -65,9 +65,9 @@ class DiffusersImageClient:
65
  import torch
66
  from diffusers import AutoPipelineForText2Image
67
 
 
68
  pipe = AutoPipelineForText2Image.from_pretrained(model_id, torch_dtype=best_torch_dtype(torch))
69
  pipe.set_progress_bar_config(disable=True)
70
- pipe = move_pipe_to_device(pipe, torch)
71
  return cls(pipe, steps=steps, guidance_scale=guidance_scale, width=width, height=height)
72
 
73
 
@@ -78,6 +78,7 @@ _art_pipe: Any = None
78
  # so the forked GPU worker inherits it; returns a data URI string (picklable).
79
  @gpu
80
  def _run_art_pipe(prompt: str, steps: int, guidance_scale: float, width: int, height: int) -> str:
 
81
  result = _art_pipe(
82
  prompt=prompt,
83
  num_inference_steps=steps,
 
65
  import torch
66
  from diffusers import AutoPipelineForText2Image
67
 
68
+ # Stay on CPU here: on ZeroGPU the move to CUDA happens in _run_art_pipe.
69
  pipe = AutoPipelineForText2Image.from_pretrained(model_id, torch_dtype=best_torch_dtype(torch))
70
  pipe.set_progress_bar_config(disable=True)
 
71
  return cls(pipe, steps=steps, guidance_scale=guidance_scale, width=width, height=height)
72
 
73
 
 
78
  # so the forked GPU worker inherits it; returns a data URI string (picklable).
79
  @gpu
80
  def _run_art_pipe(prompt: str, steps: int, guidance_scale: float, width: int, height: int) -> str:
81
+ _art_pipe.to("cuda")
82
  result = _art_pipe(
83
  prompt=prompt,
84
  num_inference_steps=steps,
local_llm.py CHANGED
@@ -99,8 +99,10 @@ class NemotronTransformersChatClient:
99
  ) -> "NemotronTransformersChatClient": # pragma: no cover
100
  from transformers import AutoModelForCausalLM, AutoTokenizer
101
 
 
 
102
  tokenizer = AutoTokenizer.from_pretrained(model_path)
103
- model = AutoModelForCausalLM.from_pretrained(model_path, **transformers_model_kwargs())
104
  return cls(model.eval(), tokenizer, max_new_tokens=max_new_tokens, temperature=temperature)
105
 
106
 
@@ -112,6 +114,7 @@ _nemotron_tokenizer: Any = None
112
  # globals so the forked GPU worker inherits it (only strings cross the boundary).
113
  @gpu
114
  def _nemotron_generate(system: str, user: str, max_new_tokens: int, temperature: float) -> str:
 
115
  messages = chat_messages(system, user)
116
  inputs = _nemotron_tokenizer.apply_chat_template(
117
  messages,
@@ -181,8 +184,8 @@ class MiniCPMTransformersChatClient:
181
  attn_implementation="sdpa",
182
  torch_dtype=local_torch_dtype(torch),
183
  )
184
- if torch.cuda.is_available():
185
- model = model.cuda()
186
  tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
187
  return cls(model.eval(), tokenizer, max_new_tokens=max_new_tokens, temperature=temperature)
188
 
@@ -195,6 +198,7 @@ _minicpm_tokenizer: Any = None
195
  # globals so the forked GPU worker inherits it (only strings cross the boundary).
196
  @gpu
197
  def _minicpm_generate(system: str, user: str, max_new_tokens: int, temperature: float) -> str:
 
198
  return str(
199
  _minicpm_model.chat(
200
  msgs=[{"role": "user", "content": user}],
 
99
  ) -> "NemotronTransformersChatClient": # pragma: no cover
100
  from transformers import AutoModelForCausalLM, AutoTokenizer
101
 
102
+ # Load on CPU (no device_map="auto"): on ZeroGPU the move to CUDA must
103
+ # happen inside the @gpu call, in _nemotron_generate.
104
  tokenizer = AutoTokenizer.from_pretrained(model_path)
105
+ model = AutoModelForCausalLM.from_pretrained(model_path, torch_dtype="auto")
106
  return cls(model.eval(), tokenizer, max_new_tokens=max_new_tokens, temperature=temperature)
107
 
108
 
 
114
  # globals so the forked GPU worker inherits it (only strings cross the boundary).
115
  @gpu
116
  def _nemotron_generate(system: str, user: str, max_new_tokens: int, temperature: float) -> str:
117
+ _nemotron_model.to("cuda")
118
  messages = chat_messages(system, user)
119
  inputs = _nemotron_tokenizer.apply_chat_template(
120
  messages,
 
184
  attn_implementation="sdpa",
185
  torch_dtype=local_torch_dtype(torch),
186
  )
187
+ # Stay on CPU here: on ZeroGPU the GPU only exists inside @gpu calls,
188
+ # so the move to CUDA happens in _minicpm_generate.
189
  tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
190
  return cls(model.eval(), tokenizer, max_new_tokens=max_new_tokens, temperature=temperature)
191
 
 
198
  # globals so the forked GPU worker inherits it (only strings cross the boundary).
199
  @gpu
200
  def _minicpm_generate(system: str, user: str, max_new_tokens: int, temperature: float) -> str:
201
+ _minicpm_model.to("cuda")
202
  return str(
203
  _minicpm_model.chat(
204
  msgs=[{"role": "user", "content": user}],
tests/test_local_llm.py CHANGED
@@ -72,6 +72,10 @@ class FakeTokenizer:
72
  class FakeModel:
73
  device = "cuda"
74
 
 
 
 
 
75
  # Return a generated token sequence with prompt prefix included.
76
  def generate(self, inputs: FakeInputs, **kwargs: Any) -> list[list[int]]:
77
  self.inputs = inputs
@@ -80,6 +84,10 @@ class FakeModel:
80
 
81
 
82
  class FakeMiniCPMModel:
 
 
 
 
83
  # Capture MiniCPM chat kwargs.
84
  def chat(self, **kwargs: Any) -> str:
85
  self.kwargs = kwargs
 
72
  class FakeModel:
73
  device = "cuda"
74
 
75
+ # No-op device move (ZeroGPU does the real one inside the @gpu call).
76
+ def to(self, device: str) -> "FakeModel":
77
+ return self
78
+
79
  # Return a generated token sequence with prompt prefix included.
80
  def generate(self, inputs: FakeInputs, **kwargs: Any) -> list[list[int]]:
81
  self.inputs = inputs
 
84
 
85
 
86
  class FakeMiniCPMModel:
87
+ # No-op device move (ZeroGPU does the real one inside the @gpu call).
88
+ def to(self, device: str) -> "FakeMiniCPMModel":
89
+ return self
90
+
91
  # Capture MiniCPM chat kwargs.
92
  def chat(self, **kwargs: Any) -> str:
93
  self.kwargs = kwargs