Skip to content

Commit a36e276

Browse files
authored
allow generators.Base.generate() to take an optional param specifying generation count (#600)
* generate now takes an optional parameter specifying how many strings to get as output * logging and value checking for generations_this_call --------- Signed-off-by: Leon Derczynski <leonderczynski@gmail.com>
1 parent 9b1e475 commit a36e276

17 files changed

Lines changed: 100 additions & 63 deletions

garak/generators/base.py

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,9 @@ def __init__(self, name="", generations=10):
4747
)
4848
logging.info("generator init: %s", self)
4949

50-
def _call_model(self, prompt: str) -> Union[List[str], str, None]:
50+
def _call_model(
51+
self, prompt: str, generations_this_call: int = 1
52+
) -> Union[List[str], str, None]:
5153
"""Takes a prompt and returns an API output
5254
5355
_call_api() is fully responsible for the request, and should either
@@ -63,7 +65,7 @@ def _pre_generate_hook(self):
6365
def clear_history(self):
6466
pass
6567

66-
def generate(self, prompt: str) -> List[str]:
68+
def generate(self, prompt: str, generations_this_call: int = -1) -> List[str]:
6769
"""Manages the process of getting generations out from a prompt
6870
6971
This will involve iterating through prompts, getting the generations
@@ -74,11 +76,22 @@ def generate(self, prompt: str) -> List[str]:
7476

7577
self._pre_generate_hook()
7678

79+
assert (
80+
generations_this_call >= -1
81+
), f"Unexpected value for generations_per_call: {generations_this_call}"
82+
83+
if generations_this_call == -1:
84+
generations_this_call = self.generations
85+
86+
elif generations_this_call == 0:
87+
logging.debug("generate() called with generations_this_call = 0")
88+
return []
89+
7790
if self.supports_multiple_generations:
78-
return self._call_model(prompt)
91+
return self._call_model(prompt, generations_this_call)
7992

80-
elif self.generations <= 1:
81-
return [self._call_model(prompt)]
93+
elif generations_this_call <= 1:
94+
return [self._call_model(prompt, generations_this_call)]
8295

8396
else:
8497
outputs = []
@@ -90,23 +103,23 @@ def generate(self, prompt: str) -> List[str]:
90103
):
91104
from multiprocessing import Pool
92105

93-
bar = tqdm.tqdm(total=self.generations, leave=False)
106+
bar = tqdm.tqdm(total=generations_this_call, leave=False)
94107
bar.set_description(self.fullname[:55])
95108

96109
with Pool(_config.system.parallel_requests) as pool:
97110
for result in pool.imap_unordered(
98-
self._call_model, [prompt] * self.generations
111+
self._call_model, [prompt] * generations_this_call
99112
):
100113
outputs.append(result)
101114
bar.update(1)
102115

103116
else:
104117
generation_iterator = tqdm.tqdm(
105-
list(range(self.generations)), leave=False
118+
list(range(generations_this_call)), leave=False
106119
)
107120
generation_iterator.set_description(self.fullname[:55])
108121
for i in generation_iterator:
109-
outputs.append(self._call_model(prompt))
122+
outputs.append(self._call_model(prompt, generations_this_call))
110123

111124
cleaned_outputs = [
112125
o for o in outputs if o is not None

garak/generators/cohere.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import logging
1111
import os
12+
from typing import List, Union
1213

1314
import backoff
1415
import cohere
@@ -81,10 +82,12 @@ def _call_cohere_api(self, prompt, request_size=COHERE_GENERATION_LIMIT):
8182
)
8283
return [g.text for g in response]
8384

84-
def _call_model(self, prompt):
85+
def _call_model(
86+
self, prompt: str, generations_this_call: int = 1
87+
) -> Union[List[str], str, None]:
8588
"""Cohere's _call_model does sub-batching before calling,
8689
and so manages chunking internally"""
87-
quotient, remainder = divmod(self.generations, COHERE_GENERATION_LIMIT)
90+
quotient, remainder = divmod(generations_this_call, COHERE_GENERATION_LIMIT)
8891
request_sizes = [COHERE_GENERATION_LIMIT] * quotient
8992
if remainder:
9093
request_sizes += [remainder]

