{
  "id": 684206,
  "title": "[22nd] CPT, GRPO and Extensive Work on the Data (No SFT)",
  "url": "/competitions/deep-past-initiative-machine-translation/writeups/23rd-place-cpt-grpo-no-sft",
  "author_name": "",
  "post_date": "2026-03-24T02:21:21.620Z",
  "votes": 23,
  "comment_count": 2,
  "views": 0,
  "content": "<h2>Overview</h2>\n<p>This was an interesting one. Thanks to the organizers for putting together this competition. The model I submitted was trained in two stages: <strong>continued pre-training (CPT)</strong> on a large domain corpus, followed directly by <strong>group relative policy optimization (GRPO)</strong>. No supervised fine-tuning (SFT) step was used.</p>\n<hr>\n<h2>Data</h2>\n<h3>Sources</h3>\n<p>Training on <code>train.csv</code> directly gave mixed results due to data quality issues, so most of the work went into building a cleaner, larger corpus from external sources instead. The training corpus was assembled from several publicly available Old Assyrian and Akkadian sources:</p>\n<ul>\n<li><strong>OARE</strong> (Old Assyrian Research Environment): transliterations and translations of Old Assyrian tablets</li>\n<li><strong>eBL</strong> (Electronic Babylonian Library): lexical data and sign lists</li>\n<li><strong>Academic PDFs and corpora</strong>: additional Old Assyrian texts and commentaries</li>\n</ul>\n<p>And pretty much every other publicly available Old Assyrian/Akkadian corpus I could find. Data collection alone took up roughly the first two months of the competition. The raw sources contained Old Assyrian sentences translated into some combination of English, French, Turkish, and German; DeepSeek was used to fill in the missing translations for each sentence, bringing the total to ~<strong>1 million sentence pairs</strong> after augmentation. About half of those pairs were Old Assyrian; the remainder covered closely related Akkadian dialects.</p>\n<h3>Cleaning</h3>\n<p>Some work was done by hand, but after that <strong>DeepSeek</strong> and <strong>Gemini</strong> were used to:</p>\n<ol>\n<li>Remove boilerplate and fix OCR artefacts</li>\n<li>Tag and extract sentence pairs from document-level texts</li>\n<li>Split document-level translations into aligned sentence pairs (non-trivial due to the lack of punctuation in the transliterations)</li>\n<li>Score translation quality and filter or fix low-quality pairs</li>\n</ol>\n<p>DeepSeek was also used to modernize some older translations to better match the style found in <code>train.csv</code>, though it's unclear whether this helped or hurt — the test set style was an unknown.</p>\n<h3>Preprocessing</h3>\n<p>Rule-based preprocessing was applied to both source (transliteration) and target (translation) sides:</p>\n<ul>\n<li><strong>Name normalization</strong>: resolving logographic vs. phonetic spellings of the same entity</li>\n<li><strong>Domain term normalization</strong>: shekel fractions, standard formulaic expressions</li>\n<li><strong>Gap handling</strong>: consistent <code>&lt;gap&gt;</code> markers for damaged or missing text</li>\n</ul>\n<hr>\n<h2>Training</h2>\n<h3>Stage 1: CPT</h3>\n<p>The base <strong>Qwen3-8B</strong> model was adapted to the Old Assyrian domain via causal language modeling on the full cleaned corpus (~250M tokens) that I had assembled.</p>\n<ul>\n<li>LoRA rank: 128</li>\n<li>Learning rate: 5e-5 (embeddings: 1e-5), cosine schedule with 5% warmup</li>\n<li>Max sequence length: 2048</li>\n<li>Single epoch</li>\n<li>Framework: Unsloth</li>\n</ul>\n<h3>Stage 2: GRPO</h3>\n<p>GRPO was applied <strong>directly on the CPT checkpoint</strong>, skipping SFT entirely. The full dataset was filtered using a combination of heuristics (such as relative sentence length ratios) and LLM-as-judge quality scores, then deduplicated using MinHash, and only the ~30k highest-quality Old Assyrian–English sentence pairs were retained for this step. The primary reward signal was the competition metric: <strong>geometric mean of sentence-level BLEU and chrF++</strong>. Additional penalties were added for length divergence and poor whitespace ratios to keep outputs well-formed.</p>\n<ul>\n<li>LoRA rank: 32</li>\n<li>Learning rate: 2e-5, cosine schedule with 10% warmup</li>\n<li>Temperature: 0.7</li>\n<li>Effective batch size: 32 prompts × 16 generations each</li>\n<li>KL penalty (beta): 0.05</li>\n<li>3000 steps</li>\n<li>Framework: Unsloth</li>\n</ul>\n<p>Training went surprisingly smoothly. The model learned to use its stop token and started producing reasonably formatted translations after ~70 steps. Metrics plateaued around step 1500, though I let it run to 3000.</p>\n<hr>\n<h2>Inference</h2>\n<p>Candidates were generated with <strong>vLLM</strong>. Final selection used <strong>length-normalized average log-probability</strong> — picking whichever candidate the model was most confident in.</p>\n<p>Post-processing included forbidden character removal, name normalization to match source-side canonical forms, and number consistency checks. These steps were mostly put in place for earlier models that needed them more — the final model rarely triggered them — but were kept as a safeguard.</p>\n<hr>\n<h2>What Worked</h2>\n<ul>\n<li><strong>Skipping SFT</strong>: GRPO directly on the CPT checkpoint outperformed the full CPT → SFT → RL pipeline.</li>\n<li><strong>Domain preprocessing</strong>: Name normalization and consistent gap markers gave the model cleaner, more regular inputs.</li>\n<li><strong>LLM-assisted cleaning</strong>: Scoring and filtering with DeepSeek/Gemini noticeably improved corpus quality.</li>\n<li><strong>Logprob selection</strong>: Simple but effective.</li>\n</ul>\n<h2>What Didn't Work</h2>\n<ul>\n<li><strong>SFT</strong>: Both CPT → SFT → GRPO and CPT → GRPO → SFT were tried. The latter subjectively produced the best translations, but neither scored as well on the competition metric as the CPT → GRPO pipeline without any SFT step.</li>\n<li><strong>MBR decoding</strong>: Didn't consistently beat logprob selection.</li>\n<li><strong>Beam search</strong>: Caused looping issues, possibly because GRPO reshaped the probability landscape in ways that made beam search's greedy expansion unstable.</li>\n<li><strong>Curriculum learning</strong>: CPT → SFT on a ~300k Q&amp;A dataset and ~1 million sentence pairs → final RL didn't beat simply putting everything in CPT and following up with GRPO directly.</li>\n<li><strong>Trying to use a too recent model</strong>: I spent a significant amount of time fine-tuning Qwen3.5-9B, which was released during the competition and gave strong results locally. Unfortunately, compatibility issues with the Kaggle GPUs forced a switch back to the older, more widely supported Qwen3-8B for the final submission.</li>\n<li><strong>Prefill system</strong>: Built using two complementary approaches: formulaic pattern matching for known Old Assyrian constructions (e.g. detecting <code>umma X-ma ana Y qibīma</code> to seed the decoder with <code>From X to Y:</code>), and a statistical method that mined the training data for recurring source prefix → target prefix correspondences (longest match first, filtered by frequency and consistency). Both showed local promise, but inference crashes on Kaggle ruled it out of the final submission.</li>\n</ul>",
  "messages": [
    {
      "id": "3427367",
      "postDate": "03/24/2026 02:18:17",
      "content": "<h2>Overview</h2>\n<p>This was an interesting one. Thanks to the organizers for putting together this competition. The model I submitted was trained in two stages: <strong>continued pre-training (CPT)</strong> on a large domain corpus, followed directly by <strong>group relative policy optimization (GRPO)</strong>. No supervised fine-tuning (SFT) step was used.</p>\n<hr>\n<h2>Data</h2>\n<h3>Sources</h3>\n<p>Training on <code>train.csv</code> directly gave mixed results due to data quality issues, so most of the work went into building a cleaner, larger corpus from external sources instead. The training corpus was assembled from several publicly available Old Assyrian and Akkadian sources:</p>\n<ul>\n<li><strong>OARE</strong> (Old Assyrian Research Environment): transliterations and translations of Old Assyrian tablets</li>\n<li><strong>eBL</strong> (Electronic Babylonian Library): lexical data and sign lists</li>\n<li><strong>Academic PDFs and corpora</strong>: additional Old Assyrian texts and commentaries</li>\n</ul>\n<p>And pretty much every other publicly available Old Assyrian/Akkadian corpus I could find. Data collection alone took up roughly the first two months of the competition. The raw sources contained Old Assyrian sentences translated into some combination of English, French, Turkish, and German; DeepSeek was used to fill in the missing translations for each sentence, bringing the total to ~<strong>1 million sentence pairs</strong> after augmentation. About half of those pairs were Old Assyrian; the remainder covered closely related Akkadian dialects.</p>\n<h3>Cleaning</h3>\n<p>Some work was done by hand, but after that <strong>DeepSeek</strong> and <strong>Gemini</strong> were used to:</p>\n<ol>\n<li>Remove boilerplate and fix OCR artefacts</li>\n<li>Tag and extract sentence pairs from document-level texts</li>\n<li>Split document-level translations into aligned sentence pairs (non-trivial due to the lack of punctuation in the transliterations)</li>\n<li>Score translation quality and filter or fix low-quality pairs</li>\n</ol>\n<p>DeepSeek was also used to modernize some older translations to better match the style found in <code>train.csv</code>, though it's unclear whether this helped or hurt — the test set style was an unknown.</p>\n<h3>Preprocessing</h3>\n<p>Rule-based preprocessing was applied to both source (transliteration) and target (translation) sides:</p>\n<ul>\n<li><strong>Name normalization</strong>: resolving logographic vs. phonetic spellings of the same entity</li>\n<li><strong>Domain term normalization</strong>: shekel fractions, standard formulaic expressions</li>\n<li><strong>Gap handling</strong>: consistent <code>&lt;gap&gt;</code> markers for damaged or missing text</li>\n</ul>\n<hr>\n<h2>Training</h2>\n<h3>Stage 1: CPT</h3>\n<p>The base <strong>Qwen3-8B</strong> model was adapted to the Old Assyrian domain via causal language modeling on the full cleaned corpus (~250M tokens) that I had assembled.</p>\n<ul>\n<li>LoRA rank: 128</li>\n<li>Learning rate: 5e-5 (embeddings: 1e-5), cosine schedule with 5% warmup</li>\n<li>Max sequence length: 2048</li>\n<li>Single epoch</li>\n<li>Framework: Unsloth</li>\n</ul>\n<h3>Stage 2: GRPO</h3>\n<p>GRPO was applied <strong>directly on the CPT checkpoint</strong>, skipping SFT entirely. The full dataset was filtered using a combination of heuristics (such as relative sentence length ratios) and LLM-as-judge quality scores, then deduplicated using MinHash, and only the ~30k highest-quality Old Assyrian–English sentence pairs were retained for this step. The primary reward signal was the competition metric: <strong>geometric mean of sentence-level BLEU and chrF++</strong>. Additional penalties were added for length divergence and poor whitespace ratios to keep outputs well-formed.</p>\n<ul>\n<li>LoRA rank: 32</li>\n<li>Learning rate: 2e-5, cosine schedule with 10% warmup</li>\n<li>Temperature: 0.7</li>\n<li>Effective batch size: 32 prompts × 16 generations each</li>\n<li>KL penalty (beta): 0.05</li>\n<li>3000 steps</li>\n<li>Framework: Unsloth</li>\n</ul>\n<p>Training went surprisingly smoothly. The model learned to use its stop token and started producing reasonably formatted translations after ~70 steps. Metrics plateaued around step 1500, though I let it run to 3000.</p>\n<hr>\n<h2>Inference</h2>\n<p>Candidates were generated with <strong>vLLM</strong>. Final selection used <strong>length-normalized average log-probability</strong> — picking whichever candidate the model was most confident in.</p>\n<p>Post-processing included forbidden character removal, name normalization to match source-side canonical forms, and number consistency checks. These steps were mostly put in place for earlier models that needed them more — the final model rarely triggered them — but were kept as a safeguard.</p>\n<hr>\n<h2>What Worked</h2>\n<ul>\n<li><strong>Skipping SFT</strong>: GRPO directly on the CPT checkpoint outperformed the full CPT → SFT → RL pipeline.</li>\n<li><strong>Domain preprocessing</strong>: Name normalization and consistent gap markers gave the model cleaner, more regular inputs.</li>\n<li><strong>LLM-assisted cleaning</strong>: Scoring and filtering with DeepSeek/Gemini noticeably improved corpus quality.</li>\n<li><strong>Logprob selection</strong>: Simple but effective.</li>\n</ul>\n<h2>What Didn't Work</h2>\n<ul>\n<li><strong>SFT</strong>: Both CPT → SFT → GRPO and CPT → GRPO → SFT were tried. The latter subjectively produced the best translations, but neither scored as well on the competition metric as the CPT → GRPO pipeline without any SFT step.</li>\n<li><strong>MBR decoding</strong>: Didn't consistently beat logprob selection.</li>\n<li><strong>Beam search</strong>: Caused looping issues, possibly because GRPO reshaped the probability landscape in ways that made beam search's greedy expansion unstable.</li>\n<li><strong>Curriculum learning</strong>: CPT → SFT on a ~300k Q&amp;A dataset and ~1 million sentence pairs → final RL didn't beat simply putting everything in CPT and following up with GRPO directly.</li>\n<li><strong>Trying to use a too recent model</strong>: I spent a significant amount of time fine-tuning Qwen3.5-9B, which was released during the competition and gave strong results locally. Unfortunately, compatibility issues with the Kaggle GPUs forced a switch back to the older, more widely supported Qwen3-8B for the final submission.</li>\n<li><strong>Prefill system</strong>: Built using two complementary approaches: formulaic pattern matching for known Old Assyrian constructions (e.g. detecting <code>umma X-ma ana Y qibīma</code> to seed the decoder with <code>From X to Y:</code>), and a statistical method that mined the training data for recurring source prefix → target prefix correspondences (longest match first, filtered by frequency and consistency). Both showed local promise, but inference crashes on Kaggle ruled it out of the final submission.</li>\n</ul>",
      "rawMarkdown": "## Overview\n\nThis was an interesting one. Thanks to the organizers for putting together this competition. The model I submitted was trained in two stages: **continued pre-training (CPT)** on a large domain corpus, followed directly by **group relative policy optimization (GRPO)**. No supervised fine-tuning (SFT) step was used.\n\n---\n\n## Data\n\n### Sources\n\nTraining on `train.csv` directly gave mixed results due to data quality issues, so most of the work went into building a cleaner, larger corpus from external sources instead. The training corpus was assembled from several publicly available Old Assyrian and Akkadian sources:\n\n- **OARE** (Old Assyrian Research Environment): transliterations and translations of Old Assyrian tablets\n- **eBL** (Electronic Babylonian Library): lexical data and sign lists\n- **Academic PDFs and corpora**: additional Old Assyrian texts and commentaries\n\nAnd pretty much every other publicly available Old Assyrian/Akkadian corpus I could find. Data collection alone took up roughly the first two months of the competition. The raw sources contained Old Assyrian sentences translated into some combination of English, French, Turkish, and German; DeepSeek was used to fill in the missing translations for each sentence, bringing the total to ~**1 million sentence pairs** after augmentation. About half of those pairs were Old Assyrian; the remainder covered closely related Akkadian dialects.\n\n### Cleaning\n\nSome work was done by hand, but after that **DeepSeek** and **Gemini** were used to:\n\n1. Remove boilerplate and fix OCR artefacts\n2. Tag and extract sentence pairs from document-level texts\n3. Split document-level translations into aligned sentence pairs (non-trivial due to the lack of punctuation in the transliterations)\n4. Score translation quality and filter or fix low-quality pairs\n\nDeepSeek was also used to modernize some older translations to better match the style found in `train.csv`, though it's unclear whether this helped or hurt — the test set style was an unknown.\n\n### Preprocessing\n\nRule-based preprocessing was applied to both source (transliteration) and target (translation) sides:\n\n- **Name normalization**: resolving logographic vs. phonetic spellings of the same entity\n- **Domain term normalization**: shekel fractions, standard formulaic expressions\n- **Gap handling**: consistent `<gap>` markers for damaged or missing text\n\n---\n\n## Training\n\n### Stage 1: CPT\n\nThe base **Qwen3-8B** model was adapted to the Old Assyrian domain via causal language modeling on the full cleaned corpus (~250M tokens) that I had assembled.\n\n- LoRA rank: 128\n- Learning rate: 5e-5 (embeddings: 1e-5), cosine schedule with 5% warmup\n- Max sequence length: 2048\n- Single epoch\n- Framework: Unsloth\n\n### Stage 2: GRPO\n\nGRPO was applied **directly on the CPT checkpoint**, skipping SFT entirely. The full dataset was filtered using a combination of heuristics (such as relative sentence length ratios) and LLM-as-judge quality scores, then deduplicated using MinHash, and only the ~30k highest-quality Old Assyrian–English sentence pairs were retained for this step. The primary reward signal was the competition metric: **geometric mean of sentence-level BLEU and chrF++**. Additional penalties were added for length divergence and poor whitespace ratios to keep outputs well-formed.\n\n- LoRA rank: 32\n- Learning rate: 2e-5, cosine schedule with 10% warmup\n- Temperature: 0.7\n- Effective batch size: 32 prompts × 16 generations each\n- KL penalty (beta): 0.05\n- 3000 steps\n- Framework: Unsloth\n\nTraining went surprisingly smoothly. The model learned to use its stop token and started producing reasonably formatted translations after ~70 steps. Metrics plateaued around step 1500, though I let it run to 3000.\n\n---\n\n## Inference\n\nCandidates were generated with **vLLM**. Final selection used **length-normalized average log-probability** — picking whichever candidate the model was most confident in.\n\nPost-processing included forbidden character removal, name normalization to match source-side canonical forms, and number consistency checks. These steps were mostly put in place for earlier models that needed them more — the final model rarely triggered them — but were kept as a safeguard.\n\n---\n\n## What Worked\n\n- **Skipping SFT**: GRPO directly on the CPT checkpoint outperformed the full CPT → SFT → RL pipeline.\n- **Domain preprocessing**: Name normalization and consistent gap markers gave the model cleaner, more regular inputs.\n- **LLM-assisted cleaning**: Scoring and filtering with DeepSeek/Gemini noticeably improved corpus quality.\n- **Logprob selection**: Simple but effective.\n\n\n## What Didn't Work\n\n- **SFT**: Both CPT → SFT → GRPO and CPT → GRPO → SFT were tried. The latter subjectively produced the best translations, but neither scored as well on the competition metric as the CPT → GRPO pipeline without any SFT step.\n- **MBR decoding**: Didn't consistently beat logprob selection.\n- **Beam search**: Caused looping issues, possibly because GRPO reshaped the probability landscape in ways that made beam search's greedy expansion unstable.\n- **Curriculum learning**: CPT → SFT on a ~300k Q&A dataset and ~1 million sentence pairs → final RL didn't beat simply putting everything in CPT and following up with GRPO directly.\n- **Trying to use a too recent model**: I spent a significant amount of time fine-tuning Qwen3.5-9B, which was released during the competition and gave strong results locally. Unfortunately, compatibility issues with the Kaggle GPUs forced a switch back to the older, more widely supported Qwen3-8B for the final submission.\n- **Prefill system**: Built using two complementary approaches: formulaic pattern matching for known Old Assyrian constructions (e.g. detecting `umma X-ma ana Y qibīma` to seed the decoder with `From X to Y:`), and a statistical method that mined the training data for recurring source prefix → target prefix correspondences (longest match first, filtered by frequency and consistency). Both showed local promise, but inference crashes on Kaggle ruled it out of the final submission.",
      "votes": null
    },
    {
      "id": "3427566",
      "postDate": "03/24/2026 08:54:05",
      "content": "<p>Thank you for your solution. May I ask, after step 1 (CPT), how will you merge the LoRa into the base model to obtain the CPT model?</p>",
      "rawMarkdown": "Thank you for your solution. May I ask, after step 1 (CPT), how will you merge the LoRa into the base model to obtain the CPT model?",
      "votes": null
    },
    {
      "id": "3427713",
      "postDate": "03/24/2026 12:53:22",
      "content": "<p>I had some issues merging at that step. In the end here is what worked:</p>\n<pre><code>model, tokenizer = FastLanguageModel.from_pretrained(\n    model_name = adapter_path,\n    max_seq_length = 2048,\n    dtype = torch.bfloat16,\n    load_in_4bit = False,\n    device_map = \"cpu\",\n)\n\nmodel = model.merge_and_unload()\nmodel.config.tie_word_embeddings = False\n\nmodel.save_pretrained(save_path)\ntokenizer.save_pretrained(save_path)\n</code></pre>",
      "rawMarkdown": "I had some issues merging at that step. In the end here is what worked:\n\n```\nmodel, tokenizer = FastLanguageModel.from_pretrained(\n    model_name = adapter_path,\n    max_seq_length = 2048,\n    dtype = torch.bfloat16,\n    load_in_4bit = False,\n    device_map = \"cpu\",\n)\n\nmodel = model.merge_and_unload()\nmodel.config.tie_word_embeddings = False\n\nmodel.save_pretrained(save_path)\ntokenizer.save_pretrained(save_path)\n```",
      "votes": null
    }
  ],
  "comments": [
    {
      "id": 3427566,
      "author_name": "kingkong153",
      "author_url": "",
      "post_date": "03/24/2026 08:54:05",
      "content": "<p>Thank you for your solution. May I ask, after step 1 (CPT), how will you merge the LoRa into the base model to obtain the CPT model?</p>",
      "votes": null,
      "replies": [
        {
          "id": 3427713,
          "author_name": "epiktistes",
          "author_url": "",
          "post_date": "03/24/2026 12:53:22",
          "content": "<p>I had some issues merging at that step. In the end here is what worked:</p>\n<pre><code>model, tokenizer = FastLanguageModel.from_pretrained(\n    model_name = adapter_path,\n    max_seq_length = 2048,\n    dtype = torch.bfloat16,\n    load_in_4bit = False,\n    device_map = \"cpu\",\n)\n\nmodel = model.merge_and_unload()\nmodel.config.tie_word_embeddings = False\n\nmodel.save_pretrained(save_path)\ntokenizer.save_pretrained(save_path)\n</code></pre>",
          "votes": null,
          "replies": []
        }
      ]
    }
  ],
  "raw_markdown_by_id": {
    "3427367": "## Overview\n\nThis was an interesting one. Thanks to the organizers for putting together this competition. The model I submitted was trained in two stages: **continued pre-training (CPT)** on a large domain corpus, followed directly by **group relative policy optimization (GRPO)**. No supervised fine-tuning (SFT) step was used.\n\n---\n\n## Data\n\n### Sources\n\nTraining on `train.csv` directly gave mixed results due to data quality issues, so most of the work went into building a cleaner, larger corpus from external sources instead. The training corpus was assembled from several publicly available Old Assyrian and Akkadian sources:\n\n- **OARE** (Old Assyrian Research Environment): transliterations and translations of Old Assyrian tablets\n- **eBL** (Electronic Babylonian Library): lexical data and sign lists\n- **Academic PDFs and corpora**: additional Old Assyrian texts and commentaries\n\nAnd pretty much every other publicly available Old Assyrian/Akkadian corpus I could find. Data collection alone took up roughly the first two months of the competition. The raw sources contained Old Assyrian sentences translated into some combination of English, French, Turkish, and German; DeepSeek was used to fill in the missing translations for each sentence, bringing the total to ~**1 million sentence pairs** after augmentation. About half of those pairs were Old Assyrian; the remainder covered closely related Akkadian dialects.\n\n### Cleaning\n\nSome work was done by hand, but after that **DeepSeek** and **Gemini** were used to:\n\n1. Remove boilerplate and fix OCR artefacts\n2. Tag and extract sentence pairs from document-level texts\n3. Split document-level translations into aligned sentence pairs (non-trivial due to the lack of punctuation in the transliterations)\n4. Score translation quality and filter or fix low-quality pairs\n\nDeepSeek was also used to modernize some older translations to better match the style found in `train.csv`, though it's unclear whether this helped or hurt — the test set style was an unknown.\n\n### Preprocessing\n\nRule-based preprocessing was applied to both source (transliteration) and target (translation) sides:\n\n- **Name normalization**: resolving logographic vs. phonetic spellings of the same entity\n- **Domain term normalization**: shekel fractions, standard formulaic expressions\n- **Gap handling**: consistent `<gap>` markers for damaged or missing text\n\n---\n\n## Training\n\n### Stage 1: CPT\n\nThe base **Qwen3-8B** model was adapted to the Old Assyrian domain via causal language modeling on the full cleaned corpus (~250M tokens) that I had assembled.\n\n- LoRA rank: 128\n- Learning rate: 5e-5 (embeddings: 1e-5), cosine schedule with 5% warmup\n- Max sequence length: 2048\n- Single epoch\n- Framework: Unsloth\n\n### Stage 2: GRPO\n\nGRPO was applied **directly on the CPT checkpoint**, skipping SFT entirely. The full dataset was filtered using a combination of heuristics (such as relative sentence length ratios) and LLM-as-judge quality scores, then deduplicated using MinHash, and only the ~30k highest-quality Old Assyrian–English sentence pairs were retained for this step. The primary reward signal was the competition metric: **geometric mean of sentence-level BLEU and chrF++**. Additional penalties were added for length divergence and poor whitespace ratios to keep outputs well-formed.\n\n- LoRA rank: 32\n- Learning rate: 2e-5, cosine schedule with 10% warmup\n- Temperature: 0.7\n- Effective batch size: 32 prompts × 16 generations each\n- KL penalty (beta): 0.05\n- 3000 steps\n- Framework: Unsloth\n\nTraining went surprisingly smoothly. The model learned to use its stop token and started producing reasonably formatted translations after ~70 steps. Metrics plateaued around step 1500, though I let it run to 3000.\n\n---\n\n## Inference\n\nCandidates were generated with **vLLM**. Final selection used **length-normalized average log-probability** — picking whichever candidate the model was most confident in.\n\nPost-processing included forbidden character removal, name normalization to match source-side canonical forms, and number consistency checks. These steps were mostly put in place for earlier models that needed them more — the final model rarely triggered them — but were kept as a safeguard.\n\n---\n\n## What Worked\n\n- **Skipping SFT**: GRPO directly on the CPT checkpoint outperformed the full CPT → SFT → RL pipeline.\n- **Domain preprocessing**: Name normalization and consistent gap markers gave the model cleaner, more regular inputs.\n- **LLM-assisted cleaning**: Scoring and filtering with DeepSeek/Gemini noticeably improved corpus quality.\n- **Logprob selection**: Simple but effective.\n\n\n## What Didn't Work\n\n- **SFT**: Both CPT → SFT → GRPO and CPT → GRPO → SFT were tried. The latter subjectively produced the best translations, but neither scored as well on the competition metric as the CPT → GRPO pipeline without any SFT step.\n- **MBR decoding**: Didn't consistently beat logprob selection.\n- **Beam search**: Caused looping issues, possibly because GRPO reshaped the probability landscape in ways that made beam search's greedy expansion unstable.\n- **Curriculum learning**: CPT → SFT on a ~300k Q&A dataset and ~1 million sentence pairs → final RL didn't beat simply putting everything in CPT and following up with GRPO directly.\n- **Trying to use a too recent model**: I spent a significant amount of time fine-tuning Qwen3.5-9B, which was released during the competition and gave strong results locally. Unfortunately, compatibility issues with the Kaggle GPUs forced a switch back to the older, more widely supported Qwen3-8B for the final submission.\n- **Prefill system**: Built using two complementary approaches: formulaic pattern matching for known Old Assyrian constructions (e.g. detecting `umma X-ma ana Y qibīma` to seed the decoder with `From X to Y:`), and a statistical method that mined the training data for recurring source prefix → target prefix correspondences (longest match first, filtered by frequency and consistency). Both showed local promise, but inference crashes on Kaggle ruled it out of the final submission.",
    "3427566": "Thank you for your solution. May I ask, after step 1 (CPT), how will you merge the LoRa into the base model to obtain the CPT model?",
    "3427713": "I had some issues merging at that step. In the end here is what worked:\n\n```\nmodel, tokenizer = FastLanguageModel.from_pretrained(\n    model_name = adapter_path,\n    max_seq_length = 2048,\n    dtype = torch.bfloat16,\n    load_in_4bit = False,\n    device_map = \"cpu\",\n)\n\nmodel = model.merge_and_unload()\nmodel.config.tie_word_embeddings = False\n\nmodel.save_pretrained(save_path)\ntokenizer.save_pretrained(save_path)\n```"
  },
  "source": "existing_discussion",
  "complete": true,
  "competition_id": 121150,
  "competition_slug": "deep-past-initiative-machine-translation",
  "team_id": 15000043,
  "solution_url": "/competitions/deep-past-initiative-machine-translation/writeups/23rd-place-cpt-grpo-no-sft"
}