auditkit 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. auditkit/README.md +99 -0
  2. auditkit/__init__.py +177 -0
  3. auditkit/__main__.py +3 -0
  4. auditkit/_bootstrap.py +77 -0
  5. auditkit/_identity_guard.py +99 -0
  6. auditkit/adapter.py +264 -0
  7. auditkit/annotator.py +339 -0
  8. auditkit/api.py +502 -0
  9. auditkit/assets/auditkit_logo.png +0 -0
  10. auditkit/cache.py +47 -0
  11. auditkit/cli.py +417 -0
  12. auditkit/comparison.py +563 -0
  13. auditkit/diff.py +265 -0
  14. auditkit/errors.py +54 -0
  15. auditkit/evaluator.py +20 -0
  16. auditkit/experiment.py +145 -0
  17. auditkit/hf_publish.py +262 -0
  18. auditkit/lmeval_engine.py +550 -0
  19. auditkit/loaders.py +121 -0
  20. auditkit/logs.py +18 -0
  21. auditkit/metric.py +199 -0
  22. auditkit/metrics/README.md +15 -0
  23. auditkit/metrics/__init__.py +0 -0
  24. auditkit/metrics/code.py +222 -0
  25. auditkit/metrics/embedding.py +131 -0
  26. auditkit/metrics/encoder_judge.py +423 -0
  27. auditkit/metrics/generation.py +331 -0
  28. auditkit/metrics/guard.py +412 -0
  29. auditkit/metrics/hallucination.py +45 -0
  30. auditkit/metrics/judge.py +547 -0
  31. auditkit/metrics/pairwise.py +153 -0
  32. auditkit/metrics/perf.py +53 -0
  33. auditkit/metrics/rag.py +149 -0
  34. auditkit/metrics/security.py +64 -0
  35. auditkit/metrics/toxicity.py +238 -0
  36. auditkit/model/README.md +16 -0
  37. auditkit/model/__init__.py +485 -0
  38. auditkit/model/anthropic.py +94 -0
  39. auditkit/model/api_gen.py +133 -0
  40. auditkit/model/groq_gen.py +121 -0
  41. auditkit/model/hf_gen.py +385 -0
  42. auditkit/model/lexsi.py +155 -0
  43. auditkit/model/litellm_gen.py +65 -0
  44. auditkit/model/openai.py +90 -0
  45. auditkit/model/openrouter_gen.py +152 -0
  46. auditkit/model/vllm_gen.py +316 -0
  47. auditkit/model_compare.py +655 -0
  48. auditkit/redteam/README.md +9 -0
  49. auditkit/redteam/__init__.py +26 -0
  50. auditkit/redteam/detector.py +37 -0
  51. auditkit/redteam/detectors/README.md +5 -0
  52. auditkit/redteam/detectors/builtin.py +126 -0
  53. auditkit/redteam/probe.py +39 -0
  54. auditkit/redteam/probes/README.md +5 -0
  55. auditkit/redteam/probes/builtin.py +85 -0
  56. auditkit/redteam/runner.py +206 -0
  57. auditkit/registry.py +65 -0
  58. auditkit/report.py +278 -0
  59. auditkit/report_format.py +52 -0
  60. auditkit/router.py +54 -0
  61. auditkit/runner.py +575 -0
  62. auditkit/runspec.py +159 -0
  63. auditkit/sample.py +40 -0
  64. auditkit/scenario.py +88 -0
  65. auditkit/scenarios/README.md +10 -0
  66. auditkit/scenarios/__init__.py +4 -0
  67. auditkit/scenarios/arc.py +33 -0
  68. auditkit/scenarios/gsm8k.py +32 -0
  69. auditkit/scenarios/hellaswag.py +33 -0
  70. auditkit/scenarios/humaneval.py +32 -0
  71. auditkit/scenarios/mmlu.py +34 -0
  72. auditkit/scenarios/truthfulqa.py +33 -0
  73. auditkit/score.py +165 -0
  74. auditkit/scorers.py +117 -0
  75. auditkit/scoring.py +79 -0
  76. auditkit/types.py +69 -0
  77. auditkit-1.0.0.dist-info/METADATA +396 -0
  78. auditkit-1.0.0.dist-info/RECORD +81 -0
  79. auditkit-1.0.0.dist-info/WHEEL +4 -0
  80. auditkit-1.0.0.dist-info/entry_points.txt +2 -0
  81. auditkit-1.0.0.dist-info/licenses/LICENSE.md +92 -0
