ggml هي مكتبة للتعلم الآلي (ML) مكتوبة بلغة C وC++ مع التركيز على استنتاج المحولات. المشروع مفتوح المصدر ويتم تطويره بنشاط من قبل مجتمع متنامٍ. يشبه ggml مكتبات ML مثل PyTorch وTensorFlow، على الرغم من أنه لا يزال في المراحل الأولى من التطوير ولا تزال بعض أساسياته تتغير بسرعة.

مع مرور الوقت، اكتسب ggml شعبية إلى جانب مشاريع أخرى مثل llama.cpp وwhisper.cpp. تستخدم العديد من المشاريع الأخرى أيضًا ggml ضمن الغطاء لتمكين LLM على الجهاز، بما في ذلك ollama وjan وLM Studio وGPT4All.

الأسباب الرئيسية التي تجعل الأشخاص يختارون استخدام ggml على المكتبات الأخرى هي:

  1. تقليلية: المكتبة الأساسية متضمنة في أقل من 5 ملفات. على الرغم من أنك قد ترغب في تضمين ملفات إضافية لدعم وحدة معالجة الرسومات، إلا أن ذلك يعد أمرًا اختياريًا.
  2. تجميع سهل: لا تحتاج إلى أدوات بناء فاخرة. بدون دعم GPU، ما عليك سوى دول مجلس التعاون الخليجي أو Clang!
  3. خفيف الوزن: الحجم الثنائي المترجم أقل من 1 ميغابايت، وهو صغير مقارنة بـ PyTorch (الذي يستغرق عادةً مئات ميغابايت).
  4. التوافق الجيد: يدعم العديد من أنواع الأجهزة، بما في ذلك x86_64 وARM وApple Silicon وCUDA وما إلى ذلك.
  5. دعم للشدرات الكمية: يمكن تكميم الموترات لحفظ الذاكرة (على غرار ضغط JPEG) وفي حالات معينة لتحسين الأداء.
  6. كفاءة الذاكرة للغاية: النفقات العامة لتخزين الموترات وإجراء العمليات الحسابية ضئيلة.

ومع ذلك، يأتي ggml أيضًا مع بعض العيوب التي يجب أن تضعها في الاعتبار عند استخدامه (قد تتغير هذه القائمة في الإصدارات المستقبلية من ggml):

  • ليست كل عمليات الموتر مدعومة على جميع الواجهات الخلفية. على سبيل المثال، قد يعمل بعضها على وحدة المعالجة المركزية (CPU) ولكنها لن تعمل على CUDA.
  • قد لا يكون التطوير باستخدام ggml سهلاً وقد يتطلب معرفة عميقة بالبرمجة ذات المستوى المنخفض.
  • المشروع قيد التطوير النشط، لذا من المتوقع حدوث تغييرات جذرية.

في هذه المقالة، سنركز على أساسيات ggml للمطورين الذين يتطلعون إلى البدء في المكتبة. نحن لا نغطي المهام ذات المستوى الأعلى مثل استدلال LLM باستخدام llama.cpp، الذي يعتمد على ggml. وبدلاً من ذلك، سنستكشف المفاهيم الأساسية والاستخدام الأساسي لـ ggml لتوفير أساس متين لمزيد من التعلم والتطوير.

ابدء

عظيم، فكيف تبدأ؟

للتبسيط، سيوضح لك هذا الدليل كيفية تجميع ggml أوبونتو. في الواقع، يمكنك تجميع ggml على أي نظام أساسي تقريبًا (بما في ذلك Windows وmacOS وBSD).



sudo apt install build-essential cmake git gdb


git clone https://github.com/ggerganov/ggml.git
cd ggml


cmake -B build
cmake --build build --config Release --target simple-ctx


./build/bin/simple-ctx

الناتج المتوقع:

mul mat (4 x 3) (transposed result):
[ 60.00 55.00 50.00 110.00
 90.00 54.00 54.00 126.00
 42.00 29.00 28.00 64.00 ]

