Skip to content

Latest commit

 

History

History
316 lines (268 loc) · 9.26 KB

finetuning-example-tinyusb.md

File metadata and controls

316 lines (268 loc) · 9.26 KB

Fine-Tuning Example: TinyUSB

This guide shows a full CodeFinetuner run on the TinyUSB examples. The result is a local autocomplete model that has learned the structure on how to use the TinyUSB API.

The run was done on a RunPod pod with a single NVIDIA RTX 4090 (24 GB).

Table of Contents

Why the Examples Folder

TinyUSB has two kinds of code:

  • src/ — the implementation of the stack.
  • examples/ — the usage of the stack.

We want a model that calls the API correctly, so only examples/ is used as raw_data_path.

Setup

Start a RunPod pod with an RTX 4090 and the CodeFinetuner GPU image:

ghcr.io/cuolm/codefinetuner:gpu

The container starts an SSH service automatically. See the RunPod Setup Guide for pod creation and SSH access, and the NVIDIA GPU / CUDA Compatibility Guide if training falls back to CPU.

Check that the GPU is visible:

nvidia-smi

Data Preparation

Clone TinyUSB into the data directory of your workspace:

mkdir -p data
cd data
git clone https://github.com/hathach/tinyusb.git
cd ..

The split is done by the pipeline (split_mode: "auto", 80/10/10).

Configuration

Create config/codefinetuner_config.yaml:

# shared across all stages
globals: &globals
  workspace_path: null  # null: defaults to current working directory (CWD)
  model_name: "unsloth/Qwen2.5-Coder-3B"
  fim_prefix_token: "<|fim_prefix|>"
  fim_middle_token: "<|fim_middle|>"
  fim_suffix_token: "<|fim_suffix|>"
  fim_pad_token: "<|fim_pad|>"
  eos_token: "<|endoftext|>"
  label_pad_token_id: -100
  max_token_sequence_length: 2048
  data_language: "c"
  data_extensions: [".c", ".h"]
  use_unsloth: true

# preprocess stage
preprocess:
  <<: *globals
  split_mode: "auto"
  train_ratio: 0.8  # ignored when split mode manual
  eval_ratio: 0.1   # ignored when split mode manual
  test_ratio: 0.1   # ignored when split mode manual
  max_code_blocks_ast_depth: 8
  min_middle_tokens_length: 8
  max_middle_tokens_length: 256
  fim_examples_per_subblock_ratio: 1.0
  rand_to_ast_fim_examples_ratio: 0.0
  rand_examples_min_prefix_suffix_tokens_length: 8
  tokenizer_batch_size: 32
  raw_data_path: data/tinyusb/examples
  tree_sitter_parser_path: null       # uses tree_sitter_language_pack parser
  tree_sitter_definitions_path: null  # uses package internal definitions
  rng_seed: 0

# finetune stage
finetune:
  <<: *globals
  model_attn_implementation: "sdpa"
  lora_r: 32
  lora_alpha: 64
  lora_dropout: 0.0
  lora_bias: "none"
  lora_target_modules: ["q_proj", "v_proj", "k_proj", "o_proj", "gate_proj", "down_proj", "up_proj"]
  selected_checkpoint_strategy: "best"  # "best" or "last"
  trainer_resume_from_checkpoint: null  # null->fresh start, "last"->take last checkpoint
  trainer_clear_checkpoint_dir: false
  trainer_num_train_epochs: 1
  trainer_per_device_train_batch_size: 8
  trainer_per_device_eval_batch_size: 8
  trainer_gradient_accumulation_steps: 4
  trainer_learning_rate: 5e-5
  trainer_weight_decay: 0.01
  trainer_max_grad_norm: 1.0
  trainer_lr_scheduler_type: "cosine"
  trainer_warmup_steps: 20
  trainer_gradient_checkpointing: true
  trainer_logging_steps: 10
  trainer_eval_strategy: "steps"
  trainer_eval_steps: 20
  trainer_save_strategy: "steps"
  trainer_save_steps: 20
  trainer_logging_strategy: "steps"
  dataset_shuffle_buffer_size: 50000
  dataset_shuffle_seed: 0

# evaluate stage
evaluate:
  <<: *globals
  benchmark_sample_size: 500
  benchmark_shuffle_buffer_size: 10000000
  benchmark_shuffle_seed: 42
  generation_checkpoint: "pipeline"  # "pipeline" -> selected checkpoint from finetune stage
  generation_batch_size: 5
  generation_max_new_tokens: 128
  generation_do_sample: false
  generation_temperature: 0.1  # only active if generation_do_sample: true
  generation_top_p: 0.99       # only active if generation_do_sample: true
  codebleu_ngram_weight: 0.1
  codebleu_weighted_ngram_weight: 0.1
  codebleu_syntax_ast_weight: 0.4
  codebleu_dataflow_weight: 0.4
  sentencebleu_ngram_weight_1: 0.25
  sentencebleu_ngram_weight_2: 0.25
  sentencebleu_ngram_weight_3: 0.25
  sentencebleu_ngram_weight_4: 0.25
  line_match_number_of_lines: 2
  plot_only: false
  benchmark_use_existing_dataset: false