garak/generators/function.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131

3232

3333
import importlib
34-
from typing import List
34+
from typing import List, Union
3535

3636
from garak.generators.base import Generator
3737

@@ -56,17 +56,23 @@ def __init__(self, name="", **kwargs): # name="", generations=self.generations)
5656

5757
super().__init__(name, generations=self.generations)
5858

59-
def _call_model(self, prompt: str) -> str:
60-
return self.generator(prompt, **self.kwargs)
59+
def _call_model(
60+
self, prompt: str, generations_this_call: int = 1
61+
) -> Union[List[str], str, None]:
62+
return self.generator(
63+
prompt, generations_this_call=generations_this_call, **self.kwargs
64+
)
6165

6266

6367
class Multiple(Single):
6468
"""pass a module#function to be called as generator, with format function(prompt:str, generations:int, **kwargs)->List[str]"""
6569

6670
supports_multiple_generations = True
6771

68-
def _call_model(self, prompt) -> List[str]:
69-
return self.generator(prompt, generations=self.generations, **self.kwargs)
72+
def _call_model(
73+
self, prompt: str, generations_this_call: int = 1
74+
) -> Union[List[str], str, None]:
75+
return self.generator(prompt, generations=generations_this_call, **self.kwargs)
7076

7177

7278
default_class = "Single"

garak/generators/ggml.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import os
1717
import re
1818
import subprocess
19+
from typing import List, Union
1920

2021
from garak import _config
2122
from garak.generators.base import Generator
@@ -77,7 +78,14 @@ def __init__(self, name, generations=10):
7778

7879
super().__init__(name, generations=generations)
7980

80-
def _call_model(self, prompt):
81+
def _call_model(
82+
self, prompt: str, generations_this_call: int = 1
83+
) -> Union[List[str], str, None]:
84+
if generations_this_call != 1:
85+
logging.warning(
86+
"GgmlGenerator._call_model invokes with generations_this_call=%s but only 1 supported",
87+
generations_this_call,
88+
)
8189
command = [
8290
self.path_to_ggml_main,
8391
"-p",

garak/generators/guardrails.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from contextlib import redirect_stderr
77
import io
8-
from typing import List
8+
from typing import List, Union
99

1010
from garak.generators.base import Generator
1111

@@ -35,7 +35,9 @@ def __init__(self, name, generations=1):
3535

3636
super().__init__(name, generations=generations)
3737

38-
def _call_model(self, prompt: str) -> List[str]:
38+
def _call_model(
39+
self, prompt: str, generations_this_call: int = 1
40+
) -> Union[List[str], str, None]:
3941
with redirect_stderr(io.StringIO()) as f: # quieten the tqdm
4042
result = self.rails.generate(prompt)
4143

garak/generators/huggingface.py

Lines changed: 26 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,9 @@ def __init__(self, name, do_sample=True, generations=10, device=0):
8888
if _config.run.deprefix is True:
8989
self.deprefix_prompt = True
9090

91-
self._set_hf_context_len(self.generator.model.config)
91+
self._set_hf_context_len(self.generator.model.config)
9292

93-
def _call_model(self, prompt: str) -> List[str]:
93+
def _call_model(self, prompt: str, generations_this_call: int = 1) -> List[str]:
9494
with warnings.catch_warnings():
9595
warnings.simplefilter("ignore", category=UserWarning)
9696
try:
@@ -104,23 +104,22 @@ def _call_model(self, prompt: str) -> List[str]:
104104
truncated_prompt,
105105
pad_token_id=self.generator.tokenizer.eos_token_id,
106106
max_new_tokens=self.max_tokens,
107-
num_return_sequences=self.generations,
107+
num_return_sequences=generations_this_call,
108108
)
109109
except Exception as e:
110110
logging.error(e)
111111
raw_output = [] # could handle better than this
112112