إذا رأيت النتيجة المتوقعة، فهذا يعني أننا جاهزون للانطلاق!

المصطلحات والمفاهيم

قبل التعمق في ggml، يجب أن نفهم بعض المفاهيم الأساسية. إذا كنت قادمًا من مكتبات عالية المستوى مثل PyTorch أو TensorFlow، فقد يبدو من الصعب فهمها. ومع ذلك، ضع في اعتبارك أن ggml هو ملف مستوى منخفض مكتبة. إن فهم هذه المصطلحات يمكن أن يمنحك المزيد من التحكم في الأداء:

  • ggml_context: “حاوية” تحتوي على كائنات مثل الموترات والرسوم البيانية والبيانات الاختيارية
  • ggml_cgraph: يمثل رسمًا بيانيًا حسابيًا. فكر في الأمر على أنه “ترتيب الحساب” الذي سيتم نقله إلى الواجهة الخلفية.
  • ggml_backend: يمثل واجهة لتنفيذ الرسوم البيانية الحسابية. هناك العديد من أنواع الواجهات الخلفية: وحدة المعالجة المركزية (افتراضية)، وCUDA، وMetal (Apple Silicon)، وVulkan، وRPC، وما إلى ذلك.
  • ggml_backend_buffer_type: يمثل نوع المخزن المؤقت. فكر في الأمر على أنه “مخصص ذاكرة” متصل بكل منهما ggml_backend. على سبيل المثال، إذا كنت تريد إجراء عمليات حسابية على وحدة معالجة الرسومات، فأنت بحاجة إلى تخصيص الذاكرة على وحدة معالجة الرسومات عبر buffer_type (عادة ما يتم اختصاره كـ buft).
  • ggml_backend_buffer: يمثل المخزن المؤقت المخصص بواسطة buffer_type. تذكر: يمكن للمخزن المؤقت أن يحتفظ ببيانات موترات متعددة.
  • ggml_gallocr: يمثل مخصص ذاكرة الرسم البياني، المستخدم لتخصيص المودمات المستخدمة في الرسم البياني الحسابي بكفاءة.
  • ggml_backend_sched: برنامج جدولة يتيح الاستخدام المتزامن لواجهات خلفية متعددة. يمكنه توزيع العمليات الحسابية عبر أجهزة مختلفة (على سبيل المثال، وحدة معالجة الرسومات ووحدة المعالجة المركزية) عند التعامل مع نماذج كبيرة أو وحدات معالجة رسوميات متعددة. يمكن للمجدول أيضًا تعيين العمليات غير المدعومة لوحدة معالجة الرسومات تلقائيًا إلى وحدة المعالجة المركزية، مما يضمن الاستخدام الأمثل للموارد والتوافق.

مثال بسيط

في هذا المثال، سنستعرض خطوات تكرار التعليمات البرمجية التي قمنا بتشغيلها في “البدء”. نحتاج إلى إنشاء مصفوفتين وضربهما والحصول على النتيجة. باستخدام PyTorch، يبدو الرمز كما يلي:

import torch


matrix1 = torch.tensor([
  [2, 8],
  [5, 1],
  [4, 2],
  [8, 6],
])
matrix2 = torch.tensor([
  [10, 5],
  [9, 9],
  [5, 4],
])


result = torch.matmul(matrix1, matrix2.T)
print(result.T)

مع ggml، يجب القيام بالخطوات التالية لتحقيق نفس النتيجة:

  1. تخصيص ggml_context لتخزين بيانات الموتر
  2. إنشاء الموترات وتعيين البيانات
  3. إنشاء أ ggml_cgraph لعملية mul_mat
  4. تشغيل الحساب
  5. استرجاع النتائج (موترات الإخراج)
  6. الذاكرة الحرة والخروج

