Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ td.log
/run-*.sh
/strata-*.json
/strata-*.log
/metal-tiles*.txt
/build-strata.bat
/build-strata-cuda12.bat
/build-cuda12/
Expand Down
10 changes: 9 additions & 1 deletion docs/MAC.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,15 @@ from each other: top-1 agreement 95.4% against 95.8%, mean KL 0.0207 against 0.0
- **Context:** measured up to 32K (needles, KL). Setup recommends 32K below 96 GB and 64K from 96 GB. Up to the model's 262K can
be set, but it is not measured.
- **Several requests at once.** `--parallel N` keeps N sequences side by side in the engine, each with its own running state and K/V (0.65 GB each at a 36,864-token context). Measured on an M5 Max with IQ3_S, the MTP draft layer and `--spec 2`, N requests of 150 tokens sent at the same time, tokens a second over all of them: 1 request 85, 2 requests 95 to 102, 3 requests 115, 4 requests 119 to 125. One request alone decodes at 96 to 100; with 2, 3 and 4 at once each decodes at 53 to 61, 41 to 48 and 32 to 40. The engine uses 8.3, 12.0, 14.0 and 16.0 GB for 1 to 4.
- **Not ported:** the control vector (`--control-vector-scaled`) and the PC tuning (`--calibrate`).
- **Not ported:** the control vector (`--control-vector-scaled`).
- **`./setup.sh --calibrate`:** the engine measures the tiles of its decode kernels on this Mac (the dense GEMVs, the
hyper-connection read, and the routed experts when the model is in memory - not when they stream from the SSD; the
defaults were tuned on the M4 and M5), then they race the defaults and go into the config's `env` (`STRATA_METAL_TILES`)
only when they are over 3% faster. The fp16 kernels (`STRATA_METAL_F16=1`) keep their default tiles. The server must be stopped.
A few minutes with the model in memory, longer with streaming; again after an update that changes the kernels. The
answers may differ slightly from the defaults' (the order of a sum: a near-tie can flip). Measured on an M1 Max only,
with fp16 off: decode +30-32% on short chats, +26% after a 6K-token prompt; with fp16 on: +6.8% and +2.7% in two
runs (the second under the 3% bar: the defaults stayed).
- **Streaming** is measured at 32 GB only.
- **Families:** the original Qwen3.8-Flash-Next, Unsloth's UD-Q4/Q5/Q6_K_XL and agentionai's Gyro-S are measured. Setup repacks
Unsloth's shards into one file for the GPU; a Gyro file is used as it is. The Swift and Coder releases are not set up on Mac
Expand Down
41 changes: 40 additions & 1 deletion metal/setup_mac.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,33 @@ def start(cfg_path: Path, port: int | None, open_browser: bool = True) -> int:


# ------------------------------------------------------------------------------------------------ main
def calibrate_config(cfg_path: Path) -> bool:
"""The Mac's tuning (tools/calibrate.py): the engine measures its GPU kernels' tiles, they race the defaults, and the
config's "env" names them (STRATA_METAL_TILES) only when they are faster."""
running = engine_pids()
if running: # a second engine beside it can freeze this Mac (repack_running)
S.warn(f"a Strata engine is running (pid {running[0]}): stop the server (close its window), then run "
"./setup.sh --calibrate; the config is unchanged")
return False
sys.path.insert(0, str(ROOT / "tools"))
import calibrate as CAL
cfg = json.loads(cfg_path.read_text(encoding="utf-8-sig"))
S.say()
S.say(" Tuning Strata for this Mac: the engine measures its GPU kernels' tiles, then they race the defaults.")
S.say(" It takes a few minutes (longer when the experts stream from the SSD); the Mac is busy meanwhile.")
try:
res = CAL.run(cfg, say=S.say)
except Exception as e: # never stops an install: the defaults stay
S.warn(f"the tuning did not finish ({e}): the config is unchanged")
return False
cfg["env"] = CAL.apply_env(cfg.get("env"), res["env"])
S.write_config(cfg_path, cfg)
tok_s = res["report"].get("tok_s")
S.ok(("tuned for this Mac: the measured tiles" if res["env"] else "tuned for this Mac: the default tiles are already the fastest here")
+ (f" ({tok_s} tok/s)" if tok_s else ""))
return True


def main(argv=None) -> int:
ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
ap.add_argument("--family", choices=["qwen", "unsloth", "gyro"], default=None,
Expand All @@ -352,6 +379,8 @@ def main(argv=None) -> int:
help="where model files come from: auto (huggingface), huggingface, modelscope; "
"same as STRATA_SOURCE")
ap.add_argument("--setup", action="store_true")
ap.add_argument("--calibrate", action="store_true",
help="measure the GPU kernels' tiles on this Mac (a few minutes) and keep them when they are faster")
ap.add_argument("--no-start", action="store_true")
ap.add_argument("--check", action="store_true")
ap.add_argument("--update", action="store_true")
Expand All @@ -373,6 +402,8 @@ def main(argv=None) -> int:
models_root = Path(a.models_dir) if a.models_dir else data / "models"

have = mac_configs()
if have and a.calibrate and not (a.setup or a.model or a.check or a.update or a.build):
return 0 if calibrate_config(have[0]) else 1
if have and not (a.setup or a.model or a.check or a.no_start or a.update or a.build):
return start(have[0], a.port, a.browser is not False)

Expand Down Expand Up @@ -553,6 +584,13 @@ def main(argv=None) -> int:
cfg = mac_config(exe, engine_args(pack, shards, rt, ctx, stream, info["ram_gb"], model), pack, f"{fam['name']}-{model.lower()}", tag, port)
cfg["env"] = {"STRATA_METAL_STREAM": "1" if stream else "0", # the engine follows setup's measured choice
**_fit.engine_env(model, stream)} # STRATA_METAL_F16=1 for a resident IQ3_S / Q2_0 (mac_fit.F16_MODELS)
cfg_path = ROOT / f"strata-{tag.lower()}.json"
try: # an earlier --calibrate survives a reinstall
tiles = (json.loads(cfg_path.read_text(encoding="utf-8-sig")).get("env") or {}).get("STRATA_METAL_TILES")
except (OSError, ValueError):
tiles = None
if tiles and Path(tiles).exists():
cfg["env"]["STRATA_METAL_TILES"] = tiles
if vision == "yes":
cfg["args"] += ["--vision"]
cfg["vision"] = {"exe": str(vexe), "mmproj": str(mmproj), "model": str(files[0]), "gpu": vision_gpu,
Expand All @@ -565,8 +603,9 @@ def main(argv=None) -> int:
cfg["draft_vocab"] = a.draft_vocab
if a.browser is not None:
cfg["open_browser"] = a.browser
cfg_path = ROOT / f"strata-{tag.lower()}.json"
S.write_config(cfg_path, cfg)
if a.calibrate:
calibrate_config(cfg_path)
script = S.write_run_script(tag, cfg_path, port, cfg.get("open_browser") is not False)
S.ok(f"start script: {script.name}")
S.say()
Expand Down
Loading
Loading