Text Generation
Transformers
Safetensors
English
qwen2
code
codeqwen
chat
qwen
qwen-coder
conversational
text-generation-inference
4-bit precision
awq
Instructions to use divpasta123/Qwen2.5-Coder-14B-Instruct-AWQ with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use divpasta123/Qwen2.5-Coder-14B-Instruct-AWQ with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-generation", model="divpasta123/Qwen2.5-Coder-14B-Instruct-AWQ") messages = [ {"role": "user", "content": "Who are you?"}, ] pipe(messages)# Load model directly from transformers import AutoTokenizer, AutoModelForCausalLM tokenizer = AutoTokenizer.from_pretrained("divpasta123/Qwen2.5-Coder-14B-Instruct-AWQ") model = AutoModelForCausalLM.from_pretrained("divpasta123/Qwen2.5-Coder-14B-Instruct-AWQ", device_map="auto") messages = [ {"role": "user", "content": "Who are you?"}, ] inputs = tokenizer.apply_chat_template( messages, add_generation_prompt=True, tokenize=True, return_dict=True, return_tensors="pt", ).to(model.device) outputs = model.generate(**inputs, max_new_tokens=40) print(tokenizer.decode(outputs[0][inputs["input_ids"].shape[-1]:])) - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use divpasta123/Qwen2.5-Coder-14B-Instruct-AWQ with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "divpasta123/Qwen2.5-Coder-14B-Instruct-AWQ" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "divpasta123/Qwen2.5-Coder-14B-Instruct-AWQ", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }'Use Docker
docker model run hf.co/divpasta123/Qwen2.5-Coder-14B-Instruct-AWQ
- SGLang
How to use divpasta123/Qwen2.5-Coder-14B-Instruct-AWQ with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "divpasta123/Qwen2.5-Coder-14B-Instruct-AWQ" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "divpasta123/Qwen2.5-Coder-14B-Instruct-AWQ", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "divpasta123/Qwen2.5-Coder-14B-Instruct-AWQ" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "divpasta123/Qwen2.5-Coder-14B-Instruct-AWQ", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }' - Docker Model Runner
How to use divpasta123/Qwen2.5-Coder-14B-Instruct-AWQ with Docker Model Runner:
docker model run hf.co/divpasta123/Qwen2.5-Coder-14B-Instruct-AWQ
| #!/usr/bin/env python | |
| """IOL-AI 2026 submission -- International Linguistics Olympiad solver | |
| * Item-count detection now also understands lowercase sub-item letters and | |
| fill-in-the-blank underscores, which the previous regex set missed. | |
| * Answer voting no longer requires byte-identical normalized strings to | |
| agree -- it clusters samples by chrF similarity, so "iepure" and | |
| "iepure " and "Iepure" (or a stray trailing gloss) still count as the | |
| same vote instead of splitting support three ways. | |
| * Self-consistency sampling is convergence-aware: once a problem block's | |
| items all have a confident majority, that block is dropped from the | |
| prompt list for later rounds, so remaining time budget concentrates on | |
| the rows that are still undecided rather than re-rolling rows that have | |
| already settled. | |
| * The matching-task assignment solver is kept (free-form generation on | |
| permutation tasks tends to just emit the identity ordering) but is now | |
| reused for the "which digit" family of tasks too, with a lighter | |
| single-token constraint. | |
| * A rough token-length probe runs right after the tokenizer loads, so the | |
| starting batch size is chosen from the actual prompt lengths for this | |
| file instead of always starting at a fixed guess and paying for at least | |
| one guaranteed OOM. | |
| Environment: transformers 4.44.1 / torch 2.4.0 / autoawq, 16GB T4, fp16 only, | |
| no internet, hard 30-minute wall clock. | |
| """ | |
| import os | |
| import re | |
| import json | |
| import time | |
| import unicodedata | |
| from collections import Counter, defaultdict | |
| START = time.time() | |
| # --------------------------------------------------------------------------- | |
| # Budget | |
| # --------------------------------------------------------------------------- | |
| # The harness kills the process at the wall-clock limit with zero credit for | |
| # work in flight, so every stage aims to finish with a margin, not exactly on | |
| # time. SAFETY is subtracted up front rather than checked ad hoc so every | |
| # caller of time_left() sees the same conservative deadline. | |
| TIME_LIMIT = float(os.environ.get("IOL_TIME_LIMIT", "1800")) | |
| SAFETY = float(os.environ.get("IOL_SAFETY", "150")) | |
| DEADLINE = START + TIME_LIMIT - SAFETY | |
| TEST_CSV = os.environ.get("IOL_TEST_CSV", "/tmp/data/test.csv") | |
| OUT_CSV = os.environ.get("IOL_OUT_CSV", "submission.csv") | |
| MODEL_ID = os.environ.get("IOL_MODEL", ".") | |
| WANT_EXPLANATION = os.environ.get("IOL_EXPLAIN", "1") == "1" | |
| MAX_NEW = int(os.environ.get("IOL_MAXNEW", "900")) | |
| MAX_SAMPLES = int(os.environ.get("IOL_MAXSAMPLES", "8")) | |
| # Earlier runs found the organizers' plain, no-chain-of-thought prompt style | |
| # outscored an elaborate CoT harness on the hidden set. Default to that | |
| # "faithful" style, but keep the richer CoT prompt available for A/B use -- | |
| # the parsing/voting/repair layers below are written to help either mode | |
| # rather than assuming one. | |
| PROMPT_STYLE = os.environ.get("IOL_PROMPT_STYLE", "faithful") # faithful | guided | |
| SAMPLE_TEMP = float(os.environ.get("IOL_TEMP", "0.5")) | |
| os.environ.setdefault("HF_HUB_OFFLINE", "1") | |
| os.environ.setdefault("TRANSFORMERS_OFFLINE", "1") | |
| os.environ.setdefault("TOKENIZERS_PARALLELISM", "false") | |
| os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True") | |
| def log(msg): | |
| print(f"[{time.time() - START:7.1f}s] {msg}", flush=True) | |
| def time_left(): | |
| return DEADLINE - time.time() | |
| # --------------------------------------------------------------------------- | |
| # How many items does this problem block ask for? | |
| # --------------------------------------------------------------------------- | |
| _NUM_LINE = re.compile(r"^[ \t]*(\d{1,3})[.)\]]", re.M) | |
| _NUM_PAREN = re.compile(r"\((\d{1,3})\)") | |
| _NUM_RANGE = re.compile(r"\(?(\d{1,3})\s*(?:[-\u2013\u2014]|to)\s*(\d{1,3})\)?") | |
| _LETTER_LINE = re.compile(r"^[ \t]*([A-Za-z])[.)\]]\s", re.M) | |
| _LETTER_PAREN = re.compile(r"\(([A-Za-z])\)") | |
| _BLANK_MARK = re.compile(r"_{2,}|\[\s*(?:blank|___*)\s*\]", re.I) | |
| def count_items(query, task_type="", context=""): | |
| """Best-effort count of the sub-answers a block expects. Never returns 0.""" | |
| q = query or "" | |
| nums = set(int(m) for m in _NUM_LINE.findall(q)) | set( | |
| int(m) for m in _NUM_PAREN.findall(q)) | |
| letters = set(_LETTER_LINE.findall(q)) | set(_LETTER_PAREN.findall(q)) | |
| span = 0 | |
| for a, b in _NUM_RANGE.findall(q): | |
| a, b = int(a), int(b) | |
| if 0 < b - a < 60: | |
| span = max(span, b - a + 1) | |
| marker_count = max(len(nums), len(letters)) | |
| if span and marker_count and span != marker_count: | |
| # Explicit markers on the page are what must actually be answered, | |
| # even if a stated range like "items 1-5" disagrees with them. | |
| n = marker_count | |
| else: | |
| n = max(span, marker_count) | |
| if n > 1: | |
| return n | |
| blanks = len(_BLANK_MARK.findall(q)) | |
| if blanks > 1: | |
| return blanks | |
| # No numbering at all: "Translate the following into Kalam:" followed by | |
| # one bare item per line. | |
| body_lines = [l.strip() for l in q.splitlines() if l.strip()] | |
| if len(body_lines) > 1: | |
| head, rest = body_lines[0], body_lines[1:] | |
| if rest and (head.endswith((":", ".")) or len(rest) >= 2): | |
| return len(rest) | |
| # A bare instruction ("State the correspondence for each pair.") whose | |
| # items actually live in the shared context block. | |
| if context: | |
| cn = len(set(int(m) for m in _NUM_LINE.findall(context))) | |
| if cn > 1: | |
| return cn | |
| cl = len(set(_LETTER_LINE.findall(context))) | |
| if cl > 1: | |
| return cl | |
| return max(n, 1) | |
| # --------------------------------------------------------------------------- | |
| # Cleaning and parsing model output into exactly n answers | |
| # --------------------------------------------------------------------------- | |
| _LEAD_MARK = re.compile(r"^\s*(?:\(?\d{1,3}\)?[.):\]]\s*|[-*\u2022]\s+)") | |
| _CODE_FENCE = re.compile(r"^```[a-zA-Z]*\s*$") | |
| _META_TALK = re.compile( | |
| r"^\s*(?:here (?:are|is)\b|answers?\s*:?\s*$|explanation\b|note\b|okay\b|" | |
| r"solution\b|reasoning\b|analysis\b|translations?\s*:?\s*$|the answers?\b|" | |
| r"let me\b|first,|so,|therefore\b|thus\b|to summarize\b)", | |
| re.I, | |
| ) | |
| def tidy(line): | |
| s = line.strip() | |
| s = _LEAD_MARK.sub("", s) | |
| s = s.strip().strip("`").strip() | |
| if len(s) >= 2 and s[0] == s[-1] and s[0] in "\"'\u201c\u201d": | |
| s = s[1:-1].strip() | |
| return s.strip() | |
| def source_fallback(query, n): | |
| """Last-resort answers when the model produced nothing usable. | |
| A blank answer scores zero on both exact-match and chrF, but echoing the | |
| prompt's own item text is nearly free and, on transcription-adjacent | |
| tasks, shares enough surface form with the true answer to pick up | |
| partial chrF credit. Better than nothing, never claimed to be more. | |
| """ | |
| q = query or "" | |
| picked = [] | |
| for ln in q.splitlines(): | |
| s = ln.strip() | |
| if not s: | |
| continue | |
| m = re.match(r"^\(?(\d{1,3})\)?[.):\]]\s*(.+)$", s) | |
| if m: | |
| picked.append(m.group(2).strip()) | |
| if not picked: | |
| lines = [l.strip() for l in q.splitlines() if l.strip()] | |
| if len(lines) > 1 and lines[0].endswith((":", ".")): | |
| picked = lines[1:] | |
| picked = [p.split("|")[0].strip() if "|" in p else p for p in picked] | |
| picked = [p for p in picked if p] | |
| while len(picked) < n: | |
| picked.append(picked[-1] if picked else "?") | |
| return picked[:n] | |
| def squeeze_to_n(items, n, fallback=None): | |
| items = [i for i in items if i and i.strip()] | |
| if len(items) > n: | |
| # The model reasons first and answers last; absent an explicit | |
| # marker to slice on, the tail of the output is the answer block. | |
| items = items[-n:] | |
| while len(items) < n: | |
| if fallback and len(items) < len(fallback): | |
| items.append(fallback[len(items)]) | |
| else: | |
| items.append(items[-1] if items else "?") | |
| return items[:n] | |
| def parse_model_output(text, n, fallback=None): | |
| if not text or not text.strip(): | |
| return list(fallback[:n]) if fallback else ["?"] * n | |
| marker = None | |
| for m in re.finditer(r"(?:^|\n)\s*(?:final\s+)?answers?\s*:\s*\n?", text, re.I): | |
| marker = m | |
| body = text[marker.end():] if marker else text | |
| labelled, plain = [], [] | |
| for ln in body.splitlines(): | |
| if _CODE_FENCE.match(ln): | |
| continue | |
| m = re.match(r"^\s*\(?(\d{1,3})\)?[.):\]]\s*(.+)$", ln.strip()) | |
| if m: | |
| val = tidy(m.group(2)) | |
| if val and not _META_TALK.match(val): | |
| labelled.append((int(m.group(1)), val)) | |
| c = tidy(ln) | |
| if c and not _META_TALK.match(c): | |
| plain.append(c) | |
| if len(labelled) >= n: | |
| # Numbered labels, when present, are a stronger placement signal than | |
| # line order -- keep the last value written under each label since | |
| # models sometimes restate an earlier answer while reasoning. | |
| by_label = {} | |
| for lab, val in labelled: | |
| by_label[lab] = val | |
| keys = sorted(by_label) | |
| if len(keys) >= n: | |
| return [by_label[k] for k in keys[:n]] | |
| return squeeze_to_n(plain, n, fallback) | |
| _DIGITS_ONLY = re.compile(r"-?\d+") | |
| def coerce_numeral(answer, task_type): | |
| """text_to_num answers should be bare digits; pull them out if the model | |
| wrapped them in words or punctuation instead of reworking the parser for | |
| one task family.""" | |
| if (task_type or "").strip().lower() != "text_to_num": | |
| return answer | |
| m = _DIGITS_ONLY.search(answer or "") | |
| return m.group(0) if m else answer | |
| def normalize(s): | |
| s = unicodedata.normalize("NFC", (s or "").strip().lower()) | |
| s = re.sub(r"\s+", " ", s) | |
| return s.strip(" .!?;:,") | |
| # --------------------------------------------------------------------------- | |
| # chrF, computed inline since sacrebleu may not be present in the sandbox | |
| # --------------------------------------------------------------------------- | |
| def _char_ngrams(s, k): | |
| s = re.sub(r"\s+", "", s) | |
| if len(s) < k: | |
| return Counter() | |
| return Counter(s[i:i + k] for i in range(len(s) - k + 1)) | |
| def chrf(hyp, ref, order=6, beta=2.0): | |
| if not hyp or not ref: | |
| return 0.0 | |
| precisions, recalls = [], [] | |
| for k in range(1, order + 1): | |
| h, r = _char_ngrams(hyp, k), _char_ngrams(ref, k) | |
| if not h or not r: | |
| continue | |
| overlap = sum((h & r).values()) | |
| precisions.append(overlap / max(1, sum(h.values()))) | |
| recalls.append(overlap / max(1, sum(r.values()))) | |
| if not precisions: | |
| return 0.0 | |
| p = sum(precisions) / len(precisions) | |
| r = sum(recalls) / len(recalls) | |
| if p + r == 0: | |
| return 0.0 | |
| b2 = beta * beta | |
| return (1 + b2) * p * r / (b2 * p + r) | |
| # --------------------------------------------------------------------------- | |
| # Voting: cluster candidates by similarity rather than exact string match | |
| # --------------------------------------------------------------------------- | |
| CLUSTER_THRESHOLD = 0.90 # chrF similarity above which two answers "agree" | |
| def cluster_candidates(cands): | |
| """Group near-duplicate strings together (single-linkage on chrF). | |
| Exact-match voting undercounts agreement: "iepurele" and "iepurele " or a | |
| stray trailing gloss are the same underlying answer but would previously | |
| split into separate buckets and never reach a majority. Grouping by | |
| similarity instead means minor formatting noise doesn't cost a block its | |
| vote. | |
| """ | |
| groups = [] # list of [members] | |
| for c in cands: | |
| placed = False | |
| for g in groups: | |
| if chrf(c, g[0]) >= CLUSTER_THRESHOLD or normalize(c) == normalize(g[0]): | |
| g.append(c) | |
| placed = True | |
| break | |
| if not placed: | |
| groups.append([c]) | |
| return groups | |
| def vote_answer(cands, anchor=None): | |
| """Choose one answer for an item from the greedy pass plus samples. | |
| The greedy (temperature 0) answer is the default and can only be | |
| displaced by a cluster that (a) has at least two members and (b) is | |
| strictly larger than the cluster the anchor itself belongs to. This | |
| asymmetry matters: with only a handful of samples, treating all | |
| candidates as equal and breaking ties by "closest to the others" is | |
| poorly conditioned -- with two candidates that comparison is symmetric | |
| and degenerates into picking whichever string happens to be shorter. On | |
| a held-out mock set that variant swapped out the greedy answer for a | |
| sampled one roughly half the time and cost a large chunk of exact match. | |
| Anchoring keeps voting monotone: it only overrides on genuine, repeated | |
| agreement. | |
| """ | |
| cands = [c for c in cands if c and c.strip()] | |
| if anchor is None: | |
| anchor = cands[0] if cands else "?" | |
| if len(cands) < 3: | |
| return anchor | |
| groups = cluster_candidates(cands) | |
| anchor_group_size = 1 | |
| for g in groups: | |
| if any(normalize(m) == normalize(anchor) or chrf(m, anchor) >= CLUSTER_THRESHOLD | |
| for m in g): | |
| anchor_group_size = len(g) | |
| break | |
| best_group = max(groups, key=len) | |
| if len(best_group) >= 2 and len(best_group) > anchor_group_size: | |
| # Within the winning cluster, prefer whichever exact form is most | |
| # common; ties fall back to the first (greedy-adjacent) occurrence. | |
| return Counter(best_group).most_common(1)[0][0] | |
| return anchor | |
| # --------------------------------------------------------------------------- | |
| # Matching / assignment problems: read next-token logits instead of trusting | |
| # free-form generation, which tends to just emit the option letters in order. | |
| # --------------------------------------------------------------------------- | |
| _ROW_OPT = re.compile(r"^[ \t]*([A-Za-z])[.)]\s+(.+)$", re.M) | |
| _ROW_ITEM = re.compile(r"^[ \t]*(\d{1,3})[.)]\s+(.+)$", re.M) | |
| def split_matching_block(context): | |
| items = [(int(a), b.strip()) for a, b in _ROW_ITEM.findall(context or "")] | |
| opts = [(a, b.strip()) for a, b in _ROW_OPT.findall(context or "")] | |
| seen = set() | |
| items = [x for x in items if not (x[0] in seen or seen.add(x[0]))] | |
| seen = set() | |
| opts = [x for x in opts if not (x[0] in seen or seen.add(x[0]))] | |
| return items, opts | |
| def assign_max_weight(score): | |
| """One-to-one assignment maximizing total score. scipy if available, | |
| otherwise a greedy pass plus a few rounds of local 2-swaps.""" | |
| n, m = len(score), len(score[0]) | |
| try: | |
| import numpy as _np | |
| from scipy.optimize import linear_sum_assignment | |
| _, cols = linear_sum_assignment(-_np.array(score)) | |
| return list(cols) | |
| except Exception: | |
| pass | |
| used, out = set(), [0] * n | |
| order = sorted( | |
| range(n), | |
| key=lambda i: -(max(score[i]) - sorted(score[i])[-2] if m > 1 else 0), | |
| ) | |
| for i in order: | |
| j = max((j for j in range(m) if j not in used), key=lambda j: score[i][j], | |
| default=0) | |
| used.add(j) | |
| out[i] = j | |
| for _ in range(4): | |
| improved = False | |
| for a in range(n): | |
| for b in range(a + 1, n): | |
| cur = score[a][out[a]] + score[b][out[b]] | |
| alt = score[a][out[b]] + score[b][out[a]] | |
| if alt > cur + 1e-9: | |
| out[a], out[b] = out[b], out[a] | |
| improved = True | |
| if not improved: | |
| break | |
| return out | |
| def enforce_bijection(answers): | |
| """If every answer is a single letter and duplicates exist where the full | |
| alphabet of options wasn't used, reassign duplicated slots to the unused | |
| letters. Tightly scoped so it never touches anything but this exact | |
| single-letter-permutation shape.""" | |
| if len(answers) < 3 or not all(re.fullmatch(r"[A-Za-z]", a or "") for a in answers): | |
| return answers | |
| n = len(answers) | |
| universe = [chr(ord("A") + i) for i in range(n)] | |
| upper = [a.upper() for a in answers] | |
| if len(set(upper)) == n: | |
| return answers | |
| unused = [l for l in universe if l not in set(upper)] | |
| if not unused: | |
| return answers | |
| seen, out = set(), [] | |
| for a in upper: | |
| if a in seen and unused: | |
| out.append(unused.pop(0)) | |
| else: | |
| seen.add(a) | |
| out.append(a) | |
| return out | |
| # --------------------------------------------------------------------------- | |
| # Prompting | |
| # --------------------------------------------------------------------------- | |
| FAITHFUL_SYSTEM = ( | |
| "You solve International Linguistics Olympiad problems. Answer every " | |
| "numbered item. Put each answer on its own line, in order, with no " | |
| "numbering and no extra text." | |
| ) | |
| GUIDED_SYSTEM = ( | |
| "You are a gold medallist at the International Linguistics Olympiad.\n" | |
| "Each problem gives data from a language you have never seen before; " | |
| "everything needed is in the problem itself, and no outside knowledge is " | |
| "required.\n" | |
| "Work method: align the given examples, segment the forms, find the " | |
| "recurring morphemes and the rule that orders them, verify the rule " | |
| "against every example given, then apply it to the items you are asked " | |
| "for.\n" | |
| "Reason concisely. Finish with a block that starts on its own line with " | |
| "exactly ANSWERS: followed by one answer per line, in order, with no " | |
| "numbering, no commentary, and no blank lines.\n" | |
| "Always give your best guess for every item -- never leave one blank." | |
| ) | |
| TASK_HINTS = { | |
| "translation": "Give only the translation itself -- no source text, no " | |
| "gloss, no quotation marks.", | |
| "match_letters": "Give a single capital letter per item, identifying its " | |
| "match. Every letter is used exactly once; none repeats.", | |
| "fill_blanks": "Give only the missing form for that blank -- not the " | |
| "whole line, not a gloss.", | |
| "text_to_num": "Give the number in digits only (e.g. 111).", | |
| "num_to_text": "Give the number spelled out in words, in the problem " | |
| "language.", | |
| } | |
| def build_prompt(row, n): | |
| if PROMPT_STYLE == "faithful": | |
| return f"{row['context'].strip()}\n\n{row['query'].strip()}" | |
| hint = TASK_HINTS.get((row.get("task_type") or "").strip().lower(), "") | |
| return ( | |
| f"{row['context'].strip()}\n\n{row['query'].strip()}\n\n" | |
| f"There are exactly {n} item{'s' if n != 1 else ''} to answer." | |
| + (f" {hint}" if hint else "") | |
| + f"\nAfter reasoning, write ANSWERS: on its own line, then exactly " | |
| f"{n} line{'s' if n != 1 else ''}, one answer per item, in order." | |
| ) | |
| EXPLAIN_SYSTEM = ( | |
| "You explain International Linguistics Olympiad solutions to a human " | |
| "judge. Given a problem and the answers produced, state the key rules of " | |
| "the language that justify them -- relevant morphemes, word order, any " | |
| "sound changes. Be specific and brief (2-4 sentences or a few short " | |
| "bullets); do not restate a chain of reasoning." | |
| ) | |
| def build_explain_prompt(row, answers): | |
| return ( | |
| f"{row['context'].strip()}\n\n{row['query'].strip()}\n\n" | |
| "Answers given:\n" + "\n".join(f"- {a}" for a in answers) + | |
| "\n\nBriefly explain the linguistic rules behind these answers." | |
| ) | |
| # --------------------------------------------------------------------------- | |
| # Offline diagnostics (never runs on the platform -- gold labels aren't | |
| # present there, so IOL_GOLD stays unset) | |
| # --------------------------------------------------------------------------- | |
| def report_dev_score(preds): | |
| gold_path = os.environ.get("IOL_GOLD") | |
| if not gold_path or not os.path.exists(gold_path): | |
| return | |
| try: | |
| import ast | |
| import pandas as pd | |
| gold_df = pd.read_csv(gold_path, dtype=str) | |
| exact, chrfs = [], [] | |
| for _, r in gold_df.iterrows(): | |
| gold = ast.literal_eval(r["answer"]) | |
| p = preds.get(str(r["id"]), []) | |
| p = list(p)[:len(gold)] + [""] * max(0, len(gold) - len(p)) | |
| for gi, pi in zip(gold, p): | |
| alts = gi if isinstance(gi, (list, tuple)) else [gi] | |
| alts = [str(a) for a in alts] | |
| exact.append(1.0 if any(pi.strip() == a.strip() for a in alts) else 0.0) | |
| chrfs.append(max(chrf(pi, a) for a in alts)) | |
| em = sum(exact) / max(1, len(exact)) | |
| cf = sum(chrfs) / max(1, len(chrfs)) | |
| log(f" [dev] EM={em:.4f} chrF~={cf:.4f} score~={(em * cf) ** 0.5:.4f} " | |
| f"over {len(exact)} items") | |
| except Exception as e: | |
| log(f" [dev] scoring failed: {type(e).__name__}: {e}") | |
| def write_submission(path, ids, preds, explanations=None): | |
| import pandas as pd | |
| rows = [] | |
| for i in ids: | |
| rec = {"id": i, "pred": json.dumps(preds[i], ensure_ascii=False)} | |
| if explanations is not None: | |
| rec["explanation"] = explanations.get(i, "") | |
| rows.append(rec) | |
| pd.DataFrame(rows).to_csv(path, index=False) | |
| # --------------------------------------------------------------------------- | |
| # Main | |
| # --------------------------------------------------------------------------- | |
| def main(): | |
| import pandas as pd | |
| df = pd.read_csv(TEST_CSV, dtype=str).fillna("") | |
| ids = [str(x) for x in df["id"].tolist()] | |
| ns = [count_items(r.get("query", ""), r.get("task_type", ""), r.get("context", "")) | |
| for _, r in df.iterrows()] | |
| total = sum(ns) | |
| log(f"loaded {len(df)} problems, {total} items " | |
| f"(min={min(ns)} max={max(ns)} mean={total / len(ns):.1f})") | |
| fallbacks = {i: source_fallback(r.get("query", ""), n) | |
| for i, (_, r), n in zip(ids, df.iterrows(), ns)} | |
| # 1. A complete, correctly-shaped submission before the model even loads. | |
| preds = {i: list(fallbacks[i]) for i in ids} | |
| explanations = {i: "" for i in ids} if WANT_EXPLANATION else None | |
| write_submission(OUT_CSV, ids, preds, explanations) | |
| log(f"wrote placeholder {OUT_CSV} ({len(ids)} rows)") | |
| # 2. Load model. | |
| import torch | |
| from transformers import (AutoTokenizer, AutoModelForCausalLM, | |
| StoppingCriteria, StoppingCriteriaList) | |
| class Deadline(StoppingCriteria): | |
| def __init__(self, stop_at): | |
| self.stop_at = stop_at | |
| def __call__(self, input_ids, scores, **kw): | |
| return time.time() > self.stop_at | |
| log("loading tokenizer/model ...") | |
| tok = AutoTokenizer.from_pretrained(MODEL_ID, trust_remote_code=True) | |
| if tok.pad_token is None: | |
| tok.pad_token = tok.eos_token | |
| tok.padding_side = "left" | |
| def _load(dev_map): | |
| try: | |
| return AutoModelForCausalLM.from_pretrained( | |
| MODEL_ID, torch_dtype=torch.float16, device_map=dev_map, | |
| trust_remote_code=True).eval() | |
| except TypeError: | |
| return AutoModelForCausalLM.from_pretrained( | |
| MODEL_ID, dtype=torch.float16, device_map=dev_map, | |
| trust_remote_code=True).eval() | |
| try: | |
| # Pin every layer to the GPU explicitly. device_map="auto" is free to | |
| # spill a layer or two to CPU when VRAM looks tight, which is nearly | |
| # invisible in logs but makes generation roughly two orders of | |
| # magnitude slower -- the worst possible failure mode under a hard | |
| # deadline. Only fall back to "auto" if the explicit placement fails | |
| # outright. | |
| model = _load({"": 0} if torch.cuda.is_available() else "auto") | |
| except Exception as e: | |
| log(f"pinned load failed ({type(e).__name__}: {e}); falling back to auto") | |
| model = _load("auto") | |
| devs = set(str(p.device) for p in model.parameters()) | |
| log(f"model ready on {sorted(devs)} ({time_left():.0f}s of budget left)") | |
| if any(d.startswith("cpu") or d == "meta" for d in devs): | |
| log("WARNING: part of the model is off-GPU; generation will be very slow") | |
| if torch.cuda.is_available(): | |
| log(f" VRAM allocated {torch.cuda.memory_allocated()/1e9:.2f} GB / " | |
| f"{torch.cuda.get_device_properties(0).total_memory/1e9:.1f} GB") | |
| prompts = [] | |
| for (_, r), n in zip(df.iterrows(), ns): | |
| system = FAITHFUL_SYSTEM if PROMPT_STYLE == "faithful" else GUIDED_SYSTEM | |
| msgs = [{"role": "system", "content": system}, | |
| {"role": "user", "content": build_prompt(r, n)}] | |
| prompts.append(tok.apply_chat_template(msgs, tokenize=False, | |
| add_generation_prompt=True)) | |
| # Probe actual prompt lengths so the starting batch size reflects this | |
| # file's problems instead of a fixed guess that either wastes headroom on | |
| # short prompts or guarantees an OOM on long ones. | |
| sample_lens = [len(tok.encode(p, add_special_tokens=False)) | |
| for p in prompts[: min(8, len(prompts))]] | |
| approx_max_len = max(sample_lens) if sample_lens else 512 | |
| default_batch = 4 if approx_max_len < 2500 else (2 if approx_max_len < 4500 else 1) | |
| batch_size = int(os.environ.get("IOL_BATCH", str(default_batch))) | |
| log(f"prompt length probe: ~{approx_max_len} tok max of {len(sample_lens)} " | |
| f"sampled -> starting batch_size={batch_size}") | |
| def generate(texts, max_new, sample, temp=0.7): | |
| nonlocal batch_size | |
| out = [""] * len(texts) | |
| order = sorted(range(len(texts)), key=lambda i: len(texts[i])) | |
| i = 0 | |
| while i < len(order): | |
| if time_left() < 25: | |
| log(" out of time inside generate(); returning partial batch") | |
| break | |
| idx = order[i:i + batch_size] | |
| chunk = [texts[j] for j in idx] | |
| try: | |
| enc = tok(chunk, return_tensors="pt", padding=True, | |
| truncation=True, max_length=6144).to(model.device) | |
| # repetition_penalty=1.0 explicitly. Several AWQ chat models | |
| # ship a generation_config.json with a >1.0 repetition | |
| # penalty baked in, and unlike temperature/top_p (which | |
| # greedy decoding ignores, with a warning) a repetition | |
| # penalty is applied under greedy decoding silently. A large | |
| # share of gold answers in agglutinative languages legitimately | |
| # repeat a character run, so leaving the shipped penalty in | |
| # place pushes generation away from exactly the strings | |
| # needed. | |
| kw = dict(max_new_tokens=max_new, pad_token_id=tok.pad_token_id, | |
| repetition_penalty=1.0, | |
| stopping_criteria=StoppingCriteriaList( | |
| [Deadline(DEADLINE - 10)])) | |
| if sample: | |
| kw.update(do_sample=True, temperature=temp, top_p=0.95) | |
| else: | |
| kw.update(do_sample=False) | |
| with torch.no_grad(): | |
| o = model.generate(**enc, **kw) | |
| for k, j in enumerate(idx): | |
| out[j] = tok.decode(o[k][enc["input_ids"].shape[1]:], | |
| skip_special_tokens=True) | |
| i += batch_size | |
| except torch.cuda.OutOfMemoryError: | |
| torch.cuda.empty_cache() | |
| if batch_size == 1: | |
| log(" OOM at batch=1; skipping this item") | |
| i += 1 | |
| else: | |
| batch_size = max(1, batch_size // 2) | |
| log(f" OOM -> batch_size={batch_size}") | |
| except Exception as e: | |
| log(f" generate error: {type(e).__name__}: {e}") | |
| i += batch_size | |
| return out | |
| def solve_by_assignment(row, n): | |
| """Score every (item, option-letter) pair via next-token logits and | |
| take the best one-to-one assignment, instead of trusting free-form | |
| text for match_letters problems (which tends to just emit A, B, C... | |
| in order -- a valid permutation, so no repair catches it, and it | |
| scores near zero). | |
| Raw next-token logprobs are known to carry a systematic per-token | |
| prior independent of content (Zhao et al. 2021, "Calibrate Before | |
| Use") -- e.g. a model may just prefer the token for "C" regardless of | |
| which option is actually correct. We estimate that per-letter prior | |
| with a content-free version of the same prompt and subtract it before | |
| assignment, so the score reflects evidence from the item text rather | |
| than a baked-in letter preference. | |
| """ | |
| items, opts = split_matching_block(row.get("context", "")) | |
| if len(items) < 3 or len(opts) < 3 or len(items) != n: | |
| return None | |
| letters = [o[0] for o in opts] | |
| cand_ids = [] | |
| for L in letters: | |
| token_ids = set() | |
| for form in (L, " " + L): | |
| enc = tok.encode(form, add_special_tokens=False) | |
| if enc: | |
| token_ids.add(enc[0]) | |
| cand_ids.append(sorted(token_ids)) | |
| ctx = row["context"].strip() | |
| def _logprob_batch(msgs_list): | |
| """Run one forward pass per prompt in msgs_list, return a list of | |
| per-candidate-letter logprob lists (same shape as `scores` rows).""" | |
| texts = [tok.apply_chat_template(m, tokenize=False, | |
| add_generation_prompt=True) | |
| for m in msgs_list] | |
| out = [] | |
| step = 4 | |
| for s0 in range(0, len(texts), step): | |
| if time_left() < 30: | |
| return None | |
| chunk = texts[s0:s0 + step] | |
| enc = tok(chunk, return_tensors="pt", padding=True, | |
| truncation=True, max_length=6144).to(model.device) | |
| with torch.no_grad(): | |
| logits = model(**enc).logits[:, -1, :].float() | |
| logp = torch.log_softmax(logits, dim=-1) | |
| for b in range(len(chunk)): | |
| out.append([max(logp[b, i].item() for i in ids_) | |
| for ids_ in cand_ids]) | |
| return out | |
| # --- real per-item prompts ------------------------------------- | |
| q_msgs = [] | |
| for num, itext in items: | |
| q_msgs.append([ | |
| {"role": "system", "content": | |
| "You match items to their correct counterparts in a " | |
| "linguistics problem. Reply with one option letter only."}, | |
| {"role": "user", "content": | |
| f"{ctx}\n\nWhich lettered option corresponds to item {num} " | |
| f"({itext})? Reply with the option letter only."}, | |
| ]) | |
| scores = _logprob_batch(q_msgs) | |
| if scores is None: | |
| return None | |
| # --- content-free baseline prompt ------------------------------- | |
| # Same instruction and option list (letters need to stay visible | |
| # since the token candidates depend on the model's tokenization in | |
| # context), but with the item text and its identifying number | |
| # blanked out, so the only thing driving the letter choice is the | |
| # model's own prior over "A"/"B"/"C"... in this response format -- | |
| # exactly the confound we want to remove from `scores`. | |
| opts_block = "\n".join(f"{L}. {t}" for L, t in opts) | |
| baseline_msgs = [{"role": "system", "content": | |
| "You match items to their correct counterparts in a " | |
| "linguistics problem. Reply with one option letter only."}, | |
| {"role": "user", "content": | |
| f"{opts_block}\n\nWhich lettered option corresponds " | |
| f"to item ___ (___)? Reply with the option letter " | |
| f"only."}] | |
| baseline_scores = _logprob_batch([baseline_msgs]) | |
| if baseline_scores is None: | |
| # Baseline pass failed/timed out -- fall back to uncalibrated | |
| # scores rather than losing the whole solver over one extra call. | |
| log(" calibration baseline unavailable; using raw scores") | |
| calibrated = scores | |
| else: | |
| baseline = baseline_scores[0] # one row, per-letter logprob | |
| calibrated = [[s - baseline[j] for j, s in enumerate(row_scores)] | |
| for row_scores in scores] | |
| cols = assign_max_weight(calibrated) | |
| return [letters[c] for c in cols] | |
| def apply_parsers(row_id, row, n, text): | |
| answer = enforce_bijection(parse_model_output(text, n, fallbacks[row_id])) | |
| task_type = (row.get("task_type") or "").strip().lower() | |
| if task_type == "match_letters": | |
| try: | |
| mm = solve_by_assignment(row, n) | |
| if mm and len(mm) == n: | |
| return mm | |
| except Exception as e: | |
| log(f" assignment solver failed on {row_id}: {type(e).__name__}: {e}") | |
| if task_type == "text_to_num": | |
| answer = [coerce_numeral(a, task_type) for a in answer] | |
| return answer | |
| # 3. Pass 1: greedy decoding, guarantees a complete answer set. | |
| TOK_PER_S = float(os.environ.get("IOL_TOKS", "30")) | |
| adaptive = int(0.40 * max(1.0, time_left()) * TOK_PER_S / max(1, len(df))) | |
| max_new = max(192, min(MAX_NEW, adaptive)) | |
| log(f"reasoning budget: {max_new} new tokens/problem " | |
| f"(adaptive={adaptive}, cap={MAX_NEW}, {len(df)} problems)") | |
| t0 = time.time() | |
| texts = generate(prompts, max_new=max_new, sample=False) | |
| pass1_cost = time.time() - t0 | |
| samples = {i: [] for i in ids} | |
| match_rows = 0 | |
| for (row_id, n, txt), (_, row) in zip(zip(ids, ns, texts), df.iterrows()): | |
| a = apply_parsers(row_id, row, n, txt) | |
| if (row.get("task_type") or "").strip().lower() == "match_letters": | |
| match_rows += 1 | |
| preds[row_id] = a | |
| samples[row_id].append(a) | |
| if match_rows: | |
| log(f"assignment solver attempted on {match_rows} match_letters problem(s)") | |
| write_submission(OUT_CSV, ids, preds, explanations) | |
| no_block = sum(1 for t in texts if not re.search(r"answers?\s*:", t or "", re.I)) | |
| empty = sum(1 for t in texts if not (t or "").strip()) | |
| log(f"pass 1 (greedy) done in {pass1_cost:.0f}s -> submission written " | |
| f"({no_block}/{len(texts)} without an ANSWERS: block, {empty} empty)") | |
| report_dev_score(preds) | |
| # 4. Convergence-aware self-consistency: only re-sample rows whose items | |
| # don't yet have a confident majority, so budget concentrates on the rows | |
| # that are actually still undecided instead of re-rolling settled ones. | |
| def row_converged(row_id): | |
| s = samples[row_id] | |
| if len(s) < 3: | |
| return False | |
| for k in range(len(s[0])): | |
| col = [x[k] for x in s if k < len(x)] | |
| groups = cluster_candidates(col) | |
| if not any(len(g) >= 2 for g in groups): | |
| return False | |
| return True | |
| reserve = 0.0 | |
| if WANT_EXPLANATION: | |
| reserve = min(300.0, 0.25 * pass1_cost + 60) | |
| active_ids = list(ids) | |
| n_extra = 0 | |
| while time_left() - reserve > pass1_cost * 0.5 and n_extra < MAX_SAMPLES and active_ids: | |
| n_extra += 1 | |
| active_ids = [i for i in active_ids if not row_converged(i)] | |
| if not active_ids: | |
| log(" every row has converged; stopping self-consistency early") | |
| break | |
| log(f"self-consistency pass {n_extra} on {len(active_ids)}/{len(ids)} " | |
| f"undecided rows ({time_left():.0f}s left)") | |
| pos = {rid: k for k, rid in enumerate(ids)} | |
| sub_prompts = [prompts[pos[i]] for i in active_ids] | |
| texts = generate(sub_prompts, max_new=max_new, sample=True, temp=SAMPLE_TEMP) | |
| row_lookup = {str(rid): row for rid, (_, row) in zip(ids, df.iterrows())} | |
| n_lookup = dict(zip(ids, ns)) | |
| for rid, txt in zip(active_ids, texts): | |
| if txt: | |
| a = apply_parsers(rid, row_lookup[rid], n_lookup[rid], txt) | |
| samples[rid].append(a) | |
| for rid in ids: | |
| if len(samples[rid]) >= 3: | |
| greedy = samples[rid][0] | |
| n = n_lookup[rid] if rid in n_lookup else len(greedy) | |
| preds[rid] = enforce_bijection([ | |
| vote_answer([s[k] for s in samples[rid] if k < len(s)], | |
| anchor=greedy[k] if k < len(greedy) else None) | |
| for k in range(n) | |
| ]) | |
| write_submission(OUT_CSV, ids, preds, explanations) | |
| log(f" voted over up to {n_extra + 1} samples/row -> written") | |
| report_dev_score(preds) | |
| # 5. Explanations for the jury track. | |
| if WANT_EXPLANATION and time_left() > 60: | |
| log(f"generating explanations ({time_left():.0f}s left)") | |
| ex_prompts = [] | |
| for (_, r), i in zip(df.iterrows(), ids): | |
| msgs = [{"role": "system", "content": EXPLAIN_SYSTEM}, | |
| {"role": "user", "content": build_explain_prompt(r, preds[i])}] | |
| ex_prompts.append(tok.apply_chat_template( | |
| msgs, tokenize=False, add_generation_prompt=True)) | |
| ex_texts = generate(ex_prompts, max_new=200, sample=False) | |
| for i, e in zip(ids, ex_texts): | |
| e = re.sub(r"\s+", " ", (e or "").strip()) | |
| if e: | |
| explanations[i] = e[:1200] | |
| write_submission(OUT_CSV, ids, preds, explanations) | |
| log("explanations written") | |
| # 6. Final integrity pass: guarantee shape and non-blank answers no | |
| # matter what happened above. | |
| n_lookup = dict(zip(ids, ns)) | |
| malformed = [i for i in ids if len(preds[i]) != n_lookup[i] | |
| or any(not str(x).strip() for x in preds[i])] | |
| if malformed: | |
| log(f"repairing {len(malformed)} malformed rows") | |
| for i in malformed: | |
| preds[i] = squeeze_to_n( | |
| [x for x in preds[i] if str(x).strip()], n_lookup[i], fallbacks[i]) | |
| write_submission(OUT_CSV, ids, preds, explanations) | |
| log(f"DONE. {len(ids)} rows, {sum(len(v) for v in preds.values())} answers, " | |
| f"{time.time() - START:.0f}s elapsed") | |
| if __name__ == "__main__": | |
| main() |