Реальная эффективность Qwen 2.5 Coder против ChatGPT (или можно ли сэкономить 20$?)

в 16:16, , рубрики: 20$, chatgpt, qwen

Являясь активным пользователем ChatGPT, а также прочитав достаточно большое количество публикаций относительно использования Opensource‑LLM, я невольно заинтересовался реальной возможностью сэкономить 20$ в месяц. Тем более, что в описании Qwen 2.5 Coder заманчиво обещается «соответствие возможностям кодирования GPT-4o», а также «сильные и всесторонние способности к кодированию». Да и сама возможность использовать LLM локально действительно привлекает внимание (я отношусь к той самой группе людей, которых Slack забанил невзирая на все VPN).

В то же время внимательный читатель обратит внимание, что упомянутое «соответствие возможностям ChatGPT» касается исключительно Qwen2.5-Coder-32B‑Instruct, модели на 32 миллиарда параметров. Являясь счастливым обладателем core i9–14 900k, 128Gb памяти и RTX 3080 Ti я лично не способен локально использовать 32B‑модель на практике. Даже 14B‑модель в квантизации Q8 уже обрабатывает запросы намного медленнее, чем я сам делаю те же действия руками. Соответственно, сегодня мы попытаемся проверить, на что действительно способен Qwen. Ориентиром будет ChatGPT, которому мы задаём следующий промпт, в котором мы попросим перевести из VB в C# древний код проверки СНИЛС, найденный на просторах сети Интернет:

Переделай из VB в c#:
            function CheckPFNumber(const Value:String):Boolean;
            var I,Summa:Integer;
            begin
              Result:=False;
              if Length(Value)<>11 then
               exit;
              for I:=1 to 11 do
               if not (Value[I] in ['0'..'9']) then
                exit;
              Summa:=0;
              for I:=1 to 9 do
               Inc(Summa,StrToInt(Value[I])*(10-I)); // вычисляем контрольную сумму
              Result:=StrToInt(Copy(Value,10,2))=(Summa mod 101); // проверяем остаток от деления контрольной суммы на 101 с двумя последними числам
            end;

Несмотря на внешнюю тривиальность задачи, в ней есть серьёзный подвох для LLM - вычисление контрольной суммы производится в цикле от 1 до 9, а при переходе на c# нумерация сменится и начнётся с нуля, соответственно, придётся отредактировать формулу. Человек Грамотный программист должен это увидеть и исправить.

Вот вывод ChatGPT:

using System;

class Program
{
    public static bool CheckPFNumber(string value)
    {
        if (value.Length != 11)
            return false;

        if (!long.TryParse(value, out _))
            return false;

        int summa = 0;
        for (int i = 0; i < 9; i++)
        {
            summa += int.Parse(value[i].ToString()) * (10 - i);
        }

        int controlSum = int.Parse(value.Substring(9, 2));
        return controlSum == (summa % 101);
    }
}

Не будем придираться к использованию int.Parse, в конце концов, мы даже не просили оптимизировать код. Важно, что при первом подходе даже ChatGPT не в состоянии увидеть произошедшее искажение логики (10 - i уже не работает) . Но может ли он хотя бы исправить ошибку? Новый промпт:

Проверь соответствие CheckPFNumber следующему алгоритму: Контрольное число Страхового номера рассчитывается следующим образом:
- каждая цифра Страхового номера умножается на номер своей позиции (позиции отсчитываются с конца)
- полученные произведения суммируются
- сумма делится на 101
- последние две цифры остатка от деления являются Контрольным числом.

что мы получаем в выводе:

Алгоритм в функции CheckPFNumber в текущем виде не соответствует описанному алгоритму расчета контрольного числа Страхового номера.

Проблемы:

  1. В текущей реализации каждая цифра умножается на 10 - i, где i — позиция цифры (считая с начала строки). Это не соответствует описанным требованиям, где позиции отсчитываются с конца.

  2. Остаток от деления на 101 действительно используется, но результат напрямую сравнивается с двумя последними цифрами номера. Однако, в описанном алгоритме контрольное число — это именно последние две цифры остатка от деления суммы на 101.

    Исправленная реализация CheckPFNumber:

     static bool CheckPFNumber(string value)
    {
        if (value.Length != 11 || !value.All(char.IsDigit))
            return false;
    
        int summa = 0;
        for (int i = 0; i < 9; i++)
        {
            int digit = value[i] - '0';
            int position = 9 - i; // Позиция цифры с конца
            summa += digit * position;
        }
    
        int remainder = summa % 101;
        int controlSum = (value[9] - '0') * 10 + (value[10] - '0'); // Две последние цифры как число
    
        return remainder == controlSum;
    }
    

Отличия:

  1. Позиции для умножения берутся с конца: position = 9 - i.

  2. Остаток от деления (remainder) используется для сравнения с последними двумя цифрами номера.

  3. Логика полностью соответствует описанному алгоритму.

Итак, ChatGPT нашёл ошибку и поправил её.