ملحوظة: في هذا المثال، سوف نقوم بتخصيص بيانات الموتر داخل ال ggml_context من أجل البساطة. ومن الناحية العملية، يجب تخصيص الذاكرة كمخزن مؤقت للجهاز، كما سنرى في القسم التالي.

للبدء، دعونا نقوم بإنشاء دليل جديد examples/demo

cd ggml 


touch examples/demo/demo.c
touch examples/demo/CMakeLists.txt

يعتمد رمز هذا المثال على simple-ctx.cpp

يحرر examples/demo/demo.c مع المحتوى أدناه:

#include "ggml.h"
#include "ggml-cpu.h"
#include <string.h>
#include <stdio.h>

int main(void) {
    
    const int rows_A = 4, cols_A = 2;
    float matrix_A[rows_A * cols_A] = {
        2, 8,
        5, 1,
        4, 2,
        8, 6
    };
    const int rows_B = 3, cols_B = 2;
    float matrix_B[rows_B * cols_B] = {
        10, 5,
        9, 9,
        5, 4
    };

    
    
    size_t ctx_size = 0;
    ctx_size += rows_A * cols_A * ggml_type_size(GGML_TYPE_F32); 
    ctx_size += rows_B * cols_B * ggml_type_size(GGML_TYPE_F32); 
    ctx_size += rows_A * rows_B * ggml_type_size(GGML_TYPE_F32); 
    ctx_size += 3 * ggml_tensor_overhead(); 
    ctx_size += ggml_graph_overhead(); 
    ctx_size += 1024; 

    
    struct ggml_init_params params = {
         ctx_size,
         NULL,
         false,
    };
    struct ggml_context * ctx = ggml_init(params);

    
    struct ggml_tensor * tensor_a = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, cols_A, rows_A);
    struct ggml_tensor * tensor_b = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, cols_B, rows_B);
    memcpy(tensor_a->data, matrix_A, ggml_nbytes(tensor_a));
    memcpy(tensor_b->data, matrix_B, ggml_nbytes(tensor_b));


    
    struct ggml_cgraph * gf = ggml_new_graph(ctx);

    
    
    
    struct ggml_tensor * result = ggml_mul_mat(ctx, tensor_a, tensor_b);

    
    ggml_build_forward_expand(gf, result);

    
    int n_threads = 1; 
    ggml_graph_compute_with_ctx(ctx, gf, n_threads);

    
    float * result_data = (float *) result->data;
    printf("mul mat (%d x %d) (transposed result):\n[", (int) result->ne[0], (int) result->ne[1]);
    for (int j = 0; j < result->ne[1] ; j++) {
        if (j > 0) {
            printf("\n");
        }

        for (int i = 0; i < result->ne[0] ; i++) {
            printf(" %.2f", result_data[j * result->ne[0] + i]);
        }
    }
    printf(" ]\n");

    
    ggml_free(ctx);
    return 0;
}

اكتب هذه السطور في examples/demo/CMakeLists.txt الملف الذي قمت بإنشائه:

set(TEST_TARGET demo)
add_executable(${TEST_TARGET} demo)
target_link_libraries(${TEST_TARGET} PRIVATE ggml)

يحرر examples/CMakeLists.txt، أضف هذا السطر في النهاية:

add_subdirectory(demo)

تجميعها وتشغيلها:

cmake -B build
cmake --build build --config Release --target demo


./build/bin/demo

النتيجة المتوقعة:

mul mat (4 x 3) (transposed result):
[ 60.00 55.00 50.00 110.00
 90.00 54.00 54.00 126.00
 42.00 29.00 28.00 64.00 ]

مثال مع الخلفية

تشير كلمة “Backend” في ggml إلى واجهة يمكنها التعامل مع عمليات الموتر. الواجهة الخلفية يمكن أن تكون CPU، CUDA، Vulkan، إلخ.

