msrh-zindi-magic / RECIPE.md
MagicCard's picture
Docs
028081c
|
Raw
History Blame Contribute Delete
22.2 kB

Top 1 Private Solution - Magic Team

Multilingual Health QA in Low-Resource African Languages Challenge

Submitter: Magic Best submission: go.csv (Zindi ID vtbP7bCH, submitted 21 Jun 17:12) Public LB: 0.738783 Β· Private LB: Top 1 πŸ†


1. Solution overview

19-candidate ensemble built with V2 medoid_ngram consensus:

  • Base: 12 fewshot LoRA adapters across Qwen3.5/Qwen3.6/Qwen3-32B Γ— K=3,4,5,7
  • v8 prompt swap: 1 Qwen3.5-27B K=5 LoRA trained with anchored-extraction prompt
  • 3 cross-arch "less-overfit" checkpoints
  • 3 no-fewshot mediators

Per Test row, the medoid (highest sum of pairwise ROUGE-1.F + ROUGE-2.F) is chosen as the final answer.

Why it wins

Six independent decorrelation axes simultaneously:

  1. Architecture: Qwen3.5-27B + Qwen3.6-27B + Qwen3-32B (3 base models)
  2. K-count: K=3, K=4, K=5, K=7 fewshot demos
  3. Prompt recipe: v1 baseline vs v8 anchored-extraction
  4. Training schedule: 3ep vs 5ep
  5. Mediator: 3 no-fewshot baselines

2. Workspace layout

All paths in this package are hard-coded to /mnt/msrh/Magic_submission/. Extract this archive to that exact location (or mount/symlink it there) so every script β€” data builders, training YAMLs, inference launcher, ensemble β€” works without editing.

# One-time setup β€” choose ONE of these:

# (a) Extract the archive to /mnt/msrh/
sudo mkdir -p /mnt/msrh && sudo chown $USER /mnt/msrh
tar -xf Magic_submission.tar -C /mnt/msrh/        # extracts to /mnt/msrh/Magic_submission/

# (b) OR symlink an existing copy
ln -s /path/to/existing/Magic_submission /mnt/msrh/Magic_submission

# Verify
ls /mnt/msrh/Magic_submission/{go.csv,checkpoints,configs,scripts,data_builders}

After extraction, you must also place under /mnt/msrh/Magic_submission/:

  • data/Train.csv, data/Val.csv, data/Test.csv, data/SampleSubmission.csv (Zindi competition data β€” download separately). Or you can manually download it from the Zindi data source.
  • hub/Qwen3.5-27B/, hub/Qwen3.6-27B/, hub/Qwen3-32B/ (HF snapshot of each base model)
  • hub/AfriE5-Large-instruct/ (HF snapshot of McGill-NLP/AfriE5-Large-instruct)