Теперь то же самое я попытался проверить на нескольких моделях Qwen - qwen2.5-coder-3b-instruct-q8_0, qwen2.5-coder-14b-instruct-q5_k_m, qwen2.5-coder-14b-instruct-q8_0, qwen2.5-coder-32b-instruct-q4_0, qwen2.5-coder-32b-instruct-q5_k_m. Для чистоты эксперимента также зададим вопрос Qwen2.5-72B-instruct. Она не специализирована для кодирования, но число параметров позволяет ей делать довольно глубокие выводы, и она свободно доступна (https://huggingface.co/spaces/Qwen/Qwen2.5).

Буду краток. Qwen2.5-coder-3b-instruct-q8_0 ожидаемо не годен. 3 миллиарда параметров просто мало. Вот вывод на первый промпт:

public static bool CheckPFNumber(string value)
    {
        if (value.Length != 11) return false;

        foreach (char c in value)
        {
            if (!"0123456789".Contains(c)) return false;
        }

        int summa = 0;
        for (int i = 1; i <= 9; i++)
        {
            summa += Convert.ToInt32(value[i]) * (10 - i);
        }

        return Convert.ToInt32(value[9].ToString() + value[10].ToString()) == (summa % 101);
    }

Вот после уточнения. В некотором смысле, 3B-модель даже ухудшает ситуацию каждый раз, когда ты пытаешься её направить в верном направлении. Мозгов не хватает:

public static bool CheckPFNumber(string value)
{
    if (value.Length != 11) return false;

    foreach (char c in value)
    {
        if (!"0123456789".Contains(c)) return false;
    }

    int summa = 0;
    for (int i = 1; i <= 11; i++) // Pозиции отсчитываются с конца, поэтому начинаем с 1
    {
        summa += Convert.ToInt32(value[11 - i]) * i; // Используем [11 - i] для обратного порядка
    }

    return (summa % 101) == 0;
}

Qwen2.5-coder-14b-instruct-q5_k_m. Уже 14B параметров, и квантизация Q5_k_m позволяет хоть как-то пытаться её реально использовать. Вот вывод:

public bool CheckPFNumber(string value)
{
    if (value.Length != 11) return false;
    for (int i = 0; i < value.Length; i++)
    {
        if (!char.IsDigit(value[i]))
            return false;
    }
    
    int summa = 0;
    for (int i = 0; i < 9; i++)
    {
        summa += (value[i] - '0') * (10 - i); // вычисляем контрольную сумму
    }

    int controlNumber = int.Parse(value.Substring(8, 2));
    return controlNumber == (summa % 101);
}

Приятно, что модель умеет в "value[i] - '0'". Однако формула все равно побита. И попытка найти проблему анализом оригинального алгоритма:

Да, ваш алгоритм полностью соответствует реализации метода CheckPFNumber. Давайте разберем шаги:

  1. Умножение каждой цифры на номер своей позиции (отсчитываются с конца):

    • Для каждого символа от 0 до 8 включительно (i от 0 до 8), он умножается на (10 - i).

  2. Суммирование произведений:

    • Сумма всех произведений сохраняется в переменной summa.

  3. Расчет контрольного числа:

    • Контрольное число вычисляется как остаток от деления summa на 101, а затем берутся последние две цифры этого остатка.

  4. Проверка соответствия:

    • Полученное контрольное число сравнивается с двумя последними символами входной строки value.

Ну, хотя бы не портит. Для очистки совести я попытался использовать ту же модель, но с квантизацией Q8. Понятно, что квантизация влияет на результат заметно меньше, чем число параметров, но вдруг? Не буду загромождать текст, результат был тот же. Текста было чуть больше, но принципиально ничего не изменилось, спишем разницу на рандомность seed.

Модель с 32B параметров я даже не стал пытаться использовать с Q8. На моем железе даже Q5_k_m время между промптом и получением ответа позволяет спокойно пойти покушать, потом погулять, потом увидеть, что ответ ещё формируется. Но вот предсказуемый результат первого запроса:

public static bool CheckPFNumber(string value)
    {
        if (value.Length != 11)
            return false;
        
        for (int i = 0; i < 11; i++)
        {
            if (!char.IsDigit(value[i]))
                return false;
        }

        int summa = 0;
        for (int i = 0; i < 9; i++)
        {
            summa += (value[i] - '0') * (10 - i); // вычисляем контрольную сумму
        }
        
        int checkSum = int.Parse(value.Substring(9, 2)); // последние два символа
        return checkSum == (summa % 101);
    }

А вот второго:

public static bool CheckPFNumber(string value)
    {
        if (value.Length != 11)
            return false;
        
        for (int i = 0; i < 11; i++)
        {
            if (!char.IsDigit(value[i]))
                return false;
        }

        int summa = 0;
        for (int i = 0; i < 9; i++)
        {
            // Умножаем каждую цифру на её позицию, отсчитываемую с конца
            summa += (value[i] - '0') * (9 - i);
        }

        int remainder = summa % 101;
        string controlNumberFromValue = value.Substring(9, 2); // последние две цифры Страхового номера
        string calculatedControlNumber = remainder.ToString("D2"); // формируем контрольное число из остатка

        return controlNumberFromValue == calculatedControlNumber;
    }

Итак 32B параметров действительно справляются с задачей! Qwen 2.5 Coder вычисляет контрольную сумму первых 9 цифр по формуле (value[i] - '0') * (9 - i)! Начало строки наконец-то нашлось, пусть и не с первого промпта, но не хуже, чем ChatGPT.

Однако сэкономить 20$ мы все ещё не можем, модель работает нереально медленно. RTX 5090 мы купим чуть позднее, а пока, может быть, попробовать Q4?

С первым промптом немного лоботомированная qwen2.5-coder-32b-instruct-q4_0 справляется не сильно хуже и заметно быстрее.

public static bool CheckPFNumber(string value)
    {
        if (value.Length != 11)
            return false;

        for (int i = 0; i < 11; i++)
            if (!char.IsDigit(value[i]))
                return false;

        int summa = 0;
        for (int i = 0; i < 9; i++)
            summa += (value[i] - '0') * (10 - i); // вычисляем контрольную сумму

        return Convert.ToInt32(value.Substring(9, 2)) == (summa % 101); // проверяем остаток от деления контрольной суммы на 101 с двумя последними числами
    }

А вот попытка исправить решение...

        public static bool CheckPFNumber(string value)
        {
            if (value.Length != 11)
                return false;

            for (int i = 0; i < 11; i++)
                if (!char.IsDigit(value[i]))
                    return false;

            int summa = 0;
            for (int i = 0; i < 9; i++)
                summa += (value[8 - i] - '0') * (i + 1); // умножаем каждую цифру на номер своей позиции, отсчитываемый с конца

            int controlNumber = summa % 101; // остаток от деления суммы на 101
            string expectedControlNumber = controlNumber.ToString("D2"); // последние две цифры контрольного числа

            return value.Substring(9, 2) == expectedControlNumber; // сравниваем с двумя последними цифрами Страхового номера
        }

Немного неожиданно, да? Но я проверил, summa += (value[8 - i] - '0') * (i + 1) работает, хотя до меня не сразу дошло почему. Честно говоря, для меня Qwen 2.5 coder даже на Q4 все равно катастрофически медленный. Но опускать квантизацию ещё ниже я уже не рискнул. Решения модели даже на Q4 становятся слишком странными. Она ощущается лоботомированной в прямом смысле.

Попробуем подойти к задаче с другой стороны? Qwen 2.5 Coder заточена на большом количестве кода и медленна сама по себе. Huggingface онлайн позволяет её использовать в вариации не более 7B (https://huggingface.co/spaces/Qwen/Qwen2.5). В то же время просто Qwen2.5-72B-instruct вполне доступен онлайн тут же, работает терпимо шустро. Но что могут 72B без специализированного coder натаскивания? Итак, ответ на первый промпт:

    public static bool CheckPFNumber(string value)
    {
        if (value.Length != 11)
        {
            return false;
        }

        for (int i = 0; i < 11; i++)
        {
            if (value[i] < '0' || value[i] > '9')
            {
                return false;
            }
        }

        int summa = 0;
        for (int i = 0; i < 9; i++)
        {
            summa += (value[i] - '0') * (10 - i); // Вычисляем контрольную сумму
        }

        int lastTwoDigits = int.Parse(value.Substring(9, 2));
        return lastTwoDigits == (summa % 101); // Проверяем остаток от деления контрольной суммы на 101 с двумя последними числами
    }

Что мы видим? Неспециализированная модель не умеет в isDigit, что в данном случае даже плюс. Уж если наводить красоту в коде, то value.All(char.IsDigit) вам в помощь. А так и читаемо, и не громоздко. Но базовая ошибка логики на месте. Ответ на второй промпт:

public static bool CheckPFNumber(string value)
    {
        if (value.Length != 11)
        {
            return false;
        }

        for (int i = 0; i < 11; i++)
        {
            if (value[i] < '0' || value[i] > '9')
            {
                return false;
            }
        }

        int summa = 0;
        for (int i = 0; i < 11; i++)
        {
            summa += (value[i] - '0') * (11 - i); // Умножаем каждую цифру на её позицию, отсчитывая с конца
        }

        int remainder = summa % 101;
        int lastTwoDigits = remainder < 10 ? remainder : remainder % 100;

        int expectedLastTwoDigits = int.Parse(value.Substring(9, 2));
        return expectedLastTwoDigits == lastTwoDigits; // Проверяем, совпадают ли последние две цифры остатка с контрольным числом
    }
}

Увы. Не-Coder 72B делает такую же ошибку, как и Coder 3B. Недостаток обучающего материала даёт себя знать.

Перейдём к выводам?

Итак, даже потратив довольно существенные денежные средства на вполне актуальную на январь 2025 года рабочую станцию, сэкономить двадцатку на ChatGPT нам пока не светит. По крайней мере путём использования локального AI-ассистента.

Ждём RTX 5090? Новый Ryzen у меня уже есть, попробуем дадут ли что-нибудь 32 Гб памяти на ускорителе.

Автор: pizhonkov

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js