# convert stage
convert:
  <<: *globals

# mlflow tracking (optional)
tracking:
  <<: *globals
  use_mlflow: true
  mlflow_tracking_path: "/workspace/mlflow"  # null -> <workspace_path>/outputs/mlflow
  mlflow_experiment_name: "codefinetuner"
  mlflow_model_logging_strategy: "adapter"   # "none", "adapter", "gguf", or "all"

Run the Pipeline

As a tool:

codefinetuner --config="config/codefinetuner_config.yaml"

Or, from source:

uv run codefinetuner --config="config/codefinetuner_config.yaml"

Follow the run in MLflow:

uv run mlflow ui --backend-store-uri sqlite:///workspace/mlflow/mlflow.db

Training Results

One epoch is 135 steps with an effective batch size of 32 (8 × 4 gradient accumulation).

Training and Evaluation Loss

Step Train Loss Eval Loss
20 1.028 1.128
40 0.757 0.833
60 0.496 0.707
80 0.384 0.692
100 0.328 0.689
120 0.282 0.687
135 – 0.687

The eval loss is flat at ~0.687 from step 60 on, while the train loss keeps falling. One epoch is enough.

Evaluation Results

The evaluate stage compares the base model (unsloth/Qwen2.5-Coder-3B) with the fine-tuned LoRA model on 500 held-out FIM examples.

Base vs LoRA Average Scores

Metric Base LoRA Δ Change N
SentenceBLEU ↑ 0.267 0.509 +0.242 +90.8 % 500
CodeBLEU ↑ 0.430 0.608 +0.178 +41.5 % 252
Exact Match ↑ 0.070 0.306 +0.236 +337.1 % 500
Line Match ↑ 0.070 0.344 +0.274 +391.4 % 500
Edit Similarity ↑ 0.418 0.631 +0.213 +50.9 % 500
Perplexity ↓ 3.949 2.947 −1.002 −25.4 % 500

Exact match rises from 7 % to 31 %. CodeBLEU is only computed on the 252 samples that parse as valid C on their own.

Edit Similarity Scores

Most points in the scatter plot are above the y = x line, with a large group at 1.0. Some points are below it, so the adapter is better on average, not on every sample.

Example Completions

All examples come from the held-out benchmark.

1. Wrong API Names

// reference
for (uint8_t i = 1; i <= CFG_TUH_DEVICE_MAX; i++) {
    if (tuh_msc_mounted(i)) {
      dev_addr = i;
      break;
    }
  }
// base
for (uint8_t i = 0; i < TUSB_MSC_DEVICE_MAX; i++) {
    if (tuh_msc_is_mounted(i)) {
      dev_addr = i + 1;
      break;
    }
  }
// lora
for (uint8_t i = 1; i <= CFG_TUH_DEVICE_MAX; i++) {
    if (tuh_msc_mounted(i)) {
      dev_addr = i;
      break;
    }
  }

codebleu 0.603 -> 1.000, exact_match 0 -> 1

tuh_msc_is_mounted() and TUSB_MSC_DEVICE_MAX do not exist in TinyUSB.

2. Descriptor Bit Math

// reference
_desc_str[0] = (uint16_t) ((TUSB_DESC_STRING << 8) | (2 * chr_count + 2));
// base
_desc_str[0] = (uint16_t) (2 + chr_count);
// lora
_desc_str[0] = (uint16_t) ((TUSB_DESC_STRING << 8) | (2 * chr_count + 2));

codebleu 0.260 -> 1.000, perplexity 1.544 -> 1.001

3. Init Signature

// reference
tusb_init(BOARD_TUD_RHPORT, &dev_init);
// base
tud_init(&dev_init);
// lora
tusb_init(BOARD_TUD_RHPORT, &dev_init);

perplexity 2.762 -> 1.040

4. Repetition Collapse of the Base Model

// reference
{
  blink_interval_ms = tud_mounted() ? BLINK_MOUNTED : BLINK_NOT_MOUNTED;
}
// base
{
  if (tud_composite_enabled()) {
    tud_composite_set_state(TUD_COMPOSITE_STATE_POWERUP);
  } else {
    tud_set_state(TUD_STATE_POWERUP);
  }
}
// lora
{
  blink_interval_ms = tud_mounted() ? BLINK_MOUNTED : BLINK_NOT_MOUNTED;
}

codebleu 0.519 -> 1.000, perplexity 6.029 -> 1.021

Note: It is difficult to tell if the model has overfitted too much on the training data. The real testing and usefulness are only visible when using the model for autocomplete tasks, because tools such as llama.vscode and llama.vim do not sample deterministically, they use top_k, top_p, and temperature values, unlike the benchmark's greedy decoding.

Use the Model in VS Code

The convert stage writes the GGUF model to:

outputs/convert/results/Qwen2.5-Coder-3B-lora-merged.gguf

Download it from the pod, serve it with llama.cpp and point llama.vscode at it. See the inference-vscode guide for the full setup.