/mnt/msrh/Magic_submission/
β”œβ”€β”€ go.csv                            # Final submission CSV (== Zindi sub vtbP7bCH)
β”œβ”€β”€ README.md                         # This file β€” full reproduction guide
β”œβ”€β”€ environment.md                    # All env setup (conda, packages, models)
β”œβ”€β”€ checkpoints/                      # ⭐ 19 LoRA adapters (35 GB) β€” INPUT to inference
β”‚   # Each folder = 1 LoRA adapter with:
β”‚   #   adapter_model.safetensors  (1.8 GB for 27B, 2.1 GB for 32B)
β”‚   #   adapter_config.json
β”‚   #   chat_template.jinja
β”‚   #   tokenizer_config.json + tokenizer.json + processor_config.json
β”‚   β”œβ”€β”€ Qwen3.5-27B-3fewshots-bs64-3eps-ckpt-1200/
β”‚   β”œβ”€β”€ Qwen3.5-27B-3fewshots-bs64-3eps-ckpt-1100/
β”‚   β”œβ”€β”€ Qwen3.5-27B-3fewshots-bs64-5eps-ckpt-1200/
β”‚   β”œβ”€β”€ Qwen3.5-27B-4fewshots-bs64-3eps-ckpt-1600/
β”‚   β”œβ”€β”€ Qwen3.5-27B-5fewshots-bs64-3eps-v8prompt-ckpt-1500/    
β”‚   β”œβ”€β”€ Qwen3.5-27B-7fewshots-bs64-3eps-ckpt-1600/
β”‚   β”œβ”€β”€ Qwen3.5-27B-7fewshots-bs64-3eps-ckpt-1200/             
β”‚   β”œβ”€β”€ Qwen3.6-27B-3fewshots-bs64-3eps-ckpt-1600/
β”‚   β”œβ”€β”€ Qwen3.6-27B-4fewshots-bs64-3eps-ckpt-1400/
β”‚   β”œβ”€β”€ Qwen3.6-27B-5fewshots-bs64-3eps-ckpt-1200/
β”‚   β”œβ”€β”€ Qwen3.6-27B-5fewshots-bs64-3eps-ckpt-1000/             
β”‚   β”œβ”€β”€ Qwen3.6-27B-7fewshots-bs64-3eps-ckpt-1600/
β”‚   β”œβ”€β”€ Qwen3-32B-3fewshots-bs64-3eps-ckpt-1400/
β”‚   β”œβ”€β”€ Qwen3-32B-5fewshots-bs64-3eps-ckpt-1700/
β”‚   β”œβ”€β”€ Qwen3-32B-7fewshots-bs64-3eps-ckpt-1600/
β”‚   β”œβ”€β”€ Qwen3-32B-7fewshots-bs64-3eps-ckpt-1200/              
β”‚   β”œβ”€β”€ Qwen3.5-27B-NoFewshots-bs64-5eps-ckpt-2800/            
β”‚   β”œβ”€β”€ Qwen3.6-27B-NoFewshots-bs64-5eps-ckpt-2600/            
β”‚   └── Qwen3-32B-NoFewshots-bs64-4eps-ckpt-6500/              
β”œβ”€β”€ candidate_csvs/                   # OUTPUT folder for 19 per-model prediction CSVs
β”‚   # NOTE: any CSVs here are reference/audit-trail copies.
β”‚   # For verification, REGENERATE all 19 from checkpoints/ via launch_all_predicts.sh.
β”‚   # The ensemble script (build_ensemble.py) reads CSVs from this folder.
β”œβ”€β”€ LF/
β”‚   └── data/
β”‚       └── dataset_info.json         
β”œβ”€β”€ requirements/                     # per-phase pip pinned deps
β”‚   β”œβ”€β”€ train.txt                     #   Phase 1 (data builders) + Phase 2 (SFT)
β”‚   β”œβ”€β”€ infer.txt                     #   Phase 3 (vLLM inference)
β”‚   └── ensemble.txt                  #   Phase 4 (CPU medoid ensemble)
β”œβ”€β”€ data_builders/                    
β”‚   β”œβ”€β”€ build_afrie5_k5.py            
β”‚   β”œβ”€β”€ build_fewshot_train.py        
β”‚   β”œβ”€β”€ build_fewshot_train_k4.py
β”‚   β”œβ”€β”€ build_fewshot_train_k5.py
β”‚   β”œβ”€β”€ build_fewshot_train_k7.py
β”‚   β”œβ”€β”€ build_fewshot_test.py         
β”‚   β”œβ”€β”€ build_fewshot_test_k4.py
β”‚   β”œβ”€β”€ build_fewshot_test_k5.py
β”‚   └── build_fewshot_test_k7.py
β”œβ”€β”€ configs/                          
β”‚   # ── Fewshot SFT runs (13) ──
β”‚   β”œβ”€β”€ qwen35_27b_fewshot_3ep_8gpu.yaml             
β”‚   β”œβ”€β”€ qwen35_27b_fewshot_k4_3ep_8gpu.yaml          
β”‚   β”œβ”€β”€ qwen35_27b_fewshot_k5_v8_3ep_8gpu.yaml       
β”‚   β”œβ”€β”€ qwen35_27b_fewshot_k7_3ep_8gpu.yaml         
β”‚   β”œβ”€β”€ qwen35_27b_fewshot_2ep_8gpu.yaml             
β”‚   β”œβ”€β”€ qwen36_27b_fewshot_k4_3ep_8gpu.yaml          
β”‚   β”œβ”€β”€ qwen36_27b_fewshot_k5_3ep_8gpu.yaml          
β”‚   β”œβ”€β”€ qwen36_27b_fewshot_k7_v8_3ep_8gpu.yaml       # Q3.6 K=7 v8 prompt 3ep  β†’ ck-1600
β”‚   β”œβ”€β”€ qwen3_32b_fewshot_3ep_bs64_r128_all_8gpu.yaml      
β”‚   β”œβ”€β”€ qwen3_32b_fewshot_3ep_r128_all_8gpu.yaml           
β”‚   β”œβ”€β”€ qwen3_32b_fewshot_k5_3ep_bs64_r128_all_8gpu.yaml   
β”‚   β”œβ”€β”€ qwen3_32b_fewshot_k7_3ep_bs64_r128_all_8gpu.yaml   
β”‚   # ── NoFewshots (mediator) SFT runs (3) ──
β”‚   β”œβ”€β”€ qwen35_27b_nofewshots_5ep_bs64_8gpu.yaml     
β”‚   β”œβ”€β”€ qwen36_27b_nofewshots_5ep_bs64_8gpu.yaml    
β”‚   β”œβ”€β”€ qwen3_32b_nofewshots_4ep_bs64_8gpu.yaml      
β”‚   β”œβ”€β”€ fewshot_e3_Q36.sh                                  
β”‚   β”œβ”€β”€ ds_z3_config.json                                  
β”œβ”€β”€ scripts/                          # Inference + ensemble scripts
β”‚   β”œβ”€β”€ build_v8_k5_fewshot.py        # v8 prompt swap (V1 β†’ V8 prefix)
β”‚   β”œβ”€β”€ vllm_predict_extra.py         # vLLM batch inference w/ LoRA
β”‚   β”œβ”€β”€ jsonl_rowidx_to_csv.py        # jsonl β†’ CSV converter
β”‚   β”œβ”€β”€ rag_to_zindi.py               # Alternate CSV builder (Plan D path)
β”‚   β”œβ”€β”€ launch_all_predicts.sh        # ⭐ Orchestrator: runs all 19 inferences β†’ candidate_csvs/<descriptive>.csv
β”‚   β”œβ”€β”€ build_ensemble.py  # ⭐ FINAL ensemble medoid (uses candidate_csvs/)
β”‚   └── ds_z3_config.json
└── docs/
    └── (additional documentation if needed)

