Actualización del Workflow #19
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI - MiniML Embedded Pipeline | |
| on: | |
| push: | |
| branches: [ "main", "master", "develop" ] | |
| pull_request: | |
| branches: [ "main", "master" ] | |
| workflow_dispatch: | |
| inputs: | |
| target_arch: | |
| description: 'Arquitectura objetivo (arm/xtensa)' | |
| required: false | |
| default: 'arm' | |
| test_quantization: | |
| description: 'Probar cuantificación (true/false)' | |
| required: false | |
| default: 'true' | |
| jobs: | |
| # ------------------------------------------------------------------ | |
| # JOB 1: Python Logic & Unit Tests | |
| tests: | |
| name: 🐍 Core Tests | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup Python | |
| uses: actions/setup-python@v4 | |
| with: | |
| python-version: '3.10' | |
| cache: 'pip' | |
| - name: Install MiniML & Deps | |
| run: | | |
| pip install --upgrade pip | |
| pip install pytest | |
| pip install -e . | |
| - name: Verify Package Installation | |
| run: | | |
| python -c "import miniml; import estimators; import adapters; print('✓ All packages imported successfully')" | |
| - name: Run Tests | |
| run: | | |
| pytest -v --junitxml=test-report.xml tests/ | |
| - name: Upload Test Results | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: test-results | |
| path: test-report.xml | |
| # ------------------------------------------------------------------ | |
| # JOB 2: Embedded ML Training & C Code Generation | |
| build-firmware: | |
| name: ⚙️ Build Firmware (${{ inputs.target_arch || 'arm' }}) | |
| needs: tests | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup Toolchains (ARM + AVR) | |
| id: toolchain | |
| run: | | |
| ARCH=${{ inputs.target_arch || 'arm' }} | |
| echo "Setting up toolchains for $ARCH and AVR..." | |
| # Instalar toolchain ARM | |
| if [ "$ARCH" == "arm" ] || [ "$ARCH" == "all" ]; then | |
| sudo apt-get update && sudo apt-get install -y gcc-arm-none-eabi | |
| echo "compiler_arm=arm-none-eabi-gcc" >> $GITHUB_ENV | |
| echo "cflags_arm=-mcpu=cortex-m4 -mthumb -mfloat-abi=hard -mfpu=fpv4-sp-d16 -Wall -Werror" >> $GITHUB_ENV | |
| echo "ARM_TOOLCHAIN=installed" >> $GITHUB_ENV | |
| fi | |
| # Instalar toolchain AVR (para demostrar código Arduino) | |
| sudo apt-get update && sudo apt-get install -y gcc-avr binutils-avr avr-libc | |
| echo "compiler_avr=avr-gcc" >> $GITHUB_ENV | |
| echo "cflags_avr=-mmcu=atmega328p -Wall -Werror -Os" >> $GITHUB_ENV | |
| echo "AVR_TOOLCHAIN=installed" >> $GITHUB_ENV | |
| # Toolchain genérico para validación de sintaxis | |
| echo "compiler_gcc=gcc" >> $GITHUB_ENV | |
| echo "cflags_gcc=-Wall -Werror" >> $GITHUB_ENV | |
| if [ "$ARCH" == "xtensa" ]; then | |
| echo "Assuming Xtensa toolchain is pre-installed on self-hosted runner." | |
| echo "compiler_xtensa=xtensa-esp32-elf-gcc" >> $GITHUB_ENV | |
| echo "cflags_xtensa=-Wall -Werror" >> $GITHUB_ENV | |
| fi | |
| - name: Install MiniML | |
| uses: actions/setup-python@v4 | |
| with: | |
| python-version: '3.10' | |
| - run: pip install . | |
| - name: Generate Embedded ML Models (Realistic Scenarios) | |
| run: | | |
| mkdir -p build/generated | |
| cat <<'PYEOF' > generate_embedded_models.py | |
| """ | |
| Genera modelos ML realistas para casos de uso embebidos: | |
| 1. Neural Network con cuantificación (XOR - caso clásico) | |
| 2. Decision Tree (clasificación simple) | |
| 3. Linear Model (regresión para sensores) | |
| """ | |
| import miniml | |
| import json | |
| import os | |
| # ============================================ | |
| # CASO 1: Neural Network con Cuantificación | |
| # ============================================ | |
| # Dataset XOR: Caso clásico para validar NN en embebido | |
| xor_dataset = [ | |
| [0.0, 0.0, 0], | |
| [0.0, 1.0, 1], | |
| [1.0, 0.0, 1], | |
| [1.0, 1.0, 0] | |
| ] | |
| print("[1/3] Entrenando Neural Network (XOR) con cuantificación...") | |
| nn_result = miniml.train_pipeline( | |
| model_name="nn_xor_embedded", | |
| dataset=xor_dataset, | |
| model_type="neural_network", | |
| params={ | |
| "n_inputs": 2, | |
| "n_hidden": 4, | |
| "n_outputs": 1, | |
| "epochs": 2000, | |
| "learning_rate": 0.1, | |
| "seed": 42 | |
| }, | |
| scaling="minmax" # Escalado para normalizar inputs | |
| ) | |
| # Validar que act_scales se calcularon (esencial para cuantificación) | |
| model_nn = nn_result['model'] | |
| if not hasattr(model_nn, 'act_scales') or not model_nn.act_scales: | |
| raise RuntimeError("act_scales no se calcularon - cuantificación fallará") | |
| print(f" ✓ act_scales: {model_nn.act_scales}") | |
| # Asegurar que el modelo esté cuantificado antes de exportar | |
| if not model_nn.quantized: | |
| print(" → Cuantificando modelo antes de exportar...") | |
| model_nn.quantize() | |
| # Exportar código C cuantificado (intentará CMSISAdapter primero) | |
| nn_code = miniml.export_to_c("nn_xor_embedded") | |
| # Detectar tipo de código generado (verificación estricta) | |
| is_cmsis = ("CMSIS" in nn_code or "arm_fully_connected_s8" in nn_code or | |
| "predict_int8" in nn_code) and "avr/pgmspace.h" not in nn_code | |
| is_avr = "avr/pgmspace.h" in nn_code or ("PROGMEM" in nn_code and "CMSIS" not in nn_code) | |
| if is_cmsis: | |
| print(" ✓ Código generado con CMSISAdapter (compatible ARM Cortex-M)") | |
| with open("build/generated/nn_xor_arm.h", "w") as f: | |
| f.write(nn_code) | |
| print(" ✓ Guardado como: nn_xor_arm.h (ARM)") | |
| elif is_avr: | |
| print(" ✓ Código generado para AVR/Arduino (PROGMEM)") | |
| with open("build/generated/nn_xor_avr.h", "w") as f: | |
| f.write(nn_code) | |
| print(" ✓ Guardado como: nn_xor_avr.h (AVR)") | |
| else: | |
| print(" ⚠️ Tipo de código no determinado claramente") | |
| # Guardar también con nombre genérico | |
| with open("build/generated/nn_xor_quantized.h", "w") as f: | |
| f.write(nn_code) | |
| print(" ✓ Código C generado: nn_xor_quantized.h") | |
| # ============================================ | |
| # CASO 2: Decision Tree (Clasificación) | |
| # ============================================ | |
| # Dataset simple para clasificación binaria (ej: detección de anomalías) | |
| tree_dataset = [ | |
| [0.1, 0.2, 0], | |
| [0.3, 0.4, 0], | |
| [0.7, 0.8, 1], | |
| [0.9, 0.95, 1] | |
| ] | |
| print("[2/3] Entrenando Decision Tree...") | |
| miniml.train_pipeline( | |
| model_name="dt_classifier_embedded", | |
| dataset=tree_dataset, | |
| model_type="DecisionTreeClassifier", | |
| params={"max_depth": 3}, | |
| scaling="minmax" | |
| ) | |
| dt_code = miniml.export_to_c("dt_classifier_embedded") | |
| with open("build/generated/dt_classifier.h", "w") as f: | |
| f.write(dt_code) | |
| print(" ✓ Código C generado: dt_classifier.h") | |
| # ============================================ | |
| # CASO 3: Linear Regression (Sensor Data) | |
| # ============================================ | |
| # Dataset de regresión (ej: temperatura vs voltaje) | |
| linear_dataset = [ | |
| [0.0, 20.0], | |
| [1.0, 22.5], | |
| [2.0, 25.0], | |
| [3.0, 27.5], | |
| [4.0, 30.0] | |
| ] | |
| print("[3/3] Entrenando Linear Model (regresión)...") | |
| miniml.train_pipeline( | |
| model_name="linear_sensor_embedded", | |
| dataset=linear_dataset, | |
| model_type="linear_regression", | |
| params={"learning_rate": 0.01, "epochs": 100}, | |
| scaling="standard" | |
| ) | |
| linear_code = miniml.export_to_c("linear_sensor_embedded") | |
| with open("build/generated/linear_sensor.h", "w") as f: | |
| f.write(linear_code) | |
| print(" ✓ Código C generado: linear_sensor.h") | |
| # ============================================ | |
| # Validación: Verificar características embebidas | |
| # ============================================ | |
| print("\n[VALIDACIÓN] Verificando características embebidas...") | |
| # Verificar que el código NN tiene cuantificación | |
| with open("build/generated/nn_xor_quantized.h", "r") as f: | |
| nn_content = f.read() | |
| checks = { | |
| "int8_t": "Pesos cuantificados a int8" in nn_content or "int8_t" in nn_content, | |
| "PROGMEM": "PROGMEM" in nn_content or "const" in nn_content, | |
| "CMSIS": "CMSIS" in nn_content or "arm_" in nn_content or "predict_int8" in nn_content, | |
| "Scaler": "preprocess" in nn_content.lower() or "scaler" in nn_content.lower() | |
| } | |
| for check, passed in checks.items(): | |
| status = "✓" if passed else "✗" | |
| print(f" {status} {check}: {'OK' if passed else 'FALTA'}") | |
| if not all(checks.values()): | |
| print(" ⚠️ Algunas características embebidas no se detectaron") | |
| print("\n[COMPLETADO] Todos los modelos generados exitosamente") | |
| PYEOF | |
| python generate_embedded_models.py | |
| - name: Compile C Artifacts (ARM + AVR) | |
| run: | | |
| echo "==========================================" | |
| echo "🔨 COMPILANDO MODELOS PARA ARM Y AVR" | |
| echo "==========================================" | |
| # Función helper para compilar un modelo | |
| compile_model() { | |
| local model_file=$1 | |
| local compiler=$2 | |
| local cflags=$3 | |
| local target_name=$4 | |
| if [ ! -f "build/generated/$model_file" ]; then | |
| echo "⚠️ $model_file no encontrado" | |
| return 1 | |
| fi | |
| echo "" | |
| echo "📦 Compilando $model_file para $target_name..." | |
| # Crear wrapper .c | |
| # El código generado es código C completo, no un header, así que lo copiamos directamente | |
| temp_c="build/generated/${model_file%.h}_${target_name}_temp.c" | |
| # Verificar si el archivo ya tiene includes para evitar duplicados | |
| has_stdint=$(grep -q "^#include <stdint.h>" "build/generated/$model_file" && echo "yes" || echo "no") | |
| has_math=$(grep -q "^#include <math.h>" "build/generated/$model_file" && echo "yes" || echo "no") | |
| has_avr=$(grep -q "^#include <avr/pgmspace.h>" "build/generated/$model_file" && echo "yes" || echo "no") | |
| { | |
| echo "// Wrapper para compilación de ${model_file} para ${target_name}" | |
| echo "" | |
| # Agregar includes solo si no están ya presentes | |
| if [ "$has_stdint" == "no" ]; then | |
| echo "#include <stdint.h>" | |
| fi | |
| if [ "$has_math" == "no" ]; then | |
| echo "#include <math.h>" | |
| fi | |
| # Para AVR, asegurar que __AVR__ esté definido y avr/pgmspace.h incluido | |
| if [ "$target_name" == "AVR (Arduino)" ]; then | |
| if [ "$has_avr" == "no" ]; then | |
| echo "#define __AVR__" | |
| echo "#include <avr/pgmspace.h>" | |
| fi | |
| fi | |
| echo "" | |
| echo "// ========================================" | |
| echo "// Código del modelo generado:" | |
| echo "// ========================================" | |
| # Leer el contenido del archivo .h y copiarlo (no incluirlo) | |
| # Filtrar líneas vacías al final que puedan causar problemas | |
| cat "build/generated/$model_file" | sed '/^$/d' | sed -e :a -e '/^\n*$/d;N;ba' | |
| echo "" | |
| echo "// ========================================" | |
| echo "// Función dummy para evitar linker errors" | |
| echo "// ========================================" | |
| echo "int main() { return 0; }" | |
| } > "$temp_c" | |
| # Validar sintaxis básica: contar llaves abiertas y cerradas | |
| open_braces=$(grep -o '{' "$temp_c" | wc -l) | |
| close_braces=$(grep -o '}' "$temp_c" | wc -l) | |
| if [ "$open_braces" -ne "$close_braces" ]; then | |
| echo " ⚠️ Advertencia: Desbalance de llaves (abiertas: $open_braces, cerradas: $close_braces)" | |
| echo " → Intentando compilar de todas formas..." | |
| fi | |
| # Validar que el archivo temporal se creó correctamente | |
| if [ ! -f "$temp_c" ]; then | |
| echo " ❌ No se pudo crear el archivo temporal $temp_c" | |
| return 1 | |
| fi | |
| # Mostrar primeras líneas del archivo para debugging (solo si falla) | |
| if ! $compiler $cflags -c "$temp_c" -o "${temp_c%.c}.o" 2>&1; then | |
| echo " ❌ Error compilando $model_file para $target_name" | |
| echo " 📄 Primeras 20 líneas del código generado:" | |
| head -n 20 "$temp_c" | sed 's/^/ /' | |
| echo " 📄 Últimas 10 líneas del código generado:" | |
| tail -n 10 "$temp_c" | sed 's/^/ /' | |
| return 1 | |
| else | |
| echo " ✅ $model_file compilado exitosamente para $target_name" | |
| return 0 | |
| fi | |
| } | |
| # ========================================== | |
| # COMPILAR PARA ARM CORTEX-M | |
| # ========================================== | |
| if [ "$ARM_TOOLCHAIN" == "installed" ]; then | |
| echo "" | |
| echo "🎯 === COMPILACIÓN ARM CORTEX-M4 ===" | |
| # Función para verificar si un archivo es código ARM (no AVR) | |
| is_arm_code() { | |
| local file=$1 | |
| if [ ! -f "build/generated/$file" ]; then | |
| return 1 | |
| fi | |
| # Si tiene avr/pgmspace.h sin protección, NO es ARM | |
| if grep -q "^#include <avr/pgmspace.h>" "build/generated/$file"; then | |
| return 1 | |
| fi | |
| # Si tiene CMSIS o predict_int8, SÍ es ARM | |
| if grep -q "CMSIS\|arm_fully_connected_s8\|predict_int8" "build/generated/$file"; then | |
| return 0 | |
| fi | |
| # Si NO tiene avr/pgmspace.h, puede ser ARM | |
| if ! grep -q "avr/pgmspace.h\|PROGMEM" "build/generated/$file"; then | |
| return 0 | |
| fi | |
| return 1 | |
| } | |
| # Intentar compilar código ARM (CMSIS) si existe y es realmente ARM | |
| if [ -f "build/generated/nn_xor_arm.h" ]; then | |
| if is_arm_code "nn_xor_arm.h"; then | |
| compile_model "nn_xor_arm.h" "$compiler_arm" "$cflags_arm" "ARM Cortex-M4" | |
| else | |
| echo " ⚠️ nn_xor_arm.h contiene código AVR, saltando compilación ARM" | |
| fi | |
| fi | |
| # Compilar otros modelos que no sean AVR específicos | |
| for model in "dt_classifier.h" "linear_sensor.h"; do | |
| if [ -f "build/generated/$model" ]; then | |
| # Solo compilar si NO es código AVR específico | |
| if is_arm_code "$model"; then | |
| compile_model "$model" "$compiler_arm" "$cflags_arm" "ARM Cortex-M4" | |
| else | |
| echo " ⚠️ $model es código AVR específico, saltando compilación ARM" | |
| fi | |
| fi | |
| done | |
| fi | |
| # ========================================== | |
| # COMPILAR PARA AVR (ARDUINO) | |
| # ========================================== | |
| if [ "$AVR_TOOLCHAIN" == "installed" ]; then | |
| echo "" | |
| echo "🎯 === COMPILACIÓN AVR (ARDUINO UNO) ===" | |
| # Compilar código AVR si existe | |
| if [ -f "build/generated/nn_xor_avr.h" ]; then | |
| compile_model "nn_xor_avr.h" "$compiler_avr" "$cflags_avr" "AVR (Arduino)" | |
| fi | |
| # También intentar compilar el genérico si es AVR | |
| if [ -f "build/generated/nn_xor_quantized.h" ]; then | |
| if grep -q "avr/pgmspace.h\|PROGMEM" "build/generated/nn_xor_quantized.h"; then | |
| compile_model "nn_xor_quantized.h" "$compiler_avr" "$cflags_avr" "AVR (Arduino)" | |
| fi | |
| fi | |
| # Compilar otros modelos que sean compatibles con AVR | |
| for model in "dt_classifier.h" "linear_sensor.h"; do | |
| if [ -f "build/generated/$model" ]; then | |
| # Crear versión AVR-friendly (definir __AVR__) | |
| compile_model "$model" "$compiler_avr" "$cflags_avr" "AVR (Arduino)" | |
| fi | |
| done | |
| fi | |
| echo "" | |
| echo "==========================================" | |
| echo "✅ COMPILACIÓN COMPLETADA" | |
| echo "==========================================" | |
| echo "MiniML demuestra funcionamiento en:" | |
| [ "$ARM_TOOLCHAIN" == "installed" ] && echo " ✓ ARM Cortex-M4" | |
| [ "$AVR_TOOLCHAIN" == "installed" ] && echo " ✓ AVR (Arduino Uno/Nano)" | |
| echo "==========================================" | |
| - name: Validate Embedded Features | |
| run: | | |
| echo "Validando características específicas de embebido..." | |
| # Verificar que los archivos generados tienen características embebidas | |
| if [ -f "build/generated/nn_xor_quantized.h" ]; then | |
| echo "Verificando nn_xor_quantized.h..." | |
| # Verificar cuantificación | |
| if grep -q "int8_t" build/generated/nn_xor_quantized.h; then | |
| echo " ✓ Contiene cuantificación int8" | |
| else | |
| echo " ✗ No se detectó cuantificación int8" | |
| exit 1 | |
| fi | |
| # Verificar que no usa float64 o double (solo float32) | |
| if grep -q "double\|float64" build/generated/nn_xor_quantized.h; then | |
| echo " ⚠️ Advertencia: Usa double/float64 (no ideal para embebido)" | |
| fi | |
| # Verificar tamaño razonable (no debería ser enorme) | |
| size=$(wc -c < build/generated/nn_xor_quantized.h) | |
| if [ $size -lt 100000 ]; then | |
| echo " ✓ Tamaño razonable: ${size} bytes" | |
| else | |
| echo " ⚠️ Tamaño grande: ${size} bytes" | |
| fi | |
| fi | |
| - name: Upload Binary Artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: firmware-build-${{ inputs.target_arch || 'arm' }} | |
| path: build/generated/ | |
| retention-days: 7 | |
| #----------------------------------------------------------------- |