Instructions to use MikCil/IOL-AI-Qwen35-9B-IT-LoRA-Direct-Prompt-v2 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- PEFT
How to use MikCil/IOL-AI-Qwen35-9B-IT-LoRA-Direct-Prompt-v2 with PEFT:
Task type is invalid.
- Notebooks
- Google Colab
- Kaggle
| """Offline IOL-AI 2026 direct-pass submission for Qwen3.5-9B + our LoRA.""" | |
| from __future__ import annotations | |
| import csv | |
| import gc | |
| import os | |
| from pathlib import Path | |
| import random | |
| import time | |
| from typing import Any | |
| from iol_contract import ( | |
| expected_answer_count, | |
| parse_answer_lines, | |
| serialized_prediction, | |
| validate_id_sequence, | |
| ) | |
| from runtime_bootstrap import ( | |
| assert_runtime_versions, | |
| bootstrap_local_runtime, | |
| configure_offline_environment, | |
| patch_torch24_for_single_gpu, | |
| ) | |
| ROOT = Path(__file__).resolve().parent | |
| INPUT_CSV = Path(os.environ.get("IOL_INPUT_CSV", "/tmp/data/test.csv")) | |
| OUTPUT_CSV = Path(os.environ.get("IOL_OUTPUT_CSV", str(ROOT / "submission.csv"))) | |
| ADAPTER_DIR = ROOT / "adapter" | |
| SYSTEM_PROMPT_PATH = ROOT / "system_prompt.txt" | |
| SEED = 3407 | |
| MAX_NEW_TOKENS = int(os.environ.get("IOL_MAX_NEW_TOKENS", "512")) | |
| HARD_STOP_SECONDS = float(os.environ.get("IOL_HARD_STOP_SECONDS", str(27.5 * 60))) | |
| def set_reproducible_seed(torch: Any) -> None: | |
| random.seed(SEED) | |
| torch.manual_seed(SEED) | |
| if torch.cuda.is_available(): | |
| torch.cuda.manual_seed_all(SEED) | |
| def read_input() -> list[dict[str, str]]: | |
| if INPUT_CSV.resolve().parent != Path("/tmp/data") and "IOL_INPUT_CSV" not in os.environ: | |
| raise RuntimeError("competition input must be /tmp/data/test.csv") | |
| with INPUT_CSV.open("r", encoding="utf-8-sig", newline="") as handle: | |
| reader = csv.DictReader(handle) | |
| required = {"id", "context", "query"} | |
| missing = required.difference(reader.fieldnames or []) | |
| if missing: | |
| raise ValueError(f"test.csv is missing columns: {sorted(missing)}") | |
| rows = [{key: value or "" for key, value in row.items()} for row in reader] | |
| if not rows: | |
| raise ValueError("test.csv is empty") | |
| ids = [row["id"] for row in rows] | |
| validate_id_sequence(ids, ids) | |
| return rows | |
| def build_direct_prompt(row: dict[str, str], expected_n: int) -> str: | |
| # This matches the non-thinking DIRECT interface used during our SFT. | |
| return ( | |
| "TASK_MODE=DIRECT\n" | |
| f"Return exactly {expected_n} answer line(s), in order, with no explanation.\n\n" | |
| "CONTEXT\n" | |
| f"{row['context'].strip()}\n\n" | |
| "QUERY\n" | |
| f"{row['query'].strip()}" | |
| ) | |
| def typed_text_messages(prompt: str) -> list[dict[str, Any]]: | |
| if not SYSTEM_PROMPT_PATH.is_file(): | |
| raise RuntimeError("system_prompt.txt is missing") | |
| system_prompt = SYSTEM_PROMPT_PATH.read_text(encoding="utf-8").strip() | |
| if not system_prompt: | |
| raise RuntimeError("system_prompt.txt is empty") | |
| return [ | |
| { | |
| "role": "system", | |
| "content": [{"type": "text", "text": system_prompt}], | |
| }, | |
| { | |
| "role": "user", | |
| "content": [{"type": "text", "text": prompt}], | |
| }, | |
| ] | |
| def load_model_and_processor(torch: Any): | |
| from peft import PeftModel | |
| from transformers import AutoModelForMultimodalLM, AutoProcessor, BitsAndBytesConfig | |
| if not ADAPTER_DIR.joinpath("adapter_config.json").is_file(): | |
| raise RuntimeError("adapter/adapter_config.json is missing") | |
| processor = AutoProcessor.from_pretrained( | |
| ROOT, | |
| local_files_only=True, | |
| trust_remote_code=False, | |
| ) | |
| quantization = BitsAndBytesConfig( | |
| load_in_4bit=True, | |
| bnb_4bit_quant_type="nf4", | |
| bnb_4bit_use_double_quant=True, | |
| bnb_4bit_compute_dtype=torch.float16, | |
| ) | |
| base = AutoModelForMultimodalLM.from_pretrained( | |
| ROOT, | |
| local_files_only=True, | |
| trust_remote_code=False, | |
| dtype=torch.float16, | |
| quantization_config=quantization, | |
| device_map="auto", | |
| low_cpu_mem_usage=True, | |
| ) | |
| model = PeftModel.from_pretrained(base, ADAPTER_DIR, is_trainable=False) | |
| model.eval() | |
| return model, processor | |
| def generate_direct(model: Any, processor: Any, row: dict[str, str], expected_n: int, torch: Any) -> str: | |
| prompt = build_direct_prompt(row, expected_n) | |
| rendered_prompt = processor.apply_chat_template( | |
| typed_text_messages(prompt), | |
| tokenize=False, | |
| add_generation_prompt=True, | |
| enable_thinking=False, | |
| ) | |
| encoded = processor( | |
| text=rendered_prompt, | |
| return_tensors="pt", | |
| ) | |
| device = next(model.parameters()).device | |
| encoded = {name: tensor.to(device) for name, tensor in encoded.items()} | |
| input_length = encoded["input_ids"].shape[-1] | |
| with torch.inference_mode(): | |
| output = model.generate( | |
| **encoded, | |
| max_new_tokens=MAX_NEW_TOKENS, | |
| do_sample=False, | |
| use_cache=True, | |
| pad_token_id=getattr(processor.tokenizer, "pad_token_id", None), | |
| eos_token_id=getattr(processor.tokenizer, "eos_token_id", None), | |
| ) | |
| generated = output[0, input_length:] | |
| text = processor.decode(generated, skip_special_tokens=True) | |
| del encoded, output, generated | |
| return text | |
| def write_submission(rows: list[dict[str, str]], predictions: dict[str, list[str]]) -> None: | |
| temp = OUTPUT_CSV.with_suffix(OUTPUT_CSV.suffix + ".tmp") | |
| output_ids: list[str] = [] | |
| with temp.open("w", encoding="utf-8", newline="") as handle: | |
| writer = csv.DictWriter(handle, fieldnames=["id", "pred"]) | |
| writer.writeheader() | |
| for row in rows: | |
| row_id = row["id"] | |
| answers = predictions[row_id] | |
| expected_n = expected_answer_count(row) | |
| if len(answers) != expected_n or not all(isinstance(item, str) for item in answers): | |
| raise ValueError(f"invalid prediction shape for id={row_id}") | |
| writer.writerow({"id": row_id, "pred": serialized_prediction(answers)}) | |
| output_ids.append(row_id) | |
| handle.flush() | |
| os.fsync(handle.fileno()) | |
| validate_id_sequence([row["id"] for row in rows], output_ids) | |
| os.replace(temp, OUTPUT_CSV) | |
| def main() -> None: | |
| configure_offline_environment() | |
| started = time.monotonic() | |
| rows = read_input() | |
| expected = {row["id"]: expected_answer_count(row) for row in rows} | |
| predictions = {row["id"]: [""] * expected[row["id"]] for row in rows} | |
| vendor = bootstrap_local_runtime(ROOT) | |
| versions = assert_runtime_versions(vendor) | |
| import torch | |
| if not torch.cuda.is_available(): | |
| raise RuntimeError("a CUDA GPU is required") | |
| patches = patch_torch24_for_single_gpu() | |
| set_reproducible_seed(torch) | |
| print( | |
| f"runtime ready: rows={len(rows)} torch={torch.__version__} " | |
| f"transformers={versions['transformers']} patches={','.join(patches) or 'none'}", | |
| flush=True, | |
| ) | |
| model, processor = load_model_and_processor(torch) | |
| print( | |
| f"model loaded: gpu={torch.cuda.get_device_name(0)} " | |
| f"vram_gib={torch.cuda.memory_allocated() / 2**30:.2f}", | |
| flush=True, | |
| ) | |
| for index, row in enumerate(rows, 1): | |
| elapsed = time.monotonic() - started | |
| if elapsed >= HARD_STOP_SECONDS: | |
| print(f"hard stop reached after {index - 1}/{len(rows)} rows", flush=True) | |
| break | |
| row_id = row["id"] | |
| row_started = time.monotonic() | |
| try: | |
| raw = generate_direct(model, processor, row, expected[row_id], torch) | |
| predictions[row_id] = parse_answer_lines(raw, expected[row_id]) | |
| except torch.cuda.OutOfMemoryError: | |
| gc.collect() | |
| torch.cuda.empty_cache() | |
| print(f"row={row_id} failed: OutOfMemoryError", flush=True) | |
| except Exception as exc: | |
| print(f"row={row_id} failed: {type(exc).__name__}", flush=True) | |
| write_submission(rows, predictions) | |
| print( | |
| f"row={row_id} done {index}/{len(rows)} " | |
| f"answers={len(predictions[row_id])} seconds={time.monotonic() - row_started:.1f}", | |
| flush=True, | |
| ) | |
| write_submission(rows, predictions) | |
| print(f"wrote {OUTPUT_CSV.name} in {time.monotonic() - started:.1f}s", flush=True) | |
| if __name__ == "__main__": | |
| main() | |