3. Verification flow (RECOMMENDED for reviewers β€” uses provided checkpoints)

The 19 LoRA adapters in checkpoints/ are the WINNING trained weights β€” the official artifacts that produced go.csv. Reviewers do NOT need to retrain. Use them directly to regenerate the 19 prediction CSVs, then run the ensemble.

All commands below run from /mnt/msrh/Magic_submission/ (paths are hard-coded β€” no editing required).

cd /mnt/msrh/Magic_submission

# Step 1: Setup envs (see environment.md for full conda setup)
conda activate llama-qa35 && pip install -r requirements/train.txt    # Phase 1 + 2
conda activate vllm       && pip install -r requirements/infer.txt    # Phase 3
conda activate rouge      && pip install -r requirements/ensemble.txt # Phase 4

# Step 2: Build TEST data (AfriE5 retrieval + fewshot demos) β€” train data not needed for verification
conda activate llama-qa35
python data_builders/build_afrie5_k5.py --k 3       # K=3 base retrieval (NoFewshots test JSON)
python data_builders/build_fewshot_test.py          # K=3 fewshot test demos
python data_builders/build_fewshot_test_k4.py       # K=4 fewshot test demos
python data_builders/build_fewshot_test_k5.py       # K=5 fewshot test demos
python data_builders/build_fewshot_test_k7.py       # K=7 fewshot test demos
# v8 variants β€” REQUIRE K=5 / K=7 fewshot files to already exist:
python scripts/build_v8_k5_fewshot.py               # rebuilds *_k5_v8.json from K=5 fewshot
python scripts/build_v8_k7_fewshot.py               # rebuilds *_k7_v8.json from K=7 fewshot