113+
outputs = []
113114
if raw_output is not None:
114-
generations = [
115+
outputs = [
115116
i["generated_text"] for i in raw_output
116117
] # generator returns 10 outputs by default in __init__
117-
else:
118-
generations = []
119118

120119
if not self.deprefix_prompt:
121-
return generations
120+
return outputs
122121
else:
123-
return [re.sub("^" + re.escape(prompt), "", i) for i in generations]
122+
return [re.sub("^" + re.escape(prompt), "", _o) for _o in outputs]
124123

125124

126125
class OptimumPipeline(Pipeline, HFCompatible):
@@ -211,7 +210,9 @@ def clear_history(self):
211210

212211
self.conversation = Conversation()
213212

214-
def _call_model(self, prompt: Union[str, list[dict]]) -> List[str]:
213+
def _call_model(
214+
self, prompt: Union[str, list[dict]], generations_this_call: int = 1
215+
) -> List[str]:
215216
"""Take a conversation as a list of dictionaries and feed it to the model"""
216217

217218
# If conversation is provided as a list of dicts, create the conversation.
@@ -230,14 +231,14 @@ def _call_model(self, prompt: Union[str, list[dict]]) -> List[str]:
230231
with torch.no_grad():
231232
conversation = self.generator(conversation)
232233

233-
generations = [conversation[-1]["content"]]
234+
outputs = [conversation[-1]["content"]]
234235
else:
235236
raise TypeError(f"Expected list or str, got {type(prompt)}")
236237

237238
if not self.deprefix_prompt:
238-
return generations
239+
return outputs
239240
else:
240-
return [re.sub("^" + re.escape(prompt), "", i) for i in generations]
241+
return [re.sub("^" + re.escape(prompt), "", _o) for _o in outputs]
241242

242243

243244
class InferenceAPI(Generator, HFCompatible):
@@ -275,15 +276,15 @@ def __init__(self, name="", generations=10):
275276
),
276277
max_value=125,
277278
)
278-
def _call_model(self, prompt: str) -> List[str]:
279+
def _call_model(self, prompt: str, generations_this_call: int = 1) -> List[str]:
279280
import json
280281
import requests
281282