@@ -0,0 +1,316 @@
1
+ """vLLM inference backend (optional extra: auditkit[vllm])."""
2
+ from __future__ import annotations
3
+
4
+ import contextlib
5
+ import os
6
+ import sys
7
+ from typing import Any
8
+
9
+ from . import (
10
+ Model, Request, Result_, Generated, resolve_params, DEFAULT_TEMPERATURE,
11
+ reject_generation_kwargs, _free_torch_memory,
12
+ )
13
+ from ..errors import ExtraNotInstalled
14
+
15
+ def _apply_environment_defaults() -> None:
16
+ """Best-effort environment fixes for real, live-confirmed (on Colab)
17
+ vLLM issues, applied once at import time so ordinary users of this
18
+ backend never need to know about them, on any platform.
19
+
20
+ - ``VLLM_ENABLE_V1_MULTIPROCESSING=0``: vLLM's V1 engine forks a
21
+ separate worker process by default, which can't inherit an
22
+ already-initialized CUDA context from the parent -- a real,
23
+ reproducible crash ("Cannot re-initialize CUDA in forked
24
+ subprocess") in any process that touches CUDA before constructing
25
+ an ``LLM`` (nearly guaranteed in a notebook/REPL). Running the
26
+ engine in-process instead sidesteps this unconditionally.
27
+ - ``HF_HUB_DISABLE_XET=1``: ``huggingface_hub``'s newer "Xet Storage"
28
+ download path has 404'd in practice for some legacy repos
29
+ (confirmed live: bare ``"gpt2"``) that aren't Xet-enabled
30
+ server-side. Best-effort only: this only helps if this module is
31
+ imported before user code imports ``transformers``/
32
+ ``huggingface_hub`` directly, since ``huggingface_hub`` caches its
33
+ Xet-availability check at first import -- setting this env var
34
+ later has no effect. If a caller hits a Xet 404 despite this, the
35
+ real fix is setting ``HF_HUB_DISABLE_XET=1`` at the very top of
36
+ their own script/notebook, before any other imports.
37
+
38
+ ``setdefault`` throughout, so an explicit value the caller already
39
+ set (e.g. to force real multi-process throughput in a dedicated
40
+ script) always wins.
41
+ """
42
+ os.environ.setdefault("VLLM_ENABLE_V1_MULTIPROCESSING", "0")
43
+ os.environ.setdefault("HF_HUB_DISABLE_XET", "1")
44
+
45
+
46
+ # Must run at import time, not inside a method -- some other import path
47
+ # (e.g. `import auditkit` pulling in a different backend first) could
48
+ # otherwise import vllm/huggingface_hub before this class is ever touched.
49
+ _apply_environment_defaults()
50
+
51
+
52
+ @contextlib.contextmanager
53
+ def _stdout_fix():
54
+ """vLLM internally redirects real OS file descriptors (via
55
+ ``sys.stdout.fileno()``) to silence noisy distributed-backend init
56
+ logs during engine construction. Jupyter/Colab/most notebook kernels
57
+ replace ``sys.stdout`` with a stream that forwards over a socket
58
+ instead of a real fd, so that call raises ``UnsupportedOperation`` --
59
+ a real, reproducible crash confirmed live on Colab. Swapping to the
60
+ process's real underlying streams for just the construction call
61
+ fixes it; this is a no-op in a plain terminal, where ``sys.stdout``
62
+ already *is* ``sys.__stdout__``, so it's always safe to apply
63
+ unconditionally rather than only in a detected-notebook branch.
64
+ """
65
+ real_stdout, real_stderr = sys.stdout, sys.stderr
66
+ sys.stdout, sys.stderr = sys.__stdout__, sys.__stderr__
67
+ try:
68
+ yield
69
+ finally:
70
+ sys.stdout, sys.stderr = real_stdout, real_stderr
71
+
72
+
73
+ def _call_shutdown(obj: Any) -> bool:
74
+ """Call ``shutdown()`` if *obj* has one. Returns True if it ran."""
75
+ if obj is None:
76
+ return False
77
+ fn = getattr(obj, "shutdown", None)
78
+ if not callable(fn):
79
+ return False
80
+ with contextlib.suppress(Exception):
81
+ fn()
82
+ return True
83
+ return False
84
+
85
+
86
+ def _drop_attr(obj: Any, *names: str) -> None:
87
+ """Drop attributes that keep CUDA tensors reachable after teardown."""
88
+ if obj is None:
89
+ return
90
+ for name in names:
91
+ if hasattr(obj, name):
92
+ with contextlib.suppress(Exception):
93
+ setattr(obj, name, None)
94
+
95
+
96
+ def _shutdown_vllm_engine(llm: Any) -> None:
97
+ """Best-effort vLLM teardown across V0/V1 and in-process vs multiproc.
98
+
99
+ Confirmed live on Colab T4 (vLLM 0.19.1): EngineCore/UniProcExecutor
100
+ ``shutdown()`` ends at ``WorkerBase.shutdown()``, which is a no-op, so
101
+ weights + KV cache stay allocated. After model A, 4.14/14.56 GiB was
102
+ free and model B died on the 0.7 utilization guard (needs 10.19 GiB).
103
+ ``LLM.sleep(level=2)`` is the supported discard (needs
104
+ ``enable_sleep_mode=True`` at construction). Then drop the worker/module
105
+ refs so CUDA tensors can actually be collected.
106
+ """
107
+ with contextlib.suppress(Exception):
108
+ sleep = getattr(llm, "sleep", None)
109
+ if callable(sleep):
110
+ sleep(level=2)
111
+
112
+ engine = getattr(llm, "llm_engine", None) or getattr(llm, "engine", None)
113
+ client = getattr(engine, "engine_core", None)
114
+ core = getattr(client, "engine_core", None)
115
+ executor = (
116
+ getattr(engine, "model_executor", None)
117
+ or getattr(core, "model_executor", None)
118
+ )
119
+ worker = getattr(executor, "driver_worker", None)
120
+ runner = getattr(worker, "model_runner", None)
121
+ if runner is None:
122
+ runner = getattr(getattr(worker, "worker", None), "model_runner", None)
123
+ model = getattr(runner, "model", None)
124
+ if model is not None:
125
+ with contextlib.suppress(Exception):
126
+ model.to("cpu")
127
+ _drop_attr(runner, "model", "kv_caches", "kv_cache")
128
+
129
+ _call_shutdown(llm)
130
+ _call_shutdown(client)
131
+ _call_shutdown(core)
132
+ _call_shutdown(executor)
133
+ _call_shutdown(worker)
134
+ _drop_attr(executor, "driver_worker")
135
+ _drop_attr(engine, "model_executor")
136
+ _drop_attr(core, "model_executor", "scheduler")
137
+ _drop_attr(engine, "engine_core")
138
+ _drop_attr(llm, "llm_engine", "engine")
139
+ del llm
140
+ with contextlib.suppress(Exception):
141
+ import gc
142
+ gc.unfreeze()
143
+ with contextlib.suppress(Exception):
144
+ from vllm.distributed.parallel_state import cleanup_dist_env_and_memory
145
+ cleanup_dist_env_and_memory()
146
+ with contextlib.suppress(Exception):
147
+ from vllm.distributed.parallel_state import (
148
+ destroy_distributed_environment, destroy_model_parallel,
149
+ )
150
+ destroy_model_parallel()
151
+ destroy_distributed_environment()
152
+
153
+
154
+ # vLLM's SamplingParams supports most of the common sampling knobs directly.
155
+ _KEY_MAP = {
156
+ "temperature": "temperature",
157
+ "top_p": "top_p",
158
+ "top_k": "top_k",
159
+ "max_tokens": "max_tokens",
160
+ "stop_sequences": "stop",
161
+ "presence_penalty": "presence_penalty",
162
+ "frequency_penalty": "frequency_penalty",
163
+ "seed": "seed",
164
+ }
165
+
166
+
167
+ class VLLMModel(Model):
168
+ """Run inference using a vLLM engine.
169
+
170
+ ``is_local = True`` and ``model_info()`` attempts real parameter
171
+ count/size introspection (same approach as ``HFGenModel``), but vLLM's
172
+ internal engine structure has changed significantly between its V0/V1
173
+ architectures and varies across releases, with no single stable public
174
+ attribute for "the loaded nn.Module". ``model_info()`` tries the known
175
+ internal access paths in order and falls back to identity-only
176
+ reporting (this class's previous, unconditional behavior) if none of
177
+ them resolve on the installed vLLM version, rather than crashing.
178
+ **Not yet verified against a real vLLM install** -- vLLM is CUDA/Linux-
179
+ oriented and won't install in the environment this was written in.
180
+ Verify the numeric fields against a real install before trusting them,
181
+ the same way HFGenModel's introspection was verified against real gpt2.
182
+ """
183
+
184
+ name = "vllm"
185
+ threadsafe = False
186
+ is_local = True
187
+
188
+ def __init__(self, model: str = "gpt2", name: str = "vllm", **kwargs: Any) -> None:
189
+ """``**kwargs`` are real ``vllm.LLM()`` engine-construction arguments
190
+ (e.g. ``gpu_memory_utilization=0.3``, ``dtype="float16"``,
191
+ ``tensor_parallel_size=2``, ``max_model_len=2048``,
192
+ ``trust_remote_code=True``, ``quantization="awq"``) -- forwarded to
193
+ ``LLM()`` at load time in :meth:`_ensure_llm`, not to
194
+ ``SamplingParams`` (a previous version of this class forwarded them
195
+ to ``SamplingParams`` instead, which doesn't accept engine-level
196
+ arguments at all and would raise ``TypeError`` for any of the ones
197
+ above). Generation-time sampling knobs belong on the per-request
198
+ ``RunConfig``/``Request.params`` instead (see ``_KEY_MAP``), not
199
+ here.
200
+ """
201
+ reject_generation_kwargs(kwargs, "VLLMModel")
202
+ self._model_name = model
203
+ self._extra_kwargs = kwargs
204
+ self._llm = None
205
+ self.name = name
206
+
207
+ def unload(self) -> None:
208
+ """Tear down the loaded vLLM engine and free the GPU, so
209
+ ``compare_models()`` can load the next model without stacking VRAM.
210
+
211
+ Confirmed live on Colab T4 (vLLM 0.19.1, in-process V1 via
212
+ ``VLLM_ENABLE_V1_MULTIPROCESSING=0``): dropping ``self._llm`` and
213
+ calling ``torch.cuda.empty_cache()`` leaves weights + KV cache
214
+ allocated. The next ``LLM(...)`` then fails the startup free-memory
215
+ guard (``Free memory on device cuda:0 (3.42/14.56 GiB) ... less than
216
+ desired GPU memory utilization (0.7, 10.19 GiB)``). ``LLM`` on
217
+ 0.19.1 has no ``shutdown()``; teardown is
218
+ ``llm.llm_engine.engine_core.shutdown()`` → ``EngineCore`` →
219
+ ``model_executor.shutdown()``. Later vLLM added ``LLM.shutdown()``;
220
+ we prefer that when present, then walk the known attributes.
221
+ """
222
+ llm = self._llm
223
+ self._llm = None
224
+ if llm is not None:
225
+ _shutdown_vllm_engine(llm)
226
+ _free_torch_memory()
227
+
228
+ def _find_underlying_model(self) -> Any:
229
+ """Try each known vLLM-internal attribute path to the loaded
230
+ ``nn.Module``, across engine versions/releases -- ``None`` if none
231
+ of them resolve on the installed vLLM version."""
232
+ candidates = (
233
+ lambda: self._llm.llm_engine.model_executor.driver_worker.model_runner.model,
234
+ lambda: self._llm.llm_engine.model_executor.driver_worker.worker.model_runner.model,
235
+ lambda: self._llm.llm_engine.engine_core.model_executor.driver_worker.model_runner.model,
236
+ )
237
+ for get in candidates:
238
+ try:
239
+ return get()
240
+ except AttributeError:
241
+ continue
242
+ return None
243
+
244
+ def model_info(self) -> dict[str, Any]:
245
+ """Real parameter count/sparsity/size if the installed vLLM
246
+ version's internal engine structure matches a known access path
247
+ (see :meth:`_find_underlying_model`); identity-only otherwise --
248
+ never a guessed number."""
249
+ self._ensure_llm()
250
+ model = self._find_underlying_model()
251
+ if model is None:
252
+ return {"is_local": True, "model_name": self._model_name}
253
+ params = list(model.parameters())
254
+ total_params = sum(p.numel() for p in params)
255
+ nonzero_params = sum(int((p != 0).sum().item()) for p in params)
256
+ size_bytes = sum(p.numel() * p.element_size() for p in params)
257
+ return {
258
+ "is_local": True,
259
+ "model_name": self._model_name,
260
+ "total_params": total_params,
261
+ "nonzero_params": nonzero_params,
262
+ "sparsity": 1.0 - (nonzero_params / total_params) if total_params else 0.0,
263
+ "size_mb": size_bytes / (1024 * 1024),
264
+ }
265
+
266
+ def _ensure_llm(self) -> None:
267
+ if self._llm is not None:
268
+ return
269
+ try:
270
+ from vllm import LLM, SamplingParams
271
+ except ImportError:
272
+ raise ExtraNotInstalled("vllm", "pip install auditkit[vllm]")
273
+ self._sampling_params = SamplingParams
274
+ with _stdout_fix():
275
+ self._llm = LLM(model=self._model_name, **self._extra_kwargs)
276
+
277
+ def _render_prompt(self, request: Request) -> str:
278
+ """Same wire-format rule as HFGenModel: instruct models get a chat template.
279
+
280
+ ``chat_template`` counts only as a non-empty str so MagicMock engines
281
+ in existing tests still send the raw prompt.
282
+ """
283
+ text = request.prompt if isinstance(request.prompt, str) else str(request.prompt)
284
+ if request.params.get("apply_chat_template") is False:
285
+ return text
286
+ tokenizer = None
287
+ getter = getattr(self._llm, "get_tokenizer", None) if self._llm is not None else None
288
+ if callable(getter):
289
+ try:
290
+ tokenizer = getter()
291
+ except Exception:
292
+ tokenizer = None
293
+ chat_template = getattr(tokenizer, "chat_template", None) if tokenizer is not None else None
294
+ if not isinstance(chat_template, str) or not chat_template:
295
+ return text
296
+ apply = getattr(tokenizer, "apply_chat_template", None)
297
+ if not callable(apply):
298
+ return text
299
+ messages = request.params.get("messages") or [{"role": "user", "content": text}]
300
+ extra = request.params.get("chat_template_kwargs") or {}
301
+ return apply(messages, tokenize=False, add_generation_prompt=True, **extra)
302
+
303
+ def generate(self, requests: list[Request]) -> list[Result_]:
304
+ self._ensure_llm()
305
+ prompts = [self._render_prompt(r) for r in requests]
306
+ # One shared SamplingParams for the whole batch -- same caveat as
307
+ # HFGenModel: per-request variation isn't meaningful here.
308
+ defaults = {"temperature": DEFAULT_TEMPERATURE, "max_tokens": 128}
309
+ gen_kwargs = resolve_params(requests[0], defaults, _KEY_MAP) if requests else defaults
310
+ params = self._sampling_params(**gen_kwargs)
311
+ outputs = self._llm.generate(prompts, params)
312
+ results = []
313
+ for out in outputs:
314
+ text = out.outputs[0].text if out.outputs else ""
315
+ results.append(Result_(completions=[Generated(text=text)]))
316
+ return results