char-fork/llama.cpp

git clone https://git.t4t.associates/char-fork/llama.cpp

Max Krasnyanskysnapdragon: ci updates to use new run script (#28293)d30500b83

char/aliceai-t5-moe
11.4 KiB274 linesraw
1#!/usr/bin/env python3
2#
3# Build llama.cpp for Snapdragon (via Docker or natively) and push to device.
4#
5
6import sys
7import os
8import argparse
9import subprocess
10import platform
11import shutil
12import logging
13
14from sdk import validate_windows_sdks
15
16logger = logging.getLogger("build")
17
18
19def parse_target(target_str):
20    if not target_str:
21        return None, None
22    if target_str.startswith("adb") or target_str.startswith("android"):
23        parts = target_str.split(":", 1)
24        serial = parts[1] if len(parts) > 1 else None
25        return "android", serial
26    elif target_str.startswith("lnx") or target_str.startswith("linux") or target_str.startswith("ubuntu"):
27        parts = target_str.split(":", 1)
28        host = parts[1] if len(parts) > 1 else None
29        return "linux", host
30    elif target_str in ("wos", "windows"):
31        return "windows", None
32    else:
33        return None, None
34
35
36def get_uid_gid():
37    if platform.system() != "Windows":
38        return [f"{os.getuid()}:{os.getgid()}"]
39    return []
40
41
42def main():
43    logging.basicConfig(level=logging.INFO, format='%(message)s')
44    parser = argparse.ArgumentParser(
45        description="Build llama.cpp for Snapdragon using cross-compilation docker containers or natively."
46    )
47    parser.add_argument("--target", default="android", help="Compilation target and deployment definition (e.g. android[:serial]/adb[:serial], linux:[user@]host/lnx:[user@]host/ubuntu:[user@]host, windows/wos) (default: android)")
48    parser.add_argument("--build-dir", help="Build directory name (defaults to build-TARGET[-dbg], e.g. build-android)")
49    parser.add_argument("--install-dir", help="Install directory name (defaults to pkg-TARGET[-dbg], e.g. pkg-android)")
50    parser.add_argument("--jobs", "-j", type=int, help="Number of build jobs (defaults to CPU thread count)")
51    parser.add_argument("--no-docker", action="store_true", help="Build natively on the host instead of in a docker container")
52    parser.add_argument("--preset", help="Override the CMake preset to use")
53    parser.add_argument("--debug", action="store_true", help="Build in debug mode (uses -debug presets instead of -release)")
54
55    # Push options
56    parser.add_argument("--push", action="store_true", help="Push built package to the target device via ADB or SSH/SCP")
57    parser.add_argument("--target-dir", help="Target directory on the device (default: /data/local/tmp/llama.cpp for Android, ~/llama.cpp for Linux)")
58
59    # Toolchain options
60    parser.add_argument("--toolchain-version", default="v0.7", help="Docker toolchain image version/tag (default: v0.7)")
61    parser.add_argument("--toolchain-url", default="ghcr.io/snapdragon-toolchain", help="Docker toolchain registry URL/namespace (default: ghcr.io/snapdragon-toolchain)")
62
63    args = parser.parse_args()
64
65    target_type, target_val = parse_target(args.target)
66    if not target_type:
67        logger.error(f"Error: Invalid target format '{args.target}'. Must be android[:serial]/adb[:serial], linux:[user@]host/lnx:[user@]host/ubuntu:[user@]host, or windows/wos.")
68        sys.exit(1)
69
70    if target_type == "windows":
71        logger.info("Windows target selected. Forcing native compilation...")
72        args.no_docker = True
73        if platform.system() != "Windows":
74            logger.warning("Warning: Windows compilation is intended to run on Windows arm64 hosts.")
75        validate_windows_sdks()
76
77    # Determine preset and check if it's debug
78    preset = args.preset
79    if preset:
80        is_debug = args.debug or ("debug" in preset.lower())
81    else:
82        is_debug = args.debug
83        config_type = "debug" if is_debug else "release"
84        if args.no_docker:
85            if target_type == "windows" or platform.system() == "Windows":
86                preset = f"arm64-windows-snapdragon-{config_type}"
87            elif target_type == "linux":
88                preset = f"arm64-linux-snapdragon-{config_type}"
89            else:
90                preset = f"arm64-android-snapdragon-{config_type}"
91        else:
92            preset = f"arm64-linux-snapdragon-{config_type}" if target_type == "linux" else f"arm64-android-snapdragon-{config_type}"
93
94    target_prefix = args.target.split(":", 1)[0]
95    suffix = "-dbg" if is_debug else ""
96
97    build_dir = args.build_dir
98    if not build_dir:
99        build_dir = f"build-{target_prefix}{suffix}"
100
101    install_dir = args.install_dir
102    if not install_dir:
103        install_dir = f"pkg-{target_prefix}{suffix}"
104
105    repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
106
107    # Ensure CMakeUserPresets.json is in the workspace root, update if docs version is newer
108    preset_src = os.path.join(repo_root, "docs", "backend", "snapdragon", "CMakeUserPresets.json")
109    preset_dst = os.path.join(repo_root, "CMakeUserPresets.json")
110    if os.path.exists(preset_src):
111        should_copy = False
112        if not os.path.exists(preset_dst):
113            should_copy = True
114        else:
115            # Check modification times
116            src_mtime = os.path.getmtime(preset_src)
117            dst_mtime = os.path.getmtime(preset_dst)
118            if src_mtime > dst_mtime:
119                preset_bak = preset_dst + ".bak"
120                logger.info(f"Docs CMakeUserPresets.json is newer. Backing up existing {preset_dst} to {preset_bak}")
121                shutil.copy2(preset_dst, preset_bak)
122                should_copy = True
123
124        if should_copy:
125            logger.info(f"Copying CMakeUserPresets.json from {preset_src} to {preset_dst}")
126            shutil.copy2(preset_src, preset_dst)
127    else:
128        logger.warning("Warning: CMakeUserPresets.json not found in docs/backend/snapdragon/.")
129
130    jobs = args.jobs if args.jobs else os.cpu_count() or 4
131
132    if args.no_docker:
133        # Native/local host build
134        logger.info("Running native/local CMake build...")
135        install_prefix = os.path.join(repo_root, install_dir, "llama.cpp")
136
137        # Configure
138        configure_cmd = ["cmake", f"--preset={preset}", "-B", build_dir]
139        logger.info(f"+ {' '.join(configure_cmd)}")
140        res = subprocess.run(configure_cmd, cwd=repo_root)
141        if res.returncode != 0:
142            logger.error("CMake configuration failed.")
143            sys.exit(res.returncode)
144
145        # Build
146        build_cmd = ["cmake", "--build", build_dir, "-j", str(jobs)]
147        logger.info(f"+ {' '.join(build_cmd)}")
148        res = subprocess.run(build_cmd, cwd=repo_root)
149        if res.returncode != 0:
150            logger.error("CMake build failed.")
151            sys.exit(res.returncode)
152
153        # Install
154        install_cmd = ["cmake", "--install", build_dir, "--prefix", install_prefix]
155        logger.info(f"+ {' '.join(install_cmd)}")
156        res = subprocess.run(install_cmd, cwd=repo_root)
157        if res.returncode != 0:
158            logger.error("CMake install failed.")
159            sys.exit(res.returncode)
160    else:
161        # Docker-based build
162        logger.info("Running Docker-based cross-compilation build...")
163        image_name = "arm64-linux" if target_type == "linux" else "arm64-android"
164        image = f"{args.toolchain_url}/{image_name}:{args.toolchain_version}"
165
166        install_prefix_container = f"/workspace/{install_dir}/llama.cpp"
167
168        build_sh_cmd = (
169            f"cmake --preset {preset} -B /workspace/{build_dir} && "
170            f"cmake --build /workspace/{build_dir} -j {jobs} && "
171            f"cmake --install /workspace/{build_dir} --prefix {install_prefix_container}"
172        )
173
174        docker_cmd = [
175            "docker", "run", "--rm",
176            "--volume", f"{repo_root}:/workspace",
177            "--workdir", "/workspace",
178            "--platform", "linux/amd64"
179        ]
180        uid_gid = get_uid_gid()
181        if uid_gid:
182            docker_cmd += ["-u", uid_gid[0]]
183
184        docker_cmd += [image, "bash", "-c", build_sh_cmd]
185
186        logger.info(f"+ {' '.join(docker_cmd)}")
187        res = subprocess.run(docker_cmd, cwd=repo_root)
188        if res.returncode != 0:
189            logger.error("Docker-based build failed.")
190            sys.exit(res.returncode)
191
192    logger.info("\nBuild and installation completed successfully!")
193
194    # Push/deploy if requested
195    if args.push:
196        src_path = os.path.join(repo_root, install_dir, "llama.cpp")
197        if not os.path.exists(src_path):
198            logger.error(f"Error: installation directory {src_path} does not exist. Cannot deploy.")
199            sys.exit(1)
200
201        # Resolve target directory on device
202        target_dir = args.target_dir
203        if not target_dir:
204            target_dir = "/data/local/tmp/llama.cpp" if target_type == "android" else "~/llama.cpp"
205        target_dir = target_dir.rstrip("/")
206
207        sub_items = [item for item in os.listdir(src_path) if not item.startswith(".")]
208
209        if target_type == "android":
210            logger.info("\nPushing built artifacts to Android device via ADB...")
211            adb_cmd = ["adb"]
212            if target_val: # serial
213                adb_cmd += ["-s", target_val]
214
215            # Clean stale package files on device
216            if sub_items:
217                clean_paths = " ".join(f"{target_dir}/{item}" for item in sub_items)
218                clean_cmd = adb_cmd + ["shell", f"rm -rf {clean_paths}"]
219                logger.info(f"+ {' '.join(clean_cmd)}")
220                subprocess.run(clean_cmd)
221
222            # Android destination directory is target_dir
223            push_cmd = adb_cmd + ["push", os.path.join(src_path, "."), target_dir]
224            logger.info(f"+ {' '.join(push_cmd)}")
225            res = subprocess.run(push_cmd)
226            if res.returncode != 0:
227                logger.error("ADB push failed.")
228                sys.exit(res.returncode)
229
230            chmod_cmd = adb_cmd + ["shell", f"chmod -R 755 {target_dir}/bin 2>/dev/null || true"]
231            logger.info(f"+ {' '.join(chmod_cmd)}")
232            subprocess.run(chmod_cmd)
233            logger.info("ADB push completed successfully!")
234
235        elif target_type == "linux":
236            ssh_host = target_val
237            if not ssh_host:
238                logger.error("Error: SSH host not specified in target (e.g. use linux:user@host, lnx:user@host, or ubuntu:user@host). Cannot deploy.")
239                sys.exit(1)
240            logger.info(f"\nDeploying built artifacts to Linux device {ssh_host} via SSH/SCP...")
241
242            # Clean stale package files on remote host
243            if sub_items:
244                clean_paths = " ".join(f"{target_dir}/{item}" for item in sub_items)
245                clean_cmd = ["ssh", ssh_host, f"rm -rf {clean_paths}"]
246                logger.info(f"+ {' '.join(clean_cmd)}")
247                subprocess.run(clean_cmd)
248
249            # Deploy to target_dir
250            deploy_cmd = ["scp", "-r", os.path.join(src_path, "."), f"{ssh_host}:{target_dir}"]
251            logger.info(f"+ {' '.join(deploy_cmd)}")
252            res = subprocess.run(deploy_cmd)
253            if res.returncode != 0:
254                logger.error("SSH/SCP deploy failed.")
255                sys.exit(res.returncode)
256
257            chmod_cmd = ["ssh", ssh_host, f"chmod -R 755 {target_dir}/bin 2>/dev/null || true"]
258            logger.info(f"+ {' '.join(chmod_cmd)}")
259            subprocess.run(chmod_cmd)
260            logger.info("SSH/SCP deploy completed successfully!")
261
262        elif target_type == "windows":
263            logger.info("\nPush for Windows on Snapdragon (windows) target is currently a stub.")
264
265
266if __name__ == "__main__":
267    try:
268        main()
269    except KeyboardInterrupt:
270        logger.info("\nInterrupted by user.")
271        sys.exit(130)
272    except RuntimeError as err:
273        logger.error("Error: %s", err)
274        sys.exit(1)