GenieX Documentation

repository·main·Indexed 27 days ago

https://github.com/qualcomm/geniex

An on-device Gen AI inference runtime for Qualcomm Snapdragon devices. GenieX enables local execution of LLMs and VLMs using llama.cpp (for GGUF models) or Qualcomm AI Engine Direct (for optimized NPU bundles). It provides a CLI, a Python SDK with a transformers-like API, and an Android SDK with a 4-layer JNI bridge. Features include an OpenAI-compatible local server, model management via geniex-py, and support for Windows ARM64 and Linux ARM64.

Tokens
49.2K
Snippets
120
Records
272
Agent score
90%

What's inside GenieX

  1. Overview of the GenieX SDK

    main

    The GenieX SDK is a native core providing a unified C ABI (defined in include/geniex.h) for running LLMs and VLMs across multiple inference backends on Qualcomm platforms, including Windows ARM64, Linux ARM64, and Android.

    Backends are loaded as dynamic plugins, allowing builds to link only the specific engines required. The Go CLI, Python, and Android bindings act as thin wrappers around this core library.

  2. Overview of GenieX entry points and runtimes

    main

    GenieX is an on-device Gen AI inference runtime for Qualcomm platforms (Snapdragon X, Snapdragon 8 Elite, and Dragonwing IoT). It provides a single SDK that supports five primary entry points for running LLMs and VLMs:

    • CLI: Run and serve models directly from the terminal.
    • Python: Embed inference into applications using the Python SDK.
    • Java/Kotlin: Android SDK for mobile application integration.
    • Docker: Containerized images for reproducible deployments.
    • OpenAI-compatible server: A local server that acts as a drop-in replacement for existing OpenAI clients.

    The SDK dispatches to one of two underlying runtimes depending on your model and performance needs:

    1. llama.cpp runtime: Uses GGML kernels for CPU, GPU, or Hexagon HTP. This is best for broad model coverage, allowing you to run almost any GGUF model from Hugging Face.
    2. Qualcomm® AI Engine Direct (QAIRT): An NPU-only runtime. This is best for peak performance using pre-compiled models from the Qualcomm AI Hub.
  3. Overview of GenieX Inference Framework

    main

    GenieX is an on-device generative AI inference framework designed for Qualcomm platforms. It provides a unified SDK to run frontier Large Language Models (LLMs) and Vision Language Models (VLMs) locally on Hexagon NPUs, Adreno GPUs, or CPUs. It is the community version of Qualcomm GENIE.

    GenieX supports the following target platforms:

    • Windows ARM64 (Snapdragon X)
    • Android (Snapdragon 8 Elite/Gen series)
    • Linux ARM64 (Snapdragon IoT)

    GenieX offers five primary entry points for developers:

    1. CLI: Run models directly from the terminal or provide model services.
    2. Python: Embed inference into applications via the Python SDK.
    3. Java/Kotlin: Android SDK for mobile application development.
    4. Docker: Containerized images for reproducible deployments.
    5. OpenAI-compatible server: A local server that can be used as a drop-in replacement for existing OpenAI clients.
  4. Choose a GenieX integration method

    main

    GenieX provides several ways to run frontier LLMs and VLMs locally on Qualcomm devices. Choose the method that best matches your development environment:

    • CLI: Best for quick trials. Run models in a Windows ARM64 terminal or via Docker on Linux ARM64.
    • Local Server: Provides an OpenAI-compatible API on Windows ARM64 and Linux ARM64.
    • Python SDK: Hugging Face-style API for scripts and Notebooks. Supports Windows ARM64 and Linux ARM64.
    • Linux (Docker): Linux ARM64 Docker images with NPU access. Ideal for platforms like LeapDragon IoT.
    • Android SDK: Kotlin SDK available via Maven Central, including a pre-compiled demo APK for Snapdragon 8 Elite.
  5. Understand the Android JNI Bridge Architecture

    main

    The Android bindings for GenieX use a 4-layer JNI bridge pattern to connect Kotlin code to the core C library:

    1. Layer 4: Public API (Kotlin) - High-level API (e.g., LlmWrapper.kt, VlmWrapper.kt) using Coroutines.
    2. Layer 3: JNI Interface (Kotlin) - Internal classes (e.g., Llm.kt, Vlm.kt) that declare external native methods.
    3. Layer 2: JNI Bridge (C++) - Implements the JNI methods (e.g., model_bridge_jni.cpp) and converts Kotlin types to C types.
    4. Layer 1: Core Library (C) - The libgeniex.so library providing the ml_* C API (e.g., ml_llm_create).
  6. Install the GenieX Python SDK on Windows ARM64

    main

    To install the GenieX Python SDK on Windows ARM64, ensure you are using an ARM64 version of Python 3.10 or higher. If Python is not installed, download Python 3.13.3 for ARM64.

    1. Verify Architecture: Confirm your Python installation is ARM64 and not AMD64 using:
      python -c "import platform; print(platform.machine())"
    2. Setup Environment: Create and activate a virtual environment, then install the package via pip:
      python -m venv geniex-env
      .\geniex-env\Scripts\Activate.ps1
      pip install -U geniex
    python -c "import platform; print(platform.machine())"
    python -m venv geniex-env
    .\geniex-env\Scripts\Activate.ps1
    pip install -U geniex
  7. Install and Run Prebuilt CI Releases (Windows on Snapdragon)

    main

    To use a prebuilt release on Windows ARM64, download both the installer and the SDK from the GitHub Releases page:

    1. Install the CLI: Run geniex-cli-setup.exe.
    2. Download SDK: Download geniex-sdk-windows-arm64-<tag>.zip.
      • If the filename includes -selfsigned, you must follow the self-signed fallback procedure.
      • If the release includes ggml-htp-v1.cer, it is a self-signed flavor.
    3. Setup Models: Download QAIRT models and pull them locally.
    4. Inference: Run the geniex.exe infer command.
  8. Run a model in a Kotlin Android app

    main

    The standard workflow for running a model is: Initialize SDK → Pull Weights → Load → Generate.

    Below is a minimal end-to-end example using unsloth/Qwen3-0.6B-GGUF.

    // 1. Initialize SDK (call once in Activity.onCreate)
    GenieXSdk.getInstance().init(context)
    
    // 2. Pull Model (run in Dispatchers.IO)
    ModelManagerWrapper.pullFlow(
        ModelPullInput(
            model_name = "unsloth/Qwen3-0.6B-GGUF",
            precision  = "Q4_0",
            hub        = HubSource.HUGGINGFACE,
        )
    ).collect { event ->
        when (event) {
            is ModelManagerWrapper.PullEvent.Progress  -> /* update UI */
            ModelManagerWrapper.PullEvent.Completed    -> /* done */
            is ModelManagerWrapper.PullEvent.Error     -> /* show error */
        }
    }
    
    // 3. Load Model
    val paths = ModelManagerWrapper.getPaths("unsloth/Qwen3-0.6B-GGUF")
        ?: error("Model not downloaded")
    
    val llm = LlmWrapper.builder()
        .llmCreateInput(
            LlmCreateInput(
                model_name = paths.model_name,
                model_path = paths.model_path,
                config     = ModelConfig(nCtx = 4096),
                runtime_id  = "llama_cpp",
                compute_unit  = null,   // null → NPU on Snapdragon (recommended)
            )
        )
        .build()
        .getOrThrow()
    
    // 4. Generate
    val chat = arrayListOf(ChatMessage("user", "What is AI?"))
    val templated = llm.applyChatTemplate(chat.toTypedArray(), null, false).getOrThrow()
    
    llm.generateStreamFlow(
        templated.formattedText,
        GenerationConfig(maxTokens = 2048),
    ).collect { result ->
        when (result) {
            is LlmStreamResult.Token     -> print(result.text)
            is LlmStreamResult.Completed -> println("\nDone")
            is LlmStreamResult.Error     -> println("Error: ${result.throwable}")
        }
    }
  9. Run GenieX Docker Container Interactively

    main

    To start an interactive shell inside the GenieX container with NPU access and local data persistence, use the following command.

    Note: The --privileged flag is mandatory for NPU access. The host's /usr/lib directory must be mounted to /opt/qcom-lib as read-only to provide the necessary driver libraries.

    docker run -it --rm --privileged \
      -v "$PWD/data:/data" \
      -v /usr/lib:/opt/qcom-lib:ro \
      "$IMAGE"
  10. Run LLM inference with GGUF models via llama_cpp

    main

    You can run any GGUF model from Hugging Face using the llama_cpp runtime. Model weights are automatically downloaded on the first use. Use AutoModelForCausalLM.from_pretrained() to load the model and specify the device_map to choose the compute unit.

    Supported device_map values:

    • "auto": Automatically selects NPU for both llama_cpp and qairt.
    • "cpu", "gpu", "npu", "hybrid".
    • "<runtime>:<compute-unit>" (e.g., llama_cpp:gpu).
    from geniex import AutoModelForCausalLM
    
    model = AutoModelForCausalLM.from_pretrained(
        "Qwen/Qwen3-0.6B-GGUF",     # HF repo id of a GGUF model, or a local .gguf path
        device_map="auto",          # "auto" | "cpu" | "gpu" | "npu" | "hybrid"
    )
    
    messages = [{"role": "user", "content": "What is 2+2?"}]
    prompt = model.tokenizer.apply_chat_template(
        messages, add_generation_prompt=True,
    )
    
    # Single generation
    output = model.generate(prompt, max_new_tokens=256)
    print(output.text)
    print(f"[{output.profile.generated_tokens} tok, "
          f"{output.profile.decode_speed:.1f} tok/s, stop={output.profile.stop_reason}]")
    
    # Streaming generation
    streamer = model.generate(prompt, max_new_tokens=256, stream=True)
    for chunk in streamer:
        print(chunk, end="", flush=True)
    
    model.close()
  11. Run geniex-bench in Matrix Mode

    main

    Execute a sweep of multiple (plugin, device, model) combinations in a single process. This allows Hexagon FastRPC sessions and other plugin initialization costs to be amortized across the entire sweep.

    Create a Tab-Separated Values (TSV) file with the following columns: cell_id<TAB>plugin<TAB>device<TAB>model_path[<TAB>tokenizer_path][<TAB>mmproj_path]

    Note: model_path can be a local path or a model-manager ID (e.g., org/repo[:quant]).

    cat > matrix.tsv <<EOF
    # cell_id<TAB>plugin<TAB>device<TAB>model_path_or_id
    Qwen3-0.6B-cpu	llama_cpp	cpu	bartowski/Qwen_Qwen3-0.6B-GGUF:Q4_0
    Qwen3-4B-qairt	qairt	npu	qualcomm/qwen3_4b
    EOF
    
    geniex-bench --matrix-file matrix.tsv --output-json-dir results/ \
      --mm-data-dir ./cache --chipset qualcomm-snapdragon-x-elite
  12. Initialize an LLM using Qualcomm AI Engine Direct (NPU)

    main

    To run an LLM on the NPU using Qualcomm AI Engine Direct, use LlmWrapper.builder() with LlmCreateInput.

    Configuration Requirements:

    • runtime_id must be set to "qairt".
    • compute_unit must be null (this selects the NPU, which is the only option for Qualcomm AI Engine Direct).
    • Important: Do not attempt to set nGpuLayers or nCtx (context length), as these are fixed at compile time in the AI Hub bundle. Setting them will result in a PARAM_NOT_SUPPORTED error. Use max_tokens and enable_thinking within ModelConfig instead.

    To generate text, first call applyChatTemplate to format the chat history, then pass the resulting formattedText to generateStreamFlow.

    val paths = ModelManagerWrapper.getPaths("ai-hub-models/Qwen3-4B-Instruct-2507")
        ?: error("Model not downloaded")
    
    LlmWrapper.builder()
        .llmCreateInput(
            LlmCreateInput(
                model_name = paths.model_name,
                model_path = paths.model_path,
                config     = ModelConfig(max_tokens = 2048, enable_thinking = false),
                runtime_id  = "qairt",
                compute_unit  = null,           // null → NPU (only option for Qualcomm AI Engine Direct)
            )
        )
        .build()
        .onSuccess { llmWrapper = it }
        .onFailure { println("Error: ${it.message}") }
    
    val chat = arrayListOf(ChatMessage("user", "What is AI?"))
    
    llmWrapper.applyChatTemplate(chat.toTypedArray(), null, false).onSuccess { t ->
        llmWrapper.generateStreamFlow(t.formattedText, GenerationConfig()).collect { result ->
            when (result) {
                is LlmStreamResult.Token     -> print(result.text)
                is LlmStreamResult.Completed -> println("\nDone")
                is LlmStreamResult.Error     -> println("Error: ${result.throwable}")
            }
        }
    }