# Step 3: Generate 19 prediction CSVs from provided checkpoints (Reads checkpoints/, writes candidate_csvs/)
conda activate vllm
bash scripts/launch_all_predicts.sh

# Step 4: Run ensemble (V2 medoid_ngram) β†’ final CSV
conda activate rouge
python scripts/build_ensemble.py
# Writes the regenerated submission CSV alongside the shipped go.csv.
# Reference md5 of shipped go.csv: a2ecca4a8e1aa01acf9a8b9a1d56ebf2

Note on byte-identity vs functional reproducibility

We E2E-verified this pipeline on a fresh 8Γ—H100 box (vLLM 0.19.1 + torch 2.10+cu128). Expect the regenerated CSV to NOT be md5-equal to the shipped go.csv β€” but the LB will reproduce within ~0.005 public / ~0.001 private of the winning result.

Run Public LB Private LB Row match vs go.csv
Original vtbP7bCH (shipped go.csv) 0.738783 0.730865 byte-identical (md5 a2ecca4a...)
Re-run #1 β€” max_num_seqs=32 hard-coded 0.734035 0.729336 65.0%
Re-run #2 β€” per-cand max_num_seqs matched 0.734104 0.729553 67.8%

Root cause of the ~0.005 gap: vLLM 0.19.1 paged-attention has inherent non-determinism between runs even at --temperature 0.0 with identical flags (CUDA atomic ops, bfloat16 rounding-order in attention, KV-cache block scheduling races). This affects ~30-40% of generated tokens at single-cand level; the V2 medoid_ngram ensemble suppresses most of the drift, leaving ~32% of per-row picks different but only a ~0.001 private-LB hit.

Acceptance criterion: if the regenerated CSV yields Private LB β‰₯ 0.728 (within βˆ’0.003 of the shipped 0.730865), reproduction is considered successful and the pipeline is verified. md5 byte-equality is NOT achievable on a different machine/run.


3b. Full from-scratch reproduction (optional β€” trains 13 LoRAs from base models)

Only needed if you want to retrain the LoRA adapters yourself instead of using the provided ones.

cd /mnt/msrh/Magic_submission
conda activate llama-qa35   # training env

# Build train data (same as Step 2 above but for train set)
python data_builders/build_afrie5_k5.py --k 3
python data_builders/build_afrie5_k5.py --k 5
python data_builders/build_afrie5_k5.py --k 7
python data_builders/build_fewshot_train.py
python data_builders/build_fewshot_train_k4.py
python data_builders/build_fewshot_train_k5.py
python data_builders/build_fewshot_train_k7.py
# v8 prompt swap β€” runs AFTER build_fewshot_train_k5.py and build_fewshot_test_k5.py
python scripts/build_v8_k5_fewshot.py               # rebuilds *_k5_v8.json (train + test)

# Train each of the 16 LoRA adapters (configs/*.yaml). Outputs go to
# /mnt/msrh/Magic_submission/checkpoints_trained/<config-name>/ as set inside each YAML.
# Example for the key v8 model:
FORCE_TORCHRUN=1 NPROC_PER_NODE=8 \
  llamafactory-cli train /mnt/msrh/Magic_submission/configs/qwen35_27b_fewshot_k5_v8_3ep_8gpu.yaml
# ... repeat for all 16 configs (~14h each on 8Γ— H100)

# Replace adapters in checkpoints/<descriptive_name>/ with your fresh ckpts, then
# follow Step 3 + Step 4 from Section 3 above.

4. Data preparation (full pipeline)

Inputs (required)

  • Zindi competition data: Train.csv, Val.csv, Test.csv, SampleSubmission.csv (from competition page)
  • Path expected: /mnt/msrh/Magic_submission/data/{Train,Val,Test,SampleSubmission}.csv (no editing needed β€” every builder and script reads from this fixed location)

