Compiling LLMs Artifacts
This guide describes how to generate QPC (Qualcomm Program Container) artifacts for LLMs using Qualcomm’s Cloud AI100 SDK and how to compile them into binaries for the NR1 device or simulator.
🛠️ Step 1: Create QPC
Generate a QPC for the required model using the Qualcomm Cloud AI100 SDK.
📚 References:
🔧 Create the QPC Entrypoint
Save the following as create_qpc.py:
import os
import argparse
from transformers import AutoTokenizer
from transformers.models.llama.modeling_llama import LlamaForCausalLM
import QEfficient
from QEfficient.utils import hf_download
from QEfficient.utils.constants import Constants
from QEfficient import QEFFAutoModelForCausalLM
from QEfficient.utils import load_hf_tokenizer
CACHE = "/workspace/models/"
os.environ["TRANSFORMERS_CACHE"] = CACHE
# Parse input arguments
parser = argparse.ArgumentParser(
description="Script for creating QPC (Qualcomm Program Container)"
)
parser.add_argument(
"--model",
type=str,
required=True,
help="Full name of the model, e.g. meta-llama/Meta-Llama-3.1-70B",
)
parser.add_argument("--pl", type=int, required=True, help="Prompt length")
parser.add_argument("--cl", type=int, required=True, help="Context length")
parser.add_argument("--tp", type=int, required=True, help="Tensor parallelism")
parser.add_argument(
"--cores_per_soc", type=int, required=True, help="Number of cores per SOC"
)
parser.add_argument("--bs", type=int, help="Static batch size")
parser.add_argument(
"--fbs", type=int, help="Full batch size"
) # this is for continous batching
parser.add_argument(
"--force_fp16_output",
action="store_true",
help="Force the specified output tensor (e.g., logits) to float16 precision in the custom IO configuration.",
)
parser.add_argument(
"--fp16_output_name",
type=str,
default="logits",
help="Name of the output tensor to set to float16 precision, if --force_fp16_output is enabled.",
)
args = parser.parse_args()
MODEL_NAME = args.model
model_name_without_slash = MODEL_NAME.replace("/", "_")
FULL_BATCH_SIZE = args.fbs
STATIC_BATCH_SIZE = args.bs
FORCE_FP16_OUTPUT = args.force_fp16_output
FP16_OUTPUT_NAME = args.fp16_output_name
if not FULL_BATCH_SIZE and not STATIC_BATCH_SIZE:
print("must enter either fbs or bs")
exit(1)
PROMPT_LENGTH = args.pl
CONTEXT_LENGTH = args.cl
TENSOR_PARALLELISM = args.tp
GENERATED_LENGTH = CONTEXT_LENGTH - PROMPT_LENGTH
# NUM_OF_CORES_PER_SOC: for each card, not in total # You can use `/opt/qti-aic/tools/qaic-util | grep "Nsp Total"` from Apps SDK for this.
NUM_OF_CORES_PER_SOC = args.cores_per_soc
print(f"Model name: {MODEL_NAME}")
print(f"Full batch size: {FULL_BATCH_SIZE}")
print(f"Static batch size: {STATIC_BATCH_SIZE}")
print(f"Prompt length: {PROMPT_LENGTH}")
print(f"Context length: {CONTEXT_LENGTH}")
print(f"Tensor parallelism: {TENSOR_PARALLELISM}")
print(f"Generated length: {GENERATED_LENGTH}")
print(f"Number of cores per soc: {NUM_OF_CORES_PER_SOC}")
LOGITS_FP16 = ""
if FORCE_FP16_OUTPUT:
print(f"Will force output tensor '{FP16_OUTPUT_NAME}' to float16 precision.")
LOGITS_FP16 = "-logits_fp16"
ROOT_DIR = os.path.dirname(os.path.abspath(""))
BASE_MODEL_PATH = f"/data/work/hf_models/{model_name_without_slash}/"
DEVICE_GROUP = list(range(TENSOR_PARALLELISM))
print("DEVICE_GROUP:", DEVICE_GROUP)
FBS_MODE = f"{STATIC_BATCH_SIZE}bs" if STATIC_BATCH_SIZE else f"{FULL_BATCH_SIZE}fbs"
base_path = f"/qualcomm_compiled_models/{model_name_without_slash}/{model_name_without_slash}-{FBS_MODE}-{PROMPT_LENGTH}pl-{GENERATED_LENGTH}gl-{CONTEXT_LENGTH}cl-{TENSOR_PARALLELISM}tp{LOGITS_FP16}-{NUM_OF_CORES_PER_SOC}cores-per-soc-app/models"
os.makedirs(base_path, exist_ok=True)
print("Saving model to:", base_path)
model_hf_path = BASE_MODEL_PATH
model_exist_flag = os.path.exists(model_hf_path)
if not model_exist_flag:
print("Model not found on device, downloading from huggingface")
model_hf_path = MODEL_NAME
print("Model Path:", os.path.exists(BASE_MODEL_PATH))
print("BASE_MODEL_PATH:", BASE_MODEL_PATH)
model_hf = QEFFAutoModelForCausalLM.from_pretrained(
model_hf_path, local_files_only=model_exist_flag, use_cache=True
)
print(f"########### {MODEL_NAME} from hugging-face \n", model_hf)
# Easy and minimal api to update the model
from QEfficient.exporter.export_hf_to_cloud_ai_100 import qualcomm_efficient_converter
model_transformed = QEfficient.transform(model_hf, form_factor="cloud")
print("############ Model after Optimized transformations \n", model_transformed)
tokenizer = load_hf_tokenizer(
BASE_MODEL_PATH if model_exist_flag else MODEL_NAME, use_cache=True
)
base_path, onnx_path = qualcomm_efficient_converter(
model_kv=model_transformed,
model_name=MODEL_NAME,
kv=True,
tokenizer=tokenizer,
onnx_dir_path=base_path,
form_factor="cloud",
full_batch_size=FULL_BATCH_SIZE,
seq_length=PROMPT_LENGTH,
# return_path=True
)
if FORCE_FP16_OUTPUT:
def append_output_precision_yaml(base_path, output_name):
file_list = ["custom_io_fp16.yaml", "custom_io_int8.yaml"]
for file in file_list:
io_file = os.path.join(base_path, file)
print(f"Appending '{output_name}' with float16 precision to {io_file}")
with open(io_file, "a") as f:
f.write(f" - IOName: {output_name}\n Precision: float16\n\n")
append_output_precision_yaml(base_path, FP16_OUTPUT_NAME)
else:
print("Not forcing any specific output tensor to float16.")
# Please use platform SDK to Check num_cores for your card.
from QEfficient import compile
generated_qpc_path = compile(
onnx_path=onnx_path,
num_cores=NUM_OF_CORES_PER_SOC,
qpc_path=os.path.dirname(base_path),
mxfp6=True,
mxint8=True,
aic_enable_depth_first=True,
device_group=DEVICE_GROUP,
full_batch_size=FULL_BATCH_SIZE,
prompt_len=PROMPT_LENGTH,
ctx_len=CONTEXT_LENGTH,
)
print("generated_qpc_path: ", generated_qpc_path)
🐳 Docker Setup
Save the following as Dockerfile:
FROM ubuntu:22.04
ARG QEFFICIENT_TRANSFORMERS_VERSION="v1.19"
# check https://quic.github.io/cloud-ai-sdk-pages/latest/Getting-Started/Installation/Pre-requisites/pre-requisites/#ubuntu-on-x86-64
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
python3.10 \
python3-pip \
python3-dev \
wget \
unzip \
git \
sudo \
locales \
build-essential \
libpci-dev \
libudev-dev \
libatomic1 \
pciutils \
libglib2.0-dev \
libssl-dev \
pkg-config \
libpng-dev \
libstdc++6 \
libncurses5-dev \
libtinfo5 \
&& \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# required for pyproject.toml of QEfficientTransformers
RUN pip install -U pip
# from https://quic.github.io/cloud-ai-sdk-pages/latest/Getting-Started/Installation/Pre-requisites/pre-requisites/#addupdate-environment-variables
ENV LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/opt/qti-aic/dev/lib/x86_64"
ENV PATH="/usr/local/bin:${PATH}"
ENV PATH="${PATH}:/opt/qti-aic/tools:/opt/qti-aic/exec:/opt/qti-aic/scripts"
ENV QAIC_EXAMPLES="/opt/qti-aic/examples"
ENV QAIC_APPS="/opt/qti-aic/examples/apps"
ENV QAIC_LIB="/opt/qti-aic/dev/lib/x86_64/libQAic.so"
ENV QAIC_COMPILER_LIB="/opt/qti-aic/dev/lib/x86_64/libQAicCompiler.so"
WORKDIR /qaic
RUN git clone https://github.com/quic/efficient-transformers.git
WORKDIR "efficient-transformers"
RUN git fetch origin release/${QEFFICIENT_TRANSFORMERS_VERSION}
RUN git checkout release/${QEFFICIENT_TRANSFORMERS_VERSION}
RUN pip install .
WORKDIR /workspace
COPY create_qpc.py create_qpc.py
# do not buffer output, print them immediately
ENV PYTHONUNBUFFERED=1
ENTRYPOINT ["python3", "create_qpc.py"]
🧱 Build Docker Image
⚠️ Important: Run this command on your host machine (not inside a Docker container). Building Docker images inside another container can lead to unexpected volume or environment issues, especially with nested file systems.
# Navigate to the directory with your Dockerfile
export DOCKER_IMG_NAME=qpc_builder # change if desired
docker build -t $DOCKER_IMG_NAME .
🚀 Run Docker Container to Create QPC
export TENSOR_PARALLELISM=1
export FULL_BATCH_SIZE=16
export CONTEXT_LENGTH=256
export PROMPT_LENGTH=128
export WHERE_TO_SAVE_QPCS=/qpcs # ensure this path exists, create if needed
export HF_TOKEN=<your_huggingface_token>
export MODEL_NAME=TinyLlama/TinyLlama-1.1B-Chat-v1.0
export CORES_PER_SOC=16
docker run -v $WHERE_TO_SAVE_QPCS:/qualcomm_compiled_models -v /opt:/opt -e HF_TOKEN=$HF_TOKEN $DOCKER_IMG_NAME \
--model $MODEL_NAME \
--fbs $FULL_BATCH_SIZE \
--cl $CONTEXT_LENGTH \
--tp $TENSOR_PARALLELISM \
--pl $PROMPT_LENGTH \
--cores_per_soc $CORES_PER_SOC
Your QPC will be saved under the directory specified by $WHERE_TO_SAVE_QPCS.
Within it, you’ll find subdirectories based on the model name, configuration (e.g., batch size, prompt length), and other parameters.
The actual QPC file is named programqpc.bin.
🧩 Step 2: Generate NR1 Deployment Binary
🏗️ Build and Compile with app.py
Create app.py:
from neureality.integrations.vllm import VLLModelCall
import numpy as np
import os
from neureality.common.nr_defines import NR_CONFIG_MODE_DEVICE, NR_CONFIG_MODE_SIM_TURBO_ON
from neureality import Tensor
import argparse
import json
from huggingface_hub import login
def nr_build_and_simulate(
qpc_dir: str,
batch_size: int,
qpc_input_length: int,
input_length: int,
output_length: int,
nof_activations: int,
max_tokens: int,
model_name_or_path: str,
cg_path_device: str,
cg_path_simulator: str,
compile_to_nr1: bool = True,
without_push_docker_image: bool = True,
):
PROMPT_BUFFER_SIZE = 10 * input_length
OUTPUT_BUFFER_SIZE = 10 * output_length
SAMPLING_PARAMS_BUFFER_SIZE = 2048
num_shards = int(
os.getenv("TENSOR_PARALLELISM")
) # it should be the same as the TENSOR_PARALLELISM you compiled your qpc with
# for tracing phase
trace_prompt = np.zeros((1, PROMPT_BUFFER_SIZE), np.uint8)
trace_sampling_params = np.zeros((1, SAMPLING_PARAMS_BUFFER_SIZE), np.uint8)
tracing_sample = [Tensor(trace_prompt), Tensor(trace_sampling_params)]
hf_token = os.getenv("HF_TOKEN")
if hf_token is not None:
login(hf_token)
if compile_to_nr1:
vllm_app_device = VLLModelCall(
qpc_path=f"{qpc_dir}/programqpc.bin",
max_output_size_b=OUTPUT_BUFFER_SIZE,
model_name_or_path=model_name_or_path,
num_activations=nof_activations,
num_model_shards=num_shards,
max_num_seqs=batch_size,
max_model_len=input_length + output_length, # context length (input length + output length)
max_seq_len_to_capture=qpc_input_length,
use_argmax_sampler=True, # set to True to use an argmax sampler instead of the default vLLM sampler
)
vllm_app_device.build(cg_path_device, input_sample=tracing_sample, compilation_mode=NR_CONFIG_MODE_DEVICE, push_docker_image=not without_push_docker_image)
print("Artifacts for NR1 are saved at:", cg_path_device)
else:
vllm_app_simulator = VLLModelCall(
qpc_path=f"{qpc_dir}/programqpc.bin",
max_output_size_b=OUTPUT_BUFFER_SIZE,
model_name_or_path=model_name_or_path,
# num_activations & num_model_shards is always 1 - even for multi or sharded instances, the simulation can be run as single instance (since we might not have enough cards to simulate on).
num_activations=1,
num_model_shards=1,
max_num_seqs=batch_size,
max_model_len=input_length + output_length, # context length (input length + output length)
max_seq_len_to_capture=qpc_input_length,
use_argmax_sampler=True, # for having vllm sampler of argmax, if what is needed is a regular sampler, just remove it
)
# Compile
sim_exec = vllm_app_simulator.build(
cg_path_simulator, input_sample=tracing_sample, compilation_mode=NR_CONFIG_MODE_SIM_TURBO_ON, push_docker_image=not without_push_docker_image
)
print("Artifacts for simulator are saved at:", cg_path_simulator)
# Run - for running we nead real inputs (not as for tracing) for example:
# initialize inputs
prompt = "My name is"
sampling_params = {
"temperature": 0.0, # make the model deterministic
"ignore_eos": 1,
"max_tokens": max_tokens, # usually should be as the output_length, however you can define it <= output_length if you want the asnwer to be shorter for some reason
}
# Notice: use_beam_search=True is not supported since sampling_type == SamplingType.BEAM is not supported by qualcomm.
params = json.dumps(sampling_params) # Convert the dictionary to a JSON string
prompt_np = np.frombuffer(prompt.encode("utf-8"), dtype=np.int8).reshape(1, -1)
params_np = np.frombuffer(params.encode("utf-8"), dtype=np.int8).reshape(1, -1)
# call the simulator
sim_outputs = sim_exec([prompt_np, params_np])
def main():
parser = argparse.ArgumentParser()
parser.add_argument(
"--qpc_dir",
type=str,
help="directory where the programqpc.bin is saved",
default=os.getenv("QPC_PARENT_DIR_PATH"),
)
parser.add_argument(
"--batch_size",
type=int,
help="batch size",
default=64,
)
parser.add_argument(
"--qpc_input_length",
type=int,
help="the prompt length the qpc was built with",
default=128,
)
parser.add_argument(
"--input_length",
type=int,
help="the actual input length of the prompt (in tokens)",
default=1920,
)
parser.add_argument(
"--output_length",
type=int,
help="the output length we get from the LLM (in tokens)",
default=128,
)
parser.add_argument(
"--nof_activations",
type=int,
help="number of instacnes",
default=1,
)
parser.add_argument(
"--max_tokens",
type=int,
help="max number of tokens to generate (can be <= output_length)",
default=128,
)
parser.add_argument(
"--model_name_or_path", # read in the documnetation regarding this parameter
type=str,
help="either a name of a Hugging Face model such as meta-llama/Meta-Llama-3-8B or a path to a directory containing model/tokenizer configuration files",
default="meta-llama/Meta-Llama-3-8B",
)
parser.add_argument(
"--path_for_saving_artifacs_for_device",
type=str,
help="where to save nr1 artifacts",
default="/neureality/sdk/toolchain/llama3_8b_device",
)
parser.add_argument(
"--path_for_saving_artifacs_for_simualtor",
type=str,
help="where to save simualtor artifacts",
default="/neureality/sdk/toolchain/llama3_8b_simulator",
)
parser.add_argument("--compile_to_nr1", action="store_true", help="compile to nr1 or to simulator")
parser.add_argument("--without_push_docker_image", action="store_true", help="not pushing the Docker image to the registry")
args = parser.parse_args()
qpc_dir = args.qpc_dir
batch_size = args.batch_size
qpc_input_length = args.qpc_input_length
input_length = args.input_length
output_length = args.output_length
nof_activations = args.nof_activations
max_tokens = args.max_tokens
model_name_or_path = args.model_name_or_path
path_for_saving_artifacs_for_device = args.path_for_saving_artifacs_for_device
path_for_saving_artifacs_for_simualtor = args.path_for_saving_artifacs_for_simualtor
compile_to_nr1 = args.compile_to_nr1
without_push_docker_image=args.without_push_docker_image
nr_build_and_simulate(
qpc_dir=qpc_dir,
batch_size=batch_size,
qpc_input_length=qpc_input_length,
input_length=input_length,
output_length=output_length,
nof_activations=nof_activations,
max_tokens=max_tokens,
model_name_or_path=model_name_or_path,
cg_path_device=path_for_saving_artifacs_for_device,
cg_path_simulator=path_for_saving_artifacs_for_simualtor,
compile_to_nr1=compile_to_nr1,
without_push_docker_image=without_push_docker_image
)
if __name__ == "__main__":
main()
🏃 Run Script
To compile the model to NR1:
Attach to the deployment toolchain image provided by the Neureality SDK. See here (the running part is under
Running the Toolchain Containersection): https://developer.neureality.ai/docs/_static/toolchain-api/setup.html
Notice to the next step before running the toolchain docker
If your QPC was created outside the container (as described in the section above), ensure you mount the directory containing the QPC into the toolchain container using the
-vflag when starting the container.Copy the
app.pyscript (provided above) into the any path you want inside the container, for example, under/neureality/sdk/toolchain.Run the following command inside the container to compile the model to NR1.
Please ensure that the following environment variables are defined before running:
TENSOR_PARALLELISM– the tensor parallelism value used when generating the QPC.HF_TOKEN– your Hugging Face access token (if needed for model loading).QPC_PARENT_DIR_PATH– the path to the generated QPC; this can be passed either via environment variable or using the--qpc_dirflag.Make sure to pass the appropriate arguments that match the QPC you are using, including batch size, input length, output length, model name etc.
By default, the script push the runtime docker image to the container registry automatically. If you do not want to push the image after compilation, you can disable this behavior using the
--without_push_docker_imageflag.If you’d like to retrieve and use the Docker image name manually (e.g., to push it to a different registry), you can use the following helper function:
📄
get_docker_runtime_image_from_cg()(click to expand)import os import json def get_docker_runtime_image_from_cg(cg_path: str) -> str: descriptor_path = os.path.join(cg_path, "cg_descriptor.json") print(f"Looking for descriptor at: {descriptor_path}") if not os.path.exists(descriptor_path): print(f"cg_descriptor.json not found in {cg_path}. Cannot extract Docker image.") return print("Attempting to load descriptor data...") try: with open(descriptor_path, "r") as f: descriptor_data = json.load(f) print("Descriptor data loaded.") functions = descriptor_data.get("functions", []) dcr = None for function in functions: for impl in function.get("implementations", []): binary_path = impl.get("binary_path") if binary_path and binary_path.startswith("dcr."): dcr = binary_path break if dcr: break if not dcr: print("Could not extract DCR (Docker image tag) from descriptor.") return print(f"Found DCR: {dcr}") return dcr except Exception as e: print(f"Error while reading descriptor: {e}")
✅ Usage example:
dcr = get_docker_runtime_image_from_cg("/path/to/cg_output_dir") print(f"Docker image name: {dcr}")
ℹ️ Replace
"/path/to/cg_output_dir"with the actual path used in your compilation.
This is typically the value passed to either:--path_for_saving_artifacs_for_device(for NR1 compilation), or--path_for_saving_artifacs_for_simulator(for simulator compilation).
export APP_PATH=app.py # change to the path where you put the app.py inside your contianer
python3 $APP_PATH --compile_to_nr1
If default paths are used, NR1 artifacts will be saved under:
/neureality/sdk/toolchain/llama3_8b_device
Export path for later usage:
export LLM_ARTIFACTS=/neureality/sdk/toolchain/llama3_8b_device
Available Flags
Flag |
Type |
Description |
|---|---|---|
|
|
Directory of |
|
|
Batch size (default: |
|
|
Prompt length used when QPC was built (default: |
|
|
Actual input prompt length (tokens) (default: |
|
|
Output length from LLM (tokens) (default: |
|
|
Number of instances (default: |
|
|
Maximum tokens to generate (≤ output_length) (default: |
|
|
HF model name or path (default: |
|
|
Path to save NR1 artifacts (default: |
|
|
Path to save simulator artifacts (default: |
|
|
Add this flag to compile to NR1 |
|
|
Add this flag to prevent pushing the docker images to the registry |
Note on parameter names (for clarity):
batch_sizerefers toFULL_BATCH_SIZEused during QPC creation.
qpc_input_lengthrefers to thePROMPT_LENGTHused during QPC creation.
input_lengthis the actual input length (in tokens) you want to run inference on.
It can be larger thanqpc_input_length, in which case the prefill phase will automatically run in a loop behind the scenes.
output_lengthcorresponds to theCONTEXT_LENGTHfrom the QPC creation step.
📌 For example, based on the QPC we created earlier:
batch_size = 16 # same as FULL_BATCH_SIZE
qpc_input_length = 128 # same as PROMPT_LENGTH
input_length = 128 # actual number of tokens used in prompt
output_length = 256 # same as CONTEXT_LENGTH
🧪 Optional: Compile to Simulator
python3 $APP_PATH
⚠️ Notice About model_name_or_path
There are two options for providing this parameter:
1. Hugging Face Model
model_name_or_path = "meta-llama/Meta-Llama-3-8B"
import os
os.environ["HF_TOKEN"] = "<your_hf_token>"
2. Local Path
Download and cache the model locally using Qualcomm Efficient Transformers:
touch load_model.py
Inside load_model.py:
model_name = "meta-llama/Meta-Llama-3-8B"
# Optional: export your HF token
# import os
# os.environ["HF_TOKEN"] = "<your_token>"
from QEfficient.utils import load_hf_tokenizer
tokenizer = load_hf_tokenizer(model_name, use_cache=True)
Run:
python3 /neureality/sdk/ai-pipelines/efficient-transformers/load_model.py
Locate your model snapshot:
/root/.cache/huggingface/hub/models--meta-llama--Meta-Llama-3-8B/snapshots/<hash>
Then use:
model_name_or_path = "/root/.cache/huggingface/hub/models--meta-llama--Meta-Llama-3-8B/snapshots/<hash>"
☁️ Upload to Local SSD or S3 (NR1 CG Store)
Artifacts compiled for NR1 must be uploaded to local SSD or an S3-compatible bucket (CG store).
You can upload using:
neureality.api.cg_store.upload_artifacts_to_s3
Example (from ResNet50 demo):
python3 pipelines/demos/resnet50/deploy/model_as_qpc/upload_to_cg_store.py \
--build_path $LLM_ARTIFACTS \
--bucket cg-store-dev \
--bucket_obj_key example/key
This README provides a full lifecycle from QPC generation to deployment for Qualcomm-based LLM acceleration on Neureality NR1.