Upload folder using huggingface_hub
Browse files- configs/verifier_v_7b.yaml +14 -8
- src/mathcompose/eval/parse.py +4 -1
- src/mathcompose/eval/processbench.py +14 -5
- src/mathcompose/eval/runners.py +12 -7
- src/mathcompose/train/train.py +31 -8
- tests/test_eval_plumbing.py +1 -1
configs/verifier_v_7b.yaml
CHANGED
|
@@ -12,22 +12,28 @@ hub_model_id: null # set via --hub-model-id, e.g. "42e/mathcompose-ve
|
|
| 12 |
model:
|
| 13 |
base_id: "Qwen/Qwen2.5-Math-7B-Instruct"
|
| 14 |
max_context: 4096
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
quant:
|
| 17 |
-
load_in_4bit: false
|
| 18 |
-
# and actually uses the VRAM (14GB weights vs ~4.5GB in 4-bit)
|
| 19 |
|
| 20 |
data:
|
| 21 |
train_path: "data/verifier/train.jsonl" # v2 genuine-detection data
|
| 22 |
val_path: "data/verifier/val.jsonl"
|
| 23 |
|
| 24 |
train:
|
| 25 |
-
max_length: 2560
|
| 26 |
-
per_device_train_batch_size:
|
| 27 |
-
gradient_accumulation_steps:
|
| 28 |
-
gradient_checkpointing:
|
| 29 |
-
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
eval:
|
| 33 |
processbench_splits: ["gsm8k", "math", "olympiadbench", "omnimath"]
|
|
|
|
| 12 |
model:
|
| 13 |
base_id: "Qwen/Qwen2.5-Math-7B-Instruct"
|
| 14 |
max_context: 4096
|
| 15 |
+
# For the FAST path (packing), install flash-attn and set:
|
| 16 |
+
# attn_implementation: flash_attention_2
|
| 17 |
+
attn_implementation: null
|
| 18 |
|
| 19 |
quant:
|
| 20 |
+
load_in_4bit: false # bf16 LoRA on the 80GB A100 (no dequant; uses the VRAM)
|
|
|
|
| 21 |
|
| 22 |
data:
|
| 23 |
train_path: "data/verifier/train.jsonl" # v2 genuine-detection data
|
| 24 |
val_path: "data/verifier/val.jsonl"
|
| 25 |
|
| 26 |
train:
|
| 27 |
+
max_length: 2560 # measured: V data maxes at 2334 tokens (p99 1715)
|
| 28 |
+
per_device_train_batch_size: 2 # no-checkpoint -> a long batch caps the size; 2 is safe (~47GB)
|
| 29 |
+
gradient_accumulation_steps: 8 # eff batch 16
|
| 30 |
+
gradient_checkpointing: false # 80GB + short data -> skip the recompute tax (was the >3hr cause)
|
| 31 |
+
optim: adamw_torch_fused
|
| 32 |
+
packing: false
|
| 33 |
+
# ~1.5 hr at these settings. FASTEST (~30-45 min) = packing, which needs FlashAttention-2:
|
| 34 |
+
# pip install flash-attn --no-build-isolation
|
| 35 |
+
# then set model.attn_implementation: flash_attention_2 AND train.packing: true.
|
| 36 |
+
# To push batch without packing: try --batch-size 3 (may OOM on a long batch) or --precision 4bit.
|
| 37 |
|
| 38 |
eval:
|
| 39 |
processbench_splits: ["gsm8k", "math", "olympiadbench", "omnimath"]
|
src/mathcompose/eval/parse.py
CHANGED
|
@@ -32,7 +32,10 @@ def majority_index(samples: list[str]) -> int:
|
|
| 32 |
counts = Counter(idxs)
|
| 33 |
top = max(counts.values())
|
| 34 |
winners = [i for i, c in counts.items() if c == top]
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
| 36 |
|
| 37 |
|
| 38 |
def p_correct_from_samples(samples: list[str]) -> float:
|
|
|
|
| 32 |
counts = Counter(idxs)
|
| 33 |
top = max(counts.values())
|
| 34 |
winners = [i for i, c in counts.items() if c == top]
|
| 35 |
+
# On a tie, surface the earliest actual error rather than defaulting to -1
|
| 36 |
+
# (only report all-correct when -1 is the *sole* top vote).
|
| 37 |
+
non_neg = [w for w in winners if w >= 0]
|
| 38 |
+
return min(non_neg) if non_neg else -1
|
| 39 |
|
| 40 |
|
| 41 |
def p_correct_from_samples(samples: list[str]) -> float:
|
src/mathcompose/eval/processbench.py
CHANGED
|
@@ -33,6 +33,13 @@ def harmonic_mean(a: float, b: float) -> float:
|
|
| 33 |
return 2 * a * b / (a + b)
|
| 34 |
|
| 35 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
@dataclass
|
| 37 |
class SubsetScore:
|
| 38 |
subset: str
|
|
@@ -108,10 +115,10 @@ def evaluate(
|
|
| 108 |
golds.append(int(row["label"]))
|
| 109 |
per_subset.append(score_subset(split, golds, preds))
|
| 110 |
|
| 111 |
-
avg_f1 = sum(s.f1 for s in per_subset) / len(per_subset) if per_subset else 0.0
|
| 112 |
return {
|
| 113 |
-
"subsets": {s.subset: s.as_dict() for s in per_subset},
|
| 114 |
-
"average_f1": avg_f1,
|
| 115 |
"gpt4o_reference_f1": 61.9,
|
| 116 |
}
|
| 117 |
|
|
@@ -157,9 +164,10 @@ def evaluate_codeexec(
|
|
| 157 |
"error_idx(>=0)": round(sum(1 for p in preds if p >= 0) / tot, 3),
|
| 158 |
"parse_fail(-2)": round(sum(1 for p in preds if p == -2) / tot, 3),
|
| 159 |
}
|
|
|
|
| 160 |
per_subset.append((score, sd))
|
| 161 |
|
| 162 |
-
avg_f1 = sum(s.f1 for s, _ in per_subset) / len(per_subset) if per_subset else 0.0
|
| 163 |
return {"subsets": {sd["subset"]: sd for _, sd in per_subset},
|
| 164 |
"average_f1": avg_f1, "gpt4o_reference_f1": 61.9, "mode": "code-exec"}
|
| 165 |
|
|
@@ -200,9 +208,10 @@ def evaluate_batched(
|
|
| 200 |
"error_idx(>=0)": round(sum(1 for p in preds if p >= 0) / tot, 3),
|
| 201 |
"parse_fail(-2)": round(sum(1 for p in preds if p == -2) / tot, 3),
|
| 202 |
}
|
|
|
|
| 203 |
per_subset.append((score, sd))
|
| 204 |
|
| 205 |
-
avg_f1 = sum(s.f1 for s, _ in per_subset) / len(per_subset) if per_subset else 0.0
|
| 206 |
return {
|
| 207 |
"subsets": {sd["subset"]: sd for _, sd in per_subset},
|
| 208 |
"average_f1": avg_f1,
|
|
|
|
| 33 |
return 2 * a * b / (a + b)
|
| 34 |
|
| 35 |
|
| 36 |
+
def _pct(sd: dict) -> dict:
|
| 37 |
+
"""Present accuracies/F1 on the 0-100 scale (to match GPT-4o's 61.9)."""
|
| 38 |
+
for k in ("acc_error", "acc_correct", "f1"):
|
| 39 |
+
sd[k] = round(sd[k] * 100, 2)
|
| 40 |
+
return sd
|
| 41 |
+
|
| 42 |
+
|
| 43 |
@dataclass
|
| 44 |
class SubsetScore:
|
| 45 |
subset: str
|
|
|
|
| 115 |
golds.append(int(row["label"]))
|
| 116 |
per_subset.append(score_subset(split, golds, preds))
|
| 117 |
|
| 118 |
+
avg_f1 = 100 * sum(s.f1 for s in per_subset) / len(per_subset) if per_subset else 0.0
|
| 119 |
return {
|
| 120 |
+
"subsets": {s.subset: _pct(s.as_dict()) for s in per_subset},
|
| 121 |
+
"average_f1": round(avg_f1, 2),
|
| 122 |
"gpt4o_reference_f1": 61.9,
|
| 123 |
}
|
| 124 |
|
|
|
|
| 164 |
"error_idx(>=0)": round(sum(1 for p in preds if p >= 0) / tot, 3),
|
| 165 |
"parse_fail(-2)": round(sum(1 for p in preds if p == -2) / tot, 3),
|
| 166 |
}
|
| 167 |
+
_pct(sd)
|
| 168 |
per_subset.append((score, sd))
|
| 169 |
|
| 170 |
+
avg_f1 = round(100 * sum(s.f1 for s, _ in per_subset) / len(per_subset), 2) if per_subset else 0.0
|
| 171 |
return {"subsets": {sd["subset"]: sd for _, sd in per_subset},
|
| 172 |
"average_f1": avg_f1, "gpt4o_reference_f1": 61.9, "mode": "code-exec"}
|
| 173 |
|
|
|
|
| 208 |
"error_idx(>=0)": round(sum(1 for p in preds if p >= 0) / tot, 3),
|
| 209 |
"parse_fail(-2)": round(sum(1 for p in preds if p == -2) / tot, 3),
|
| 210 |
}
|
| 211 |
+
_pct(sd)
|
| 212 |
per_subset.append((score, sd))
|
| 213 |
|
| 214 |
+
avg_f1 = round(100 * sum(s.f1 for s, _ in per_subset) / len(per_subset), 2) if per_subset else 0.0
|
| 215 |
return {
|
| 216 |
"subsets": {sd["subset"]: sd for _, sd in per_subset},
|
| 217 |
"average_f1": avg_f1,
|
src/mathcompose/eval/runners.py
CHANGED
|
@@ -50,6 +50,10 @@ class HFRunner:
|
|
| 50 |
from peft import PeftModel
|
| 51 |
|
| 52 |
model = PeftModel.from_pretrained(model, adapter_path)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
self.model = model.to(self.device).eval()
|
| 54 |
|
| 55 |
def chat(
|
|
@@ -58,7 +62,7 @@ class HFRunner:
|
|
| 58 |
n: int = 1,
|
| 59 |
temperature: float = 0.0,
|
| 60 |
max_new_tokens: int = 1024,
|
| 61 |
-
repetition_penalty: float = 1.
|
| 62 |
) -> list[str]:
|
| 63 |
"""Return n decoded completions (generated text only)."""
|
| 64 |
import torch
|
|
@@ -102,16 +106,17 @@ class HFRunner:
|
|
| 102 |
messages_list: list[list[dict]],
|
| 103 |
n: int = 1,
|
| 104 |
temperature: float = 0.0,
|
| 105 |
-
max_new_tokens: int =
|
| 106 |
-
repetition_penalty: float = 1.
|
| 107 |
) -> list[list[str]]:
|
| 108 |
"""Batched generation across many prompts (left-padded). Returns, per input
|
| 109 |
prompt, a list of n decoded completions. Far faster than looping chat()."""
|
| 110 |
import torch
|
| 111 |
|
| 112 |
tok = self.tokenizer
|
| 113 |
-
old_side = tok.padding_side
|
| 114 |
-
tok.padding_side = "left"
|
|
|
|
| 115 |
try:
|
| 116 |
rendered = [
|
| 117 |
tok.apply_chat_template(m, add_generation_prompt=True, tokenize=False)
|
|
@@ -123,7 +128,7 @@ class HFRunner:
|
|
| 123 |
add_special_tokens=False,
|
| 124 |
)
|
| 125 |
finally:
|
| 126 |
-
tok.padding_side = old_side
|
| 127 |
|
| 128 |
input_ids = enc["input_ids"].to(self.device)
|
| 129 |
attn = enc["attention_mask"].to(self.device)
|
|
@@ -179,7 +184,7 @@ def auto_concurrent_seqs() -> int:
|
|
| 179 |
|
| 180 |
|
| 181 |
def verify_batch_predictions(runner: HFRunner, items, n: int = 1, batch_size: int = 16,
|
| 182 |
-
temperature: float = 0.0, max_new_tokens: int =
|
| 183 |
desc: str = "verify") -> list[int]:
|
| 184 |
"""items: list of (problem, steps). Returns the Maj@n first-error index per item,
|
| 185 |
using batched generation. This is the fast path used by ProcessBench/PRMBench."""
|
|
|
|
| 50 |
from peft import PeftModel
|
| 51 |
|
| 52 |
model = PeftModel.from_pretrained(model, adapter_path)
|
| 53 |
+
try:
|
| 54 |
+
model = model.merge_and_unload() # fold LoRA into base -> faster forward
|
| 55 |
+
except Exception:
|
| 56 |
+
pass # can't merge a quantized base; keep the adapter attached
|
| 57 |
self.model = model.to(self.device).eval()
|
| 58 |
|
| 59 |
def chat(
|
|
|
|
| 62 |
n: int = 1,
|
| 63 |
temperature: float = 0.0,
|
| 64 |
max_new_tokens: int = 1024,
|
| 65 |
+
repetition_penalty: float = 1.0, # 1.1 fights V's intentionally repetitive verdicts
|
| 66 |
) -> list[str]:
|
| 67 |
"""Return n decoded completions (generated text only)."""
|
| 68 |
import torch
|
|
|
|
| 106 |
messages_list: list[list[dict]],
|
| 107 |
n: int = 1,
|
| 108 |
temperature: float = 0.0,
|
| 109 |
+
max_new_tokens: int = 1024,
|
| 110 |
+
repetition_penalty: float = 1.0,
|
| 111 |
) -> list[list[str]]:
|
| 112 |
"""Batched generation across many prompts (left-padded). Returns, per input
|
| 113 |
prompt, a list of n decoded completions. Far faster than looping chat()."""
|
| 114 |
import torch
|
| 115 |
|
| 116 |
tok = self.tokenizer
|
| 117 |
+
old_side, old_trunc = tok.padding_side, tok.truncation_side
|
| 118 |
+
tok.padding_side = "left" # required for correct batched generation
|
| 119 |
+
tok.truncation_side = "left" # keep the TAIL (the \boxed instruction + last steps)
|
| 120 |
try:
|
| 121 |
rendered = [
|
| 122 |
tok.apply_chat_template(m, add_generation_prompt=True, tokenize=False)
|
|
|
|
| 128 |
add_special_tokens=False,
|
| 129 |
)
|
| 130 |
finally:
|
| 131 |
+
tok.padding_side, tok.truncation_side = old_side, old_trunc
|
| 132 |
|
| 133 |
input_ids = enc["input_ids"].to(self.device)
|
| 134 |
attn = enc["attention_mask"].to(self.device)
|
|
|
|
| 184 |
|
| 185 |
|
| 186 |
def verify_batch_predictions(runner: HFRunner, items, n: int = 1, batch_size: int = 16,
|
| 187 |
+
temperature: float = 0.0, max_new_tokens: int = 1024,
|
| 188 |
desc: str = "verify") -> list[int]:
|
| 189 |
"""items: list of (problem, steps). Returns the Maj@n first-error index per item,
|
| 190 |
using batched generation. This is the fast path used by ProcessBench/PRMBench."""
|
src/mathcompose/train/train.py
CHANGED
|
@@ -80,7 +80,7 @@ def build_and_train(args) -> str:
|
|
| 80 |
import torch
|
| 81 |
|
| 82 |
if torch.cuda.is_available():
|
| 83 |
-
torch.
|
| 84 |
torch.backends.cudnn.allow_tf32 = True
|
| 85 |
from datasets import load_dataset
|
| 86 |
from transformers import AutoModelForCausalLM
|
|
@@ -119,16 +119,26 @@ def build_and_train(args) -> str:
|
|
| 119 |
if preset:
|
| 120 |
print(f"[preset] {label} -> {preset}")
|
| 121 |
|
| 122 |
-
|
| 123 |
-
if
|
| 124 |
-
|
| 125 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
|
| 127 |
tok = load_tokenizer(base_id, pad_token=cfg["model"]["pad_token"])
|
| 128 |
|
| 129 |
model_kwargs = {"dtype": compute_dtype}
|
| 130 |
if use_4bit:
|
| 131 |
model_kwargs["quantization_config"] = build_bnb_config(cfg, compute_dtype=compute_dtype)
|
|
|
|
|
|
|
| 132 |
model = AutoModelForCausalLM.from_pretrained(base_id, **model_kwargs)
|
| 133 |
|
| 134 |
if has_cuda and compute_dtype in (torch.float16, torch.float32):
|
|
@@ -181,9 +191,19 @@ def build_and_train(args) -> str:
|
|
| 181 |
if grad_ckpt and hasattr(model, "enable_input_require_grads"):
|
| 182 |
model.enable_input_require_grads()
|
| 183 |
|
| 184 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
print(f"[train] epochs={epochs} batch={batch} accum={accum} (eff={batch * accum}) "
|
| 186 |
-
f"max_length={maxlen} grad_ckpt={grad_ckpt}")
|
| 187 |
|
| 188 |
sft_args = SFTConfig(
|
| 189 |
output_dir=output_dir,
|
|
@@ -197,11 +217,12 @@ def build_and_train(args) -> str:
|
|
| 197 |
save_steps=t.get("save_steps", 50),
|
| 198 |
max_length=maxlen,
|
| 199 |
completion_only_loss=t.get("completion_only_loss", True),
|
| 200 |
-
packing=t.get("packing", False),
|
| 201 |
gradient_checkpointing=grad_ckpt,
|
|
|
|
| 202 |
optim=optim,
|
| 203 |
bf16=bf16_ok and not amp_off,
|
| 204 |
fp16=has_cuda and not bf16_ok and not amp_off,
|
|
|
|
| 205 |
seed=t.get("seed", 0),
|
| 206 |
report_to=(["wandb"] if args.wandb else "none"),
|
| 207 |
max_steps=args.max_steps if args.max_steps else -1,
|
|
@@ -255,6 +276,8 @@ def main() -> None:
|
|
| 255 |
default="auto", help="GPU-tuned defaults (auto-detects by default)")
|
| 256 |
ap.add_argument("--no-amp", action="store_true",
|
| 257 |
help="disable fp16/bf16 mixed precision (T4 fallback if the scaler errors)")
|
|
|
|
|
|
|
| 258 |
ap.add_argument("--wandb", action="store_true")
|
| 259 |
ap.add_argument("--push", action="store_true", help="push adapter to HF Hub")
|
| 260 |
ap.add_argument("--hub-model-id", default=None)
|
|
|
|
| 80 |
import torch
|
| 81 |
|
| 82 |
if torch.cuda.is_available():
|
| 83 |
+
torch.set_float32_matmul_precision("high") # TF32 matmuls (A100+): ~1.3x, current API
|
| 84 |
torch.backends.cudnn.allow_tf32 = True
|
| 85 |
from datasets import load_dataset
|
| 86 |
from transformers import AutoModelForCausalLM
|
|
|
|
| 119 |
if preset:
|
| 120 |
print(f"[preset] {label} -> {preset}")
|
| 121 |
|
| 122 |
+
prec = getattr(args, "precision", "auto")
|
| 123 |
+
if prec == "4bit":
|
| 124 |
+
want_4bit = True
|
| 125 |
+
elif prec == "bf16":
|
| 126 |
+
want_4bit = False
|
| 127 |
+
else:
|
| 128 |
+
want_4bit = bool(cfg["quant"]["load_in_4bit"])
|
| 129 |
+
if getattr(args, "no_4bit", False):
|
| 130 |
+
want_4bit = False
|
| 131 |
+
use_4bit = want_4bit and has_cuda
|
| 132 |
+
if want_4bit and not has_cuda:
|
| 133 |
+
print("[warn] 4-bit requested but no CUDA — full-precision on CPU.")
|
| 134 |
|
| 135 |
tok = load_tokenizer(base_id, pad_token=cfg["model"]["pad_token"])
|
| 136 |
|
| 137 |
model_kwargs = {"dtype": compute_dtype}
|
| 138 |
if use_4bit:
|
| 139 |
model_kwargs["quantization_config"] = build_bnb_config(cfg, compute_dtype=compute_dtype)
|
| 140 |
+
if cfg["model"].get("attn_implementation"):
|
| 141 |
+
model_kwargs["attn_implementation"] = cfg["model"]["attn_implementation"]
|
| 142 |
model = AutoModelForCausalLM.from_pretrained(base_id, **model_kwargs)
|
| 143 |
|
| 144 |
if has_cuda and compute_dtype in (torch.float16, torch.float32):
|
|
|
|
| 191 |
if grad_ckpt and hasattr(model, "enable_input_require_grads"):
|
| 192 |
model.enable_input_require_grads()
|
| 193 |
|
| 194 |
+
if not has_cuda:
|
| 195 |
+
optim = "adamw_torch" # CPU: bnb/fused optimizers need CUDA
|
| 196 |
+
elif use_4bit:
|
| 197 |
+
optim = t.get("optim") or "paged_adamw_8bit"
|
| 198 |
+
else:
|
| 199 |
+
optim = t.get("optim") or "adamw_torch_fused"
|
| 200 |
+
packing = bool(t.get("packing", False))
|
| 201 |
+
if packing and cfg["model"].get("attn_implementation") != "flash_attention_2":
|
| 202 |
+
print("[warn] packing WITHOUT flash_attention_2 cross-contaminates examples "
|
| 203 |
+
"(attends across packed rows). Install flash-attn and set "
|
| 204 |
+
"model.attn_implementation: flash_attention_2, or disable packing.")
|
| 205 |
print(f"[train] epochs={epochs} batch={batch} accum={accum} (eff={batch * accum}) "
|
| 206 |
+
f"max_length={maxlen} grad_ckpt={grad_ckpt} packing={packing} optim={optim}")
|
| 207 |
|
| 208 |
sft_args = SFTConfig(
|
| 209 |
output_dir=output_dir,
|
|
|
|
| 217 |
save_steps=t.get("save_steps", 50),
|
| 218 |
max_length=maxlen,
|
| 219 |
completion_only_loss=t.get("completion_only_loss", True),
|
|
|
|
| 220 |
gradient_checkpointing=grad_ckpt,
|
| 221 |
+
packing=packing, # concatenate rows -> ~no padding (needs FA2; see guard)
|
| 222 |
optim=optim,
|
| 223 |
bf16=bf16_ok and not amp_off,
|
| 224 |
fp16=has_cuda and not bf16_ok and not amp_off,
|
| 225 |
+
save_total_limit=1, # don't fill ephemeral disk with checkpoints
|
| 226 |
seed=t.get("seed", 0),
|
| 227 |
report_to=(["wandb"] if args.wandb else "none"),
|
| 228 |
max_steps=args.max_steps if args.max_steps else -1,
|
|
|
|
| 276 |
default="auto", help="GPU-tuned defaults (auto-detects by default)")
|
| 277 |
ap.add_argument("--no-amp", action="store_true",
|
| 278 |
help="disable fp16/bf16 mixed precision (T4 fallback if the scaler errors)")
|
| 279 |
+
ap.add_argument("--precision", choices=["auto", "4bit", "bf16"], default="auto",
|
| 280 |
+
help="override base precision: 4bit (QLoRA) or bf16 (LoRA); auto=config")
|
| 281 |
ap.add_argument("--wandb", action="store_true")
|
| 282 |
ap.add_argument("--push", action="store_true", help="push adapter to HF Hub")
|
| 283 |
ap.add_argument("--hub-model-id", default=None)
|
tests/test_eval_plumbing.py
CHANGED
|
@@ -43,5 +43,5 @@ def test_batched_processbench_eval_runs():
|
|
| 43 |
|
| 44 |
runner = HFRunner(TINY, max_context=512)
|
| 45 |
res = evaluate_batched(runner, splits=["gsm8k"], limit=4, n=1, batch_size=2, max_new_tokens=6)
|
| 46 |
-
assert 0.0 <= res["average_f1"] <=
|
| 47 |
assert "gsm8k" in res["subsets"]
|
|
|
|
| 43 |
|
| 44 |
runner = HFRunner(TINY, max_context=512)
|
| 45 |
res = evaluate_batched(runner, splits=["gsm8k"], limit=4, n=1, batch_size=2, max_new_tokens=6)
|
| 46 |
+
assert 0.0 <= res["average_f1"] <= 100.0 # now reported on the 0-100 scale
|
| 47 |
assert "gsm8k" in res["subsets"]
|