Pipeline

  1. AfriE5 base retrieval (build_afrie5_k5.py)

    • For each train query, retrieve top-K from Train+Val pool using AfriE5 cosine similarity
    • For each test query, retrieve top-K from Train+Val pool
    • Output: msrh_rag_train_afrie5_TV_k{K}.json + msrh_rag_test_k3_AfriE5_TV.json
    • Includes language-specific instruction tag (Akan/Amharic/Luganda/Swahili/English)
  2. Fewshot demo prep (build_fewshot_train_k{K}.py + build_fewshot_test_k{K}.py)

    • Per row, retrieve K AfriE5-similar same-subset (Q,A) pairs from Train+Val
    • Prepend as "Example N:" demos in user message
    • Output: msrh_rag_train_afrie5_TV_k{K}_fewshot.json
  3. v8 prompt swap (build_v8_k5_fewshot.py)

    • Replace v1 prompt:
      Use the retrieved contexts as your primary sources β€” copy exact phrasing where
      the contexts already address the question. Be concise and factually accurate.
      
    • With v8 prompt (33 words, 220 chars):
      The retrieved contexts are your source of truth β€” copy or paraphrase their
      exact phrasing to answer. Reply in the same language and script as the question.
      Plain prose, no disclaimers or meta-commentary.
      
    • Output: msrh_rag_train_afrie5_TV_k5_fewshot_v8.json

5. Training (13 LoRA adapters)

All training uses LlamaFactory + DeepSpeed ZeRO-3 on 8Γ— H100 (80GB).

Common hyperparameters (all 13 runs)

finetuning_type: lora
lora_rank: 128
lora_alpha: 256
lora_dropout: 0.05
lora_target: all          # all linear layers
learning_rate: 2.0e-4
lr_scheduler_type: cosine
warmup_ratio: 0.05
bf16: true
gradient_checkpointing: true
deepspeed: scripts/ds_z3_config.json

Per-model details

