Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmake/deps.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ pybind11;https://github.com/pybind/pybind11/archive/refs/tags/v2.13.6.zip;f78029
googletest;https://github.com/google/googletest/archive/530d5c8c84abd2a46f38583ee817743c9b3a42b4.zip;5e3a61db2aa975cfd0f97ba92c818744e7fa7034
microsoft_wil;https://github.com/microsoft/wil/archive/refs/tags/v1.0.230629.1.zip;e4a542a323c070376f7c2d1973d0f7ddbc1d2fa5
directx_headers;https://github.com/microsoft/DirectX-Headers/archive/refs/tags/v1.613.1.zip;47653509a3371eabb156360f42faf582f314bf2e
onnxruntime_extensions;https://github.com/microsoft/onnxruntime-extensions.git;539d380ce9c2fcdfc9fd9f151ef5604425215aa9
onnxruntime_extensions;https://github.com/microsoft/onnxruntime-extensions.git;e094cc816679d0b2b5fe2b4fd7f73e5b1844b425

# These two dependencies are for the optional constrained decoding feature (USE_GUIDANCE)
llguidance;https://github.com/microsoft/llguidance.git;94fa39128ef184ffeda33845f6d333f332a34b4d
Expand Down
139 changes: 112 additions & 27 deletions examples/python/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
import argparse
import json
import os
from dataclasses import asdict, dataclass
from typing import Any

import onnxruntime_genai as og

from dataclasses import dataclass, asdict
from typing import Any

def set_logger(inputs: bool = True, outputs: bool = True) -> None:
"""
Expand All @@ -21,6 +22,7 @@ def set_logger(inputs: bool = True, outputs: bool = True) -> None:
"""
og.set_log_options(enabled=True, model_input_values=inputs, model_output_values=outputs)