282283
payload = {
283284
"inputs": prompt,
284285
"parameters": {
285286
"return_full_text": not self.deprefix_prompt,
286-
"num_return_sequences": self.generations,
287+
"num_return_sequences": generations_this_call,
287288
"max_time": self.max_time,
288289
},
289290
"options": {
@@ -293,7 +294,7 @@ def _call_model(self, prompt: str) -> List[str]:
293294
if self.max_tokens:
294295
payload["parameters"]["max_new_tokens"] = self.max_tokens
295296

296-
if self.generations > 1:
297+
if generations_this_call > 1:
297298
payload["parameters"]["do_sample"] = True
298299

299300
req_response = requests.request(
@@ -366,6 +367,8 @@ class InferenceEndpoint(InferenceAPI, HFCompatible):
366367
supports_multiple_generations = False
367368
import requests
368369

370+
timeout = 120
371+
369372
def __init__(self, name="", generations=10):
370373
super().__init__(name, generations=generations)
371374
self.api_url = name
@@ -380,7 +383,7 @@ def __init__(self, name="", generations=10):
380383
),
381384
max_value=125,
382385
)
383-
def _call_model(self, prompt: str) -> List[str]:
386+
def _call_model(self, prompt: str, generations_this_call: int = 1) -> List[str]:
384387
import requests
385388

386389
payload = {
@@ -396,18 +399,18 @@ def _call_model(self, prompt: str) -> List[str]:
396399
if self.max_tokens:
397400
payload["parameters"]["max_new_tokens"] = self.max_tokens
398401

399-
if self.generations > 1:
402+
if generations_this_call > 1:
400403
payload["parameters"]["do_sample"] = True
401404

402405
response = requests.post(
403-
self.api_url, headers=self.headers, json=payload
406+
self.api_url, headers=self.headers, json=payload, timeout=self.timeout
404407
).json()
405408
try:
406409
output = response[0]["generated_text"]
407-
except:
410+
except Exception as exc:
408411
raise IOError(
409412
"Hugging Face 🤗 endpoint didn't generate a response. Make sure the endpoint is active."
410-
)
413+
) from exc
411414
return output
412415

413416

@@ -471,10 +474,10 @@ def __init__(self, name, do_sample=True, generations=10, device=0):
471474
self.generation_config.eos_token_id = self.model.config.eos_token_id
472475
self.generation_config.pad_token_id = self.model.config.eos_token_id
473476

474-
def _call_model(self, prompt):
477+
def _call_model(self, prompt: str, generations_this_call: int = 1):
475478
self.generation_config.max_new_tokens = self.max_tokens
476479
self.generation_config.do_sample = self.do_sample
477-
self.generation_config.num_return_sequences = self.generations
480+
self.generation_config.num_return_sequences = generations_this_call
478481
if self.temperature is not None:
479482
self.generation_config.temperature = self.temperature
480483
if self.top_k is not None:
@@ -494,7 +497,7 @@ def _call_model(self, prompt):
494497
)
495498
except IndexError as e:
496499
if len(prompt) == 0:
497-
return [""] * self.generations
500+
return [""] * generations_this_call
498501
else:
499502
raise e
500503
text_output = self.tokenizer.batch_decode(

garak/generators/langchain.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def __init__(self, name, generations=10):
5656

5757
self.generator = llm
5858

59-
def _call_model(self, prompt: str) -> str:
59+
def _call_model(self, prompt: str, generations_this_call: int = 1) -> str:
6060
"""
6161
Continuation generation method for LangChain LLM integrations.
6262

garak/generators/litellm.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ def __init__(self, name: str, generations: int = 10):
135135

136136
@backoff.on_exception(backoff.fibo, Exception, max_value=70)
137137
def _call_model(
138-
self, prompt: Union[str, List[dict]]
138+
self, prompt: str, generations_this_call: int = 1
139139
) -> Union[List[str], str, None]:
140140
if isinstance(prompt, str):
141141
prompt = [{"role": "user", "content": prompt}]
@@ -155,7 +155,7 @@ def _call_model(
155155
messages=prompt,
156156
temperature=self.temperature,
157157
top_p=self.top_p,
158-
n=self.generations,
158+
n=generations_this_call,
159159
stop=self.stop,
160160
max_tokens=self.max_tokens,
161161
frequency_penalty=self.frequency_penalty,

garak/generators/nemo.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ def __init__(self, name=None, generations=10):
7070
),
7171
max_value=70,
7272
)
73-
def _call_model(self, prompt):
73+
def _call_model(self, prompt: str, generations_this_call: int = 1):
7474
# avoid:
7575
# doesn't match schema #/components/schemas/CompletionRequestBody: Error at "/prompt": minimum string length is 1
7676
if prompt == "":
@@ -80,7 +80,7 @@ def _call_model(self, prompt):
8080
if self.seed is None: # nemo gives the same result every time
8181
reset_none_seed = True
8282
self.seed = random.randint(0, 2147483648 - 1)
83-
elif self.generations > 1:
83+
elif generations_this_call > 1:
8484
logging.info(
8585
"fixing a seed means nemollm gives the same result every time, recommend setting generations=1"
8686
)

garak/generators/nvcf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ def __init__(self, name=None, generations=10):
6464
),
6565
max_value=70,
6666
)
67-
def _call_model(self, prompt: str) -> str:
67+
def _call_model(self, prompt: str, generations_this_call: int = 1) -> str:
6868
if prompt == "":
6969
return ""
7070

0 commit comments

Comments
 (0)