Per-model details (16 SFT runs total β€” all reproducible via configs/*.yaml)

The 19 ensemble candidates come from 16 distinct SFT training runs (3 of the runs export 2 checkpoints each as "early-tap" anti-overfit cands).

Fewshot runs (13 SFT, each prepends top-K AfriE5-similar (Q,A) demos)

# Model K Prompt Epochs cutoff_len eff_bs YAML Best ckpt Cand(s)
1 Qwen3.5-27B 3 v1 3 4096 64 qwen35_27b_fewshot_3ep_8gpu.yaml ck-1200 ck-1200 + ck-1100 (early-tap)
2 Qwen3.5-27B 3 v1 5 4096 64 qwen35_27b_fewshot_5ep_8gpu.yaml (modify epochs=5 in run 1's YAML) ck-1200 1 cand
3 Qwen3.5-27B 4 v1 3 5120 64 qwen35_27b_fewshot_k4_3ep_8gpu.yaml ck-1600 1 cand
4 Qwen3.5-27B 5 v8 3 6144 64 qwen35_27b_fewshot_k5_v8_3ep_8gpu.yaml ck-1500 ⭐ 1 cand ⭐
5 Qwen3.5-27B 7 v1 3 8192 64 qwen35_27b_fewshot_k7_3ep_8gpu.yaml ck-1600 ck-1600 + ck-1200 (early-tap)
6 Qwen3.6-27B 3 v1 (RecA) 3 4096 64 qwen36_27b_fewshot_3eps_bs64.yaml (Q3.5 K=3 YAML w/ model+template swap) ck-1600 1 cand
7 Qwen3.6-27B 4 v1 3 5120 64 qwen36_27b_fewshot_k4_3ep_8gpu.yaml ck-1400 1 cand
8 Qwen3.6-27B 5 v1 3 6144 64 qwen36_27b_fewshot_k5_3ep_8gpu.yaml ck-1200 ck-1200 + ck-1000 (early-tap)
9 Qwen3.6-27B 7 v8 3 8192 64 qwen36_27b_fewshot_k7_v8_3ep_8gpu.yaml ck-1600 1 cand
10 Qwen3-32B 3 v1 3 4096 64 qwen3_32b_fewshot_3ep_bs64_r128_all_8gpu.yaml ck-1400 1 cand
11 Qwen3-32B 5 v1 3 6144 64 qwen3_32b_fewshot_k5_3ep_bs64_r128_all_8gpu.yaml ck-1700 1 cand
12 Qwen3-32B 7 v1 3 8192 64 qwen3_32b_fewshot_k7_3ep_bs64_r128_all_8gpu.yaml ck-1600 ck-1600 + ck-1200 (early-tap)

NoFewshots runs (3 SFT, mediators β€” same K=3 AfriE5 retrieval but WITHOUT demos prepended)

These 3 are trained the same way as the fewshot runs but use the non-fewshot K=3 dataset (msrh_rag_train_afrie5_TV_k3.json β€” output of build_afrie5_k5.py --k 3 only, no build_fewshot_train*.py step). They serve as decorrelated "no-context-demos" mediators in the ensemble.

# Model K Prompt Epochs cutoff_len eff_bs YAML Best ckpt Cand
13 Qwen3.5-27B 3 v1 5 4096 64 qwen35_27b_nofewshots_5ep_bs64_8gpu.yaml ck-2800 1 cand
14 Qwen3.6-27B 3 v1 5 4096 64 qwen36_27b_nofewshots_5ep_bs64_8gpu.yaml ck-2600 1 cand
15 Qwen3-32B 3 v1 4 4096 64 qwen3_32b_nofewshots_4ep_bs64_8gpu.yaml ck-6500 1 cand

Differences vs fewshot runs: only dataset: msrh_rag_train_afrie5_TV_k3 (vs *_k3_fewshot) and longer epoch budget (4-5 vs 3) since no-demo trains more slowly.

Total: 15 SFT runs β†’ 19 ensemble cands (3 runs contribute 2 ckpts each)

Launch example (any config)

HF_DATASETS_OFFLINE=1 TRANSFORMERS_OFFLINE=1 \
CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 \
FORCE_TORCHRUN=1 NNODES=1 NPROC_PER_NODE=8 \
llamafactory-cli train /mnt/msrh/Magic_submission/configs/qwen35_27b_fewshot_k5_v8_3ep_8gpu.yaml \
  > /mnt/msrh/Magic_submission/training_logs/q35_k5_v8.log 2>&1

Save_steps=100-500 (depends on config), save_total_limit=25 (keep multiple ckpts for early-tap predictions).

Wall time per run (8Γ— H100 80GB)

  • Q3.5-27B 3ep: ~7-8h
  • Q3.6-27B 3ep: ~8-10h
  • Q3-32B 3ep: ~13-14h
  • Q3-32B 4ep (NoFewshots): ~18h
  • Q3.5/3.6-27B 5ep (NoFewshots): ~12-15h
  • Total for 15 SFT runs (sequential): ~7-9 days
  • With 2-3 nodes parallel: ~3-4 days

6. Inference (each LoRA adapter)

Single-GPU inference per adapter via vLLM (env: vllm). The wrapper launch_all_predicts.sh orchestrates all 19 runs and writes outputs into candidate_csvs/<descriptive_name>.csv ready for ensemble.

One-shot run all 19 predictions

bash /mnt/msrh/Magic_submission/scripts/launch_all_predicts.sh
# Outputs: /mnt/msrh/Magic_submission/candidate_csvs/{Qwen3.5-27B-..., Qwen3.6-27B-..., Qwen3-32B-...}.csv (19 files)

Manual single-cand example (the key v8 ckpt)

ROOT=/mnt/msrh/Magic_submission
CUDA_VISIBLE_DEVICES=0 python $ROOT/scripts/vllm_predict_extra.py \
  --base   $ROOT/hub/Qwen3.5-27B \
  --adapter $ROOT/checkpoints/Qwen3.5-27B-5fewshots-bs64-3eps-v8prompt-ckpt-1500 \
  --rag_test $ROOT/LF/data/msrh_rag_test_k3_AfriE5_TV_fewshot_k5_v8.json \
  --out_jsonl $ROOT/_predict_workdir/Qwen3.5-27B-5fewshots-bs64-3eps-v8prompt-ckpt-1500/predictions.jsonl \
  --max_lora_rank 128 --max_new 512 \
  --max_model_len 8192 --mem_util 0.88 --max_num_seqs 32 \
  --temperature 0.0 --top_p 1.0 --best_of 1 --no_think

python $ROOT/scripts/jsonl_rowidx_to_csv.py \
  --jsonl $ROOT/_predict_workdir/Qwen3.5-27B-5fewshots-bs64-3eps-v8prompt-ckpt-1500/predictions.jsonl \
  --out_csv $ROOT/candidate_csvs/Qwen3.5-27B-5fewshots-bs64-3eps-v8prompt-ckpt-1500.csv

Critical inference flags

  • --no_think β€” passes enable_thinking=False to chat template (Qwen3.5/6 trained with qwen3_5_nothink template). Default ON wastes generation budget on reasoning; LB drops 0.10-0.25.
  • --temperature 0.0 --best_of 1 β€” greedy deterministic decoding
  • --max_model_len: 6144 for K=3/4; 8192 for K=5; 10240 for K=7 (long prompts)

Provide --adapter checkpoints/<descriptive_name> pointing into the provided checkpoints/ folder. Run inference for all 19 candidates (each on a different GPU in parallel via the launcher). The output 19 CSVs go to candidate_csvs/, ready for ensemble.

Note: This package does NOT ship pre-computed candidate_csvs/*.csv. They must be regenerated from checkpoints/ via scripts/launch_all_predicts.sh so the reviewer can independently verify the inference step.


7. Final ensemble (V2 medoid_ngram)

# (no GPU needed β€” pure CPU + ROUGE scoring)
pip install rouge-score==0.1.2

python scripts/build_ensemble.py
# Reads 19 CSVs from candidate_csvs/<descriptive_name>.csv
# Computes per-row medoid, writes the regenerated submission CSV
# Compares md5 against the shipped go.csv: a2ecca4a8e1aa01acf9a8b9a1d56ebf2

V2 medoid_ngram algorithm

For each Test row:

  1. Compute pairwise ROUGE-1.F + ROUGE-2.F between all 19 candidate answers
  2. For each candidate, sum its similarities to all OTHERS
  3. Pick the candidate with HIGHEST sum (the "medoid" of the cluster)
  4. Use its answer text as final prediction

Implementation (excerpt from build_ensemble.py):

from rouge_score import rouge_scorer
scorer = rouge_scorer.RougeScorer(["rouge1", "rouge2"], use_stemmer=False)

def medoid(texts):
    best, best_s = 0, -1.0
    for i in range(len(texts)):
        s = sum(
            scorer.score(texts[j], texts[i])["rouge1"].fmeasure
          + scorer.score(texts[j], texts[i])["rouge2"].fmeasure
            for j in range(len(texts)) if i != j
        )
        if s > best_s: best_s, best = s, i
    return best

8. Reproducibility

Hash of final submission

$ md5sum go.csv
a2ecca4a8e1aa01acf9a8b9a1d56ebf2  go.csv

Pinned versions (env)

See environment.md β€” exact torch / transformers / vllm / llamafactory versions.

Random seed

  • LlamaFactory default seed (42) used for all trainings β€” reproducible LoRA weights given same env/data
  • vLLM inference is deterministic with --temperature 0.0 --best_of 1
  • Ensemble medoid is purely deterministic (no random selection)

Sanity checks

  1. Verify CSV row order matches data/Test.csv (2618 rows, 4 target columns: TargetRLF1, TargetR1F1, TargetLLMJudge, all = same answer)
  2. Confirm md5sum of go.csv matches above

9. Acknowledgments

  • AfriE5 (McGill-NLP/AfriE5-Large-instruct) β€” multilingual retrieval encoder
  • LlamaFactory β€” LoRA training framework
  • vLLM β€” inference engine (LoRA + batched generation)
  • DeepSpeed β€” ZeRO-3 distributed training
  • Qwen team (Alibaba) β€” Qwen3.5/3.6/Qwen3 base models

10. Contact

Submitter: Magic Submission deadline: 23 Jun 2026 17:00 GMT (handled in time βœ“)