def register_ep(ep: str, ep_path: str, use_winml: bool) -> None:
"""
Register execution provider if path is provided or via Windows ML
Expand All @@ -42,6 +44,7 @@ def register_ep(ep: str, ep_path: str, use_winml: bool) -> None:
# Modified from here: https://learn.microsoft.com/en-us/windows/ai/new-windows-ml/tutorial?tabs=python#acquiring-the-model-and-preprocessing
try:
import winml

print(winml.register_execution_providers(ort=False, ort_genai=True))
except ImportError:
print("WinML not available, using default execution providers")
Expand All @@ -53,11 +56,14 @@ def register_ep(ep: str, ep_path: str, use_winml: bool) -> None:
og.register_execution_provider_library("NvTensorRTRTXExecutionProvider", ep_path)
else:
print(f"Warning: EP registration not supported for {ep}")
print("Only 'cuda' and 'NvTensorRtRtx' support plug-in libraries. Use Windows ML via '--use_winml' to register EPs.")
print(
"Only 'cuda' and 'NvTensorRtRtx' support plug-in libraries. Use Windows ML via '--use_winml' to register EPs."
)
return

print(f"Registered {ep} successfully!")


def get_config(path: str, ep: str, ep_options: dict[str, str] = {}, search_options: dict[str, int] = {}) -> og.Config:
"""
Get og.Config object and set EP-specific and search-specific options inside it
Expand Down Expand Up @@ -98,6 +104,7 @@ def get_config(path: str, ep: str, ep_options: dict[str, str] = {}, search_optio
config.overlay(json.dumps({"search": search_options}))
return config


def get_search_options(args: argparse.Namespace):
"""
Get search options for a generator's params during decoding
Expand Down Expand Up @@ -128,7 +135,10 @@ def get_search_options(args: argparse.Namespace):
search_options["batch_size"] = search_options.get("batch_size", 1)
return search_options

def apply_chat_template(model_path: str, tokenizer: og.Tokenizer, messages: str, add_generation_prompt: bool, tools: str = "") -> str:

def apply_chat_template(
model_path: str, tokenizer: og.Tokenizer, messages: str, add_generation_prompt: bool, tools: str = ""
) -> str:
"""
Apply the chat template with various fallback options

Expand All @@ -151,6 +161,7 @@ def apply_chat_template(model_path: str, tokenizer: og.Tokenizer, messages: str,
)
return prompt


def get_user_prompt(prompt: str, non_interactive: bool) -> str:
"""
Get prompt for 'user' role in chat template
Expand Down Expand Up @@ -179,6 +190,7 @@ def get_user_prompt(prompt: str, non_interactive: bool) -> str:

return text


def get_user_media_paths(media_paths: list[str], non_interactive: bool, media_type: str) -> list[str]:
"""
Get paths to media for user
Expand All @@ -202,7 +214,9 @@ def get_user_media_paths(media_paths: list[str], non_interactive: bool, media_ty
# If interactive mode is on
paths = [
path.strip()
for path in input(f"{media_type.capitalize()} Path (comma separated; leave empty if no {media_type}): ").split(",")
for path in input(
f"{media_type.capitalize()} Path (comma separated; leave empty if no {media_type}): "
).split(",")
]

paths = [path for path in paths if path]
Expand All @@ -213,6 +227,7 @@ def get_user_media_paths(media_paths: list[str], non_interactive: bool, media_ty

return paths


def get_user_images(image_paths: list[str], non_interactive: bool) -> tuple[og.Images, int]:
"""
Get images for user
Expand All @@ -232,6 +247,7 @@ def get_user_images(image_paths: list[str], non_interactive: bool) -> tuple[og.I
images = og.Images.open(*paths)
return images, len(paths)


def get_user_audios(audio_paths: list[str], non_interactive: bool) -> tuple[og.Audios, int]:
"""
Get audios for user
Expand All @@ -251,6 +267,7 @@ def get_user_audios(audio_paths: list[str], non_interactive: bool) -> tuple[og.A
audios = og.Audios.open(*paths)
return audios, len(paths)


def get_user_content(model_type: str, num_images: int, num_audios: int, prompt: str) -> str | list[dict[str, str]]:
"""
Get content for 'user' role in chat template
Expand Down Expand Up @@ -284,49 +301,59 @@ def get_user_content(model_type: str, num_images: int, num_audios: int, prompt:
image_tags = "".join(["[IMG]" for _ in range(num_images)])
content = image_tags + prompt
else:
# Gemma-3 style: structured content
# Gemma-3/4 style: structured content with image and audio entries
image_tags = [{"type": "image"} for _ in range(num_images)]
content = image_tags + [{"type": "text", "text": prompt}]
audio_tags = [{"type": "audio"} for _ in range(num_audios)]
content = image_tags + audio_tags + [{"type": "text", "text": prompt}]
return content


@dataclass
class ToolSchema:
"""
A class for defining a tool in a JSON schema compatible way
"""

description: str
type: str
properties: dict[str, Any]
required: list[str]
additionalProperties: bool


@dataclass
class JsonSchema:
"""
A class for defining a JSON schema for guidance
"""

x_guidance: dict[str, Any]
type: str
items: dict[str, list[ToolSchema]]
minItems: int


@dataclass
class FunctionDefinition:
"""
A class for defining a function in an OpenAI-compatible way
"""

name: str
description: str
parameters: dict[str, Any]


@dataclass
class Tool:
"""
A class for defining a tool in an OpenAI-compatible way
"""

type: str
function: FunctionDefinition


def tools_to_schemas(tools: list[Tool]) -> list[ToolSchema]:
"""
Convert a list of tools to a list of tool schemas
Expand Down Expand Up @@ -360,6 +387,7 @@ def tools_to_schemas(tools: list[Tool]) -> list[ToolSchema]:
tool_schemas.append(tool_schema)
return tool_schemas


def get_json_schema(tools: list[Tool], tool_output: bool) -> str:
"""
Create a JSON schema from a list of tools
Expand All @@ -376,6 +404,7 @@ def get_json_schema(tools: list[Tool], tool_output: bool) -> str:
d = {k.replace("x_guidance", "x-guidance"): v for k, v in asdict(json_schema).items()}
return json.dumps(d)


def get_lark_grammar(
tools: list[Tool],
text_output: bool,
Expand Down Expand Up @@ -423,6 +452,7 @@ def get_lark_grammar(

return "\n".join(rows)


def to_tool(tool_defs: list[dict[str, Any]]) -> list[Tool]:
"""
Convert a JSON-deserialized object of tools to a list of Tool objects
Expand All @@ -443,6 +473,7 @@ def to_tool(tool_defs: list[dict[str, Any]]) -> list[Tool]:
tools.append(tool)
return tools


def get_guidance(
response_format: str = "",
filepath: str = "",
Expand Down Expand Up @@ -474,7 +505,7 @@ def get_guidance(
if tool_output:
if os.path.exists(filepath):
# If tools are provided as a file
with open(filepath, 'r') as f:
with open(filepath) as f:
tool_defs = json.load(f)
tools = to_tool(tool_defs)
elif tools_str != "":
Expand All @@ -488,14 +519,18 @@ def get_guidance(
if type(tools[0]) != Tool:
tools = to_tool(tools)
else:
raise ValueError("Please provide the list of tools through a file, JSON-serialized string, or a list of tools")
raise ValueError(
"Please provide the list of tools through a file, JSON-serialized string, or a list of tools"
)

assert len(tools) > 0, "Could not obtain a list of tools in memory"

# Create guidance based on user-provided response format
if response_format in {"text", "lark_grammar"}:
if response_format == "text":
assert text_output and not tool_output, "A response format of 'text' requires text_output = True and tool_output = False"
assert text_output and not tool_output, (
"A response format of 'text' requires text_output = True and tool_output = False"
)

guidance_type = "lark_grammar"
guidance_data = get_lark_grammar(
Expand All @@ -506,7 +541,9 @@ def get_guidance(
tool_call_end=tool_call_end,
)
elif response_format in {"json_schema", "json_object"}:
assert tool_output and not text_output, "A response format of 'json_schema' or 'json_object' requires text_output = False and tool_output = True"
assert tool_output and not text_output, (
"A response format of 'json_schema' or 'json_object' requires text_output = False and tool_output = True"
)

guidance_type = "json_schema"
guidance_data = get_json_schema(tools=tools, tool_output=tool_output)
Expand All @@ -515,6 +552,7 @@ def get_guidance(

return guidance_type, guidance_data, json.dumps([asdict(tool) for tool in tools])


def get_generator_params_args(parser: argparse.ArgumentParser) -> None:
"""
Add an argument group for the generator params
Expand All @@ -525,16 +563,34 @@ def get_generator_params_args(parser: argparse.ArgumentParser) -> None:
None
"""
generator_params = parser.add_argument_group("Generator Params")
generator_params.add_argument('-c', '--chunk_size', type=int, default=0, help="Chunk size for prefill chunking during context processing (default: 0 = disabled, >0 = enabled)")
generator_params.add_argument('-s', '--do_sample', action='store_true', help='Do random sampling. When false, greedy or beam search are used to generate the output. Defaults to false')
generator_params.add_argument('-i', '--min_length', type=int, help='Min number of tokens to generate including the prompt')
generator_params.add_argument('-l', '--max_length', type=int, help='Max number of tokens to generate including the prompt')
generator_params.add_argument('-b', '--num_beams', type=int, default=1, help='Number of beams to create')
generator_params.add_argument('-rs', '--num_return_sequences', type=int, default=1, help='Number of return sequences to produce')
generator_params.add_argument('-r', '--repetition_penalty', type=float, help='Repetition penalty to sample with')
generator_params.add_argument('-t', '--temperature', type=float, help='Temperature to sample with')
generator_params.add_argument('-k', '--top_k', type=int, help='Top k tokens to sample from')
generator_params.add_argument('-p', '--top_p', type=float, help='Top p probability to sample with')
generator_params.add_argument(
"-c",
"--chunk_size",
type=int,
default=0,
help="Chunk size for prefill chunking during context processing (default: 0 = disabled, >0 = enabled)",
)
generator_params.add_argument(
"-s",
"--do_sample",
action="store_true",
help="Do random sampling. When false, greedy or beam search are used to generate the output. Defaults to false",
)
generator_params.add_argument(
"-i", "--min_length", type=int, help="Min number of tokens to generate including the prompt"
)
generator_params.add_argument(
"-l", "--max_length", type=int, help="Max number of tokens to generate including the prompt"
)
generator_params.add_argument("-b", "--num_beams", type=int, default=1, help="Number of beams to create")
generator_params.add_argument(
"-rs", "--num_return_sequences", type=int, default=1, help="Number of return sequences to produce"
)
generator_params.add_argument("-r", "--repetition_penalty", type=float, help="Repetition penalty to sample with")
generator_params.add_argument("-t", "--temperature", type=float, help="Temperature to sample with")
generator_params.add_argument("-k", "--top_k", type=int, help="Top k tokens to sample from")
generator_params.add_argument("-p", "--top_p", type=float, help="Top p probability to sample with")


def get_guidance_args(parser: argparse.ArgumentParser) -> None:
"""
Expand All @@ -546,9 +602,38 @@ def get_guidance_args(parser: argparse.ArgumentParser) -> None:
None
"""
guidance = parser.add_argument_group("Guidance Arguments")
guidance.add_argument('-rf', '--response_format', type=str, default="", choices=["", "text", "json_object", "json_schema", "lark_grammar"], help='Provide response format for the model')
guidance.add_argument('-tf', '--tools_file', type=str, default="", help='Path to file containing list of OpenAI-compatible tool definitions. Ex: test/test_models/tool-definitions/weather.json')
guidance.add_argument('-text', '--text_output', action='store_true', default=False, help='Produce a text response in the output')
guidance.add_argument('-tool', '--tool_output', action='store_true', default=False, help='Produce a tool call in the output')
guidance.add_argument('-tcs', '--tool_call_start', type=str, default="", help='String representation of tool call start (ex: <|tool_call|>). Needs to be marked as special in tokenizer.json for guidance to work.')
guidance.add_argument('-tce', '--tool_call_end', type=str, default="", help='String representation of tool call end (ex: <|/tool_call|>). Needs to be marked as special in tokenizer.json for guidance to work.')
guidance.add_argument(
"-rf",
"--response_format",
type=str,
default="",
choices=["", "text", "json_object", "json_schema", "lark_grammar"],
help="Provide response format for the model",
)
guidance.add_argument(
"-tf",
"--tools_file",
type=str,
default="",
help="Path to file containing list of OpenAI-compatible tool definitions. Ex: test/test_models/tool-definitions/weather.json",
)
guidance.add_argument(
"-text", "--text_output", action="store_true", default=False, help="Produce a text response in the output"
)
guidance.add_argument(
"-tool", "--tool_output", action="store_true", default=False, help="Produce a tool call in the output"
)
guidance.add_argument(
"-tcs",
"--tool_call_start",
type=str,
default="",
help="String representation of tool call start (ex: <|tool_call|>). Needs to be marked as special in tokenizer.json for guidance to work.",
)
guidance.add_argument(
"-tce",
"--tool_call_end",
type=str,
default="",
help="String representation of tool call end (ex: <|/tool_call|>). Needs to be marked as special in tokenizer.json for guidance to work.",
)
6 changes: 6 additions & 0 deletions src/config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,8 @@ struct VisionInputs_Element : JSON::Element {
void OnValue(std::string_view name, JSON::Value value) override {
if (name == "pixel_values") {
v_.pixel_values = JSON::Get<std::string_view>(value);
} else if (name == "pixel_position_ids") {
v_.pixel_position_ids = JSON::Get<std::string_view>(value);
} else if (name == "image_sizes") {
v_.image_sizes = JSON::Get<std::string_view>(value);
} else if (name == "image_grid_thw") {
Expand Down Expand Up @@ -1096,6 +1098,10 @@ struct Model_Element : JSON::Element {
v_.sep_token_id = static_cast<int>(JSON::Get<double>(value));
} else if (name == "image_token_id") {
v_.image_token_id = static_cast<int>(JSON::Get<double>(value));
} else if (name == "audio_token_id") {
v_.audio_token_id = static_cast<int>(JSON::Get<double>(value));
} else if (name == "boa_token_id") {
v_.boa_token_id = static_cast<int>(JSON::Get<double>(value));
} else if (name == "video_token_id") {
v_.video_token_id = static_cast<int>(JSON::Get<double>(value));
} else if (name == "vision_start_token_id") {
Expand Down
Loading
Loading