Temper 0.5B
Temper 0.5B — небольшая модель, дообученная на базе Qwen2.5-Coder-0.5B-Instruct, упор которой сделан на языке Rust. Это позволило ей обойти модели крупнее, вплоть до x3 по параметрам. Целью было проверить возможности крошечной модели в узкой задаче.
Первая версия
Изначально в качестве учителя был выбран Qwen3.6-35B-A3B, который на карте NVIDIA A40 48GB за ~9 часов смог выдать 2700 примеров на Rust. Формат данных для обучения был проще, чем в итоговой версии Temper. В каждом примере была функция с интерфейсом, doc-комментарием, просьбой дописать и пустым телом с одним todo!(). Первая попытка с этим датасетом уже дала заметный рост по сравнению с базовым Qwen.
Задача из датасета первой версии (переведено с английского)
Допиши код на Rust ниже. Замени каждый todo!() рабочей реализацией.
Не меняй определения типов и сигнатуры функций. Используй только стандартную библиотеку Rust.
Ответь полным кодом в одном блоке ```rust.
/// Calculates the number of full fuel tanks required and the amount of remaining fuel.
///
/// Given `total_fuel` and `tank_capacity`, returns a tuple `(full_tanks, remainder)`.
/// If `tank_capacity` is zero, returns `(0, total_fuel)`.
pub fn fuel_logistics(total_fuel: i64, tank_capacity: i64) -> (i64, i64) {
todo!()
}
Ответ учителя
/// Calculates the number of full fuel tanks required and the amount of remaining fuel.
///
/// Given `total_fuel` and `tank_capacity`, returns a tuple `(full_tanks, remainder)`.
/// If `tank_capacity` is zero, returns `(0, total_fuel)`.
pub fn fuel_logistics(total_fuel: i64, tank_capacity: i64) -> (i64, i64) {
if tank_capacity == 0 {
return (0, total_fuel);
}
let full_tanks = total_fuel / tank_capacity;
let remainder = total_fuel % tank_capacity;
(full_tanks, remainder)
}
После трёх эпох обучения на этих примерах первая версия решала с первой попытки 64,6% тривиальных задач против 45,6% у базовой Qwen2.5-Coder-0.5B. На простых задачах её результат вырос больше чем вдвое, с 15,0% до 34,3%. Прирост на наборах MultiPL-E оказался скромнее и составил 10 пунктов на humaneval-rs и 7 на mbpp-rs.
Второй датасет
После этого датасет вырос до 10 тысяч примеров, а учителем стал DeepSeek-V4-Flash. Он в целом сильнее Qwen3.6-35B-A3B в программировании и к тому же дешевле по API. Данные стали качественнее, из них убрали дубликаты и утечки бенчмарков. Из генерации DeepSeek были приняты ~60% примеров, которые прошли компиляцию и тесты. Из них ~65% составляли просьбы без кода, ~35% были задачами с заготовками. Около 35% всего датасета пришлось на алгоритмические примеры.
Задача без заготовки (переведено с английского)
Напиши функцию, которая принимает два отсортированных списка строк и сливает их в один отсортированный список, оставляя только строки, которые не начинаются с 'a' и не заканчиваются на 'z'.
Используй только стандартную библиотеку Rust.
Ответ учителя
pub fn merge_filtered(list1: Vec<String>, list2: Vec<String>) -> Vec<String> {
let mut result = Vec::with_capacity(list1.len() + list2.len());
let mut i = 0;
let mut j = 0;
while i < list1.len() && j < list2.len() {
if list1[i] <= list2[j] {
let s = &list1[i];
if !s.starts_with('a') && !s.ends_with('z') {
result.push(s.clone());
}
i += 1;
} else {
let s = &list2[j];
if !s.starts_with('a') && !s.ends_with('z') {
result.push(s.clone());
}
j += 1;
}
}
while i < list1.len() {
let s = &list1[i];
if !s.starts_with('a') && !s.ends_with('z') {
result.push(s.clone());
}
i += 1;
}
while j < list2.len() {
let s = &list2[j];
if !s.starts_with('a') && !s.ends_with('z') {
result.push(s.clone());
}
j += 1;
}
result
}
После обучения на этом датасете Temper дообучили с подкреплением. Модель писала по несколько решений каждой задачи, награду получали те из них, что проходят тесты. Так она училась на собственных ответах, которые проверял компилятор Rust.
Результаты
По сравнению с первой версией эта заметно прибавила на Rust-версиях классических бенчмарков из MultiPL-E. Доля задач, решённых с первой попытки, на humaneval-rs выросла на 7,6 пункта, а на mbpp-rs на 7,9. С вероятностью 95% реальный прирост лежит между 3,0 и 12,3 пункта на первом наборе и между 5,1 и 10,7 на втором, так что это не случайность.
Свой бенчмарк
Для Rust почти нет бенчмарков, на которых имеет смысл мерить крошечную модель. MultiPL-E переводит на Rust задачи HumanEval и MBPP, но это алгоритмические функции над числами и списками, исходно написанные для Python. Структуры с методами, трейты, обработка ошибок через Result и работа с крейтами в них почти не встречаются, хотя именно из этого состоит повседневный код на Rust.
Поэтому к двум наборам MultiPL-E добавлен собственный бенчмарк из 100 задач трёх уровней сложности. Каждая задача состоит из просьбы обычным текстом, публичного интерфейса и скрытых тестов. Модель возвращает полный код, он компилируется вместе с тестами, и задача засчитывается, только если проходят все тесты. Тесты, которые модель напишет сама, на результат не влияют. Для каждой задачи проверено, что эталонное решение проходит все тесты, заготовка с todo!() компилируется и не проходит ни одного.
Просьбы в бенчмарке написаны в том же стиле, что и обучающие данные Temper. Значит, независимое сравнение с другими моделями дают именно наборы MultiPL-E, а свои задачи показывают, насколько модель справляется с обычным кодом на Rust.
Напиши функцию, которая возвращает число символов в строке. Каждый символ должен считаться отдельно, чтобы функция одинаково работала с пустой строкой, обычным английским текстом и строкой с неанглийскими буквами. Используй только стандартную библиотеку Rust. Используй ровно такой интерфейс:
pub fn char_count(s: &str) -> usize;
Ответ Temper 0.5B
Решено в 8 попытках из 10. Ниже одна из решённых.
pub fn char_count(s: &str) -> usize {
s.chars().count()
}
Сделай банковский счёт с балансом, который можно посмотреть, пополнить и с которого можно снять деньги. Если денег не хватает, снятие должно вернуть ошибку ровно с текстом "insufficient funds", а баланс при этом не должен меняться. Используй только стандартную библиотеку Rust. Используй ровно такой интерфейс:
pub struct Account { /* private fields */ }
impl Account {
pub fn new(balance: u64) -> Self;
pub fn balance(&self) -> u64;
pub fn deposit(&mut self, amount: u64);
pub fn withdraw(&mut self, amount: u64) -> Result<(), String>;
}
Ответ Temper 0.5B
Решено в 9 попытках из 10. Ниже одна из решённых.
pub struct Account {
balance: u64,
}
impl Account {
pub fn new(balance: u64) -> Self {
Account { balance }
}
pub fn balance(&self) -> u64 {
self.balance
}
pub fn deposit(&mut self, amount: u64) {
self.balance += amount;
}
pub fn withdraw(&mut self, amount: u64) -> Result<(), String> {
if amount > self.balance {
return Err("insufficient funds".to_string());
}
self.balance -= amount;
Ok(())
}
}
Напиши тип дерева со значением и списком детей, где дети могут быть общими (один и тот же ребёнок встречается в нескольких местах). Добавь две функции. Одна возвращает сумму значений узла и всего, что под ним, другая прибавляет число к значению узла и всего, что под ним. Общее поддерево нужно обходить и учитывать один раз для каждого места, где оно встречается. Используй только стандартную библиотеку Rust. Используй ровно такой интерфейс:
use std::cell::RefCell;
use std::rc::Rc;
pub struct Node {
pub value: i32,
pub children: Vec<Rc<RefCell<Node>>>,
}
pub fn tree_sum(node: &Rc<RefCell<Node>>) -> i32;
pub fn add_to_all(node: &Rc<RefCell<Node>>, delta: i32);
Ответ Temper 0.5B
Решено в 7 попытках из 10. Пример выбран как показательный и не типичен для уровня, в среднем Temper решает с первой попытки 18% задач medium.
use std::cell::RefCell;
use std::rc::Rc;
pub struct Node {
pub value: i32,
pub children: Vec<Rc<RefCell<Node>>>,
}
pub fn tree_sum(node: &Rc<RefCell<Node>>) -> i32 {
let mut sum = node.borrow().value;
for child in node.borrow().children.iter() {
sum += tree_sum(child);
}
sum
}
pub fn add_to_all(node: &Rc<RefCell<Node>>, delta: i32) {
let mut current = node.borrow_mut();
for child in current.children.iter_mut() {
add_to_all(child, delta);
}
current.value += delta;
}
Другие модели для Rust
Кроме моделей Qwen в сравнение вошли две открытые модели того же размера, которые другие авторы дообучили на Rust. ThoxEdge-RustCoder-0.5B от THOX.ai построена на той же базе, что и Temper, и обучена на открытом датасете Strandset-Rust-v1. rust-mentor-0.6b получена из Qwen3-0.6B через QLoRA на том же датасете и задумана как помощник для изучения Rust и ревью кода. Её LoRA-адаптер перед замером был слит с исходной Qwen3-0.6B. Обе модели проверялись в тех же условиях, что и остальные, без системного промпта и без рассуждений.
Табл. 1. Решено с первой попытки (pass@1). Мелким шрифтом доля задач, решённых хотя бы в одной из 10 попыток (pass@10).
| Temper 0.5B | ThoxEdge- RustCoder 0.5B | rust-mentor 0.6B | Qwen2.5-Coder 1.5B | Qwen3.5 0.8B | Qwen2.5-Coder 0.5B | Qwen2.5-Coder 3Bдля сравнения | DeepSeek-V4 Flashучитель, 284B | |
|---|---|---|---|---|---|---|---|---|
| Тривиальные задачиtrivial¹ | 81,8%pass@10 94,0% | 55,8%pass@10 88,0% | 35,4%pass@10 68,0% | 70,0%pass@10 94,0% | 45,2%pass@10 84,0% | 45,6%pass@10 88,0% | 77,0%pass@10 96,0% | 97,8%pass@10 100,0% |
| Простые задачиeasy¹ | 59,0%pass@10 76,7% | 13,0%pass@10 50,0% | 13,7%pass@10 30,0% | 42,7%pass@10 73,3% | 13,3%pass@10 36,7% | 15,0%pass@10 50,0% | 51,0%pass@10 83,3% | 97,3%pass@10 100,0% |
| Средние задачиmedium¹ | 18,0%pass@10 35,0% | 1,5%pass@10 10,0% | 0,0%pass@10 0,0% | 11,0%pass@10 45,0% | 3,0%pass@10 5,0% | 1,0%pass@10 5,0% | 25,0%pass@10 50,0% | 79,5%pass@10 100,0% |
| Генерация функцийHumanEval-RS | 34,7%pass@10 57,1% | 13,3%pass@10 41,7% | 4,6%pass@10 16,7% | 31,0%pass@10 69,2% | 6,9%pass@10 20,5% | 16,9%pass@10 42,3% | 50,6%pass@10 87,2% | 89,6%pass@10 98,1% |
| Базовые задачиMBPP-RS | 38,8%pass@10 54,8% | 12,8%pass@10 44,4% | 9,7%pass@10 27,1% | 34,4%pass@10 64,7% | 11,6%pass@10 33,3% | 23,4%pass@10 50,8% | 41,3%pass@10 72,9% | 81,2%pass@10 89,8% |
Подсвечен лучший pass@1 среди моделей до 1.5B. Знаком ▲ отмечена 3B там, где она выше всех моделей до 1.5B. Все модели Qwen взяты в версиях Instruct. Замер: temperature 0.8, top_p 0.95, 10 ответов на задачу, pass@k по несмещённой оценке из статьи Codex. ¹ Внутренний набор Rust-задач.
Ограничения и планы
У Temper есть заметные ограничения. Модель лучше всех среди сравнимых по размеру решает задачу с первой попытки, но при десяти попытках её преимущество пропадает. По pass@10 Qwen2.5-Coder-1.5B обходит её на medium, HumanEval-RS и MBPP-RS, и на последних двух разница доходит до 10–12 пунктов. Отчасти это цена обучения с подкреплением, которое сделало ответы модели увереннее и однообразнее. Qwen2.5-Coder-3B остаётся сильнее Temper на средних задачах и на обоих наборах MultiPL-E. Собственные задачи бенчмарка написаны в стиле обучающих данных Temper, поэтому независимую оценку дают только наборы MultiPL-E.
Датасет будет расти, сейчас в нём не хватает задач на веб-фреймворки и асинхронный код. Для автодополнения в редакторе ещё предстоит решить, нужен ли модели режим заполнения середины кода (FIM).