تلخص الواجهة الخلفية تنفيذ الرسوم البيانية الحسابية. بمجرد التحديد، يمكن حساب الرسم البياني باستخدام الأجهزة المتاحة باستخدام تطبيق الواجهة الخلفية المعني. لاحظ أن ggml سيحتفظ تلقائيًا بالذاكرة لأي موترات وسيطة ضرورية للحساب وسيعمل على تحسين استخدام الذاكرة بناءً على عمر هذه النتائج الوسيطة.

عند إجراء عملية حسابية أو استدلال باستخدام الواجهة الخلفية، فإن الخطوات الشائعة التي يجب القيام بها هي:

  1. تهيئة ggml_backend
  2. تخصيص ggml_context لتخزين بيانات تعريف الموتر (نحن لا تحتاج لتخصيص بيانات الموتر على الفور)
  3. إنشاء بيانات تعريف الموترات (فقط الأشكال وأنواع البيانات الخاصة بها)
  4. خصص أ ggml_backend_buffer لتخزين جميع الموترات
  5. انسخ بيانات الموتر من الذاكرة الرئيسية (RAM) إلى المخزن المؤقت الخلفي
  6. إنشاء أ ggml_cgraph لعملية mul_mat
  7. إنشاء أ ggml_gallocr لتخصيص Cgraph
  8. اختياريًا: قم بجدولة الرسم البياني باستخدام ggml_backend_sched
  9. تشغيل الحساب
  10. استرجاع النتائج (موترات الإخراج)
  11. الذاكرة الحرة والخروج

يعتمد رمز هذا المثال على simple-backend.cpp

#include "ggml.h"
#include "ggml-alloc.h"
#include "ggml-backend.h"
#ifdef GGML_USE_CUDA
#include "ggml-cuda.h"
#endif

#include <stdlib.h>
#include <string.h>
#include <stdio.h>

int main(void) {
    
    const int rows_A = 4, cols_A = 2;
    float matrix_A[rows_A * cols_A] = {
        2, 8,
        5, 1,
        4, 2,
        8, 6
    };
    const int rows_B = 3, cols_B = 2;
    float matrix_B[rows_B * cols_B] = {
        10, 5,
        9, 9,
        5, 4
    };

    
    ggml_backend_t backend = NULL;
#ifdef GGML_USE_CUDA
    fprintf(stderr, "%s: using CUDA backend\n", __func__);
    backend = ggml_backend_cuda_init(0); 
    if (!backend) {
        fprintf(stderr, "%s: ggml_backend_cuda_init() failed\n", __func__);
    }
#endif
    
    if (!backend) {
        backend = ggml_backend_cpu_init();
    }

    
    size_t ctx_size = 0;
    ctx_size += 2 * ggml_tensor_overhead(); 
    

    
    struct ggml_init_params params = {
         ctx_size,
         NULL,
         true, 
    };
    struct ggml_context * ctx = ggml_init(params);

    
    struct ggml_tensor * tensor_a = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, cols_A, rows_A);
    struct ggml_tensor * tensor_b = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, cols_B, rows_B);

    
    ggml_backend_buffer_t buffer = ggml_backend_alloc_ctx_tensors(ctx, backend);

    
    ggml_backend_tensor_set(tensor_a, matrix_A, 0, ggml_nbytes(tensor_a));
    ggml_backend_tensor_set(tensor_b, matrix_B, 0, ggml_nbytes(tensor_b));

    
    struct ggml_cgraph * gf = NULL;
    struct ggml_context * ctx_cgraph = NULL;
    {
        
        struct ggml_init_params params0 = {
             ggml_tensor_overhead()*GGML_DEFAULT_GRAPH_SIZE + ggml_graph_overhead(),
             NULL,
             true, 
        };
        ctx_cgraph = ggml_init(params0);
        gf = ggml_new_graph(ctx_cgraph);

        
        
        
        struct ggml_tensor * result0 = ggml_mul_mat(ctx_cgraph, tensor_a, tensor_b);

        
        ggml_build_forward_expand(gf, result0);
    }

    
    ggml_gallocr_t allocr = ggml_gallocr_new(ggml_backend_get_default_buffer_type(backend));
    ggml_gallocr_alloc_graph(allocr, gf);

    

    
    int n_threads = 1; 
    if (ggml_backend_is_cpu(backend)) {
        ggml_backend_cpu_set_n_threads(backend, n_threads);
    }
    ggml_backend_graph_compute(backend, gf);

    
    
    struct ggml_tensor * result = ggml_graph_node(gf, -1);
    float * result_data = malloc(ggml_nbytes(result));
    
    ggml_backend_tensor_get(result, result_data, 0, ggml_nbytes(result));
    printf("mul mat (%d x %d) (transposed result):\n[", (int) result->ne[0], (int) result->ne[1]);
    for (int j = 0; j < result->ne[1] ; j++) {
        if (j > 0) {
            printf("\n");
        }

        for (int i = 0; i < result->ne[0] ; i++) {
            printf(" %.2f", result_data[j * result->ne[0] + i]);
        }
    }
    printf(" ]\n");
    free(result_data);

    
    ggml_free(ctx_cgraph);
    ggml_gallocr_free(allocr);
    ggml_free(ctx);
    ggml_backend_buffer_free(buffer);
    ggml_backend_free(backend);
    return 0;
}

