ryanyen22 commited on
Commit
76a5bfd
·
verified ·
1 Parent(s): bee361b

feat: add reason_first_program/sampling.py

Browse files
Files changed (1) hide show
  1. reason_first_program/sampling.py +473 -0
reason_first_program/sampling.py ADDED
@@ -0,0 +1,473 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Stage 1: Program Space Sampling
3
+
4
+ Generate diverse valid implementations of a stub using multiple strategies:
5
+ - Direct sampling from LLMs at various temperatures
6
+ - SFS-inspired scattering (2411.05010): diversify via textual gradient directions
7
+ - Multi-model heterogeneous sampling (AlgoDiv finding: diversity requires multiple models)
8
+ - Concept-guided sampling: steer toward specific concept regions
9
+
10
+ Supports both API-based models (OpenAI, Anthropic, HF Inference) and local models.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import re
16
+ import time
17
+ import logging
18
+ from abc import ABC, abstractmethod
19
+ from dataclasses import dataclass, field
20
+ from typing import Any, Optional
21
+
22
+ from reason_first_program.stub import Stub
23
+ from reason_first_program.program_space import Program, ProgramSpace, execute_program
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ @dataclass
29
+ class SamplingConfig:
30
+ """Configuration for program sampling."""
31
+
32
+ n_samples: int = 100
33
+ temperatures: list[float] = field(
34
+ default_factory=lambda: [0.2, 0.6, 0.8, 1.0, 1.2]
35
+ )
36
+ models: list[str] = field(
37
+ default_factory=lambda: ["deepseek-coder"]
38
+ )
39
+ prompt_styles: list[str] = field(
40
+ default_factory=lambda: ["direct", "diverse"]
41
+ )
42
+ max_tokens: int = 1024
43
+ timeout_per_execution: float = 5.0
44
+ deduplicate: bool = True
45
+ filter_valid: bool = True
46
+
47
+
48
+ class ModelBackend(ABC):
49
+ """Abstract backend for code generation."""
50
+
51
+ @abstractmethod
52
+ def generate(
53
+ self,
54
+ prompt: str,
55
+ temperature: float = 0.8,
56
+ max_tokens: int = 1024,
57
+ n: int = 1,
58
+ ) -> list[str]:
59
+ """Generate n completions for the given prompt."""
60
+ ...
61
+
62
+ @property
63
+ @abstractmethod
64
+ def model_id(self) -> str:
65
+ ...
66
+
67
+
68
+ class HFInferenceBackend(ModelBackend):
69
+ """HuggingFace Inference API backend."""
70
+
71
+ def __init__(self, model_name: str = "deepseek-ai/DeepSeek-Coder-V2-Lite-Instruct", token: Optional[str] = None):
72
+ self._model_name = model_name
73
+ self._token = token
74
+
75
+ @property
76
+ def model_id(self) -> str:
77
+ return self._model_name
78
+
79
+ def generate(
80
+ self,
81
+ prompt: str,
82
+ temperature: float = 0.8,
83
+ max_tokens: int = 1024,
84
+ n: int = 1,
85
+ ) -> list[str]:
86
+ try:
87
+ from huggingface_hub import InferenceClient
88
+ except ImportError:
89
+ raise ImportError("pip install huggingface_hub")
90
+
91
+ client = InferenceClient(model=self._model_name, token=self._token)
92
+ results = []
93
+ for _ in range(n):
94
+ try:
95
+ response = client.text_generation(
96
+ prompt,
97
+ max_new_tokens=max_tokens,
98
+ temperature=max(temperature, 0.01),
99
+ do_sample=True,
100
+ )
101
+ results.append(response)
102
+ except Exception as e:
103
+ logger.warning(f"Generation failed: {e}")
104
+ continue
105
+ return results
106
+
107
+
108
+ class OpenAIBackend(ModelBackend):
109
+ """OpenAI API backend."""
110
+
111
+ def __init__(self, model_name: str = "gpt-4o", api_key: Optional[str] = None):
112
+ self._model_name = model_name
113
+ self._api_key = api_key
114
+
115
+ @property
116
+ def model_id(self) -> str:
117
+ return self._model_name
118
+
119
+ def generate(
120
+ self,
121
+ prompt: str,
122
+ temperature: float = 0.8,
123
+ max_tokens: int = 1024,
124
+ n: int = 1,
125
+ ) -> list[str]:
126
+ try:
127
+ import openai
128
+ except ImportError:
129
+ raise ImportError("pip install openai")
130
+
131
+ client = openai.OpenAI(api_key=self._api_key)
132
+ try:
133
+ response = client.chat.completions.create(
134
+ model=self._model_name,
135
+ messages=[
136
+ {"role": "system", "content": "You are an expert Python programmer. Output only the function body, no explanation."},
137
+ {"role": "user", "content": prompt},
138
+ ],
139
+ temperature=temperature,
140
+ max_tokens=max_tokens,
141
+ n=n,
142
+ )
143
+ return [choice.message.content for choice in response.choices]
144
+ except Exception as e:
145
+ logger.warning(f"OpenAI generation failed: {e}")
146
+ return []
147
+
148
+
149
+ class LocalModelBackend(ModelBackend):
150
+ """Local model backend using transformers."""
151
+
152
+ def __init__(self, model_name: str = "deepseek-ai/deepseek-coder-1.3b-instruct", device: str = "auto"):
153
+ self._model_name = model_name
154
+ self._device = device
155
+ self._pipeline = None
156
+
157
+ @property
158
+ def model_id(self) -> str:
159
+ return self._model_name
160
+
161
+ def _load(self):
162
+ if self._pipeline is None:
163
+ try:
164
+ from transformers import pipeline
165
+ except ImportError:
166
+ raise ImportError("pip install transformers torch")
167
+ self._pipeline = pipeline(
168
+ "text-generation",
169
+ model=self._model_name,
170
+ device_map=self._device,
171
+ trust_remote_code=True,
172
+ )
173
+
174
+ def generate(
175
+ self,
176
+ prompt: str,
177
+ temperature: float = 0.8,
178
+ max_tokens: int = 1024,
179
+ n: int = 1,
180
+ ) -> list[str]:
181
+ self._load()
182
+ results = []
183
+ for _ in range(n):
184
+ try:
185
+ out = self._pipeline(
186
+ prompt,
187
+ max_new_tokens=max_tokens,
188
+ temperature=max(temperature, 0.01),
189
+ do_sample=True,
190
+ return_full_text=False,
191
+ )
192
+ results.append(out[0]["generated_text"])
193
+ except Exception as e:
194
+ logger.warning(f"Local generation failed: {e}")
195
+ return results
196
+
197
+
198
+ def _extract_function_body(raw_output: str, stub: Stub) -> Optional[str]:
199
+ """
200
+ Extract a clean function body from LLM output.
201
+ Handles markdown code blocks, extra commentary, etc.
202
+ """
203
+ text = raw_output.strip()
204
+
205
+ # Remove markdown code fences
206
+ code_block = re.search(r"```(?:python)?\s*\n(.*?)```", text, re.DOTALL)
207
+ if code_block:
208
+ text = code_block.group(1).strip()
209
+
210
+ # If the output contains a full function def, extract it
211
+ func_match = re.search(
212
+ rf"def\s+{re.escape(stub.name)}\s*\(.*?\).*?:\s*\n(.*)",
213
+ text,
214
+ re.DOTALL,
215
+ )
216
+ if func_match:
217
+ text = func_match.group(1)
218
+
219
+ # Remove any leading/trailing non-code lines
220
+ lines = text.split("\n")
221
+ code_lines = []
222
+ in_code = False
223
+ for line in lines:
224
+ stripped = line.strip()
225
+ if stripped and not stripped.startswith("#") and not in_code:
226
+ in_code = True
227
+ if in_code or stripped.startswith("#"):
228
+ code_lines.append(line)
229
+
230
+ if not code_lines:
231
+ return None
232
+
233
+ return "\n".join(code_lines)
234
+
235
+
236
+ def _build_full_source(body: str, stub: Stub) -> str:
237
+ """Reconstruct full function source from body and stub signature."""
238
+ # Extract just the def line from the stub source
239
+ for line in stub.source.split("\n"):
240
+ if line.strip().startswith("def "):
241
+ def_line = line
242
+ break
243
+ else:
244
+ def_line = f"def {stub.name}{stub.signature}:"
245
+
246
+ # Ensure proper indentation of body
247
+ indented_body = "\n".join(
248
+ f" {line}" if line.strip() else line for line in body.split("\n")
249
+ )
250
+ return f"{def_line}\n{indented_body}"
251
+
252
+
253
+ class ProgramSampler:
254
+ """
255
+ Basic program sampler: generates completions from a single backend.
256
+ """
257
+
258
+ def __init__(self, backend: ModelBackend, config: Optional[SamplingConfig] = None):
259
+ self.backend = backend
260
+ self.config = config or SamplingConfig()
261
+
262
+ def sample(self, stub: Stub) -> ProgramSpace:
263
+ """Sample programs for a stub and return a ProgramSpace."""
264
+ space = ProgramSpace(stub)
265
+ samples_per_config = max(
266
+ 1,
267
+ self.config.n_samples
268
+ // (len(self.config.temperatures) * len(self.config.prompt_styles)),
269
+ )
270
+
271
+ for temp in self.config.temperatures:
272
+ for style in self.config.prompt_styles:
273
+ prompt = stub.to_completion_prompt(style=style)
274
+ logger.info(
275
+ f"Sampling {samples_per_config} programs "
276
+ f"(temp={temp}, style={style}, model={self.backend.model_id})"
277
+ )
278
+
279
+ raw_outputs = self.backend.generate(
280
+ prompt=prompt,
281
+ temperature=temp,
282
+ max_tokens=self.config.max_tokens,
283
+ n=samples_per_config,
284
+ )
285
+
286
+ for raw in raw_outputs:
287
+ body = _extract_function_body(raw, stub)
288
+ if body is None:
289
+ continue
290
+
291
+ full_source = _build_full_source(body, stub)
292
+ program = Program(
293
+ source=body,
294
+ full_source=full_source,
295
+ stub_id=stub.stub_id,
296
+ model_id=self.backend.model_id,
297
+ metadata={
298
+ "temperature": temp,
299
+ "prompt_style": style,
300
+ },
301
+ )
302
+
303
+ # Execute and validate
304
+ if stub.test_inputs:
305
+ program = execute_program(
306
+ program, stub, stub.test_inputs,
307
+ timeout_seconds=self.config.timeout_per_execution,
308
+ )
309
+
310
+ space.add(program)
311
+
312
+ # Post-processing
313
+ if self.config.deduplicate:
314
+ space = space.deduplicate_syntactic()
315
+ if self.config.filter_valid and stub.test_inputs:
316
+ space = space.filter_valid()
317
+
318
+ return space
319
+
320
+
321
+ class DiverseSampler:
322
+ """
323
+ Diverse program sampler using multiple backends and SFS-inspired scattering.
324
+
325
+ Key insight from AlgoDiv (2503.00691): combining solutions from heterogeneous
326
+ models increases algorithmic diversity more than any single-model technique.
327
+ """
328
+
329
+ def __init__(
330
+ self,
331
+ backends: list[ModelBackend],
332
+ config: Optional[SamplingConfig] = None,
333
+ ):
334
+ self.backends = backends
335
+ self.config = config or SamplingConfig()
336
+
337
+ def sample(self, stub: Stub) -> ProgramSpace:
338
+ """Sample from all backends and merge into a single ProgramSpace."""
339
+ space = ProgramSpace(stub)
340
+ samples_per_backend = max(1, self.config.n_samples // len(self.backends))
341
+
342
+ for backend in self.backends:
343
+ backend_config = SamplingConfig(
344
+ n_samples=samples_per_backend,
345
+ temperatures=self.config.temperatures,
346
+ models=[backend.model_id],
347
+ prompt_styles=self.config.prompt_styles,
348
+ max_tokens=self.config.max_tokens,
349
+ timeout_per_execution=self.config.timeout_per_execution,
350
+ deduplicate=False, # We'll deduplicate at the end
351
+ filter_valid=False,
352
+ )
353
+ sampler = ProgramSampler(backend, backend_config)
354
+ backend_space = sampler.sample(stub)
355
+
356
+ for program in backend_space.programs:
357
+ space.add(program)
358
+
359
+ logger.info(
360
+ f"Backend {backend.model_id}: generated {len(backend_space)} programs"
361
+ )
362
+
363
+ # Post-processing across all backends
364
+ if self.config.deduplicate:
365
+ space = space.deduplicate_syntactic()
366
+ if self.config.filter_valid and stub.test_inputs:
367
+ space = space.filter_valid()
368
+
369
+ logger.info(
370
+ f"DiverseSampler: {len(space)} total programs "
371
+ f"({len(space.valid_programs)} valid)"
372
+ )
373
+ return space
374
+
375
+ def sample_with_scattering(
376
+ self, stub: Stub, n_directions: int = 5
377
+ ) -> ProgramSpace:
378
+ """
379
+ SFS-inspired scattering (2411.05010): first discover diverse algorithmic
380
+ directions, then sample implementations along each direction.
381
+ """
382
+ # Phase 1: Discover algorithmic directions
383
+ scout_backend = self.backends[0]
384
+ direction_prompt = (
385
+ f"Consider this Python function stub:\n\n"
386
+ f"```python\n{stub.source}\n```\n\n"
387
+ f"{stub.constraints.to_prompt_context()}\n\n"
388
+ f"List {n_directions} fundamentally different algorithmic approaches "
389
+ f"to implement this function. For each, give a short name and 1-sentence "
390
+ f"description. Format: '1. NAME: description'"
391
+ )
392
+ direction_outputs = scout_backend.generate(
393
+ direction_prompt, temperature=0.7, n=1
394
+ )
395
+
396
+ directions = []
397
+ if direction_outputs:
398
+ for line in direction_outputs[0].split("\n"):
399
+ line = line.strip()
400
+ if line and line[0].isdigit():
401
+ # Extract direction name
402
+ match = re.match(r"\d+\.\s*(.+?)(?::|$)", line)
403
+ if match:
404
+ directions.append(match.group(1).strip())
405
+
406
+ if not directions:
407
+ directions = [
408
+ "iterative approach",
409
+ "recursive approach",
410
+ "functional/map-reduce approach",
411
+ "optimized in-place approach",
412
+ "library-heavy approach",
413
+ ]
414
+
415
+ logger.info(f"Discovered {len(directions)} algorithmic directions: {directions}")
416
+
417
+ # Phase 2: Sample along each direction
418
+ space = ProgramSpace(stub)
419
+ samples_per_direction = max(
420
+ 1, self.config.n_samples // (len(directions) * len(self.backends))
421
+ )
422
+
423
+ for direction in directions:
424
+ directed_prompt = (
425
+ f"Complete this Python function using the following approach: "
426
+ f"**{direction}**\n\n"
427
+ f"```python\n{stub.source}\n```\n\n"
428
+ f"{stub.constraints.to_prompt_context()}\n\n"
429
+ f"Only output the function body. Use the {direction} approach."
430
+ )
431
+
432
+ for backend in self.backends:
433
+ for temp in self.config.temperatures:
434
+ raw_outputs = backend.generate(
435
+ directed_prompt,
436
+ temperature=temp,
437
+ max_tokens=self.config.max_tokens,
438
+ n=samples_per_direction,
439
+ )
440
+
441
+ for raw in raw_outputs:
442
+ body = _extract_function_body(raw, stub)
443
+ if body is None:
444
+ continue
445
+
446
+ full_source = _build_full_source(body, stub)
447
+ program = Program(
448
+ source=body,
449
+ full_source=full_source,
450
+ stub_id=stub.stub_id,
451
+ model_id=backend.model_id,
452
+ metadata={
453
+ "temperature": temp,
454
+ "direction": direction,
455
+ "prompt_style": "scattered",
456
+ },
457
+ )
458
+
459
+ if stub.test_inputs:
460
+ program = execute_program(
461
+ program, stub, stub.test_inputs,
462
+ timeout_seconds=self.config.timeout_per_execution,
463
+ )
464
+
465
+ space.add(program)
466
+
467
+ # Post-processing
468
+ if self.config.deduplicate:
469
+ space = space.deduplicate_syntactic()
470
+ if self.config.filter_valid and stub.test_inputs:
471
+ space = space.filter_valid()
472
+
473
+ return space