Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
db18ffe
Comment out example sys.argv configurations
Atomic-Germ May 5, 2026
7757023
swap shebang for env
Atomic-Germ Jul 6, 2026
bbd219c
fix: correctly quantize linear-layer weights and embeddings for full-…
uppiy47g Jul 9, 2026
8364eed
Merge remote-tracking branch 'Atomic-Germ/dont-debug-in-production' i…
Atomic-Germ Aug 5, 2026
e2473d8
Merge remote-tracking branch 'Atomic-Germ/patch-1' into HEAD
Atomic-Germ Aug 5, 2026
4c2cdfa
stop needlessly downloading llama.cpp
Atomic-Germ Aug 5, 2026
f6170d8
New setup instructions
Atomic-Germ Aug 5, 2026
9041873
multiple fixes
Atomic-Germ Aug 5, 2026
0edba7b
fixes conversions and adds missing files
Atomic-Germ Aug 6, 2026
6493a9f
add --deploy to register converted models with flm
Atomic-Germ Aug 6, 2026
a972703
Adds support for qwen35moe type
Atomic-Germ Aug 6, 2026
3cddf70
fix qwythos 9b
Atomic-Germ Aug 7, 2026
42e6748
Nanbeige4.2 support
Atomic-Germ Aug 7, 2026
f85b59b
adds quick installer script
Atomic-Germ Aug 7, 2026
a467a75
better family matching
Atomic-Germ Aug 7, 2026
bf90dfb
easy install script
Atomic-Germ Aug 9, 2026
d8d659f
improves ease-of-use and ux, adds tools for xclbin inspection
Atomic-Germ Aug 11, 2026
e4a6471
fixes for Ornith
Atomic-Germ Aug 11, 2026
d7f1ad5
generate q4nx readme from source model card
Atomic-Germ Aug 11, 2026
30c4671
detects model type from metadata or heuristic if no flag present
Atomic-Germ Aug 11, 2026
8d63ce3
ready for end-user use
Atomic-Germ Aug 12, 2026
ee1d513
Merge branch 'main' into qwen36moe
Atomic-Germ Aug 12, 2026
264d4bf
Merge pull request #3 from Atomic-Germ/qwen36moe
Atomic-Germ Aug 12, 2026
83619f4
Gguf family map (#5)
Atomic-Germ Aug 14, 2026
57144da
float passthrough on f32 broken quant for q4_0
Atomic-Germ Aug 14, 2026
ed93473
Update analyze_xclbins.py
Atomic-Germ Aug 17, 2026
f6ac5e7
Update axlf.py
Atomic-Germ Aug 17, 2026
e064205
Update xclbin_catalog.py
Atomic-Germ Aug 17, 2026
696b0a3
Update analyze_xclbins.py
Atomic-Germ Aug 17, 2026
28033cb
Update xclbin_catalog.py
Atomic-Germ Aug 17, 2026
2d97274
Cleanup (#8)
Atomic-Germ Aug 17, 2026
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Byte-compiled / optimized / DLL files
gguf_files/
q4nx_files/
log/
*.ipynb
*.bin
*.gguf
Expand Down
19 changes: 10 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# FLM Q4NX Converter

A utility for converting GGUF model files into the Q4NX format. This tool supports converting language, vision, and audio model weights.
A utility for converting GGUF model files, usually fine-tunes, into the Q4NX format. This tool supports converting language, vision, and audio model weights.

## Supported Models
Based on the configuration, the converter supports several model architectures, including:
Expand All @@ -14,25 +14,26 @@ Based on the configuration, the converter supports several model architectures,
- Qwen 3
- Qwen 3 VL
- Qwen 3.5
- Qwen 3.5 MoE
- Qwen 3.6 MoE

### Weight Type Support
- `language`: supported for model families in `configs/`
- `vision`: supported for vision-capable architectures (for example, Gemma 4 and Qwen3-VL)
- `audio`: currently supported for **Gemma 4** (`-t audio`)
- `audio`: currently supported for **Gemma 4** variants (`-t audio`)

## Setup

The project includes a setup script that automatically creates a Python virtual environment, installs required dependencies (like `torch`, `amd-quark`, `transformers`), and sets up the `gguf` package from `llama.cpp`.
Create a virtual environment using python 3.13

1. Make the setup script executable (if it isn't already) and run it:
1. Create and activate a virtual environment using pip or uv:
```bash
chmod +x setup_venv.sh
./setup_venv.sh
uv venv venv
venv/bin/activate
```

2. Activate the environment using the provided activation script. **Note:** Always use `activate.sh` instead of directly sourcing the venv, as it also sets up necessary environment variables like `HF_HOME` and CUDA paths.
2. Install dependencies (one time)
```bash
source activate.sh
uv pip install -r requirements.txt
```

## Usage
Expand Down
19 changes: 0 additions & 19 deletions activate.sh

This file was deleted.

3 changes: 2 additions & 1 deletion configs/qwen2vl.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
},
"vision_config":{
"vision_MM_K": 256,
"vision_MM_N": 64
"vision_MM_N": 64,
"vision_file": "vision_weights.q4nx"
},
"default_tensor_type": "Q4_1",
"name_map": {
Expand Down
15 changes: 15 additions & 0 deletions configs/qwen35moe.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"q4nx_config": {
"row_block_size": 32,
"col_block_size": 256,
"parallel_size": 16,
"keep_block_in_2D": true
},
"default_tensor_type": "Q4_1",
"name_map": {},
"addr_qk": 9216,
"addr_kv": 36864,
"addr_kk": 45056,
"addr_l_begin_mha": 53760,
"addr_l_end_mha": 25088
}
174 changes: 115 additions & 59 deletions convert.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,138 @@
#!./venv/bin/python3
#!/usr/bin/env python3
import argparse
import os
from pathlib import Path
from q4nx import create_converter
from q4nx import create_converter, create_hf_converter
from q4nx.model_assets import assemble_model_assets, assemble_model_assets_hf, get_default_flm_version, find_repo_gguf


def convert_gguf_to_q4nx(gguf_path: str, q4nx_path: str, override_model_arch:str, weights_type: str = 'language'):
model = create_converter(gguf_path, override_model_arch)
model.convert(q4nx_path=q4nx_path, weights_type=weights_type)
def is_hf_repo_id(path: str) -> bool:
"""HF hub repo id like 'org/name' (not a local path, not a .gguf)."""
if not path or path.endswith(".gguf") or os.path.exists(path):
return False
if path.startswith(("http://", "https://", "file:")):
return False
# org/name with no filesystem separators beyond the single slash
parts = path.split("/")
return len(parts) == 2 and all(parts) and "\\" not in path


def is_hf_repo_id(path: str) -> bool:
"""HF hub repo id like 'org/name' (not a local path, not a .gguf)."""
if not path or path.endswith(".gguf") or os.path.exists(path):
return False
if path.startswith(("http://", "https://", "file:")):
return False
# org/name with no filesystem separators beyond the single slash
parts = path.split("/")
return len(parts) == 2 and all(parts) and "\\" not in path


def is_hf_source(path: str) -> bool:
if os.path.isdir(path):
return (
os.path.exists(os.path.join(path, "model.safetensors"))
or os.path.exists(os.path.join(path, "model.safetensors.index.json"))
)
return is_hf_repo_id(path)


def convert_gguf_to_q4nx(gguf_path: str, q4nx_path: str, override_model_arch:str, weights_type: str = 'language', source_model: str = None, flm_version: str = None, deploy_tag: str = None, deploy_from: str = None, deploy_name: str = None):
if flm_version is None:
flm_version = get_default_flm_version()
# The weight source is always -i: an HF dir / repo id (Darwin-style HF
# path) or a GGUF. --source-model only supplies tokenizer/config assets and
# is never treated as a weight source, so it can't trigger a weights
# download when converting from GGUF.
#
# New: -i <hf-repo-id> prefers a quantized GGUF shipped in the repo itself,
# chosen in a family-preferred order (default q4_1, then q4_0, then q8_0;
# e.g. LFM prefers q4_0 first, gpt-oss also accepts mxfp4 last). The order
# is driven by -f when given, otherwise by a best-effort repo-id/filename
# match. The chosen GGUF is downloaded via the HF cache; if the repo has
# none, we fall back to the HF-safetensors source path below.
hf_input = None
source_file = None
if is_hf_repo_id(gguf_path):
repo_id = gguf_path
found = find_repo_gguf(repo_id, override_model_arch)
if found is not None:
gguf_path, source_file = found
source_model = source_model or repo_id
else:
hf_input = repo_id
elif is_hf_source(gguf_path):
hf_input = gguf_path

if hf_input is not None:
model = create_hf_converter(hf_input, override_model_arch)
model.convert(q4nx_path=q4nx_path, weights_type=weights_type)
assemble_model_assets_hf(
model.hf_source,
model.q4nx_config,
q4nx_path,
source_model=source_model or hf_input,
flm_version=flm_version,
source_file=source_file,
)
else:
model = create_converter(gguf_path, override_model_arch)
model.convert(q4nx_path=q4nx_path, weights_type=weights_type)
assemble_model_assets(
model.gguf_reader,
model.q4nx_config,
q4nx_path,
source_model=source_model,
flm_version=flm_version,
source_file=source_file,
)
if deploy_tag:
from q4nx.deploy import deploy_model
deploy_model(
q4nx_path,
deploy_tag,
model.model_arch,
model_dir_name=deploy_name,
deploy_from=deploy_from,
)
return model


def main():
parser = argparse.ArgumentParser(
description='Convert GGUF model files to Q4NX format (output always named model.q4nx)',
description='Convert GGUF or HF-safetensors model files to Q4NX format (output always named model.q4nx). '
'-i also accepts an HF repo id: the repo is searched for a quantized GGUF, chosen in a '
'family-preferred order (default q4_1 / q4_0 / q8_0; e.g. LFM prefers q4_0 first). Pass -f to '
'force the family and override the auto-detected source GGUF.',
epilog='Examples:\n'
' python convert.py -i model.gguf\n'
' python convert.py -i model.gguf -o output_folder\n'
' python convert.py model.gguf output_folder\n'
' python convert.py model.gguf .\n'
' python convert.py -i Qwen/Qwen3.5-9B -o output_folder (HF repo: picks a q4_1/q4_0/q8_0 GGUF from it)\n'
' python convert.py -i LiquidAI/LFM2-1.2B -o out -f lfm2 (force LFM family -> prefers q4_0 source)\n'
' python convert.py -i /path/to/hf_model_dir -o output_folder (HF safetensors source, e.g. Darwin-36B-Opus)\n'
' python convert.py -i vision_model.gguf -o output_folder -t vision',
formatter_class=argparse.RawDescriptionHelpFormatter
)

# Add support for both flag-based and positional arguments
parser.add_argument('input_file', nargs='?', help='Input GGUF file (positional)')
parser.add_argument('output_folder', nargs='?', help='Output folder (positional, optional)')
parser.add_argument('-i', '--input', dest='input_flag', help='Input GGUF file')
parser.add_argument('-i', '--input', dest='input_flag', help='Input GGUF file, or an HF repo id (a quantized GGUF is auto-selected in family-preferred order; use -f to force the family)')
parser.add_argument('-o', '--output', dest='output_flag', help='Output folder (optional, defaults to input file directory)')
parser.add_argument('-t', '--type', dest='weights_type', default='language', help='Type of weights to convert (default: language)',
choices=['language', 'vision', 'audio'])
parser.add_argument('-f', '--force', dest='force_model_type', default="", help="Model type. Empty string for automatic recognition from gguf file")
parser.add_argument('-s', '--source-model', dest='source_model', default=None,
help="Source HF/ModelScope model for tokenizer/config assets. A local dir, an HF cache repo name, or a repo id like 'Qwen/Qwen3.5-9B'. If omitted, the GGUF's provenance metadata is followed (local HF cache first, then SDK download).")
parser.add_argument('--flm-version', dest='flm_version', default=None,
help="flm_version to write into the generated config.json (default: detected from `flm --version`)" )
parser.add_argument('-d', '--deploy', dest='deploy_tag', default=None, metavar='NAME:SIZE',
help="Deploy the converted model into flm's models directory and register it under this tag (e.g. 'qwen3.5-claude:9b'). Uses a user-level model_list.json (point FLM_CONFIG_PATH at it to make `flm run` see the tag).")
parser.add_argument('--deploy-from', dest='deploy_from', default=None, metavar='SOURCE_TAG',
help="Official registry entry to copy defaults from (e.g. 'qwen3.5:9b'). Auto-detected from the model architecture if omitted.")
parser.add_argument('--deploy-name', dest='deploy_name', default=None, metavar='DIR',
help="Directory name inside flm's models dir (default: derived from the deploy tag, e.g. Qwen3.5-Claude-9B-NPU2).")

args = parser.parse_args()

Expand All @@ -42,69 +145,22 @@ def main():
# Determine output folder (prioritize flag, then positional)
output_folder = args.output_flag or args.output_folder

# Check if input file exists
if not os.path.exists(input_path):
# Local paths must exist; HF repo ids are resolved later by the converter.
if not is_hf_repo_id(input_path) and not os.path.exists(input_path):
parser.error(f'Input file does not exist: {input_path}')

# Create output directory if it doesn't exist
output_dir = os.path.dirname(output_folder)
if output_dir and not os.path.exists(output_dir):
os.makedirs(output_dir, exist_ok=True)

print(f"[INFO] Converting {input_path} to {output_folder}...")
convert_gguf_to_q4nx(input_path, output_folder, args.force_model_type, weights_type=args.weights_type)
convert_gguf_to_q4nx(input_path, output_folder, args.force_model_type, weights_type=args.weights_type, source_model=args.source_model, flm_version=args.flm_version, deploy_tag=args.deploy_tag, deploy_from=args.deploy_from, deploy_name=args.deploy_name)
print(f"[INFO] Conversion complete! Output saved to {output_folder}")



if __name__ == "__main__":
# for debug, give the path and ouptut path here by directly set the command line args
import sys
# sys.argv = ['convert.py', '-i', 'unsloth_gpt-oss-20b-Q4_0.gguf', '-o', 'unsloth-gotoss20b-q40']

# sys.argv = ['convert.py', '-i', 'unsloth_gpt-oss-20b-Q4_1.gguf', '-o', 'unsloth-gotoss20b-q41']

# import sys
# # sys.argv = ['convert.py', '-i', 'gemma-3-4b-it-Q4_1.gguf', '-o', 'unsloth-gemma3-q41']
# # main()


# # sys.argv = ['convert.py', '-i', 'gemma3-mmproj-BF16.gguf', '-o', 'unsloth-gemma3-vision', '-t', 'vision']
# # main()



# # sys.argv = ['convert.py', '-i'
# , 'medgemma3-mmproj-BF16.gguf', '-o', 'unsloth-medgemma3-vision', '-t', 'vision']
# # main()
# sys.argv = ['convert.py', '-i', 'Qwen3-VL-4B-Instruct-Q4_1.gguf', '-o', 'unsloth-qwen3vl-4b-q41' ]
# main()

# sys.argv = ['convert.py', '-i', 'Qwen3-4B-Q4_1.gguf', '-o', 'unsloth-qwen3-4b-q41' ]
# main()
# sys.argv = ['convert.py', '-i', 'qwen3vl-4b-mmproj-BF16.gguf', '-o', 'unsloth-qwen3vl-vision', '-t', 'vision']
# main()



#sys.argv = ['convert.py', '-i', 'qwen3_5vl-4b-mmproj-BF16.gguf', '-o', 'unsloth-qwen3_5vl-vision', '-t', 'vision']

#sys.argv = ['convert.py', '-i', 'qwen3_5vl-9bmmproj-BF16.gguf', '-o', 'unsloth-qwen3_5_9bvl-vision', '-t', 'vision']


#sys.argv = ['convert.py', '-i', 'Qwen3.5-4B-Q4_1.gguf', '-o', 'unsloth-qwen3_5_4bq41']

#sys.argv = ['convert.py', '-i', 'Qwen3.5-9B-Uncensored-HauhauCS-Aggressive-Q4_K_M.gguf', '-o', 'unsloth-qwen3_59b_uncensored', "-f", "qwen3.5-9B"]
# sys.argv = ['convert.py', '-i', 'Qwen3.5-9B-Uncensored-HauhauCS-Aggressive-Q4_K_M.gguf', '-o', 'unsloth-qwen3_59b_uncensored', "-f", "qwen3.5-9B"]

# sys.argv = ['convert.py', '-i', 'Qwen3.5-9B-Q4_1.gguf', '-o', 'unsloth-qwen3_5_9bq41']



#sys.argv = ['convert.py', '-i', 'gemma-4-E2B-it-Q4_1.gguf', '-o', 'unsloth-gemma4-2b-it-q41']

#sys.argv = ['convert.py', '-i', 'gemma4-2b-mmproj.gguf', '-o', 'unsloth-gemma4-2b-vision', '-t', 'vision']

sys.argv = ['convert.py', '-i', 'gemma4-2b-mmproj.gguf', '-o', 'unsloth-gemma4-2b-audio', '-t', 'audio']
# sys.argv = ['convert.py', '-i', 'debug_gemma4e2b_model.gguf', '-o', 'debug-gemma4-2b-audio', '-t', 'audio', '-f', 'gemma4']
main()
Loading