قم بتجميعه وتشغيله، يجب أن تحصل على نفس نتيجة المثال الأخير:

cmake -B build
cmake --build build --config Release --target demo


./build/bin/demo

النتيجة المتوقعة:

mul mat (4 x 3) (transposed result):
[ 60.00 55.00 50.00 110.00
 90.00 54.00 54.00 126.00
 42.00 29.00 28.00 64.00 ]

طباعة الرسم البياني الحسابي

ال ggml_cgraph يمثل الرسم البياني الحسابي، الذي يحدد ترتيب العمليات التي سيتم تنفيذها بواسطة الواجهة الخلفية. يمكن أن تكون طباعة الرسم البياني أداة مفيدة لتصحيح الأخطاء، خاصة عند العمل مع نماذج وحسابات أكثر تعقيدًا.

يمكنك إضافة ggml_graph_print لطباعة الرسم البياني:

...


ggml_build_forward_expand(gf, result0);


ggml_graph_print(gf);

تشغيله:

=== GRAPH ===
n_nodes = 1
 -   0: [     4,     3,     1]          MUL_MAT  
n_leafs = 2
 -   0: [     2,     4]     NONE           leaf_0
 -   1: [     2,     3]     NONE           leaf_1
========================================

بالإضافة إلى ذلك، يمكنك رسم الرسم البياني بتنسيق graphviz dot:

ggml_graph_dump_dot(gf, NULL, "debug.dot");

يمكنك استخدام dot الأمر أو هذا الموقع على الإنترنت لتقديم debug.dot في الصورة النهائية:

خاتمة

قدمت هذه المقالة نظرة عامة تمهيدية عن ggml، تغطي المفاهيم الأساسية، ومثال استخدام بسيط، ومثالًا لاستخدام الواجهة الخلفية. بينما قمنا بتغطية الأساسيات، هناك الكثير لاستكشافه عندما يتعلق الأمر بـ ggml.

في المقالات القادمة، سنتعمق أكثر في الموضوعات الأخرى ذات الصلة بـ ggml، مثل تنسيق GGUF، والتكميم، وكيفية تنظيم الواجهات الخلفية المختلفة واستخدامها. بالإضافة إلى ذلك، يمكنك زيارة دليل أمثلة ggml لرؤية المزيد من حالات الاستخدام المتقدمة ونموذج التعليمات البرمجية. ترقبوا المزيد من محتوى ggml في المستقبل!

شاركها.
اترك تعليقاً