Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5e6a7f6895 | |||
| edc656a85c | |||
| a4c6470390 | |||
| 1d114498f2 | |||
| b1fc49a9f2 | |||
| 4781244aaf | |||
| 49ba6dff08 | |||
| c3b4a15851 | |||
| a4e32ef5a5 | |||
| cceb45d6b6 | |||
| 0b8c1668d2 | |||
| f876b3f775 | |||
| 373c63c08f | |||
| 344755a1f6 | |||
| abb185cb66 | |||
| 49919e8563 | |||
| 7168556aaa | |||
| 1e71de53e3 | |||
| f318741784 | |||
| 0edd0a0cd1 | |||
| 44c74e1d04 | |||
| 81c3c193cf | |||
| b264da7602 | |||
| cf8cfd0a1b | |||
| f96344dfe6 | |||
| 9a4039a518 | |||
| 16ca0b3a17 | |||
| e1e66c0d64 | |||
| 380789d067 | |||
| a1d079e930 | |||
| 0c0b72221f | |||
| dcb4b9ba6d | |||
| 358b4d2ab5 | |||
| 1a4d24ed5f | |||
| 47e1710f23 | |||
| 50012d153d | |||
| eaf1748ea5 | |||
| ffc0ecc1ca | |||
| 4096aabdba | |||
| 36963eec80 | |||
| f22120ef83 |
+3
-3
@@ -11,9 +11,9 @@ MLX LM was developed with contributions from the following individuals:
|
||||
- Gökdeniz Gülmez: Added support for the following architectures: OpenBMB's
|
||||
`MiniCPM` and `MiniCPM3`, Kyutai's `Helium`, State-Space's `Mamba v1`, `Mamba v2`, Z.ai &
|
||||
THUKEG's `GLM`, `GLM4`, Rednote `dots.llm1`, Baisu's `Ernie4.5 MoE`, inclusionAI's
|
||||
`Bailing MoE e.g. Ling-family`, Klear team - Kuaishou Technology's `Klear`,
|
||||
IBM's `Granite MoE`, Meituan's `LongCat`, Nvidia's `Nemotron H`, Swiss-AI's
|
||||
`Apertus`, Nikity's `Lille130m`, Alibaba Qwen's `Qwen3Next`, and Allenai's `OLMoE`;
|
||||
`Bailing MoE e.g. Ling-family`, Klear team - Kuaishou Technology's `Klear`, AI21 Lab's `Jamba`
|
||||
IBM's `Granite MoE`, Meituan's `LongCat`, Nvidia's `Nemotron H`, Swiss-AI's `Apertus`,
|
||||
Nikity's `Lille130m`, Alibaba Qwen's `Qwen3Next`, and Allenai's `OLMoE` and `Olmo 3`;
|
||||
Helped add support for the following model architectures: Alibaba Qwen's `Qwen3 & Qwen3MoE)`;
|
||||
Added support for the following training algorithms: `Full Weight Fine-Tuning`, and the `Muon`
|
||||
optimizer; Added support for the following other features: `Multiple Optimizers
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
# Benchmarks
|
||||
|
||||
## Commands
|
||||
|
||||
The command for evaluating on MMLU Pro:
|
||||
|
||||
```
|
||||
mlx_lm.evaluate --model model/repo --task mmlu_pro
|
||||
```
|
||||
|
||||
The command for efficiency benchmarks:
|
||||
|
||||
```
|
||||
mlx_lm.benchmark --model model/repo -p 2048 -g 128
|
||||
```
|
||||
|
||||
To get the package versions run:
|
||||
|
||||
```
|
||||
python -m mlx --version && python -m mlx_lm --version
|
||||
```
|
||||
|
||||
## Models
|
||||
|
||||
<details>
|
||||
|
||||
<summary> Qwen/Qwen3-4B-Instruct-2507 </summary>
|
||||
|
||||
Precision | MMLU Pro | Prompt (2048) tok/sec | Generation (128) tok/sec | Memory GB | Repo
|
||||
--------- | -------- | ------------------- | ------------------------ | --------- | ----
|
||||
bf16 | 64.05 | 1780.63 | 52.47 | 9.02 | Qwen/Qwen3-4B-Instruct-2507
|
||||
q8 | 63.85 | 1606.573| 86.907 | 5.254 | mlx-community/Qwen3-4B-Instruct-2507-8bit
|
||||
q6 | 63.53 | 1576.73 | 104.68 | 4.25 | mlx-community/Qwen3-4B-Instruct-2507-6bit
|
||||
q5 g32 | 63.16 | 1570.80 | 110.29 | 4.00 | mlx-community/Qwen3-4B-Instruct-2507-5bit-g32
|
||||
q5 | 62.38 | 1584.33 | 116.39 | 3.86 | mlx-community/Qwen3-4B-Instruct-2507-5bit
|
||||
q4 g32 | 61.46 | 1610.03 | 126.00 | 3.603 | mlx-community/Qwen3-4B-Instruct-2507-4bit-g32
|
||||
q4 | 60.72 | 1622.27 | 134.52 | 3.35 | mlx-community/Qwen3-4B-Instruct-2507-4bit
|
||||
|
||||
- Performance benchmark on 64GB M4 Max
|
||||
- mlx 0.29.2.dev20251008+85a8824a8
|
||||
- mlx-lm 0.28.2
|
||||
- macOS 26.1
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary> Qwen/Qwen3-30B-A3B-Instruct-2507 </summary>
|
||||
|
||||
Precision | MMLU Pro | Prompt (2048) tok/sec | Generation (128) tok/sec | Memory GB | Repo
|
||||
--------- | -------- | ------------------- | ------------------------ | --------- | ----
|
||||
bf16 | 72.62 | :skull: | :skull: | :skull: | Qwen/Qwen3-30B-A3B-Instruct-2507
|
||||
q8 | 72.46 | 1719.47 | 83.16 | 33.46 | mlx-community/Qwen3-30B-A3B-Instruct-2507-8bit
|
||||
q6 | 72.41 | 1667.45 | 94.14 | 25.82 | mlx-community/Qwen3-30B-A3B-Instruct-2507-6bit
|
||||
q5 | 71.97 | 1664.24 | 101.00 |22.01 | mlx-community/Qwen3-30B-A3B-Instruct-2507-5bit
|
||||
q4 | 70.71 | 1753.90 | 113.33 |18.20 | mlx-community/Qwen3-30B-A3B-Instruct-2507-4bit
|
||||
|
||||
|
||||
- Performance benchmarks on 64GB M4 Max
|
||||
- mlx 0.29.2.dev20251008+85a8824a8
|
||||
- mlx-lm 0.28.2
|
||||
- macOS 26.1
|
||||
|
||||
</details>
|
||||
@@ -129,7 +129,7 @@ mlx_lm.awq --help
|
||||
Use `mlx_lm.gptq` to run GPTQ on a given model. For example:
|
||||
|
||||
```bash
|
||||
mlx_lm.awq --model Qwen/Qwen3-0.6B
|
||||
mlx_lm.gptq --model Qwen/Qwen3-0.6B
|
||||
```
|
||||
|
||||
The script can take anywhere from a few minutes to several hours depending on
|
||||
|
||||
+4
-1
@@ -371,7 +371,10 @@ of memory. Here are some tips to reduce memory use should you need to do so:
|
||||
|
||||
2. Try using a smaller batch size with `--batch-size`. The default is `4` so
|
||||
setting this to `2` or `1` will reduce memory consumption. This may slow
|
||||
things down a little, but will also reduce the memory use.
|
||||
things down a little, but will also reduce the memory use. You can increase
|
||||
the effective batch size without increasing the memory use by accumulating
|
||||
gradients using `--grad-accumulation-steps <N>` which will accumulate the
|
||||
gradient of `<N>` batches before updating the parameters.
|
||||
|
||||
3. Reduce the number of layers to fine-tune with `--num-layers`. The default
|
||||
is `16`, so you can try `8` or `4`. This reduces the amount of memory
|
||||
|
||||
+1
-19
@@ -30,8 +30,6 @@ To see a full list of options run:
|
||||
mlx_lm.server --help
|
||||
```
|
||||
|
||||
## Chat completions API
|
||||
|
||||
You can make a request to the model by running:
|
||||
|
||||
```shell
|
||||
@@ -130,23 +128,7 @@ curl localhost:8080/v1/chat/completions \
|
||||
- `completion_tokens`: The number of tokens generated.
|
||||
- `total_tokens`: The total number of tokens, i.e. the sum of the above two fields.
|
||||
|
||||
## Responses API
|
||||
|
||||
The responses API follows the [OpenAI responses API
|
||||
spec](https://platform.openai.com/docs/quickstart?api-mode=responses)
|
||||
|
||||
To make a request, use the `/reponses` endpoint. For exapmle:
|
||||
|
||||
```shell
|
||||
curl localhost:8080/responses \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"input": [{"role": "user", "content": "Say this is a test!"}],
|
||||
"temperature": 0.7
|
||||
}'
|
||||
```
|
||||
|
||||
## Models API
|
||||
### List Models
|
||||
|
||||
Use the `v1/models` endpoint to list available models:
|
||||
|
||||
|
||||
+8
-3
@@ -25,7 +25,12 @@ if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
raise ValueError(f"CLI requires a subcommand in {subcommands}")
|
||||
subcommand = sys.argv.pop(1)
|
||||
if subcommand not in subcommands:
|
||||
if subcommand in subcommands:
|
||||
submodule = importlib.import_module(f"mlx_lm.{subcommand}")
|
||||
submodule.main()
|
||||
elif subcommand == "--version":
|
||||
from mlx_lm import __version__
|
||||
|
||||
print(__version__)
|
||||
else:
|
||||
raise ValueError(f"CLI requires a subcommand in {subcommands}")
|
||||
submodule = importlib.import_module(f"mlx_lm.{subcommand}")
|
||||
submodule.main()
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
# Copyright © 2023-2025 Apple Inc.
|
||||
|
||||
__version__ = "0.28.0"
|
||||
__version__ = "0.28.3"
|
||||
|
||||
+10
-18
@@ -4,13 +4,8 @@ import argparse
|
||||
|
||||
import mlx.core as mx
|
||||
|
||||
from mlx_lm import batch_generate, stream_generate
|
||||
from mlx_lm import batch_generate, load, stream_generate
|
||||
from mlx_lm.generate import DEFAULT_MODEL
|
||||
from mlx_lm.tokenizer_utils import load_tokenizer
|
||||
from mlx_lm.utils import (
|
||||
fetch_from_hub,
|
||||
get_model_path,
|
||||
)
|
||||
|
||||
|
||||
def setup_arg_parser():
|
||||
@@ -25,11 +20,6 @@ def setup_arg_parser():
|
||||
),
|
||||
default=None,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--trust-remote-code",
|
||||
action="store_true",
|
||||
help="Enable trusting remote code for tokenizer",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--prompt-tokens",
|
||||
"-p",
|
||||
@@ -68,14 +58,13 @@ def main():
|
||||
|
||||
model_path = args.model or DEFAULT_MODEL
|
||||
|
||||
model_path, _ = get_model_path(model_path, revision=None)
|
||||
model, config, _ = fetch_from_hub(model_path, trust_remote_code=True)
|
||||
tokenizer = load_tokenizer(
|
||||
model_path,
|
||||
eos_token_ids=[], # Empty to avoid early stopping
|
||||
tokenizer_config_extra={"trust_remote_code": True},
|
||||
model, tokenizer, config = load(
|
||||
args.model, return_config=True, tokenizer_config={"trust_remote_code": True}
|
||||
)
|
||||
|
||||
# Empty to avoid early stopping
|
||||
tokenizer._eos_token_ids = {}
|
||||
|
||||
prompt_tokens = args.prompt_tokens
|
||||
generation_tokens = args.generation_tokens
|
||||
batch_size = args.batch_size
|
||||
@@ -96,7 +85,10 @@ def main():
|
||||
model, tokenizer, prompts, max_tokens=generation_tokens
|
||||
).stats
|
||||
|
||||
_bench = batch_bench
|
||||
if batch_size == 1:
|
||||
_bench = single_bench
|
||||
else:
|
||||
_bench = batch_bench
|
||||
|
||||
print("Running warmup..")
|
||||
_bench()
|
||||
|
||||
+11
-8
@@ -10,8 +10,7 @@ from mlx.utils import tree_map_with_path
|
||||
|
||||
from .utils import (
|
||||
dequantize_model,
|
||||
fetch_from_hub,
|
||||
get_model_path,
|
||||
load,
|
||||
quantize_model,
|
||||
save,
|
||||
upload_to_hub,
|
||||
@@ -63,7 +62,9 @@ def mixed_quant_predicate_builder(
|
||||
or index >= 7 * num_layers // 8
|
||||
or (index - num_layers // 8) % 3 == 2
|
||||
)
|
||||
if "v_proj" in path and use_more_bits:
|
||||
if (
|
||||
"v_proj" in path or "v_a_proj" in path or "v_b_proj" in path
|
||||
) and use_more_bits:
|
||||
return {"group_size": group_size, "bits": high_bits}
|
||||
if "down_proj" in path and use_more_bits:
|
||||
return {"group_size": group_size, "bits": high_bits}
|
||||
@@ -107,9 +108,12 @@ def convert(
|
||||
)
|
||||
|
||||
print("[INFO] Loading")
|
||||
model_path, hf_path = get_model_path(hf_path, revision=revision)
|
||||
model, config, tokenizer = fetch_from_hub(
|
||||
model_path, lazy=True, trust_remote_code=trust_remote_code
|
||||
model, tokenizer, config = load(
|
||||
hf_path,
|
||||
revision=revision,
|
||||
return_config=True,
|
||||
tokenizer_config={"trust_remote_code": trust_remote_code},
|
||||
lazy=True,
|
||||
)
|
||||
|
||||
if isinstance(quant_predicate, str):
|
||||
@@ -154,11 +158,10 @@ def convert(
|
||||
|
||||
save(
|
||||
mlx_path,
|
||||
model_path,
|
||||
hf_path,
|
||||
model,
|
||||
tokenizer,
|
||||
config,
|
||||
hf_repo=hf_path,
|
||||
)
|
||||
|
||||
if upload_repo is not None:
|
||||
|
||||
+39
-1
@@ -304,11 +304,16 @@ class MLXLM(LM):
|
||||
continuation: str
|
||||
The generated continuation.
|
||||
"""
|
||||
group = mx.distributed.init()
|
||||
|
||||
# split data accross ranks
|
||||
total_requests = len(requests)
|
||||
requests = requests[group.rank() :: group.size()]
|
||||
|
||||
logging.info("Generating continuation for %d sequences." % len(requests))
|
||||
contexts, options = zip(*[req.args for req in requests])
|
||||
# The second element of the tuple contains:
|
||||
# {'do_sample': False, 'until': ['\n\n'], 'temperature': 0}
|
||||
completions = []
|
||||
|
||||
# Tokenize all contexts
|
||||
contexts = [
|
||||
@@ -333,6 +338,39 @@ class MLXLM(LM):
|
||||
until = opt["until"]
|
||||
if any(u in text for u in until):
|
||||
completions[e] = _rstrip_until(text, until)
|
||||
|
||||
# Gather the completions
|
||||
if group.size() > 1:
|
||||
with mx.stream(mx.cpu):
|
||||
pad_to = (total_requests + group.size() - 1) // group.size()
|
||||
pad = pad_to - len(completions)
|
||||
completions = [list(c.encode("utf-8")) for c in completions]
|
||||
max_len = mx.array(max(len(c) for c in completions))
|
||||
max_len = mx.distributed.all_max(max_len).item()
|
||||
lengths = mx.array([len(c) for c in completions] + [0] * pad)
|
||||
completions = mx.array(
|
||||
[c + [0] * (max_len - len(c)) for c in completions]
|
||||
+ [[0] * max_len] * pad,
|
||||
mx.uint8,
|
||||
)
|
||||
completions = (
|
||||
mx.distributed.all_gather(completions[None])
|
||||
.swapaxes(0, 1)
|
||||
.flatten(0, 1)
|
||||
.tolist()
|
||||
)
|
||||
lengths = (
|
||||
mx.distributed.all_gather(lengths[None])
|
||||
.swapaxes(0, 1)
|
||||
.flatten(0, 1)
|
||||
.tolist()
|
||||
)
|
||||
completions = completions[:total_requests]
|
||||
lengths = lengths[:total_requests]
|
||||
completions = [
|
||||
bytearray(c[:l]).decode() for c, l in zip(completions, lengths)
|
||||
]
|
||||
|
||||
return completions
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# The path to the local model directory or Hugging Face repo.
|
||||
model: "mlx-community/Llama-3.2-1B-Instruct"
|
||||
model: "mlx-community/Llama-3.2-1B-Instruct-bf16"
|
||||
|
||||
# Whether or not to train (boolean)
|
||||
train: true
|
||||
@@ -47,6 +47,9 @@ steps_per_report: 10
|
||||
# Number of training steps between validations.
|
||||
steps_per_eval: 200
|
||||
|
||||
# Number of micro-steps to accumulate before each optimizer update.
|
||||
grad_accumulation_steps: 1
|
||||
|
||||
# Load path to resume training with the given adapter weights.
|
||||
resume_adapter_file: null
|
||||
|
||||
@@ -90,4 +93,3 @@ lora_parameters:
|
||||
# valid_split: "train[-100:]"
|
||||
# prompt_feature: "text"
|
||||
# completion_feature: "summary"
|
||||
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
"""
|
||||
Examples using the OpenAI responses endpoint with mlx_lm.server.
|
||||
|
||||
To run, first start the server:
|
||||
|
||||
>>> mlx_lm.server
|
||||
|
||||
Then run this script.
|
||||
|
||||
More documentation on the API spec here:
|
||||
https://platform.openai.com/docs/quickstart?api-mode=responses
|
||||
"""
|
||||
from openai import OpenAI
|
||||
|
||||
model = "mlx-community/Qwen3-4B-Instruct-2507-4bit"
|
||||
|
||||
### Basic response example
|
||||
|
||||
client = OpenAI(base_url="http://localhost:8080/v1", api_key="not-needed")
|
||||
|
||||
response = client.responses.create(
|
||||
model=model, input="Write a one-sentence bedtime story about a unicorn."
|
||||
)
|
||||
print(response.output_text)
|
||||
|
||||
### Input with roles
|
||||
|
||||
response = client.responses.create(
|
||||
model=model,
|
||||
input=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "input_text",
|
||||
"text": "Write a one-sentence bedtime story about a unicorn.",
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
)
|
||||
print(response.output_text)
|
||||
|
||||
### Streaming
|
||||
|
||||
stream = client.responses.create(
|
||||
model=model,
|
||||
input=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Say 'double bubble bath' ten times fast.",
|
||||
},
|
||||
],
|
||||
stream=True,
|
||||
)
|
||||
|
||||
for event in stream:
|
||||
print(event)
|
||||
+6
-14
@@ -6,8 +6,7 @@ from mlx.utils import tree_flatten, tree_unflatten
|
||||
from .gguf import convert_to_gguf
|
||||
from .tuner.utils import dequantize, load_adapters
|
||||
from .utils import (
|
||||
fetch_from_hub,
|
||||
get_model_path,
|
||||
load,
|
||||
save,
|
||||
upload_to_hub,
|
||||
)
|
||||
@@ -62,11 +61,9 @@ def main() -> None:
|
||||
print("Loading pretrained model")
|
||||
args = parse_arguments()
|
||||
|
||||
model_path, hf_path = get_model_path(args.model)
|
||||
model, config, tokenizer = fetch_from_hub(model_path)
|
||||
|
||||
model.freeze()
|
||||
model = load_adapters(model, args.adapter_path)
|
||||
model, tokenizer, config = load(
|
||||
args.model, adapter_path=args.adapter_path, return_config=True
|
||||
)
|
||||
|
||||
fused_linears = [
|
||||
(n, m.fuse(de_quantize=args.de_quantize))
|
||||
@@ -85,11 +82,10 @@ def main() -> None:
|
||||
save_path = Path(args.save_path)
|
||||
save(
|
||||
save_path,
|
||||
model_path,
|
||||
args.model,
|
||||
model,
|
||||
tokenizer,
|
||||
config,
|
||||
hf_repo=hf_path,
|
||||
donate_model=False,
|
||||
)
|
||||
|
||||
@@ -100,13 +96,9 @@ def main() -> None:
|
||||
f"Model type {model_type} not supported for GGUF conversion."
|
||||
)
|
||||
weights = dict(tree_flatten(model.parameters()))
|
||||
convert_to_gguf(model_path, weights, config, str(save_path / args.gguf_path))
|
||||
convert_to_gguf(save_path, weights, config, str(save_path / args.gguf_path))
|
||||
|
||||
if args.upload_repo is not None:
|
||||
if hf_path is None:
|
||||
raise ValueError(
|
||||
"Must provide original Hugging Face repo to upload local model."
|
||||
)
|
||||
upload_to_hub(args.save_path, args.upload_repo)
|
||||
|
||||
|
||||
|
||||
+31
-26
@@ -26,8 +26,11 @@ from .models import cache
|
||||
from .models.cache import (
|
||||
ArraysCache,
|
||||
BatchKVCache,
|
||||
BatchRotatingKVCache,
|
||||
CacheList,
|
||||
KVCache,
|
||||
QuantizedKVCache,
|
||||
RotatingKVCache,
|
||||
load_prompt_cache,
|
||||
)
|
||||
from .sample_utils import make_sampler
|
||||
@@ -284,16 +287,11 @@ class GenerationResponse:
|
||||
|
||||
|
||||
def maybe_quantize_kv_cache(prompt_cache, quantized_kv_start, kv_group_size, kv_bits):
|
||||
if (
|
||||
kv_bits is not None
|
||||
and not isinstance(prompt_cache[0], cache.QuantizedKVCache)
|
||||
and prompt_cache[0].offset > quantized_kv_start
|
||||
):
|
||||
for i in range(len(prompt_cache)):
|
||||
if isinstance(prompt_cache[i], cache.KVCache):
|
||||
prompt_cache[i] = prompt_cache[i].to_quantized(
|
||||
group_size=kv_group_size, bits=kv_bits
|
||||
)
|
||||
if kv_bits is None:
|
||||
return
|
||||
for e, c in enumerate(prompt_cache):
|
||||
if hasattr(c, "to_quantized") and c.offset >= quantized_kv_start:
|
||||
prompt_cache[e] = c.to_quantized(group_size=kv_group_size, bits=kv_bits)
|
||||
|
||||
|
||||
def generate_step(
|
||||
@@ -301,7 +299,7 @@ def generate_step(
|
||||
model: nn.Module,
|
||||
*,
|
||||
max_tokens: int = 256,
|
||||
sampler: Optional[Callable[mx.array, mx.array]] = None,
|
||||
sampler: Optional[Callable[[mx.array], mx.array]] = None,
|
||||
logits_processors: Optional[List[Callable[[mx.array, mx.array], mx.array]]] = None,
|
||||
max_kv_size: Optional[int] = None,
|
||||
prompt_cache: Optional[Any] = None,
|
||||
@@ -309,7 +307,7 @@ def generate_step(
|
||||
kv_bits: Optional[int] = None,
|
||||
kv_group_size: int = 64,
|
||||
quantized_kv_start: int = 0,
|
||||
prompt_progress_callback: Optional[Callable[int, int]] = None,
|
||||
prompt_progress_callback: Optional[Callable[[int], int]] = None,
|
||||
input_embeddings: Optional[mx.array] = None,
|
||||
) -> Generator[Tuple[mx.array, mx.array], None, None]:
|
||||
"""
|
||||
@@ -335,7 +333,7 @@ def generate_step(
|
||||
kv_group_size (int): Group size for KV cache quantization. Default: ``64``.
|
||||
quantized_kv_start (int): Step to begin using a quantized KV cache.
|
||||
when ``kv_bits`` is non-None. Default: ``0``.
|
||||
prompt_progress_callback (Callable[int, int]): A call-back which takes the
|
||||
prompt_progress_callback (Callable[[int], int]): A call-back which takes the
|
||||
prompt tokens processed so far and the total number of prompt tokens.
|
||||
input_embeddings (mx.array, optional): Input embeddings to use instead of or in
|
||||
conjunction with prompt tokens. Default: ``None``.
|
||||
@@ -468,7 +466,7 @@ def speculative_generate_step(
|
||||
*,
|
||||
num_draft_tokens: int = 2,
|
||||
max_tokens: int = 256,
|
||||
sampler: Optional[Callable[mx.array, mx.array]] = None,
|
||||
sampler: Optional[Callable[[mx.array], mx.array]] = None,
|
||||
logits_processors: Optional[List[Callable[[mx.array, mx.array], mx.array]]] = None,
|
||||
prompt_cache: Optional[Any] = None,
|
||||
prefill_step_size: int = 512,
|
||||
@@ -487,7 +485,7 @@ def speculative_generate_step(
|
||||
speculative decoding. Default: ``2``.
|
||||
max_tokens (int): The maximum number of tokens. Use``-1`` for an infinite
|
||||
generator. Default: ``256``.
|
||||
sampler (Callable[mx.array, mx.array], optional): A sampler for sampling a
|
||||
sampler (Callable[[mx.array], mx.array], optional): A sampler for sampling a
|
||||
token from a vector of log probabilities. Default: ``None``.
|
||||
logits_processors (List[Callable[[mx.array, mx.array], mx.array]], optional):
|
||||
A list of functions that take tokens and logits and return the processed
|
||||
@@ -862,18 +860,25 @@ def _make_cache(model, left_padding):
|
||||
Convert a list of regular caches into their corresponding
|
||||
batch-aware caches.
|
||||
"""
|
||||
|
||||
def to_batch_cache(c):
|
||||
if isinstance(c, KVCache):
|
||||
return BatchKVCache(left_padding)
|
||||
elif isinstance(c, ArraysCache):
|
||||
c.left_padding = mx.array(left_padding)
|
||||
return c
|
||||
elif isinstance(c, RotatingKVCache):
|
||||
if c.keep > 0:
|
||||
raise ValueError("RotatingKVCache with keep tokens is not supported.")
|
||||
return BatchRotatingKVCache(c.max_size, left_padding)
|
||||
elif isinstance(c, CacheList):
|
||||
return CacheList(*(to_batch_cache(sub_c) for sub_c in c.caches))
|
||||
else:
|
||||
raise ValueError(f"{type(c)} does not yet support batching")
|
||||
|
||||
if hasattr(model, "make_cache"):
|
||||
cache = model.make_cache()
|
||||
batch_cache = []
|
||||
for c in cache:
|
||||
if isinstance(c, KVCache):
|
||||
batch_cache.append(BatchKVCache(left_padding))
|
||||
elif isinstance(c, ArraysCache):
|
||||
c.left_padding = mx.array(left_padding)
|
||||
batch_cache.append(c)
|
||||
else:
|
||||
raise ValueError(f"{type(c)} does not yet support batching")
|
||||
return batch_cache
|
||||
return [to_batch_cache(c) for c in cache]
|
||||
else:
|
||||
return [BatchKVCache(left_padding) for _ in model.layers]
|
||||
|
||||
@@ -892,7 +897,7 @@ class BatchGenerator:
|
||||
model,
|
||||
max_tokens: int = 128,
|
||||
stop_tokens: Optional[set] = None,
|
||||
sampler: Optional[Callable[mx.array, mx.array]] = None,
|
||||
sampler: Optional[Callable[[mx.array], mx.array]] = None,
|
||||
completion_batch_size: int = 32,
|
||||
prefill_batch_size: int = 8,
|
||||
prefill_step_size: int = 2048,
|
||||
|
||||
+9
-20
@@ -40,7 +40,7 @@ yaml_loader.add_implicit_resolver(
|
||||
)
|
||||
|
||||
CONFIG_DEFAULTS = {
|
||||
"model": "mlx_model",
|
||||
"model": "Qwen/Qwen3-0.6b",
|
||||
"train": False,
|
||||
"fine_tune_type": "lora",
|
||||
"optimizer": "adam",
|
||||
@@ -51,7 +51,7 @@ CONFIG_DEFAULTS = {
|
||||
"sgd": {},
|
||||
"adafactor": {},
|
||||
},
|
||||
"data": "data/",
|
||||
"data": "mlx-community/WikiSQL",
|
||||
"seed": 0,
|
||||
"num_layers": 16,
|
||||
"batch_size": 4,
|
||||
@@ -68,10 +68,10 @@ CONFIG_DEFAULTS = {
|
||||
"max_seq_length": 2048,
|
||||
"config": None,
|
||||
"grad_checkpoint": False,
|
||||
"grad_accumulation_steps": 1,
|
||||
"lr_schedule": None,
|
||||
"lora_parameters": {"rank": 8, "dropout": 0.0, "scale": 20.0},
|
||||
"mask_prompt": False,
|
||||
"wandb": None, # will be deprecated in a future release
|
||||
"report_to": None,
|
||||
"project_name": None,
|
||||
}
|
||||
@@ -142,6 +142,11 @@ def build_parser():
|
||||
type=int,
|
||||
help="Number of training steps between validations.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--grad-accumulation-steps",
|
||||
type=int,
|
||||
help="Number of steps to accumulate before each optimizer update.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--resume-adapter-file",
|
||||
type=str,
|
||||
@@ -185,15 +190,6 @@ def build_parser():
|
||||
help="Use gradient checkpointing to reduce memory use.",
|
||||
default=None,
|
||||
)
|
||||
parser.add_argument( # will be deprecated in a future release
|
||||
"--wandb",
|
||||
type=str,
|
||||
default=None,
|
||||
help=(
|
||||
"The 'wandb' argument is deprecated and will be removed in a future release. "
|
||||
"Use 'report_to: wandb' and 'project_name' in the configuration instead."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--report-to",
|
||||
type=str,
|
||||
@@ -265,6 +261,7 @@ def train_model(
|
||||
adapter_file=adapter_file,
|
||||
max_seq_length=args.max_seq_length,
|
||||
grad_checkpoint=args.grad_checkpoint,
|
||||
grad_accumulation_steps=args.grad_accumulation_steps,
|
||||
)
|
||||
|
||||
# Initialize the selected optimizer
|
||||
@@ -314,14 +311,6 @@ def evaluate_model(args, model: nn.Module, test_set):
|
||||
|
||||
def run(args, training_callback: TrainingCallback = None):
|
||||
np.random.seed(args.seed)
|
||||
if args.wandb is not None:
|
||||
warnings.warn(
|
||||
"The 'wandb' argument is deprecated and will be removed in a future release. "
|
||||
"Use 'report_to: wandb' and 'project_name' in the configuration instead.",
|
||||
DeprecationWarning,
|
||||
)
|
||||
args.report_to = "wandb"
|
||||
args.project_name = args.wandb
|
||||
training_callback = get_reporting_callbacks(
|
||||
args.report_to,
|
||||
project_name=args.project_name,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from functools import partial
|
||||
from typing import Any, Dict, Optional, Union
|
||||
|
||||
import mlx.core as mx
|
||||
@@ -37,6 +38,7 @@ class ModelArgs(BaseModelArgs):
|
||||
use_qk_norm: bool = False
|
||||
tie_word_embeddings: bool = False
|
||||
partial_rotary_factor: float = 1.0
|
||||
rotary_dim: Optional[int] = None
|
||||
moe_router_enable_expert_bias: bool = False
|
||||
moe_router_enable_routed_scaling: bool = True
|
||||
routed_scaling_factor: float = 1.0
|
||||
@@ -47,6 +49,18 @@ class ModelArgs(BaseModelArgs):
|
||||
moe_router_enable_shared_expert: bool = True
|
||||
|
||||
|
||||
@partial(mx.compile, shapeless=True)
|
||||
def swiglu(gate, up):
|
||||
return nn.silu(gate) * up
|
||||
|
||||
|
||||
@partial(mx.compile, shapeless=True)
|
||||
def aggregate_expert_outputs(expert_outputs, scores):
|
||||
return (
|
||||
(expert_outputs * scores[..., None]).sum(axis=-2).astype(expert_outputs.dtype)
|
||||
)
|
||||
|
||||
|
||||
class BailingMoeMLP(nn.Module):
|
||||
def __init__(self, args: ModelArgs, intermediate_size: Optional[int] = None):
|
||||
super().__init__()
|
||||
@@ -67,7 +81,7 @@ class BailingMoeMLP(nn.Module):
|
||||
)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
||||
|
||||
|
||||
class BailingMoeAttention(nn.Module):
|
||||
@@ -94,8 +108,10 @@ class BailingMoeAttention(nn.Module):
|
||||
self.key_layernorm = nn.RMSNorm(self.head_dim, eps=args.rms_norm_eps)
|
||||
self.query_layernorm = nn.RMSNorm(self.head_dim, eps=args.rms_norm_eps)
|
||||
|
||||
if (rope_dim := args.rotary_dim) is None:
|
||||
rope_dim = int(self.head_dim * args.partial_rotary_factor)
|
||||
self.rope = initialize_rope(
|
||||
int(self.head_dim * args.partial_rotary_factor),
|
||||
rope_dim,
|
||||
args.rope_theta,
|
||||
traditional=False,
|
||||
scaling_config=args.rope_scaling,
|
||||
@@ -146,6 +162,7 @@ class BailingMoeAttention(nn.Module):
|
||||
return self.dense(output)
|
||||
|
||||
|
||||
@mx.compile
|
||||
def group_expert_select(
|
||||
gates,
|
||||
e_score_correction_bias,
|
||||
@@ -171,15 +188,15 @@ def group_expert_select(
|
||||
k = n_group - topk_group
|
||||
group_idx = mx.argpartition(group_scores, kth=k - 1, axis=-2)[..., :k, :]
|
||||
scores = mx.put_along_axis(
|
||||
scores, mx.stop_gradient(group_idx), mx.array(0.0), axis=-2
|
||||
scores, mx.stop_gradient(group_idx), mx.array(0.0, scores.dtype), axis=-2
|
||||
)
|
||||
scores = mx.flatten(scores, -2, -1)
|
||||
|
||||
k = top_k
|
||||
inds = mx.argpartition(-scores, kth=k - 1, axis=-1)[..., :k]
|
||||
inds = mx.argpartition(scores, kth=-k, axis=-1)[..., -k:]
|
||||
scores = mx.take_along_axis(orig_scores, inds, axis=-1)
|
||||
if top_k > 1 and norm_topk_prob:
|
||||
denominator = scores.sum(axis=-1, keepdims=True)
|
||||
denominator = scores.sum(axis=-1, keepdims=True) + 1e-20
|
||||
scores = scores / denominator
|
||||
scores = scores * routed_scaling_factor
|
||||
|
||||
@@ -245,7 +262,7 @@ class BailingMoeSparseMoeBlock(nn.Module):
|
||||
def __call__(self, x):
|
||||
topk_idx, topk_weight = self.gate(x)
|
||||
out = self.switch_mlp(x, topk_idx)
|
||||
out = (out * topk_weight[..., None]).sum(axis=-2)
|
||||
out = aggregate_expert_outputs(out, topk_weight)
|
||||
if self.shared_experts is not None:
|
||||
out = out + self.shared_experts(x)
|
||||
return out
|
||||
|
||||
+272
-23
@@ -1,5 +1,6 @@
|
||||
# Copyright © 2023-2024 Apple Inc.
|
||||
|
||||
import copy
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import mlx.core as mx
|
||||
@@ -73,10 +74,10 @@ def load_prompt_cache(file_name, return_metadata=False):
|
||||
arrays = tree_unflatten(list(arrays.items()))
|
||||
cache_metadata = tree_unflatten(list(cache_metadata.items()))
|
||||
info, metadata, classes = cache_metadata
|
||||
cache = [globals()[c]() for c in classes]
|
||||
for c, state, meta_state in zip(cache, arrays, info):
|
||||
c.state = state
|
||||
c.meta_state = meta_state
|
||||
cache = [
|
||||
globals()[c].from_state(state, meta_state)
|
||||
for c, state, meta_state in zip(classes, arrays, info)
|
||||
]
|
||||
if return_metadata:
|
||||
return cache, metadata
|
||||
return cache
|
||||
@@ -141,6 +142,14 @@ class _BaseCache:
|
||||
def is_trimmable(self):
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def from_state(cls, state, meta_state):
|
||||
# Create an instance of cls without calling __init__
|
||||
obj = cls.__new__(cls)
|
||||
obj.state = state
|
||||
obj.meta_state = meta_state
|
||||
return obj
|
||||
|
||||
|
||||
class ConcatenateKVCache(_BaseCache):
|
||||
"""ConcatenateKVCache the simplest KV cache implementation.
|
||||
@@ -188,11 +197,12 @@ class ConcatenateKVCache(_BaseCache):
|
||||
|
||||
|
||||
class QuantizedKVCache(_BaseCache):
|
||||
step = 256
|
||||
|
||||
def __init__(self, group_size: int = 64, bits: int = 8):
|
||||
self.keys = None
|
||||
self.values = None
|
||||
self.offset = 0
|
||||
self.step = 256
|
||||
self.group_size = group_size
|
||||
self.bits = bits
|
||||
|
||||
@@ -254,11 +264,11 @@ class QuantizedKVCache(_BaseCache):
|
||||
|
||||
@property
|
||||
def meta_state(self):
|
||||
return tuple(map(str, (self.step, self.offset, self.group_size, self.bits)))
|
||||
return tuple(map(str, (self.offset, self.group_size, self.bits)))
|
||||
|
||||
@meta_state.setter
|
||||
def meta_state(self, v):
|
||||
self.step, self.offset, self.group_size, self.bits = map(int, v)
|
||||
self.offset, self.group_size, self.bits = map(int, v)
|
||||
|
||||
def is_trimmable(self):
|
||||
return True
|
||||
@@ -273,11 +283,12 @@ class QuantizedKVCache(_BaseCache):
|
||||
|
||||
|
||||
class KVCache(_BaseCache):
|
||||
step = 256
|
||||
|
||||
def __init__(self):
|
||||
self.keys = None
|
||||
self.values = None
|
||||
self.offset = 0
|
||||
self.step = 256
|
||||
|
||||
def update_and_fetch(self, keys, values):
|
||||
prev = self.offset
|
||||
@@ -341,14 +352,14 @@ class KVCache(_BaseCache):
|
||||
|
||||
|
||||
class RotatingKVCache(_BaseCache):
|
||||
step = 256
|
||||
|
||||
def __init__(self, max_size=None, keep=0, step=256):
|
||||
def __init__(self, max_size, keep=0):
|
||||
self.keep = keep
|
||||
self.keys = None
|
||||
self.values = None
|
||||
self.offset = 0
|
||||
self.max_size = max_size
|
||||
self.step = step
|
||||
self._idx = 0
|
||||
|
||||
def _trim(self, trim_size, v, append=None):
|
||||
@@ -388,10 +399,11 @@ class RotatingKVCache(_BaseCache):
|
||||
# preserve context
|
||||
self.keys = self._temporal_order(self.keys)
|
||||
self.values = self._temporal_order(self.values)
|
||||
self._idx = self.keys.shape[2]
|
||||
|
||||
# The largest size is self.max_size + S to ensure
|
||||
# The largest size is self.max_size + S - 1 to ensure
|
||||
# every token gets at least self.max_size context
|
||||
trim_size = self._idx - self.max_size
|
||||
trim_size = self._idx - self.max_size + 1
|
||||
self.keys = self._trim(trim_size, self.keys, keys)
|
||||
self.values = self._trim(trim_size, self.values, values)
|
||||
self.offset += keys.shape[2]
|
||||
@@ -459,13 +471,11 @@ class RotatingKVCache(_BaseCache):
|
||||
|
||||
@property
|
||||
def meta_state(self):
|
||||
return tuple(
|
||||
map(str, (self.keep, self.max_size, self.step, self.offset, self._idx))
|
||||
)
|
||||
return tuple(map(str, (self.keep, self.max_size, self.offset, self._idx)))
|
||||
|
||||
@meta_state.setter
|
||||
def meta_state(self, v):
|
||||
self.keep, self.max_size, self.step, self.offset, self._idx = map(
|
||||
self.keep, self.max_size, self.offset, self._idx = map(
|
||||
int,
|
||||
v,
|
||||
)
|
||||
@@ -487,7 +497,7 @@ class RotatingKVCache(_BaseCache):
|
||||
):
|
||||
if N > 1:
|
||||
window_size = window_size or self.max_size
|
||||
offset = min(self.max_size, self.offset)
|
||||
offset = min(self.max_size - 1, self.offset)
|
||||
if offset + N > window_size or return_array:
|
||||
return create_causal_mask(N, offset, window_size=window_size)
|
||||
else:
|
||||
@@ -500,16 +510,19 @@ class RotatingKVCache(_BaseCache):
|
||||
idx = self._idx
|
||||
if idx >= self.max_size:
|
||||
idx = 0
|
||||
mask_size = min(self.max_size, self.offset)
|
||||
if self.offset < self.max_size:
|
||||
mask_size = self.offset + 1
|
||||
else:
|
||||
mask_size = self.max_size
|
||||
mask = mx.arange(mask_size) >= (mask_size - window_size)
|
||||
mask = mx.roll(mask, shift=idx + 1)
|
||||
return mask[:, None]
|
||||
return mask
|
||||
|
||||
|
||||
class ArraysCache(_BaseCache):
|
||||
def __init__(self, size, left_padding: Optional[List[int]] = None):
|
||||
self.cache = [None] * size
|
||||
self.left_padding = left_padding
|
||||
self.left_padding = mx.array(left_padding) if left_padding else None
|
||||
|
||||
def __setitem__(self, idx, value):
|
||||
self.cache[idx] = value
|
||||
@@ -552,7 +565,7 @@ class MambaCache(ArraysCache):
|
||||
|
||||
|
||||
class ChunkedKVCache(KVCache):
|
||||
def __init__(self, chunk_size=None):
|
||||
def __init__(self, chunk_size):
|
||||
super().__init__()
|
||||
self.chunk_size = chunk_size
|
||||
self.start_position = 0
|
||||
@@ -603,7 +616,7 @@ class ChunkedKVCache(KVCache):
|
||||
self.chunk_size, self.start_position = map(int, v)
|
||||
|
||||
|
||||
class CacheList(KVCache):
|
||||
class CacheList(_BaseCache):
|
||||
def __init__(self, *caches):
|
||||
self.caches = caches
|
||||
|
||||
@@ -631,8 +644,24 @@ class CacheList(KVCache):
|
||||
c.state = v[start : start + l]
|
||||
start += l
|
||||
|
||||
def filter(self, batch_indices):
|
||||
"""
|
||||
In-place filter to keep just the given indices in the cache.
|
||||
"""
|
||||
for c in self.caches:
|
||||
c.filter(batch_indices)
|
||||
|
||||
def extend(self, other):
|
||||
"""
|
||||
In-place extend this cache with the other cache.
|
||||
"""
|
||||
for c, o in zip(self.caches, other.caches):
|
||||
c.extend(o)
|
||||
|
||||
|
||||
class BatchKVCache(_BaseCache):
|
||||
step = 256
|
||||
|
||||
def __init__(self, left_padding: List[int]):
|
||||
"""
|
||||
The BatchKV cache expects inputs to be left-padded.
|
||||
@@ -657,7 +686,6 @@ class BatchKVCache(_BaseCache):
|
||||
self.left_padding = mx.array(left_padding)
|
||||
self.offset = mx.array([-l for l in left_padding])
|
||||
self._idx = 0
|
||||
self.step = 256
|
||||
|
||||
def update_and_fetch(self, keys, values):
|
||||
prev = self._idx
|
||||
@@ -756,3 +784,224 @@ class BatchKVCache(_BaseCache):
|
||||
mx.concatenate, zip(*(pad(self), pad(other)))
|
||||
)
|
||||
self._idx = max_idx
|
||||
|
||||
|
||||
class BatchRotatingKVCache(_BaseCache):
|
||||
step = 256
|
||||
|
||||
def __init__(self, max_size, left_padding: List[int]):
|
||||
self.keys = None
|
||||
self.values = None
|
||||
|
||||
self.left_padding = mx.array(left_padding)
|
||||
self.offset = mx.array([-l for l in left_padding])
|
||||
|
||||
self.max_size = max_size
|
||||
self._idx = 0
|
||||
self._offset = 0
|
||||
self.rotated = False
|
||||
|
||||
def _trim(self, trim_size, v, append=None):
|
||||
if trim_size > 0:
|
||||
v = v[..., trim_size:, :]
|
||||
if append is not None:
|
||||
return mx.concatenate([v, append], axis=2)
|
||||
return v
|
||||
|
||||
def _temporal_order(self):
|
||||
"""
|
||||
Rearrange the cache into temporal order.
|
||||
"""
|
||||
if self.rotated:
|
||||
self.keys = mx.roll(self.keys, -self._idx, axis=2)
|
||||
self.values = mx.roll(self.values, -self._idx, axis=2)
|
||||
self._idx = self.keys.shape[2]
|
||||
self.rotated = False
|
||||
|
||||
def _update_concat(self, keys, values):
|
||||
if self.keys is None:
|
||||
self.keys = keys
|
||||
self.values = values
|
||||
else:
|
||||
# Put the keys/values in temporal order to
|
||||
# preserve context
|
||||
self._temporal_order()
|
||||
|
||||
# Slice off the end if needed
|
||||
if self.keys.shape[2] > self._idx:
|
||||
self.keys = self.keys[..., : self._idx, :]
|
||||
self.values = self.values[..., : self._idx, :]
|
||||
|
||||
# The largest size is self.max_size + S - 1 to ensure
|
||||
# every token gets at least self.max_size context
|
||||
trim_size = self._idx - self.max_size + 1
|
||||
if trim_size > 0:
|
||||
self.left_padding -= trim_size
|
||||
self.keys = self._trim(trim_size, self.keys, keys)
|
||||
self.values = self._trim(trim_size, self.values, values)
|
||||
self.offset += keys.shape[2]
|
||||
self._offset += keys.shape[2]
|
||||
self._idx = self.keys.shape[2]
|
||||
return self.keys, self.values
|
||||
|
||||
def _update_in_place(self, keys, values):
|
||||
# May not have hit the max size yet, so potentially
|
||||
# keep growing the cache
|
||||
B, n_kv_heads, S, k_head_dim = keys.shape
|
||||
prev = self._offset
|
||||
if self.keys is None or (
|
||||
prev >= self.keys.shape[2] and self.keys.shape[2] < self.max_size
|
||||
):
|
||||
v_head_dim = values.shape[3]
|
||||
new_size = min(self.step, self.max_size - prev)
|
||||
k_shape = (B, n_kv_heads, new_size, k_head_dim)
|
||||
v_shape = (B, n_kv_heads, new_size, v_head_dim)
|
||||
new_k = mx.zeros(k_shape, keys.dtype)
|
||||
new_v = mx.zeros(v_shape, values.dtype)
|
||||
if self.keys is not None:
|
||||
self.keys = mx.concatenate([self.keys, new_k], axis=2)
|
||||
self.values = mx.concatenate([self.values, new_v], axis=2)
|
||||
else:
|
||||
self.keys, self.values = new_k, new_v
|
||||
self._idx = prev
|
||||
|
||||
# Trim if needed
|
||||
trim_size = self.keys.shape[2] - self.max_size
|
||||
if trim_size > 0:
|
||||
self.keys = self._trim(trim_size, self.keys)
|
||||
self.values = self._trim(trim_size, self.values)
|
||||
self._idx = self.max_size
|
||||
self.left_padding -= trim_size
|
||||
|
||||
# Rotate
|
||||
if self._idx == self.max_size:
|
||||
self.rotated = True
|
||||
self._idx = 0
|
||||
if self.rotated:
|
||||
self.left_padding -= S
|
||||
|
||||
# Assign
|
||||
self.keys[..., self._idx : self._idx + S, :] = keys
|
||||
self.values[..., self._idx : self._idx + S, :] = values
|
||||
self._offset += S
|
||||
self.offset += S
|
||||
self._idx += S
|
||||
|
||||
# If the buffer is not full, slice off the end
|
||||
if self._offset < self.max_size:
|
||||
return (
|
||||
self.keys[..., : self._offset, :],
|
||||
self.values[..., : self._offset, :],
|
||||
)
|
||||
return self.keys, self.values
|
||||
|
||||
def update_and_fetch(self, keys, values):
|
||||
if keys.shape[2] == 1:
|
||||
return self._update_in_place(keys, values)
|
||||
return self._update_concat(keys, values)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
k, v = self.keys, self.values
|
||||
if self._offset < k.shape[2]:
|
||||
k, v = k[..., : self._offset, :], v[..., : self._offset, :]
|
||||
return k, v, self.offset, self.left_padding
|
||||
|
||||
@state.setter
|
||||
def state(self, v):
|
||||
self.keys, self.values, self.offset, self.left_padding = v
|
||||
|
||||
@property
|
||||
def meta_state(self):
|
||||
return tuple(map(str, (self.max_size, self._offset, self._idx, self.rotated)))
|
||||
|
||||
@meta_state.setter
|
||||
def meta_state(self, v):
|
||||
self.max_size, self._offset, self._idx = map(
|
||||
int,
|
||||
v[:3],
|
||||
)
|
||||
self.rotated = bool(v[3])
|
||||
|
||||
def is_trimmable(self):
|
||||
return self._offset < self.max_size
|
||||
|
||||
def trim(self, n):
|
||||
n = min(self._offset, n)
|
||||
self._offset -= n
|
||||
self._idx -= n
|
||||
self.offset -= n
|
||||
return n
|
||||
|
||||
def to_quantized(self, group_size: int = 64, bits: int = 4) -> QuantizedKVCache:
|
||||
raise NotImplementedError("BatchRotatingKVCache Quantization NYI")
|
||||
|
||||
def make_mask(
|
||||
self, N: int, window_size: Optional[int] = None, return_array: bool = False
|
||||
):
|
||||
left_padding = self.left_padding
|
||||
window_size = window_size or self.max_size
|
||||
offset = min(self.max_size - 1, self._offset)
|
||||
rinds = mx.arange(offset + N)
|
||||
linds = mx.arange(offset, offset + N) if offset else rinds
|
||||
linds = linds[:, None]
|
||||
rinds = rinds[None]
|
||||
mask = linds >= rinds
|
||||
mask &= linds < rinds + window_size
|
||||
if (trim_size := self._idx - self.max_size + int(N > 1)) > 0:
|
||||
left_padding = left_padding - trim_size
|
||||
|
||||
rotated = N == 1 and (self.rotated or self._idx >= self.max_size)
|
||||
if rotated:
|
||||
left_padding = left_padding - 1
|
||||
|
||||
mask = mask & (rinds >= mx.expand_dims(left_padding, (1, 2, 3)))
|
||||
|
||||
if rotated:
|
||||
idx = self._idx
|
||||
if idx >= self.max_size:
|
||||
idx = 0
|
||||
mask = mx.roll(mask, shift=idx + 1, axis=-1)
|
||||
|
||||
return mask
|
||||
|
||||
def filter(self, batch_indices):
|
||||
"""
|
||||
In-place filter to keep just the given indices in the cache.
|
||||
"""
|
||||
self.keys = self.keys[batch_indices]
|
||||
self.values = self.values[batch_indices]
|
||||
self.offset = self.offset[batch_indices]
|
||||
self.left_padding = self.left_padding[batch_indices]
|
||||
|
||||
def extend(self, other):
|
||||
"""
|
||||
In-place extend this cache with the other cache.
|
||||
"""
|
||||
if (self.rotated != other.rotated) or self._idx != other._idx:
|
||||
self._temporal_order()
|
||||
other._temporal_order()
|
||||
|
||||
max_idx = max(self._idx, other._idx)
|
||||
max_size = max(self.keys.shape[2], other.keys.shape[2])
|
||||
|
||||
def pad(c):
|
||||
left = max_idx - c._idx
|
||||
right = max_size - c.keys.shape[2] - left
|
||||
k, v = c.keys, c.values
|
||||
if right < 0:
|
||||
k = k[..., :right, :]
|
||||
v = v[..., :right, :]
|
||||
right = 0
|
||||
if left != 0 or right != 0:
|
||||
pad = [(0, 0), (0, 0), (left, right), (0, 0)]
|
||||
k = mx.pad(k, pad)
|
||||
v = mx.pad(v, pad)
|
||||
left_padding = c.left_padding + left
|
||||
return k, v, c.offset, left_padding
|
||||
|
||||
self.keys, self.values, self.offset, self.left_padding = map(
|
||||
mx.concatenate, zip(*(pad(self), pad(other)))
|
||||
)
|
||||
self._idx = max_idx
|
||||
self._offset = max(self._offset, other._offset)
|
||||
|
||||
@@ -414,6 +414,8 @@ class DeepseekV2Model(nn.Module):
|
||||
# Send to the next process in the pipeline
|
||||
if pipeline_rank != 0:
|
||||
h = mx.distributed.send(h, (pipeline_rank - 1) % pipeline_size)
|
||||
if cache[-1] is not None:
|
||||
cache[-1].keys = mx.depends(cache[-1].keys, h)
|
||||
|
||||
# Broadcast h while keeping it in the graph
|
||||
h = mx.distributed.all_gather(h)[: h.shape[0]]
|
||||
|
||||
@@ -446,6 +446,8 @@ class DeepseekV3Model(nn.Module):
|
||||
# Send to the next process in the pipeline
|
||||
if pipeline_rank != 0:
|
||||
h = mx.distributed.send(h, (pipeline_rank - 1) % pipeline_size)
|
||||
if cache[-1] is not None:
|
||||
cache[-1].keys = mx.depends(cache[-1].keys, h)
|
||||
|
||||
# Broadcast h while keeping it in the graph
|
||||
h = mx.distributed.all_gather(h)[: h.shape[0]]
|
||||
|
||||
@@ -0,0 +1,479 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Optional
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .base import (
|
||||
BaseModelArgs,
|
||||
create_attention_mask,
|
||||
create_ssm_mask,
|
||||
scaled_dot_product_attention,
|
||||
)
|
||||
from .cache import CacheList, KVCache, MambaCache
|
||||
from .rope_utils import initialize_rope
|
||||
from .ssm import ssm_update
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
attention_bias: bool = False
|
||||
attention_in_multiplier: float = 1.0
|
||||
attention_out_multiplier: float = 0.9375
|
||||
embedding_multiplier: float = 5.656854249492381
|
||||
head_dim: int = 64
|
||||
hidden_size: int = 1024
|
||||
initializer_range: float = 0.02
|
||||
intermediate_size: int = 2048
|
||||
key_multiplier: float = 0.390625
|
||||
lm_head_multiplier: float = 0.0390625
|
||||
mamba_chunk_size: int = 128
|
||||
mamba_conv_bias: bool = True
|
||||
mamba_d_conv: int = 4
|
||||
mamba_d_head: int = 64
|
||||
mamba_d_ssm: int = 1536
|
||||
mamba_d_state: int = 128
|
||||
mamba_expand: int = 2
|
||||
mamba_n_groups: int = 1
|
||||
mamba_n_heads: int = 24
|
||||
mamba_norm_before_gate: bool = False
|
||||
mamba_proj_bias: bool = False
|
||||
mamba_rms_norm: bool = False
|
||||
mamba_use_mlp: bool = True
|
||||
max_position_embeddings: int = 131072
|
||||
mlp_bias: bool = False
|
||||
mlp_expansion_factor: int = 8
|
||||
mlp_multipliers: List[float] = field(
|
||||
default_factory=lambda: [0.8838834764831844, 0.5859375]
|
||||
)
|
||||
model_type: str = "falcon_h1"
|
||||
num_attention_heads: int = 8
|
||||
num_hidden_layers: int = 36
|
||||
num_key_value_heads: int = 2
|
||||
projectors_bias: bool = False
|
||||
rms_norm_eps: float = 1e-05
|
||||
rope_traditional: bool = False
|
||||
rope_scaling: Optional[float] = None
|
||||
rope_theta: float = 100000000000.0
|
||||
ssm_in_multiplier: float = 1.25
|
||||
ssm_multipliers: List[float] = field(
|
||||
default_factory=lambda: [
|
||||
0.3535533905932738,
|
||||
0.25,
|
||||
0.3535533905932738,
|
||||
0.5,
|
||||
0.3535533905932738,
|
||||
]
|
||||
)
|
||||
ssm_out_multiplier: float = 0.23570226039551587
|
||||
vocab_size: int = 32784
|
||||
|
||||
|
||||
class FalconH1RMSNormGated(nn.Module):
|
||||
def __init__(self, hidden_size, eps=1e-6, n_groups=1, norm_before_gate=True):
|
||||
super().__init__()
|
||||
self.weight = mx.ones((hidden_size,))
|
||||
self.variance_epsilon = eps
|
||||
self.n_groups = n_groups
|
||||
self.norm_before_gate = norm_before_gate
|
||||
|
||||
def __call__(self, hidden_states, gate=None):
|
||||
if not self.norm_before_gate and gate is not None:
|
||||
hidden_states = hidden_states * nn.silu(gate)
|
||||
|
||||
hidden_states = mx.fast.rms_norm(
|
||||
hidden_states, self.weight, self.variance_epsilon
|
||||
)
|
||||
|
||||
if self.norm_before_gate and gate is not None:
|
||||
hidden_states = hidden_states * nn.silu(gate)
|
||||
return hidden_states
|
||||
|
||||
|
||||
def compute_mup_vector(args):
|
||||
intermediate_size = args.mamba_d_ssm
|
||||
groups_time_state_size = args.mamba_n_groups * args.mamba_d_state
|
||||
num_heads = args.mamba_n_heads
|
||||
sizes = [
|
||||
intermediate_size,
|
||||
intermediate_size,
|
||||
groups_time_state_size,
|
||||
groups_time_state_size,
|
||||
num_heads,
|
||||
]
|
||||
return mx.concatenate(
|
||||
[
|
||||
mx.broadcast_to(mx.array(m), (s,))
|
||||
for s, m in zip(sizes, args.ssm_multipliers)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class FalconH1Attention(nn.Module):
|
||||
|
||||
def __init__(self, args):
|
||||
super().__init__()
|
||||
|
||||
self.hidden_size = args.hidden_size
|
||||
self.num_heads = args.num_attention_heads
|
||||
self.num_kv_heads = args.num_key_value_heads
|
||||
self.head_dim = args.head_dim
|
||||
self.scale = self.head_dim**-0.5
|
||||
|
||||
self.q_proj = nn.Linear(
|
||||
self.hidden_size, self.num_heads * self.head_dim, bias=args.attention_bias
|
||||
)
|
||||
self.k_proj = nn.Linear(
|
||||
self.hidden_size,
|
||||
self.num_kv_heads * self.head_dim,
|
||||
bias=args.attention_bias,
|
||||
)
|
||||
self.v_proj = nn.Linear(
|
||||
self.hidden_size,
|
||||
self.num_kv_heads * self.head_dim,
|
||||
bias=args.attention_bias,
|
||||
)
|
||||
self.o_proj = nn.Linear(
|
||||
self.num_heads * self.head_dim, self.hidden_size, bias=args.attention_bias
|
||||
)
|
||||
|
||||
self.rope = initialize_rope(
|
||||
self.head_dim,
|
||||
args.rope_theta,
|
||||
args.rope_traditional,
|
||||
args.rope_scaling,
|
||||
args.max_position_embeddings,
|
||||
)
|
||||
|
||||
def __call__(self, x, mask=None, cache=None):
|
||||
B, L, _ = x.shape
|
||||
|
||||
queries = self.q_proj(x)
|
||||
keys = self.k_proj(x)
|
||||
values = self.v_proj(x)
|
||||
|
||||
queries = queries.reshape(B, L, self.num_heads, -1).transpose(0, 2, 1, 3)
|
||||
keys = keys.reshape(B, L, self.num_kv_heads, -1).transpose(0, 2, 1, 3)
|
||||
values = values.reshape(B, L, self.num_kv_heads, -1).transpose(0, 2, 1, 3)
|
||||
|
||||
if cache is not None:
|
||||
queries = self.rope(queries, offset=cache.offset)
|
||||
keys = self.rope(keys, offset=cache.offset)
|
||||
keys, values = cache.update_and_fetch(keys, values)
|
||||
else:
|
||||
queries = self.rope(queries)
|
||||
keys = self.rope(keys)
|
||||
|
||||
output = scaled_dot_product_attention(
|
||||
queries, keys, values, mask=mask, scale=self.scale, cache=cache
|
||||
)
|
||||
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
|
||||
|
||||
return self.o_proj(output)
|
||||
|
||||
|
||||
class FalconH1Mixer(nn.Module):
|
||||
def __init__(self, args):
|
||||
super().__init__()
|
||||
self.num_heads = args.mamba_n_heads
|
||||
self.hidden_size = args.hidden_size
|
||||
self.ssm_state_size = args.mamba_d_state
|
||||
self.conv_kernel_size = args.mamba_d_conv
|
||||
self.intermediate_size = args.mamba_d_ssm
|
||||
self.use_conv_bias = args.mamba_conv_bias
|
||||
|
||||
self.layer_norm_epsilon = args.rms_norm_eps
|
||||
self.groups_time_state_size = args.mamba_n_groups * self.ssm_state_size
|
||||
|
||||
self.n_groups = args.mamba_n_groups
|
||||
self.head_dim = args.mamba_d_head
|
||||
self.chunk_size = args.mamba_chunk_size
|
||||
|
||||
self.time_step_limit = (0.0, float("inf"))
|
||||
self.time_step_min = 0.001
|
||||
self.time_step_max = 0.1
|
||||
|
||||
self.conv_dim = self.intermediate_size + 2 * self.n_groups * self.ssm_state_size
|
||||
self.conv1d = nn.Conv1d(
|
||||
in_channels=self.conv_dim,
|
||||
out_channels=self.conv_dim,
|
||||
bias=self.use_conv_bias,
|
||||
kernel_size=self.conv_kernel_size,
|
||||
groups=self.conv_dim,
|
||||
)
|
||||
|
||||
projection_size = self.intermediate_size + self.conv_dim + self.num_heads
|
||||
self.in_proj = nn.Linear(
|
||||
self.hidden_size,
|
||||
projection_size,
|
||||
bias=args.mamba_proj_bias,
|
||||
)
|
||||
|
||||
self.dt_bias = mx.ones(self.num_heads)
|
||||
|
||||
A = mx.arange(1, self.num_heads + 1)
|
||||
self.A_log = mx.log(A)
|
||||
|
||||
self.mamba_rms_norm = args.mamba_rms_norm
|
||||
if self.mamba_rms_norm:
|
||||
self.norm = FalconH1RMSNormGated(
|
||||
self.intermediate_size,
|
||||
eps=self.layer_norm_epsilon,
|
||||
n_groups=self.n_groups,
|
||||
norm_before_gate=args.mamba_norm_before_gate,
|
||||
)
|
||||
|
||||
self.D = mx.ones(self.num_heads)
|
||||
|
||||
self.out_proj = nn.Linear(
|
||||
self.intermediate_size, self.hidden_size, bias=args.projectors_bias
|
||||
)
|
||||
|
||||
def _apply_conv(
|
||||
self, conv_input: mx.array, cache: Optional[MambaCache] = None
|
||||
) -> mx.array:
|
||||
if cache is None or cache[0] is None:
|
||||
conv_state = mx.zeros(
|
||||
(conv_input.shape[0], self.conv_kernel_size - 1, self.conv_dim),
|
||||
dtype=conv_input.dtype,
|
||||
)
|
||||
else:
|
||||
conv_state = cache[0]
|
||||
|
||||
padded_input = mx.concatenate([conv_state, conv_input], axis=1)
|
||||
|
||||
if cache is not None:
|
||||
cache[0] = padded_input[:, -(self.conv_kernel_size - 1) :]
|
||||
|
||||
conv_output = self.conv1d(padded_input)
|
||||
return nn.silu(conv_output)
|
||||
|
||||
def _ssm(
|
||||
self,
|
||||
hidden_states: mx.array,
|
||||
B: mx.array,
|
||||
C: mx.array,
|
||||
dt: mx.array,
|
||||
state: Optional[mx.array] = None,
|
||||
mask: Optional[mx.array] = None,
|
||||
) -> mx.array:
|
||||
batch_size, seq_len, _ = hidden_states.shape
|
||||
|
||||
hidden_states = hidden_states.reshape(
|
||||
batch_size, seq_len, self.num_heads, self.head_dim
|
||||
)
|
||||
B = B.reshape(batch_size, seq_len, self.n_groups, self.ssm_state_size)
|
||||
C = C.reshape(batch_size, seq_len, self.n_groups, self.ssm_state_size)
|
||||
|
||||
y, state = ssm_update(
|
||||
hidden_states,
|
||||
self.A_log,
|
||||
B,
|
||||
C,
|
||||
self.D,
|
||||
dt,
|
||||
self.dt_bias,
|
||||
state,
|
||||
self.time_step_limit,
|
||||
mask,
|
||||
)
|
||||
|
||||
return y.reshape(batch_size, seq_len, self.intermediate_size), state
|
||||
|
||||
def __call__(self, input_states, cache=None, mask: Optional[mx.array] = None):
|
||||
projected_states = self.in_proj(input_states)
|
||||
|
||||
gate, conv_input, dt = mx.split(
|
||||
projected_states,
|
||||
[self.intermediate_size, self.intermediate_size + self.conv_dim],
|
||||
axis=-1,
|
||||
)
|
||||
|
||||
if mask is not None:
|
||||
conv_input = mx.where(mask[..., None], conv_input, 0)
|
||||
conv_output = self._apply_conv(conv_input, cache)
|
||||
|
||||
hidden_states_ssm, B, C = mx.split(
|
||||
conv_output,
|
||||
[
|
||||
self.intermediate_size,
|
||||
self.intermediate_size + self.n_groups * self.ssm_state_size,
|
||||
],
|
||||
axis=-1,
|
||||
)
|
||||
state = cache[1] if cache else None
|
||||
y, state = self._ssm(hidden_states_ssm, B, C, dt, state, mask)
|
||||
if cache:
|
||||
cache[1] = state
|
||||
|
||||
if self.mamba_rms_norm:
|
||||
y = self.norm(y, gate)
|
||||
else:
|
||||
y = y * nn.silu(gate)
|
||||
|
||||
return self.out_proj(y)
|
||||
|
||||
|
||||
class FalconH1MLP(nn.Module):
|
||||
|
||||
def __init__(self, args):
|
||||
super().__init__()
|
||||
|
||||
hidden_size = args.hidden_size
|
||||
intermediate_size = args.intermediate_size
|
||||
|
||||
self.gate_proj = nn.Linear(hidden_size, intermediate_size, bias=args.mlp_bias)
|
||||
self.up_proj = nn.Linear(hidden_size, intermediate_size, bias=args.mlp_bias)
|
||||
self.down_proj = nn.Linear(intermediate_size, hidden_size, bias=args.mlp_bias)
|
||||
|
||||
def __call__(self, x):
|
||||
y = self.up_proj(x) * nn.silu(self.gate_proj(x))
|
||||
y = self.down_proj(y)
|
||||
return y
|
||||
|
||||
|
||||
class FalconH1DecoderLayer(nn.Module):
|
||||
def __init__(self, args):
|
||||
super().__init__()
|
||||
self.feed_forward = FalconH1MLP(args)
|
||||
|
||||
head_dim = args.head_dim
|
||||
self.channels_attn = (
|
||||
args.num_attention_heads * head_dim
|
||||
+ 2 * args.num_key_value_heads * head_dim
|
||||
)
|
||||
|
||||
self.mamba = FalconH1Mixer(args=args)
|
||||
|
||||
self.self_attn = FalconH1Attention(args)
|
||||
|
||||
self.input_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.pre_ff_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
h: mx.array,
|
||||
cache,
|
||||
attn_mask: Optional[mx.array],
|
||||
mamba_mask: Optional[mx.array],
|
||||
) -> mx.array:
|
||||
|
||||
residual = h
|
||||
h = self.input_layernorm(h)
|
||||
|
||||
mamba_h = self.mamba(input_states=h, cache=cache[0], mask=mamba_mask)
|
||||
|
||||
attn_h = self.self_attn(
|
||||
h,
|
||||
mask=attn_mask,
|
||||
cache=cache[1],
|
||||
)
|
||||
|
||||
h = residual + mamba_h + attn_h
|
||||
|
||||
residual = h
|
||||
h = self.pre_ff_layernorm(h)
|
||||
h = self.feed_forward(h)
|
||||
return residual + h
|
||||
|
||||
|
||||
class FalconH1Model(nn.Module):
|
||||
def __init__(self, args):
|
||||
super().__init__()
|
||||
|
||||
self.args = args
|
||||
self.vocab_size = args.vocab_size
|
||||
self.hidden_size = args.hidden_size
|
||||
|
||||
self.embed_tokens = nn.Embedding(self.vocab_size, self.hidden_size)
|
||||
|
||||
self._mup_vector = compute_mup_vector(args)
|
||||
self.layers = [
|
||||
FalconH1DecoderLayer(args) for _ in range(args.num_hidden_layers)
|
||||
]
|
||||
self.final_layernorm = nn.RMSNorm(self.hidden_size, eps=args.rms_norm_eps)
|
||||
|
||||
def __call__(self, inputs, cache=None):
|
||||
|
||||
h = self.embed_tokens(inputs)
|
||||
|
||||
h = h
|
||||
|
||||
if cache is None:
|
||||
cache = [(None, None) * len(self.layers)]
|
||||
|
||||
mamba_mask = create_ssm_mask(h, cache[0][0])
|
||||
attn_mask = create_attention_mask(h, cache[0][1])
|
||||
|
||||
for layer, c in zip(self.layers, cache):
|
||||
h = layer(
|
||||
h,
|
||||
cache=c,
|
||||
attn_mask=attn_mask,
|
||||
mamba_mask=mamba_mask,
|
||||
)
|
||||
|
||||
return self.final_layernorm(h)
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
|
||||
def __init__(self, args):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.model_type = args.model_type
|
||||
self.model = FalconH1Model(args=args)
|
||||
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
|
||||
|
||||
def __call__(self, inputs, cache=None):
|
||||
hidden_states = self.model(inputs, cache=cache)
|
||||
return self.lm_head(hidden_states)
|
||||
|
||||
def sanitize(self, weights):
|
||||
# Check if needs sanitization
|
||||
c1d = weights["model.layers.0.mamba.conv1d.weight"]
|
||||
if c1d.shape[-1] <= c1d.shape[1]:
|
||||
return weights
|
||||
|
||||
sanitized_weights = {}
|
||||
args = self.args
|
||||
|
||||
for name, param in weights.items():
|
||||
# Fold-in multipliers
|
||||
if name.endswith("embed_tokens.weight"):
|
||||
param *= args.embedding_multiplier
|
||||
elif name.endswith("lm_head.weight"):
|
||||
param *= args.lm_head_multiplier
|
||||
elif name.endswith("q_proj.weight") or name.endswith("k_proj.weight"):
|
||||
param *= args.attention_in_multiplier
|
||||
elif name.endswith("key_proj.weight"):
|
||||
param *= args.attention_in_multiplier * args.key_multiplier
|
||||
elif name.endswith("o_proj.weight"):
|
||||
param *= args.attention_out_multiplier
|
||||
elif name.endswith("out_proj.weight"):
|
||||
param *= args.ssm_out_multiplier
|
||||
elif name.endswith("gate_proj.weight"):
|
||||
param *= args.mlp_multipliers[0]
|
||||
elif name.endswith("down_proj.weight"):
|
||||
param *= args.mlp_multipliers[1]
|
||||
elif name.endswith("in_proj.weight"):
|
||||
param *= (
|
||||
args.ssm_in_multiplier
|
||||
* self.model._mup_vector.astype(param.dtype)[:, None]
|
||||
)
|
||||
elif "conv1d.weight" in name:
|
||||
param = param.transpose(0, 2, 1)
|
||||
sanitized_weights[name] = param
|
||||
return sanitized_weights
|
||||
|
||||
def make_cache(self):
|
||||
return [
|
||||
CacheList(MambaCache(), KVCache())
|
||||
for _ in range(self.args.num_hidden_layers)
|
||||
]
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
@@ -12,10 +12,11 @@ def compute_g(A_log, a, dt_bias):
|
||||
)
|
||||
|
||||
|
||||
def _make_gated_delta_kernel():
|
||||
def _make_gated_delta_kernel(has_mask=False):
|
||||
if not mx.metal.is_available():
|
||||
return None
|
||||
source = """
|
||||
mask_source = "mask[b_idx * T + t]" if has_mask else "true"
|
||||
source = f"""
|
||||
auto n = thread_position_in_grid.z;
|
||||
auto b_idx = n / Hv;
|
||||
auto hv_idx = n % Hv;
|
||||
@@ -38,36 +39,38 @@ def _make_gated_delta_kernel():
|
||||
auto o_state = state_out + (n * Dv + dv_idx) * Dk;
|
||||
|
||||
float state[n_per_t];
|
||||
for (int i = 0; i < n_per_t; ++i) {
|
||||
auto s_idx = n_per_t * dk_idx + i;
|
||||
state[i] = static_cast<float>(i_state[s_idx]);
|
||||
}
|
||||
for (int i = 0; i < n_per_t; ++i) {{
|
||||
auto s_idx = n_per_t * dk_idx + i;
|
||||
state[i] = static_cast<float>(i_state[s_idx]);
|
||||
}}
|
||||
|
||||
// beta, g: [B, T, Hv]
|
||||
auto g_ = g + b_idx * T * Hv;
|
||||
auto beta_ = beta + b_idx * T * Hv;
|
||||
|
||||
for (int t = 0; t < T; ++t) {
|
||||
float kv_mem = 0.0f;
|
||||
for (int i = 0; i < n_per_t; ++i) {
|
||||
auto s_idx = n_per_t * dk_idx + i;
|
||||
state[i] = state[i] * g_[hv_idx];
|
||||
kv_mem += state[i] * k_[s_idx];
|
||||
}
|
||||
kv_mem = simd_sum(kv_mem);
|
||||
for (int t = 0; t < T; ++t) {{
|
||||
if ({mask_source}) {{
|
||||
float kv_mem = 0.0f;
|
||||
for (int i = 0; i < n_per_t; ++i) {{
|
||||
auto s_idx = n_per_t * dk_idx + i;
|
||||
state[i] = state[i] * g_[hv_idx];
|
||||
kv_mem += state[i] * k_[s_idx];
|
||||
}}
|
||||
kv_mem = simd_sum(kv_mem);
|
||||
|
||||
auto delta = (v_[dv_idx] - kv_mem) * beta_[hv_idx];
|
||||
auto delta = (v_[dv_idx] - kv_mem) * beta_[hv_idx];
|
||||
|
||||
float out = 0.0f;
|
||||
for (int i = 0; i < n_per_t; ++i) {
|
||||
auto s_idx = n_per_t * dk_idx + i;
|
||||
state[i] = state[i] + k_[s_idx] * delta;
|
||||
out += state[i] * q_[s_idx];
|
||||
}
|
||||
out = simd_sum(out);
|
||||
if (thread_index_in_simdgroup == 0) {
|
||||
y[dv_idx] = static_cast<InT>(out);
|
||||
}
|
||||
float out = 0.0f;
|
||||
for (int i = 0; i < n_per_t; ++i) {{
|
||||
auto s_idx = n_per_t * dk_idx + i;
|
||||
state[i] = state[i] + k_[s_idx] * delta;
|
||||
out += state[i] * q_[s_idx];
|
||||
}}
|
||||
out = simd_sum(out);
|
||||
if (thread_index_in_simdgroup == 0) {{
|
||||
y[dv_idx] = static_cast<InT>(out);
|
||||
}}
|
||||
}}
|
||||
// Increment data pointers to next time step
|
||||
q_ += Hk * Dk;
|
||||
k_ += Hk * Dk;
|
||||
@@ -75,23 +78,28 @@ def _make_gated_delta_kernel():
|
||||
y += Hv * Dv;
|
||||
g_ += Hv;
|
||||
beta_ += Hv;
|
||||
}
|
||||
for (int i = 0; i < n_per_t; ++i) {
|
||||
}}
|
||||
for (int i = 0; i < n_per_t; ++i) {{
|
||||
auto s_idx = n_per_t * dk_idx + i;
|
||||
o_state[s_idx] = static_cast<InT>(state[i]);
|
||||
}
|
||||
}}
|
||||
"""
|
||||
inputs = ["q", "k", "v", "g", "beta", "state_in", "T"]
|
||||
if has_mask:
|
||||
inputs.append("mask")
|
||||
return mx.fast.metal_kernel(
|
||||
name="gated_delta_step",
|
||||
input_names=["q", "k", "v", "g", "beta", "state_in", "T"],
|
||||
name="gated_delta_step" + "_mask" if has_mask else "",
|
||||
input_names=inputs,
|
||||
output_names=["y", "state_out"],
|
||||
source=source,
|
||||
)
|
||||
|
||||
|
||||
_gated_delta_kernel = _make_gated_delta_kernel()
|
||||
_gated_delta_kernel_masked = _make_gated_delta_kernel(True)
|
||||
|
||||
|
||||
@mx.compile
|
||||
def _gated_delta_step_ops(
|
||||
q: mx.array,
|
||||
k: mx.array,
|
||||
@@ -99,6 +107,7 @@ def _gated_delta_step_ops(
|
||||
g: mx.array,
|
||||
beta: mx.array,
|
||||
state: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
) -> Tuple[mx.array, mx.array]:
|
||||
"""
|
||||
Ops-based reference implementation for a single recurrent step.
|
||||
@@ -114,12 +123,15 @@ def _gated_delta_step_ops(
|
||||
"""
|
||||
|
||||
# Decay
|
||||
old_state = state
|
||||
state = state * g[..., None, None]
|
||||
kv_mem = (state * k[..., None, :]).sum(axis=-1) # [B, H, Dv]
|
||||
delta = (v - kv_mem) * beta[..., None] # [B, H, Dv]
|
||||
state = state + k[..., None, :] * delta[..., None]
|
||||
# Output projection along key dim with q
|
||||
y = (state * q[..., None, :]).sum(axis=-1) # [B, H, Dv]
|
||||
if mask is not None:
|
||||
state = mx.where(mask, state, old_state)
|
||||
return y, state
|
||||
|
||||
|
||||
@@ -130,12 +142,18 @@ def gated_delta_kernel(
|
||||
g: mx.array,
|
||||
beta: mx.array,
|
||||
state: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
) -> Tuple[mx.array, mx.array]:
|
||||
B, T, Hk, Dk = k.shape
|
||||
Hv, Dv = v.shape[2:]
|
||||
input_type = q.dtype
|
||||
return _gated_delta_kernel(
|
||||
inputs=[q, k, v, g, beta, state, T],
|
||||
kernel = _gated_delta_kernel
|
||||
inputs = [q, k, v, g, beta, state, T]
|
||||
if mask is not None:
|
||||
kernel = _gated_delta_kernel_masked
|
||||
inputs.append(mask)
|
||||
return kernel(
|
||||
inputs=inputs,
|
||||
template=[
|
||||
("InT", input_type),
|
||||
("Dk", Dk),
|
||||
@@ -157,6 +175,7 @@ def gated_delta_ops(
|
||||
g: mx.array,
|
||||
beta: mx.array,
|
||||
state: Optional[mx.array] = None,
|
||||
mask: Optional[mx.array] = None,
|
||||
) -> Tuple[mx.array, mx.array]:
|
||||
"""
|
||||
Ops-based reference implementation for prompt prefill (sequential loop).
|
||||
@@ -181,14 +200,25 @@ def gated_delta_ops(
|
||||
|
||||
ys = []
|
||||
for t in range(T):
|
||||
y, state = _gated_delta_step_ops(
|
||||
q[:, t],
|
||||
k[:, t],
|
||||
v[:, t],
|
||||
g[:, t],
|
||||
beta[:, t],
|
||||
state,
|
||||
)
|
||||
if mask is not None:
|
||||
y, state = _gated_delta_step_ops(
|
||||
q[:, t],
|
||||
k[:, t],
|
||||
v[:, t],
|
||||
g[:, t],
|
||||
beta[:, t],
|
||||
state,
|
||||
mask[:, t],
|
||||
)
|
||||
else:
|
||||
y, state = _gated_delta_step_ops(
|
||||
q[:, t],
|
||||
k[:, t],
|
||||
v[:, t],
|
||||
g[:, t],
|
||||
beta[:, t],
|
||||
state,
|
||||
)
|
||||
ys.append(y)
|
||||
y = mx.stack(ys, axis=1)
|
||||
return y, state
|
||||
@@ -203,6 +233,8 @@ def gated_delta_update(
|
||||
A_log: mx.array,
|
||||
dt_bias: mx.array,
|
||||
state: Optional[mx.array] = None,
|
||||
mask: Optional[mx.array] = None,
|
||||
use_kernel: bool = True,
|
||||
) -> Tuple[mx.array, mx.array]:
|
||||
|
||||
beta = mx.sigmoid(b)
|
||||
@@ -213,7 +245,7 @@ def gated_delta_update(
|
||||
if state is None:
|
||||
state = mx.zeros((B, Hv, Dv, Dk), dtype=q.dtype)
|
||||
|
||||
if mx.default_device() != mx.gpu or not mx.metal.is_available():
|
||||
return gated_delta_ops(q, k, v, g, beta, state)
|
||||
if not use_kernel or mx.default_device() != mx.gpu or not mx.metal.is_available():
|
||||
return gated_delta_ops(q, k, v, g, beta, state, mask)
|
||||
else:
|
||||
return gated_delta_kernel(q, k, v, g, beta, state)
|
||||
return gated_delta_kernel(q, k, v, g, beta, state, mask)
|
||||
|
||||
@@ -87,8 +87,6 @@ class Attention(nn.Module):
|
||||
keys = self.rope(keys)
|
||||
|
||||
# Sliding window
|
||||
if isinstance(mask, mx.array) and mask.shape[-1] != keys.shape[-2]:
|
||||
mask = mask[..., -keys.shape[-2] :]
|
||||
output = scaled_dot_product_attention(
|
||||
queries, keys, values, cache=cache, scale=self.scale, mask=mask
|
||||
)
|
||||
@@ -194,7 +192,6 @@ class Gemma3Model(nn.Module):
|
||||
cache[0],
|
||||
window_size=self.window_size,
|
||||
)
|
||||
|
||||
for i, (layer, c) in enumerate(zip(self.layers, cache)):
|
||||
is_global = (
|
||||
i % self.sliding_window_pattern == self.sliding_window_pattern - 1
|
||||
@@ -246,7 +243,5 @@ class Model(nn.Module):
|
||||
):
|
||||
caches.append(KVCache())
|
||||
else:
|
||||
caches.append(
|
||||
RotatingKVCache(max_size=self.args.sliding_window, keep=0)
|
||||
)
|
||||
caches.append(RotatingKVCache(max_size=self.args.sliding_window))
|
||||
return caches
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright © 2023-2024 Apple Inc.
|
||||
# Copyright © 2023 - 2024 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Optional
|
||||
@@ -138,7 +138,9 @@ class GPT2Model(nn.Module):
|
||||
if cache[0] is not None:
|
||||
offset = cache[0].offset
|
||||
|
||||
position_ids = mx.arange(offset, offset + L)
|
||||
offset = mx.array(offset)
|
||||
position_ids = mx.arange(L) + offset[..., None]
|
||||
|
||||
hidden_states += self.wpe(position_ids)
|
||||
|
||||
mask = create_attention_mask(hidden_states, cache[0])
|
||||
|
||||
@@ -20,6 +20,7 @@ from .switch_layers import SwitchGLU
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
# Required fields (no defaults)
|
||||
model_type: str
|
||||
vocab_size: int
|
||||
hidden_size: int
|
||||
@@ -29,34 +30,42 @@ class ModelArgs(BaseModelArgs):
|
||||
num_attention_heads: int
|
||||
num_key_value_heads: int
|
||||
attention_bias: bool
|
||||
|
||||
# Scalar multipliers
|
||||
embedding_multiplier: float
|
||||
attention_multiplier: float
|
||||
logits_scaling: float
|
||||
residual_multiplier: float
|
||||
|
||||
# MoE parameters
|
||||
num_local_experts: int
|
||||
num_experts_per_tok: int
|
||||
shared_intermediate_size: int
|
||||
|
||||
# Mamba parameters
|
||||
mamba_n_heads: int
|
||||
mamba_d_head: int
|
||||
mamba_proj_bias: bool
|
||||
mamba_d_state: int
|
||||
mamba_d_conv: int
|
||||
mamba_n_groups: int
|
||||
mamba_conv_bias: bool
|
||||
|
||||
layer_types: List[str]
|
||||
rms_norm_eps: float
|
||||
rope_theta: float
|
||||
|
||||
# Optional fields (with defaults)
|
||||
# MoE parameters (optional for dense mode)
|
||||
num_local_experts: Optional[int] = None
|
||||
num_experts_per_tok: Optional[int] = None
|
||||
shared_intermediate_size: Optional[int] = None
|
||||
|
||||
# Mamba parameters (optional for non-hybrid mode)
|
||||
mamba_n_heads: Optional[int] = None
|
||||
mamba_d_head: Optional[int] = None
|
||||
mamba_proj_bias: Optional[bool] = None
|
||||
mamba_d_state: Optional[int] = None
|
||||
mamba_d_conv: Optional[int] = None
|
||||
mamba_n_groups: Optional[int] = None
|
||||
mamba_conv_bias: Optional[bool] = None
|
||||
|
||||
# Dense MLP parameters (for non-MoE mode)
|
||||
mlp_bias: bool = False
|
||||
|
||||
# Other optional parameters
|
||||
position_embedding_type: str = "rope"
|
||||
tie_word_embeddings: bool = True
|
||||
time_step_limit: Tuple[float, float] = (0.001, 100.0)
|
||||
|
||||
# Mode flags - inferred from num_local_experts
|
||||
@property
|
||||
def use_moe(self) -> bool:
|
||||
return bool(self.num_local_experts)
|
||||
|
||||
|
||||
class GraniteMoeHybridRMSNormGated(nn.Module):
|
||||
def __init__(self, hidden_size: int, eps: float = 1e-6):
|
||||
@@ -314,11 +323,27 @@ class GraniteMoeHybridSharedMLP(nn.Module):
|
||||
return self.output_linear(nn.silu(gate) * up)
|
||||
|
||||
|
||||
class GraniteMoeHybridMLP(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
dim = args.hidden_size
|
||||
hidden_dim = args.intermediate_size
|
||||
mlp_bias = args.mlp_bias
|
||||
|
||||
self.gate_proj = nn.Linear(dim, hidden_dim, bias=mlp_bias)
|
||||
self.down_proj = nn.Linear(hidden_dim, dim, bias=mlp_bias)
|
||||
self.up_proj = nn.Linear(dim, hidden_dim, bias=mlp_bias)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
|
||||
|
||||
class GraniteMoeHybridLayer(nn.Module):
|
||||
def __init__(self, args: ModelArgs, layer_type: str):
|
||||
super().__init__()
|
||||
self.layer_type = layer_type
|
||||
self.residual_multiplier = args.residual_multiplier
|
||||
self.use_moe = args.use_moe
|
||||
|
||||
self.input_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
|
||||
@@ -329,8 +354,14 @@ class GraniteMoeHybridLayer(nn.Module):
|
||||
else:
|
||||
raise ValueError(f"Unknown layer type: {layer_type}")
|
||||
|
||||
self.shared_mlp = GraniteMoeHybridSharedMLP(args)
|
||||
self.block_sparse_moe = GraniteMoeHybridMoE(args)
|
||||
# MoE or dense MLP after attention/mamba
|
||||
if self.use_moe:
|
||||
self.shared_mlp = GraniteMoeHybridSharedMLP(args)
|
||||
self.block_sparse_moe = GraniteMoeHybridMoE(args)
|
||||
else:
|
||||
# Dense MLP mode
|
||||
self.mlp = GraniteMoeHybridMLP(args)
|
||||
|
||||
self.post_attention_layernorm = nn.RMSNorm(
|
||||
args.hidden_size, eps=args.rms_norm_eps
|
||||
)
|
||||
@@ -352,13 +383,16 @@ class GraniteMoeHybridLayer(nn.Module):
|
||||
|
||||
hidden_states = residual + hidden_states * self.residual_multiplier
|
||||
|
||||
# Second block: MoE + shared_mlp
|
||||
# Second block: MoE + shared_mlp OR dense MLP
|
||||
residual = hidden_states
|
||||
normed = self.post_attention_layernorm(hidden_states)
|
||||
|
||||
moe_out = self.block_sparse_moe(normed)
|
||||
shared_out = self.shared_mlp(normed)
|
||||
mlp_out = moe_out + shared_out
|
||||
if self.use_moe:
|
||||
moe_out = self.block_sparse_moe(normed)
|
||||
shared_out = self.shared_mlp(normed)
|
||||
mlp_out = moe_out + shared_out
|
||||
else:
|
||||
mlp_out = self.mlp(normed)
|
||||
|
||||
hidden_states = residual + mlp_out * self.residual_multiplier
|
||||
|
||||
@@ -375,9 +409,16 @@ class GraniteMoeHybridModel(nn.Module):
|
||||
]
|
||||
self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.embedding_multiplier = args.embedding_multiplier
|
||||
self.fa_idx = args.layer_types.index("attention")
|
||||
self.ssm_idx = args.layer_types.index("mamba")
|
||||
self.layer_types = args.layer_types
|
||||
|
||||
# Handle hybrid vs non-hybrid mode
|
||||
self.fa_idx = (
|
||||
args.layer_types.index("attention")
|
||||
if "attention" in args.layer_types
|
||||
else None
|
||||
)
|
||||
self.ssm_idx = (
|
||||
args.layer_types.index("mamba") if "mamba" in args.layer_types else None
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
@@ -389,11 +430,16 @@ class GraniteMoeHybridModel(nn.Module):
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
|
||||
attn_mask = create_attention_mask(hidden_states, cache[self.fa_idx])
|
||||
mamba_mask = create_ssm_mask(hidden_states, cache[self.ssm_idx])
|
||||
# Create masks based on what layer types exist
|
||||
attn_mask = None
|
||||
mamba_mask = None
|
||||
|
||||
cache_counter = 0
|
||||
for layer, c, layer_type in zip(self.layers, cache, self.layer_types):
|
||||
if self.fa_idx is not None:
|
||||
attn_mask = create_attention_mask(hidden_states, cache[self.fa_idx])
|
||||
if self.ssm_idx is not None:
|
||||
mamba_mask = create_ssm_mask(hidden_states, cache[self.ssm_idx])
|
||||
|
||||
for layer, c in zip(self.layers, cache):
|
||||
mask = attn_mask if layer.layer_type == "attention" else mamba_mask
|
||||
hidden_states = layer(hidden_states, mask=mask, cache=c)
|
||||
|
||||
@@ -443,8 +489,11 @@ class Model(nn.Module):
|
||||
if "conv1d.weight" in k and v.shape[-1] != 1:
|
||||
weights[k] = v.moveaxis(2, 1)
|
||||
|
||||
# Handle MoE weight transformation to SwitchGLU format
|
||||
if "model.layers.0.block_sparse_moe.input_linear.weight" in weights:
|
||||
# Handle MoE weight transformation to SwitchGLU format (only for MoE models)
|
||||
if (
|
||||
self.args.use_moe
|
||||
and "model.layers.0.block_sparse_moe.input_linear.weight" in weights
|
||||
):
|
||||
for l in range(self.args.num_hidden_layers):
|
||||
prefix = f"model.layers.{l}.block_sparse_moe"
|
||||
|
||||
@@ -461,12 +510,31 @@ class Model(nn.Module):
|
||||
f"{prefix}.output_linear.weight"
|
||||
)
|
||||
|
||||
# Handle dense MLP weight transformation (for dense models)
|
||||
elif (
|
||||
not self.args.use_moe
|
||||
and "model.layers.0.shared_mlp.input_linear.weight" in weights
|
||||
):
|
||||
for l in range(self.args.num_hidden_layers):
|
||||
prefix = f"model.layers.{l}.shared_mlp"
|
||||
|
||||
# Transform shared_mlp weights to standard mlp weights
|
||||
input_weight = weights.pop(f"{prefix}.input_linear.weight")
|
||||
# Split into gate and up projections (each half)
|
||||
gate_proj, up_proj = mx.split(input_weight, 2, axis=0)
|
||||
weights[f"model.layers.{l}.mlp.gate_proj.weight"] = gate_proj
|
||||
weights[f"model.layers.{l}.mlp.up_proj.weight"] = up_proj
|
||||
|
||||
weights[f"model.layers.{l}.mlp.down_proj.weight"] = weights.pop(
|
||||
f"{prefix}.output_linear.weight"
|
||||
)
|
||||
|
||||
return weights
|
||||
|
||||
@property
|
||||
def quant_predicate(self):
|
||||
def predicate(path, _):
|
||||
if path.endswith("router.layer"):
|
||||
if self.args.use_moe and path.endswith("router.layer"):
|
||||
return {"group_size": 64, "bits": 8}
|
||||
return True
|
||||
|
||||
|
||||
@@ -0,0 +1,375 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, List, Optional, Union
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .base import (
|
||||
BaseModelArgs,
|
||||
create_attention_mask,
|
||||
create_ssm_mask,
|
||||
scaled_dot_product_attention,
|
||||
)
|
||||
from .cache import KVCache, MambaCache
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str
|
||||
hidden_size: int
|
||||
intermediate_size: int
|
||||
num_hidden_layers: int
|
||||
num_attention_heads: int
|
||||
num_key_value_heads: int
|
||||
attn_layer_offset: int
|
||||
attn_layer_period: int
|
||||
expert_layer_offset: int
|
||||
expert_layer_period: int
|
||||
mamba_d_conv: int
|
||||
mamba_d_state: int
|
||||
mamba_expand: int
|
||||
num_experts: int
|
||||
num_experts_per_tok: int
|
||||
rms_norm_eps: float
|
||||
max_position_embeddings: int
|
||||
vocab_size: int
|
||||
mamba_dt_rank: Union[str, int] = "auto"
|
||||
mamba_proj_bias: bool = False
|
||||
mamba_conv_bias: bool = True
|
||||
layers_block_type: Optional[List[str]] = None
|
||||
tie_word_embeddings: bool = True
|
||||
|
||||
def __post_init__(self):
|
||||
if self.mamba_dt_rank == "auto":
|
||||
self.mamba_dt_rank = math.ceil(self.hidden_size / 16)
|
||||
if self.layers_block_type is None:
|
||||
self.layers_block_type = [
|
||||
(
|
||||
"attention"
|
||||
if i % self.attn_layer_period == self.attn_layer_offset
|
||||
else "mamba"
|
||||
)
|
||||
for i in range(self.num_hidden_layers)
|
||||
]
|
||||
|
||||
|
||||
class JambaMLP(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.gate_proj = nn.Linear(args.hidden_size, args.intermediate_size, bias=False)
|
||||
self.up_proj = nn.Linear(args.hidden_size, args.intermediate_size, bias=False)
|
||||
self.down_proj = nn.Linear(args.intermediate_size, args.hidden_size, bias=False)
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
|
||||
|
||||
class JambaAttention(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.num_attention_heads = args.num_attention_heads
|
||||
self.num_key_value_heads = args.num_key_value_heads
|
||||
self.head_dim = args.hidden_size // args.num_attention_heads
|
||||
self.scale = self.head_dim**-0.5
|
||||
|
||||
self.q_proj = nn.Linear(
|
||||
args.hidden_size, args.num_attention_heads * self.head_dim, bias=False
|
||||
)
|
||||
self.k_proj = nn.Linear(
|
||||
args.hidden_size, args.num_key_value_heads * self.head_dim, bias=False
|
||||
)
|
||||
self.v_proj = nn.Linear(
|
||||
args.hidden_size, args.num_key_value_heads * self.head_dim, bias=False
|
||||
)
|
||||
self.o_proj = nn.Linear(
|
||||
args.num_attention_heads * self.head_dim, args.hidden_size, bias=False
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
B, L, D = x.shape
|
||||
|
||||
queries, keys, values = self.q_proj(x), self.k_proj(x), self.v_proj(x)
|
||||
|
||||
queries = queries.reshape(B, L, self.num_attention_heads, -1).transpose(
|
||||
0, 2, 1, 3
|
||||
)
|
||||
keys = keys.reshape(B, L, self.num_key_value_heads, -1).transpose(0, 2, 1, 3)
|
||||
values = values.reshape(B, L, self.num_key_value_heads, -1).transpose(
|
||||
0, 2, 1, 3
|
||||
)
|
||||
|
||||
if cache is not None:
|
||||
keys, values = cache.update_and_fetch(keys, values)
|
||||
|
||||
output = scaled_dot_product_attention(
|
||||
queries, keys, values, cache=cache, scale=self.scale, mask=mask
|
||||
)
|
||||
|
||||
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
|
||||
return self.o_proj(output)
|
||||
|
||||
|
||||
@mx.compile
|
||||
def fma(a, b, c):
|
||||
return a * b + c
|
||||
|
||||
|
||||
class JambaMambaMixer(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.hidden_size = args.hidden_size
|
||||
self.ssm_state_size = args.mamba_d_state
|
||||
self.conv_kernel_size = args.mamba_d_conv
|
||||
self.intermediate_size = args.mamba_expand * args.hidden_size
|
||||
self.time_step_rank = args.mamba_dt_rank
|
||||
self.use_conv_bias = args.mamba_conv_bias
|
||||
self.use_bias = args.mamba_proj_bias
|
||||
|
||||
self.in_proj = nn.Linear(
|
||||
self.hidden_size, self.intermediate_size * 2, bias=self.use_bias
|
||||
)
|
||||
|
||||
self.conv1d = nn.Conv1d(
|
||||
in_channels=self.intermediate_size,
|
||||
out_channels=self.intermediate_size,
|
||||
kernel_size=self.conv_kernel_size,
|
||||
groups=self.intermediate_size,
|
||||
bias=self.use_conv_bias,
|
||||
padding=0,
|
||||
)
|
||||
self.x_proj = nn.Linear(
|
||||
self.intermediate_size,
|
||||
self.time_step_rank + self.ssm_state_size * 2,
|
||||
bias=False,
|
||||
)
|
||||
self.dt_proj = nn.Linear(self.time_step_rank, self.intermediate_size, bias=True)
|
||||
|
||||
A = mx.repeat(
|
||||
mx.arange(1.0, self.ssm_state_size + 1.0).reshape([1, self.ssm_state_size]),
|
||||
repeats=self.intermediate_size,
|
||||
axis=0,
|
||||
)
|
||||
self.A_log = mx.log(A)
|
||||
self.D = mx.ones([self.intermediate_size])
|
||||
|
||||
self.out_proj = nn.Linear(
|
||||
self.intermediate_size, self.hidden_size, bias=self.use_bias
|
||||
)
|
||||
|
||||
self.dt_layernorm = nn.RMSNorm(self.time_step_rank, eps=args.rms_norm_eps)
|
||||
self.b_layernorm = nn.RMSNorm(self.ssm_state_size, eps=args.rms_norm_eps)
|
||||
self.c_layernorm = nn.RMSNorm(self.ssm_state_size, eps=args.rms_norm_eps)
|
||||
|
||||
def ssm_step(self, x, A, state=None):
|
||||
T = x.shape[1]
|
||||
D = self.D
|
||||
deltaBC = self.x_proj(x)
|
||||
delta, B, C = mx.split(
|
||||
deltaBC,
|
||||
[self.time_step_rank, self.time_step_rank + self.ssm_state_size],
|
||||
axis=-1,
|
||||
)
|
||||
delta, B, C = self.dt_layernorm(delta), self.b_layernorm(B), self.c_layernorm(C)
|
||||
delta = nn.softplus(self.dt_proj(delta))
|
||||
new_state = mx.expand_dims(delta * x, -1) * mx.expand_dims(B, -2)
|
||||
dtA = mx.exp(mx.expand_dims(delta, -1) * A)
|
||||
|
||||
# TODO, speed up prefill with chunked scan
|
||||
for t in range(T):
|
||||
if state is not None:
|
||||
new_state[:, t] = fma(state, dtA[:, t], new_state[:, t])
|
||||
state = new_state[:, t]
|
||||
y = (new_state @ mx.expand_dims(C, -1)).squeeze(-1)
|
||||
y = y + D * x
|
||||
return y, new_state[:, -1]
|
||||
|
||||
def _process_sequence(self, x, conv_state, ssm_state):
|
||||
xz = self.in_proj(x)
|
||||
x, z = xz.split(indices_or_sections=2, axis=-1)
|
||||
K = self.conv_kernel_size
|
||||
if conv_state is not None:
|
||||
x_full = mx.concatenate([conv_state, x], axis=1)
|
||||
else:
|
||||
x_full = mx.pad(x, [(0, 0), (K - 1, 0), (0, 0)])
|
||||
conv_out = self.conv1d(x_full)
|
||||
conv_state = x_full[:, -(K - 1) :, :]
|
||||
x = nn.silu(conv_out)
|
||||
A = -mx.exp(self.A_log)
|
||||
y, ssm_state = self.ssm_step(x, A, ssm_state)
|
||||
z = self.out_proj(nn.silu(z) * y)
|
||||
return z, (conv_state, ssm_state)
|
||||
|
||||
def __call__(self, x, cache):
|
||||
if cache is None:
|
||||
conv_state, ssm_state = None, None
|
||||
else:
|
||||
conv_state, ssm_state = cache[0], cache[1]
|
||||
|
||||
output, (conv_state, ssm_state) = self._process_sequence(
|
||||
x, conv_state, ssm_state
|
||||
)
|
||||
|
||||
if cache is not None:
|
||||
cache[0] = conv_state
|
||||
cache[1] = ssm_state
|
||||
|
||||
return output
|
||||
|
||||
|
||||
class JambaSparseMoeBlock(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.num_experts_per_tok = args.num_experts_per_tok
|
||||
|
||||
self.router = nn.Linear(args.hidden_size, args.num_experts, bias=False)
|
||||
self.switch_mlp = SwitchGLU(
|
||||
args.hidden_size, args.intermediate_size, args.num_experts
|
||||
)
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
gates = self.router(x)
|
||||
k = self.num_experts_per_tok
|
||||
inds = mx.stop_gradient(mx.argpartition(-gates, kth=k - 1, axis=-1)[..., :k])
|
||||
scores = mx.take_along_axis(gates, inds, axis=-1)
|
||||
scores = mx.softmax(scores, axis=-1, precise=True)
|
||||
y = self.switch_mlp(x, inds)
|
||||
y = (y * scores[..., None]).sum(axis=-2)
|
||||
return y
|
||||
|
||||
|
||||
class JambaDecoderLayer(nn.Module):
|
||||
def __init__(self, args: ModelArgs, layer_type: str):
|
||||
super().__init__()
|
||||
self.is_attn = layer_type == "attention"
|
||||
if self.is_attn:
|
||||
self.self_attn = JambaAttention(args)
|
||||
else:
|
||||
self.mamba = JambaMambaMixer(args)
|
||||
ffn_layer_class = JambaSparseMoeBlock if args.num_experts > 1 else JambaMLP
|
||||
self.feed_forward = ffn_layer_class(args)
|
||||
self.input_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.pre_ff_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
if self.is_attn:
|
||||
h = self.self_attn(self.input_layernorm(x), mask, cache)
|
||||
else:
|
||||
h = self.mamba(self.input_layernorm(x), cache)
|
||||
r = x + h
|
||||
out = r + self.feed_forward(self.pre_ff_layernorm(r))
|
||||
return out
|
||||
|
||||
|
||||
class JambaModel(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
|
||||
|
||||
self.layers = [JambaDecoderLayer(args, t) for t in args.layers_block_type]
|
||||
self.final_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.attn_idx = args.layers_block_type.index("attention")
|
||||
self.ssm_idx = args.layers_block_type.index("mamba")
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
h = self.embed_tokens(inputs)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
|
||||
attn_mask = create_attention_mask(h, cache[self.attn_idx])
|
||||
ssm_mask = create_ssm_mask(h, cache[self.ssm_idx])
|
||||
|
||||
for layer, c in zip(self.layers, cache):
|
||||
mask = attn_mask if layer.is_attn else ssm_mask
|
||||
h = layer(h, mask=mask, cache=c)
|
||||
|
||||
return self.final_layernorm(h)
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.model_type = args.model_type
|
||||
self.args = args
|
||||
self.model = JambaModel(args)
|
||||
if not args.tie_word_embeddings:
|
||||
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
out = self.model(inputs, cache)
|
||||
if self.args.tie_word_embeddings:
|
||||
out = self.model.embed_tokens.as_linear(out)
|
||||
else:
|
||||
out = self.lm_head(out)
|
||||
return out
|
||||
|
||||
def make_cache(self):
|
||||
caches = []
|
||||
for layer in self.model.layers:
|
||||
if layer.is_attn:
|
||||
caches.append(KVCache())
|
||||
else:
|
||||
caches.append(MambaCache())
|
||||
return caches
|
||||
|
||||
def sanitize(self, weights):
|
||||
for k, v in weights.items():
|
||||
if "conv1d.weight" in k and v.shape[-1] != 1:
|
||||
weights[k] = v.moveaxis(2, 1)
|
||||
|
||||
if self.args.tie_word_embeddings:
|
||||
weights.pop("lm_head.weight", None)
|
||||
|
||||
if "model.layers.0.block_sparse_moe.experts.0.w1.weight" not in weights:
|
||||
return weights
|
||||
|
||||
for l in range(self.args.num_hidden_layers):
|
||||
prefix = f"model.layers.{l}"
|
||||
for n, m in [("w1", "gate_proj"), ("w2", "down_proj"), ("w3", "up_proj")]:
|
||||
for k in ["weight", "scales", "biases"]:
|
||||
if f"{prefix}.block_sparse_moe.experts.0.{n}.{k}" in weights:
|
||||
to_join = [
|
||||
weights.pop(
|
||||
f"{prefix}.block_sparse_moe.experts.{e}.{n}.{k}"
|
||||
)
|
||||
for e in range(self.args.num_local_experts)
|
||||
]
|
||||
weights[f"{prefix}.block_sparse_moe.switch_mlp.{m}.{k}"] = (
|
||||
mx.stack(to_join)
|
||||
)
|
||||
return weights
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
|
||||
@property
|
||||
def quant_predicate(self):
|
||||
def predicate(path, _):
|
||||
if path.endswith("router"):
|
||||
return {"group_size": 64, "bits": 8}
|
||||
return True
|
||||
|
||||
return predicate
|
||||
@@ -18,11 +18,6 @@ class ModelArgs(BaseModelArgs):
|
||||
|
||||
def __post_init__(self):
|
||||
self.text_config["tie_word_embeddings"] = False
|
||||
self.text_config["full_attn_idxs"] = [
|
||||
i
|
||||
for i, layer_type in enumerate(self.text_config["layer_types"])
|
||||
if layer_type == "full_attention"
|
||||
]
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
|
||||
+12
-2
@@ -31,8 +31,19 @@ class ModelArgs(BaseModelArgs):
|
||||
block_multiple_of: int
|
||||
block_ffn_dim_multiplier: float
|
||||
block_auto_adjust_ff_dim: bool
|
||||
full_attn_idxs: List[int]
|
||||
rope_theta: float
|
||||
full_attn_idxs: Optional[List[int]] = None
|
||||
layer_types: Optional[List[str]] = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.num_key_value_heads is None:
|
||||
self.num_key_value_heads = self.num_attention_heads
|
||||
if self.full_attn_idxs is None:
|
||||
self.full_attn_idxs = [
|
||||
i
|
||||
for i, layer_type in enumerate(self.layer_types)
|
||||
if layer_type == "full_attention"
|
||||
]
|
||||
|
||||
|
||||
class Attention(nn.Module):
|
||||
@@ -122,7 +133,6 @@ class ShortConv(nn.Module):
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
):
|
||||
seqlen = x.shape[1]
|
||||
BCx = self.in_proj(x)
|
||||
B, C, x = mx.split(BCx, 3, axis=-1)
|
||||
Bx = B * x
|
||||
|
||||
@@ -0,0 +1,372 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, List, Optional
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .base import (
|
||||
BaseModelArgs,
|
||||
create_attention_mask,
|
||||
create_ssm_mask,
|
||||
scaled_dot_product_attention,
|
||||
)
|
||||
from .cache import ArraysCache, KVCache
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str
|
||||
vocab_size: int
|
||||
hidden_size: int
|
||||
intermediate_size: int
|
||||
moe_intermediate_size: int
|
||||
num_hidden_layers: int
|
||||
num_experts: int
|
||||
num_experts_per_tok: int
|
||||
norm_topk_prob: bool
|
||||
num_attention_heads: int
|
||||
num_key_value_heads: int
|
||||
max_position_embeddings: int
|
||||
use_expert_bias: bool
|
||||
num_dense_layers: int
|
||||
norm_eps: float
|
||||
conv_bias: bool
|
||||
conv_L_cache: int
|
||||
rope_theta: float
|
||||
full_attn_idxs: Optional[List[int]] = None
|
||||
layer_types: Optional[List[str]] = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.full_attn_idxs is None:
|
||||
self.full_attn_idxs = [
|
||||
i
|
||||
for i, layer_type in enumerate(self.layer_types)
|
||||
if layer_type == "full_attention"
|
||||
]
|
||||
|
||||
|
||||
class Attention(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
|
||||
dim = args.hidden_size
|
||||
self.n_heads = n_heads = args.num_attention_heads
|
||||
self.n_kv_heads = n_kv_heads = args.num_key_value_heads
|
||||
|
||||
self.head_dim = head_dim = args.hidden_size // n_heads
|
||||
|
||||
self.scale = head_dim**-0.5
|
||||
|
||||
self.q_layernorm = nn.RMSNorm(head_dim, eps=args.norm_eps)
|
||||
self.k_layernorm = nn.RMSNorm(head_dim, eps=args.norm_eps)
|
||||
|
||||
self.q_proj = nn.Linear(dim, n_heads * head_dim, bias=False)
|
||||
self.k_proj = nn.Linear(dim, n_kv_heads * head_dim, bias=False)
|
||||
self.v_proj = nn.Linear(dim, n_kv_heads * head_dim, bias=False)
|
||||
self.out_proj = nn.Linear(n_heads * head_dim, dim, bias=False)
|
||||
|
||||
self.rope = nn.RoPE(
|
||||
self.head_dim,
|
||||
base=args.rope_theta,
|
||||
traditional=False,
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
B, L, D = x.shape
|
||||
|
||||
queries, keys, values = self.q_proj(x), self.k_proj(x), self.v_proj(x)
|
||||
|
||||
queries = self.q_layernorm(queries.reshape(B, L, self.n_heads, -1)).transpose(
|
||||
0, 2, 1, 3
|
||||
)
|
||||
keys = self.k_layernorm(keys.reshape(B, L, self.n_kv_heads, -1)).transpose(
|
||||
0, 2, 1, 3
|
||||
)
|
||||
values = values.reshape(B, L, self.n_kv_heads, -1).transpose(0, 2, 1, 3)
|
||||
|
||||
if cache is not None:
|
||||
queries = self.rope(queries, offset=cache.offset)
|
||||
keys = self.rope(keys, offset=cache.offset)
|
||||
keys, values = cache.update_and_fetch(keys, values)
|
||||
else:
|
||||
queries = self.rope(queries)
|
||||
keys = self.rope(keys)
|
||||
|
||||
output = scaled_dot_product_attention(
|
||||
queries, keys, values, cache=cache, mask=mask, scale=self.scale
|
||||
)
|
||||
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
|
||||
return self.out_proj(output)
|
||||
|
||||
|
||||
class ShortConv(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
args: ModelArgs,
|
||||
layer_idx: int,
|
||||
):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.layer_idx = layer_idx
|
||||
self.L_cache = args.conv_L_cache
|
||||
self.bias = args.conv_bias
|
||||
|
||||
self.conv = nn.Conv1d(
|
||||
in_channels=args.hidden_size,
|
||||
out_channels=args.hidden_size,
|
||||
kernel_size=self.L_cache,
|
||||
groups=args.hidden_size,
|
||||
bias=self.bias,
|
||||
)
|
||||
self.in_proj = nn.Linear(args.hidden_size, 3 * args.hidden_size, bias=self.bias)
|
||||
self.out_proj = nn.Linear(args.hidden_size, args.hidden_size, bias=self.bias)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
):
|
||||
BCx = self.in_proj(x)
|
||||
B, C, x = mx.split(BCx, 3, axis=-1)
|
||||
Bx = B * x
|
||||
if mask is not None:
|
||||
Bx = mx.where(mask[..., None], Bx, 0)
|
||||
state = None
|
||||
if cache is not None:
|
||||
state = cache[0]
|
||||
if state is None:
|
||||
state = mx.zeros(
|
||||
(Bx.shape[0], self.L_cache - 1, self.args.hidden_size), dtype=Bx.dtype
|
||||
)
|
||||
|
||||
Bx = mx.concatenate([state, Bx], axis=-2)
|
||||
if cache is not None:
|
||||
cache[0] = Bx[:, -(self.L_cache - 1) :]
|
||||
conv_out = self.conv(Bx)
|
||||
|
||||
y = C * conv_out
|
||||
return self.out_proj(y)
|
||||
|
||||
|
||||
class MLP(nn.Module):
|
||||
def __init__(self, config: ModelArgs, intermediate_size: Optional[int] = None):
|
||||
super().__init__()
|
||||
self.hidden_size = config.hidden_size
|
||||
self.intermediate_size = (
|
||||
config.intermediate_size if intermediate_size is None else intermediate_size
|
||||
)
|
||||
self.gate_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=False)
|
||||
self.up_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=False)
|
||||
self.down_proj = nn.Linear(self.intermediate_size, self.hidden_size, bias=False)
|
||||
|
||||
def __call__(self, x) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
|
||||
|
||||
class Lfm2MoeSparseMoeBlock(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
dim = args.hidden_size
|
||||
intermediate_size = args.moe_intermediate_size
|
||||
|
||||
self.num_experts = num_experts = args.num_experts
|
||||
self.top_k = args.num_experts_per_tok
|
||||
self.norm_topk_prob = args.norm_topk_prob
|
||||
self.use_expert_bias = args.use_expert_bias
|
||||
|
||||
self.gate = nn.Linear(dim, num_experts, bias=False)
|
||||
self.switch_mlp = SwitchGLU(dim, intermediate_size, num_experts)
|
||||
if self.use_expert_bias:
|
||||
self.expert_bias = mx.zeros((self.num_experts,))
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
):
|
||||
gates = self.gate(x).astype(mx.float32)
|
||||
gates = mx.softmax(gates, axis=-1)
|
||||
|
||||
if self.use_expert_bias:
|
||||
gates += self.expert_bias
|
||||
|
||||
k = self.top_k
|
||||
inds = mx.argpartition(gates, kth=-k, axis=-1)[..., -k:]
|
||||
|
||||
scores = mx.take_along_axis(gates, inds, axis=-1)
|
||||
if self.norm_topk_prob:
|
||||
scores /= mx.sum(scores, axis=-1, keepdims=True) + 1e-20
|
||||
scores = scores.astype(x.dtype)
|
||||
|
||||
y = self.switch_mlp(x, inds)
|
||||
y = (y * scores[..., None]).sum(axis=-2)
|
||||
|
||||
return y
|
||||
|
||||
|
||||
class Lfm2DecoderLayer(nn.Module):
|
||||
def __init__(self, args: ModelArgs, layer_idx: int):
|
||||
super().__init__()
|
||||
self.is_attention_layer = layer_idx in args.full_attn_idxs
|
||||
|
||||
if self.is_attention_layer:
|
||||
self.self_attn = Attention(args)
|
||||
else:
|
||||
self.conv = ShortConv(args, layer_idx)
|
||||
self.feed_forward = (
|
||||
MLP(
|
||||
config=args,
|
||||
intermediate_size=args.intermediate_size,
|
||||
)
|
||||
if layer_idx < args.num_dense_layers
|
||||
else Lfm2MoeSparseMoeBlock(args)
|
||||
)
|
||||
|
||||
self.operator_norm = nn.RMSNorm(args.hidden_size, eps=args.norm_eps)
|
||||
self.ffn_norm = nn.RMSNorm(args.hidden_size, eps=args.norm_eps)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
|
||||
if self.is_attention_layer:
|
||||
r = self.self_attn(self.operator_norm(x), mask=mask, cache=cache)
|
||||
else:
|
||||
r = self.conv(
|
||||
self.operator_norm(x),
|
||||
mask=mask,
|
||||
cache=cache,
|
||||
)
|
||||
h = x + r
|
||||
out = h + self.feed_forward(self.ffn_norm(h))
|
||||
return out
|
||||
|
||||
|
||||
class Lfm2Model(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.vocab_size = args.vocab_size
|
||||
self.num_hidden_layers = args.num_hidden_layers
|
||||
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
|
||||
self.layers = [
|
||||
Lfm2DecoderLayer(args, layer_idx=i) for i in range(args.num_hidden_layers)
|
||||
]
|
||||
|
||||
self.embedding_norm = nn.RMSNorm(args.hidden_size, eps=args.norm_eps)
|
||||
|
||||
self.fa_idx = args.full_attn_idxs[0]
|
||||
self.conv_idx = 0
|
||||
for i in range(args.num_hidden_layers):
|
||||
if i in args.full_attn_idxs:
|
||||
self.conv_idx += 1
|
||||
else:
|
||||
break
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache=None,
|
||||
input_embeddings: Optional[mx.array] = None,
|
||||
):
|
||||
if input_embeddings is not None:
|
||||
h = input_embeddings
|
||||
else:
|
||||
h = self.embed_tokens(inputs)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
|
||||
attn_mask = create_attention_mask(h, cache[self.fa_idx])
|
||||
conv_mask = create_ssm_mask(h, cache[self.conv_idx])
|
||||
|
||||
for layer, c in zip(self.layers, cache):
|
||||
mask = attn_mask if layer.is_attention_layer else conv_mask
|
||||
h = layer(h, mask, cache=c)
|
||||
|
||||
return self.embedding_norm(h)
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.model_type = args.model_type
|
||||
self.model = Lfm2Model(args)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache=None,
|
||||
input_embeddings: Optional[mx.array] = None,
|
||||
):
|
||||
out = self.model(inputs, cache, input_embeddings)
|
||||
return self.model.embed_tokens.as_linear(out)
|
||||
|
||||
def sanitize(self, weights):
|
||||
sanitized_weights = {}
|
||||
for name, param in weights.items():
|
||||
if "conv.weight" in name:
|
||||
if param.shape[-1] > param.shape[1]:
|
||||
param = param.transpose(0, 2, 1)
|
||||
replacements = {
|
||||
"w1.weight": "gate_proj.weight",
|
||||
"w2.weight": "down_proj.weight",
|
||||
"w3.weight": "up_proj.weight",
|
||||
}
|
||||
for old, new in replacements.items():
|
||||
if old in name:
|
||||
name = name.replace(old, new)
|
||||
sanitized_weights[name] = param
|
||||
|
||||
for l in range(self.args.num_hidden_layers):
|
||||
prefix = f"model.layers.{l}"
|
||||
# Only sanitize MoE layer weights
|
||||
for n in ["gate_proj", "down_proj", "up_proj"]:
|
||||
if f"{prefix}.feed_forward.experts.0.{n}.weight" in sanitized_weights:
|
||||
to_join = [
|
||||
sanitized_weights.pop(
|
||||
f"{prefix}.feed_forward.experts.{e}.{n}.weight"
|
||||
)
|
||||
for e in range(self.args.num_experts)
|
||||
]
|
||||
sanitized_weights[
|
||||
f"{prefix}.feed_forward.switch_mlp.{n}.weight"
|
||||
] = mx.stack(to_join)
|
||||
return sanitized_weights
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
|
||||
def make_cache(self):
|
||||
return [
|
||||
KVCache() if l.is_attention_layer else ArraysCache(size=1)
|
||||
for l in self.layers
|
||||
]
|
||||
|
||||
@property
|
||||
def quant_predicate(self):
|
||||
def predicate(path, _):
|
||||
if path.endswith("feed_forward.gate"):
|
||||
return {"group_size": 64, "bits": 8}
|
||||
return True
|
||||
|
||||
return predicate
|
||||
|
||||
@property
|
||||
def cast_predicate(self):
|
||||
def predicate(k):
|
||||
return "expert_bias" not in k
|
||||
|
||||
return predicate
|
||||
+37
-6
@@ -1,12 +1,13 @@
|
||||
# Copyright © 2023-2024 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, Optional, Union
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .cache import KVCache, RotatingKVCache
|
||||
from .rope_utils import initialize_rope
|
||||
|
||||
|
||||
@@ -28,11 +29,16 @@ class ModelArgs(BaseModelArgs):
|
||||
rope_traditional: bool = False
|
||||
rope_scaling: Optional[Dict[str, Union[float, str]]] = None
|
||||
tie_word_embeddings: bool = True
|
||||
layer_types: Optional[List[str]] = None
|
||||
sliding_window: Optional[int] = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.num_key_value_heads is None:
|
||||
self.num_key_value_heads = self.num_attention_heads
|
||||
|
||||
if self.layer_types is None:
|
||||
self.layer_types = ["full_attention"] * self.num_hidden_layers
|
||||
|
||||
|
||||
class Attention(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
@@ -114,10 +120,11 @@ class MLP(nn.Module):
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
def __init__(self, args: ModelArgs, use_sliding: bool = False):
|
||||
super().__init__()
|
||||
self.num_attention_heads = args.num_attention_heads
|
||||
self.hidden_size = args.hidden_size
|
||||
self.use_sliding = use_sliding
|
||||
self.self_attn = Attention(args)
|
||||
self.mlp = MLP(args)
|
||||
self.input_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
@@ -145,12 +152,21 @@ class LlamaModel(nn.Module):
|
||||
self.args = args
|
||||
self.vocab_size = args.vocab_size
|
||||
self.num_hidden_layers = args.num_hidden_layers
|
||||
self.layer_types = args.layer_types
|
||||
self.sliding_window = args.sliding_window
|
||||
assert self.vocab_size > 0
|
||||
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
|
||||
self.layers = [
|
||||
TransformerBlock(args=args) for _ in range(args.num_hidden_layers)
|
||||
TransformerBlock(args=args, use_sliding=layer_type == "sliding_attention")
|
||||
for layer_type in self.layer_types
|
||||
]
|
||||
self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.fa_idx = self.layer_types.index("full_attention")
|
||||
self.swa_idx = None
|
||||
for e, l in enumerate(self.layers):
|
||||
if l.use_sliding:
|
||||
self.swa_idx = e
|
||||
break
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
@@ -166,10 +182,15 @@ class LlamaModel(nn.Module):
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
|
||||
mask = create_attention_mask(h, cache[0])
|
||||
fa_mask = create_attention_mask(h, cache[self.fa_idx])
|
||||
if self.swa_idx is not None:
|
||||
swa_mask = create_attention_mask(
|
||||
h, cache[self.swa_idx], window_size=self.sliding_window
|
||||
)
|
||||
|
||||
for layer, c in zip(self.layers, cache):
|
||||
h = layer(h, mask, cache=c)
|
||||
for layer, cache in zip(self.layers, cache):
|
||||
mask = swa_mask if layer.use_sliding else fa_mask
|
||||
h = layer(h, mask, cache=cache)
|
||||
|
||||
return self.norm(h)
|
||||
|
||||
@@ -208,3 +229,13 @@ class Model(nn.Module):
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
|
||||
def make_cache(self):
|
||||
return [
|
||||
(
|
||||
RotatingKVCache(max_size=self.model.sliding_window)
|
||||
if layer.use_sliding
|
||||
else KVCache()
|
||||
)
|
||||
for layer in self.layers
|
||||
]
|
||||
|
||||
@@ -235,7 +235,7 @@ class LongcatFlashMoE(nn.Module):
|
||||
|
||||
regular_outputs = self.switch_mlp(hidden_states, topk_indices)
|
||||
|
||||
weighted_outputs = regular_outputs * topk_weights[..., None]
|
||||
weighted_outputs = regular_outputs * regular_weights[..., None]
|
||||
|
||||
# Add identity expert contribution if needed
|
||||
assert self.zero_expert_type == "identity"
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from functools import partial
|
||||
from typing import Any, Optional
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str = "nanochat"
|
||||
hidden_size: int = 1280
|
||||
num_hidden_layers: int = 20
|
||||
num_attention_heads: int = 10
|
||||
num_key_value_heads: int = 10
|
||||
vocab_size: int = 65536
|
||||
max_position_embeddings: int = 2048
|
||||
intermediate_size: int = 5120 # 4 * hidden_size
|
||||
rope_theta: float = 10000.0
|
||||
|
||||
|
||||
def rms_norm(x):
|
||||
"""Functional RMSNorm with no learnable parameters."""
|
||||
return mx.fast.rms_norm(x, None, 1e-5)
|
||||
|
||||
|
||||
def apply_rotary_emb(x, offset, base=10000.0, freqs=None):
|
||||
"""Apply RoPE with blocked layout.
|
||||
|
||||
|
||||
Args:
|
||||
x: Input tensor in (B, H, T, D) format
|
||||
offset: Position offset for KV caching
|
||||
base: RoPE base frequency (default 10000.0)
|
||||
freqs: Precomputed negated frequencies (optional)
|
||||
|
||||
Returns:
|
||||
Tensor with RoPE applied, same shape as input
|
||||
"""
|
||||
head_dim = x.shape[-1]
|
||||
|
||||
if freqs is None:
|
||||
# Compute negated frequencies
|
||||
half_D = head_dim // 2
|
||||
freqs = -mx.exp(
|
||||
mx.arange(0.0, half_D, dtype=mx.float32) * (math.log(base) / half_D)
|
||||
)
|
||||
|
||||
# Use traditional=False + negated freqs
|
||||
return mx.fast.rope(
|
||||
x,
|
||||
dims=head_dim,
|
||||
traditional=False,
|
||||
base=None,
|
||||
freqs=freqs,
|
||||
scale=1.0,
|
||||
offset=offset,
|
||||
)
|
||||
|
||||
|
||||
class Attention(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
|
||||
self.hidden_size = args.hidden_size
|
||||
self.num_heads = args.num_attention_heads
|
||||
self.num_kv_heads = args.num_key_value_heads
|
||||
self.head_dim = self.hidden_size // self.num_heads
|
||||
self.scale = self.head_dim**-0.5
|
||||
self.rope_theta = args.rope_theta
|
||||
|
||||
self.c_q = nn.Linear(
|
||||
self.hidden_size, self.num_heads * self.head_dim, bias=False
|
||||
)
|
||||
self.c_k = nn.Linear(
|
||||
self.hidden_size, self.num_kv_heads * self.head_dim, bias=False
|
||||
)
|
||||
self.c_v = nn.Linear(
|
||||
self.hidden_size, self.num_kv_heads * self.head_dim, bias=False
|
||||
)
|
||||
self.c_proj = nn.Linear(self.hidden_size, self.hidden_size, bias=False)
|
||||
|
||||
# Precompute negated RoPE frequencies for awni's approach
|
||||
half_D = self.head_dim // 2
|
||||
self._rope_freqs = -mx.exp(
|
||||
mx.arange(0.0, half_D, dtype=mx.float32)
|
||||
* (math.log(self.rope_theta) / half_D)
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
B, L, _ = x.shape
|
||||
|
||||
queries = self.c_q(x)
|
||||
keys = self.c_k(x)
|
||||
values = self.c_v(x)
|
||||
|
||||
# Reshape to (B, L, H, D) then transpose to (B, H, L, D)
|
||||
queries = queries.reshape(B, L, self.num_heads, self.head_dim).transpose(
|
||||
0, 2, 1, 3
|
||||
)
|
||||
keys = keys.reshape(B, L, self.num_kv_heads, self.head_dim).transpose(
|
||||
0, 2, 1, 3
|
||||
)
|
||||
values = values.reshape(B, L, self.num_kv_heads, self.head_dim).transpose(
|
||||
0, 2, 1, 3
|
||||
)
|
||||
|
||||
# Apply RoPE using precomputed frequencies (expects B, H, T, D format)
|
||||
offset = cache.offset if cache is not None else 0
|
||||
queries = apply_rotary_emb(
|
||||
queries, offset=offset, base=self.rope_theta, freqs=self._rope_freqs
|
||||
)
|
||||
keys = apply_rotary_emb(
|
||||
keys, offset=offset, base=self.rope_theta, freqs=self._rope_freqs
|
||||
)
|
||||
|
||||
# QK norm (critical feature of nanochat!)
|
||||
queries = rms_norm(queries)
|
||||
keys = rms_norm(keys)
|
||||
|
||||
# Handle KV cache after transpose
|
||||
if cache is not None:
|
||||
keys, values = cache.update_and_fetch(keys, values)
|
||||
|
||||
output = scaled_dot_product_attention(
|
||||
queries, keys, values, cache=cache, scale=self.scale, mask=mask
|
||||
)
|
||||
|
||||
# Reshape back
|
||||
output = output.transpose(0, 2, 1, 3).reshape(B, L, self.hidden_size)
|
||||
return self.c_proj(output)
|
||||
|
||||
|
||||
class MLP(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.c_fc = nn.Linear(args.hidden_size, args.intermediate_size, bias=False)
|
||||
self.c_proj = nn.Linear(args.intermediate_size, args.hidden_size, bias=False)
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
# Critical: nanochat uses ReLU^2, not GELU!
|
||||
x = self.c_fc(x)
|
||||
x = nn.relu2(x)
|
||||
return self.c_proj(x)
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.attn = Attention(args)
|
||||
self.mlp = MLP(args)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
# Pre-norm architecture with functional RMSNorm
|
||||
h = x + self.attn(rms_norm(x), mask=mask, cache=cache)
|
||||
out = h + self.mlp(rms_norm(h))
|
||||
return out
|
||||
|
||||
|
||||
class NanoChatModel(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.wte = nn.Embedding(args.vocab_size, args.hidden_size)
|
||||
self.h = [TransformerBlock(args) for _ in range(args.num_hidden_layers)]
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache=None,
|
||||
) -> mx.array:
|
||||
h = self.wte(inputs)
|
||||
# Critical: norm after token embedding
|
||||
h = rms_norm(h)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.h)
|
||||
|
||||
mask = create_attention_mask(h, cache[0])
|
||||
|
||||
for layer, c in zip(self.h, cache):
|
||||
h = layer(h, mask=mask, cache=c)
|
||||
|
||||
# Critical: final norm before lm_head
|
||||
h = rms_norm(h)
|
||||
|
||||
return h
|
||||
|
||||
|
||||
@partial(mx.compile, shapeless=True)
|
||||
def softcap(logits, cap=15.0):
|
||||
return cap * mx.tanh(logits / cap)
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.model_type = args.model_type
|
||||
self.transformer = NanoChatModel(args)
|
||||
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache=None,
|
||||
) -> mx.array:
|
||||
out = self.transformer(inputs, cache=cache)
|
||||
logits = self.lm_head(out)
|
||||
|
||||
# Critical: logits softcap (nanochat uses softcap=15)
|
||||
logits = softcap(logits)
|
||||
|
||||
return logits
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.transformer.h
|
||||
@@ -0,0 +1,226 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .rope_utils import initialize_rope
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str
|
||||
hidden_size: int
|
||||
num_hidden_layers: int
|
||||
intermediate_size: int
|
||||
num_attention_heads: int
|
||||
rms_norm_eps: float
|
||||
vocab_size: int
|
||||
max_position_embeddings: int
|
||||
sliding_window: int
|
||||
rope_theta: float
|
||||
attention_bias: bool = False
|
||||
layer_types: Optional[List[str]] = None
|
||||
num_key_value_heads: Optional[int] = None
|
||||
head_dim: Optional[int] = None
|
||||
rope_scaling: Optional[Dict[str, Union[float, str]]] = None
|
||||
tie_word_embeddings: bool = False
|
||||
|
||||
def __post_init__(self):
|
||||
if self.num_key_value_heads is None:
|
||||
self.num_key_value_heads = self.num_attention_heads
|
||||
if self.layer_types is None:
|
||||
self.layer_types = [
|
||||
"full_attention" if (i + 1) % 4 == 0 else "sliding_attention"
|
||||
for i in range(self.num_hidden_layers)
|
||||
]
|
||||
|
||||
|
||||
class Olmo3Attention(nn.Module):
|
||||
def __init__(self, args: ModelArgs, layer_idx: int):
|
||||
super().__init__()
|
||||
self.num_attention_heads = args.num_attention_heads
|
||||
self.num_key_value_heads = args.num_key_value_heads
|
||||
self.layer_idx = layer_idx
|
||||
|
||||
self.head_dim = args.head_dim or args.hidden_size // args.num_attention_heads
|
||||
self.scale = self.head_dim**-0.5
|
||||
|
||||
self.q_proj = nn.Linear(
|
||||
args.hidden_size,
|
||||
args.num_attention_heads * self.head_dim,
|
||||
bias=args.attention_bias,
|
||||
)
|
||||
self.k_proj = nn.Linear(
|
||||
args.hidden_size,
|
||||
args.num_key_value_heads * self.head_dim,
|
||||
bias=args.attention_bias,
|
||||
)
|
||||
self.v_proj = nn.Linear(
|
||||
args.hidden_size,
|
||||
args.num_key_value_heads * self.head_dim,
|
||||
bias=args.attention_bias,
|
||||
)
|
||||
self.o_proj = nn.Linear(
|
||||
args.num_attention_heads * self.head_dim,
|
||||
args.hidden_size,
|
||||
bias=args.attention_bias,
|
||||
)
|
||||
|
||||
self.q_norm = nn.RMSNorm(
|
||||
args.num_attention_heads * self.head_dim, eps=args.rms_norm_eps
|
||||
)
|
||||
self.k_norm = nn.RMSNorm(
|
||||
args.num_key_value_heads * self.head_dim, eps=args.rms_norm_eps
|
||||
)
|
||||
self.is_full = args.layer_types[layer_idx] == "full_attention"
|
||||
|
||||
if self.is_full:
|
||||
self.rope = nn.RoPE(self.head_dim, traditional=False, base=args.rope_theta)
|
||||
else:
|
||||
self.rope = initialize_rope(
|
||||
self.head_dim,
|
||||
traditional=False,
|
||||
base=args.rope_theta,
|
||||
scaling_config=args.rope_scaling,
|
||||
max_position_embeddings=args.max_position_embeddings,
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
B, L, _ = x.shape
|
||||
queries = self.q_norm(self.q_proj(x))
|
||||
keys = self.k_norm(self.k_proj(x))
|
||||
values = self.v_proj(x)
|
||||
|
||||
queries = queries.reshape(B, L, self.num_attention_heads, -1).transpose(
|
||||
0, 2, 1, 3
|
||||
)
|
||||
keys = keys.reshape(B, L, self.num_key_value_heads, -1).transpose(0, 2, 1, 3)
|
||||
values = values.reshape(B, L, self.num_key_value_heads, -1).transpose(
|
||||
0, 2, 1, 3
|
||||
)
|
||||
|
||||
if cache is not None:
|
||||
queries = self.rope(queries, offset=cache.offset)
|
||||
keys = self.rope(keys, offset=cache.offset)
|
||||
keys, values = cache.update_and_fetch(keys, values)
|
||||
else:
|
||||
queries = self.rope(queries)
|
||||
keys = self.rope(keys)
|
||||
|
||||
output = scaled_dot_product_attention(
|
||||
queries, keys, values, cache=cache, scale=self.scale, mask=mask
|
||||
)
|
||||
output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
|
||||
return self.o_proj(output)
|
||||
|
||||
|
||||
class Olmo3MLP(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.gate_proj = nn.Linear(args.hidden_size, args.intermediate_size, bias=False)
|
||||
self.down_proj = nn.Linear(args.intermediate_size, args.hidden_size, bias=False)
|
||||
self.up_proj = nn.Linear(args.hidden_size, args.intermediate_size, bias=False)
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
|
||||
|
||||
|
||||
class Olmo3DecoderLayer(nn.Module):
|
||||
def __init__(self, args: ModelArgs, layer_idx: int):
|
||||
super().__init__()
|
||||
self.num_attention_heads = args.num_attention_heads
|
||||
self.hidden_size = args.hidden_size
|
||||
self.self_attn = Olmo3Attention(args, layer_idx=layer_idx)
|
||||
self.mlp = Olmo3MLP(args)
|
||||
self.post_attention_layernorm = nn.RMSNorm(
|
||||
args.hidden_size, eps=args.rms_norm_eps
|
||||
)
|
||||
self.post_feedforward_layernorm = nn.RMSNorm(
|
||||
args.hidden_size, eps=args.rms_norm_eps
|
||||
)
|
||||
self.args = args
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
r = self.post_attention_layernorm(self.self_attn(x, mask, cache))
|
||||
h = x + r
|
||||
r = self.post_feedforward_layernorm(self.mlp(h))
|
||||
out = h + r
|
||||
return out
|
||||
|
||||
|
||||
class Olmo3Model(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.sliding_window = args.sliding_window
|
||||
|
||||
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
|
||||
self.layers = [
|
||||
Olmo3DecoderLayer(args=args, layer_idx=i)
|
||||
for i in range(args.num_hidden_layers)
|
||||
]
|
||||
self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
|
||||
self.swa_idx = args.layer_types.index("sliding_attention")
|
||||
self.ga_idx = args.layer_types.index("full_attention")
|
||||
self.layer_types = args.layer_types
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
h = self.embed_tokens(inputs)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
|
||||
full_mask = create_attention_mask(h, cache[self.ga_idx])
|
||||
sliding_window_mask = create_attention_mask(
|
||||
h, cache[self.swa_idx], window_size=self.sliding_window
|
||||
)
|
||||
|
||||
for layer, c, layer_type in zip(self.layers, cache, self.layer_types):
|
||||
mask = full_mask if layer_type == "full_attention" else sliding_window_mask
|
||||
h = layer(h, mask, cache=c)
|
||||
|
||||
return self.norm(h)
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.model_type = args.model_type
|
||||
self.model = Olmo3Model(args)
|
||||
if not args.tie_word_embeddings:
|
||||
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array:
|
||||
out = self.model(inputs, cache)
|
||||
if self.args.tie_word_embeddings:
|
||||
out = self.model.embed_tokens.as_linear(out)
|
||||
else:
|
||||
out = self.lm_head(out)
|
||||
return out
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.model.layers
|
||||
@@ -20,7 +20,7 @@ class ModelArgs(BaseModelArgs):
|
||||
def from_dict(cls, params):
|
||||
if "text_config" not in params:
|
||||
return cls(model_type=params["model_type"], text_config=params)
|
||||
return cls(**params)
|
||||
return super().from_dict(params)
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
|
||||
@@ -141,8 +141,12 @@ class Qwen3Model(nn.Module):
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache=None,
|
||||
input_embeddings: Optional[mx.array] = None,
|
||||
):
|
||||
h = self.embed_tokens(inputs)
|
||||
if input_embeddings is not None:
|
||||
h = input_embeddings
|
||||
else:
|
||||
h = self.embed_tokens(inputs)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
@@ -167,8 +171,9 @@ class Model(nn.Module):
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache=None,
|
||||
input_embeddings: Optional[mx.array] = None,
|
||||
):
|
||||
out = self.model(inputs, cache)
|
||||
out = self.model(inputs, cache, input_embeddings)
|
||||
if self.args.tie_word_embeddings:
|
||||
out = self.model.embed_tokens.as_linear(out)
|
||||
else:
|
||||
|
||||
@@ -188,8 +188,12 @@ class Qwen3MoeModel(nn.Module):
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache=None,
|
||||
input_embeddings: Optional[mx.array] = None,
|
||||
):
|
||||
h = self.embed_tokens(inputs)
|
||||
if input_embeddings is not None:
|
||||
h = input_embeddings
|
||||
else:
|
||||
h = self.embed_tokens(inputs)
|
||||
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
@@ -211,11 +215,9 @@ class Model(nn.Module):
|
||||
self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache=None,
|
||||
self, inputs: mx.array, cache=None, input_embeddings: Optional[mx.array] = None
|
||||
):
|
||||
out = self.model(inputs, cache)
|
||||
out = self.model(inputs, cache, input_embeddings)
|
||||
return self.lm_head(out)
|
||||
|
||||
def sanitize(self, weights):
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention
|
||||
from .base import (
|
||||
BaseModelArgs,
|
||||
create_attention_mask,
|
||||
create_ssm_mask,
|
||||
scaled_dot_product_attention,
|
||||
)
|
||||
from .cache import KVCache, MambaCache
|
||||
from .gated_delta import gated_delta_update
|
||||
from .rope_utils import initialize_rope
|
||||
@@ -237,6 +244,8 @@ class Qwen3NextGatedDeltaNet(nn.Module):
|
||||
mixed_qkv = mx.concatenate(
|
||||
[q.reshape(B, S, -1), k.reshape(B, S, -1), v.reshape(B, S, -1)], axis=-1
|
||||
)
|
||||
if mask is not None:
|
||||
mixed_qkv = mx.where(mask[..., None], mixed_qkv, 0)
|
||||
conv_input = mx.concatenate([conv_state, mixed_qkv], axis=1)
|
||||
if cache is not None:
|
||||
cache[0] = conv_input[:, -(self.conv_kernel_size - 1) :]
|
||||
@@ -251,14 +260,23 @@ class Qwen3NextGatedDeltaNet(nn.Module):
|
||||
)
|
||||
]
|
||||
|
||||
if cache is not None:
|
||||
state = cache[1]
|
||||
|
||||
state = cache[1] if cache else None
|
||||
inv_scale = k.shape[-1] ** -0.5
|
||||
q = (inv_scale**2) * mx.fast.rms_norm(q, None, 1e-6)
|
||||
k = inv_scale * mx.fast.rms_norm(k, None, 1e-6)
|
||||
|
||||
out, state = gated_delta_update(q, k, v, a, b, self.A_log, self.dt_bias, state)
|
||||
out, state = gated_delta_update(
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
a,
|
||||
b,
|
||||
self.A_log,
|
||||
self.dt_bias,
|
||||
state,
|
||||
mask,
|
||||
use_kernel=not self.training,
|
||||
)
|
||||
|
||||
if cache is not None:
|
||||
cache[1] = state
|
||||
@@ -350,6 +368,7 @@ class Qwen3NextModel(nn.Module):
|
||||
for i in range(args.num_hidden_layers)
|
||||
]
|
||||
self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
|
||||
self.ssm_idx = 0
|
||||
self.fa_idx = args.full_attention_interval - 1
|
||||
|
||||
def __call__(
|
||||
@@ -362,9 +381,11 @@ class Qwen3NextModel(nn.Module):
|
||||
if cache is None:
|
||||
cache = [None] * len(self.layers)
|
||||
|
||||
mask = create_attention_mask(hidden_states, cache[self.fa_idx])
|
||||
fa_mask = create_attention_mask(hidden_states, cache[self.fa_idx])
|
||||
ssm_mask = create_ssm_mask(hidden_states, cache[self.ssm_idx])
|
||||
|
||||
for layer, c in zip(self.layers, cache):
|
||||
mask = ssm_mask if layer.is_linear else fa_mask
|
||||
hidden_states = layer(hidden_states, mask=mask, cache=c)
|
||||
|
||||
return self.norm(hidden_states)
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
from mlx.utils import tree_flatten, tree_unflatten
|
||||
|
||||
from . import qwen3
|
||||
from .base import BaseModelArgs
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str
|
||||
text_config: dict
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, params):
|
||||
if "text_config" not in params:
|
||||
return cls(model_type=params["model_type"], text_config=params)
|
||||
return super().from_dict(params)
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.model_type = args.model_type
|
||||
self.language_model = qwen3.Model(qwen3.ModelArgs.from_dict(args.text_config))
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache=None,
|
||||
input_embeddings: Optional[mx.array] = None,
|
||||
):
|
||||
return self.language_model(
|
||||
inputs, cache=cache, input_embeddings=input_embeddings
|
||||
)
|
||||
|
||||
def sanitize(self, weights):
|
||||
weights = tree_unflatten(list(weights.items()))
|
||||
weights.pop("vision_tower", None)
|
||||
weights = dict(tree_flatten(weights))
|
||||
|
||||
sanitized = {}
|
||||
for key, value in weights.items():
|
||||
if not key.startswith("language_model."):
|
||||
key = "language_model." + key
|
||||
sanitized[key] = value
|
||||
return sanitized
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.language_model.model.layers
|
||||
@@ -0,0 +1,77 @@
|
||||
# Copyright © 2025 Apple Inc.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
from mlx.utils import tree_flatten, tree_unflatten
|
||||
|
||||
from . import qwen3_moe
|
||||
from .base import BaseModelArgs
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str
|
||||
text_config: dict
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, args: ModelArgs):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
self.model_type = args.model_type
|
||||
self.language_model = qwen3_moe.Model(
|
||||
qwen3_moe.ModelArgs.from_dict(args.text_config)
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache=None,
|
||||
input_embeddings: Optional[mx.array] = None,
|
||||
):
|
||||
return self.language_model(
|
||||
inputs, cache=cache, input_embeddings=input_embeddings
|
||||
)
|
||||
|
||||
def sanitize(self, weights):
|
||||
weights = tree_unflatten(list(weights.items()))
|
||||
weights.pop("visual", None)
|
||||
weights = dict(
|
||||
tree_flatten(
|
||||
{
|
||||
"language_model": {
|
||||
"model": weights["language_model"]["model"],
|
||||
"lm_head": weights["language_model"]["lm_head"],
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
for l in range(self.language_model.args.num_hidden_layers):
|
||||
prefix = f"language_model.model.layers.{l}.mlp"
|
||||
gate_up_key = f"{prefix}.experts.gate_up_proj"
|
||||
if gate_up_key in weights:
|
||||
gate_up = weights.pop(gate_up_key)
|
||||
mid = gate_up.shape[-1] // 2
|
||||
weights[f"{prefix}.switch_mlp.gate_proj.weight"] = gate_up[
|
||||
..., :mid
|
||||
].swapaxes(-2, -1)
|
||||
weights[f"{prefix}.switch_mlp.up_proj.weight"] = gate_up[
|
||||
..., mid:
|
||||
].swapaxes(-2, -1)
|
||||
weights[f"{prefix}.switch_mlp.down_proj.weight"] = weights.pop(
|
||||
f"{prefix}.experts.down_proj"
|
||||
).swapaxes(-2, -1)
|
||||
|
||||
return weights
|
||||
|
||||
@property
|
||||
def quant_predicate(self):
|
||||
return self.language_model.quant_predicate
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
return self.language_model.model.layers
|
||||
+42
-26
@@ -114,6 +114,7 @@ def ssm_attn(
|
||||
state: Optional[mx.array] = None,
|
||||
time_step_limit: Tuple[float, float] = (0.001, 100.0),
|
||||
mask: Optional[mx.array] = None,
|
||||
step: int = 256,
|
||||
) -> Tuple[mx.array, mx.array]:
|
||||
"""SSD-SSM forward pass.
|
||||
|
||||
@@ -127,6 +128,7 @@ def ssm_attn(
|
||||
dt_bias: Bias for time deltas of shape (num_heads,).
|
||||
time_step_limit: Minimum and maximum value for time deltas.
|
||||
mask: Optional multiplicative mask.
|
||||
step: Step size for processing x.
|
||||
|
||||
Code modified from
|
||||
https://github.com/cartesia-ai/edge/blob/main/cartesia-mlx/cartesia_mlx/layers/ssd/ops.py
|
||||
@@ -138,38 +140,52 @@ def ssm_attn(
|
||||
dt = compute_dt(dt, dt_bias, time_step_limit)
|
||||
repeats = h // g
|
||||
A = -mx.exp(A_log)
|
||||
B = mx.transpose(B, (0, 2, 3, 1))
|
||||
|
||||
# A * s + B * C
|
||||
CB = mx.swapaxes(C, 1, 2) @ B
|
||||
CB = mx.repeat(CB, repeats, axis=1)
|
||||
|
||||
dtA = dt * A.reshape(1, 1, -1)
|
||||
decay = mx.exp(segsum(dtA.swapaxes(1, 2), mask=mask))
|
||||
|
||||
surrogate_attention_matrix = mx.tril(CB * decay, 0)
|
||||
|
||||
dtx = dt.reshape(b, l, h, 1) * x
|
||||
y = surrogate_attention_matrix @ dtx.swapaxes(1, 2)
|
||||
y = mx.swapaxes(y, 1, 2)
|
||||
|
||||
decay = decay[:, :, -1:, :].transpose(0, 3, 1, 2)
|
||||
B = mx.repeat(B, h // g, axis=1).swapaxes(2, 3)
|
||||
dtxdecay = dtx * decay
|
||||
dtxdecay = dtxdecay.swapaxes(1, 2).swapaxes(2, 3)
|
||||
def _step(dtx, dtA, B, C, state, mask):
|
||||
s = dtx.shape[1]
|
||||
B = mx.transpose(B, (0, 2, 3, 1))
|
||||
|
||||
next_state = dtxdecay @ B
|
||||
CB = mx.swapaxes(C, 1, 2) @ B
|
||||
CB = mx.repeat(CB, repeats, axis=1)
|
||||
|
||||
if state is not None:
|
||||
exp_dtA_cumsum = mx.exp(mx.cumsum(dtA, axis=-2))
|
||||
next_state += exp_dtA_cumsum[:, -1, :, None, None] * state
|
||||
state = state.reshape((b, 1, g, repeats, dh, d))
|
||||
C = C.reshape(b, l, g, 1, d, 1)
|
||||
y_prev = (state @ C).squeeze(-1).flatten(2, 3)
|
||||
y += exp_dtA_cumsum[..., None] * y_prev
|
||||
decay = mx.exp(segsum(dtA.swapaxes(1, 2), mask=mask))
|
||||
|
||||
y += x * D.reshape(1, 1, h, 1)
|
||||
return y, next_state
|
||||
surrogate_attention_matrix = mx.tril(CB * decay, 0)
|
||||
|
||||
y = surrogate_attention_matrix @ dtx.swapaxes(1, 2)
|
||||
y = mx.swapaxes(y, 1, 2)
|
||||
|
||||
decay = decay[:, :, -1:, :].transpose(0, 3, 1, 2)
|
||||
B = mx.repeat(B, h // g, axis=1).swapaxes(2, 3)
|
||||
dtxdecay = dtx * decay
|
||||
dtxdecay = dtxdecay.swapaxes(1, 2).swapaxes(2, 3)
|
||||
|
||||
next_state = dtxdecay @ B
|
||||
|
||||
if state is not None:
|
||||
exp_dtA_cumsum = mx.exp(mx.cumsum(dtA, axis=-2))
|
||||
next_state += exp_dtA_cumsum[:, -1, :, None, None] * state
|
||||
state = state.reshape((b, 1, g, repeats, dh, d))
|
||||
C = C.reshape(b, s, g, 1, d, 1)
|
||||
y_prev = (state @ C).squeeze(-1).flatten(2, 3)
|
||||
y += exp_dtA_cumsum[..., None] * y_prev
|
||||
return y, next_state
|
||||
|
||||
ys = []
|
||||
for i in range(0, l, step):
|
||||
y, state = _step(
|
||||
dtx[:, i : i + step],
|
||||
dtA[:, i : i + step],
|
||||
B[:, i : i + step],
|
||||
C[:, i : i + step],
|
||||
state,
|
||||
None if mask is None else mask[..., i : i + step],
|
||||
)
|
||||
ys.append(y)
|
||||
y = mx.concatenate(ys, axis=1) + x * D.reshape(1, 1, h, 1)
|
||||
return y, state
|
||||
|
||||
|
||||
def ssm_update(
|
||||
|
||||
+3
-6
@@ -16,8 +16,7 @@ from mlx_lm.models.base import create_attention_mask
|
||||
from mlx_lm.models.switch_layers import SwitchLinear
|
||||
from mlx_lm.quant.utils import load_data
|
||||
from mlx_lm.utils import (
|
||||
fetch_from_hub,
|
||||
get_model_path,
|
||||
load,
|
||||
save,
|
||||
)
|
||||
|
||||
@@ -555,8 +554,7 @@ def main():
|
||||
|
||||
mx.random.seed(args.seed)
|
||||
|
||||
model_path, hf_repo = get_model_path(args.model, revision=None)
|
||||
model, config, tokenizer = fetch_from_hub(model_path, lazy=True)
|
||||
model, tokenizer, config = load(args.model, lazy=True, return_config=True)
|
||||
|
||||
model_type = config["model_type"]
|
||||
if (awq_config := AWQ_MODEL_CONFIGS.get(model_type, None)) is None:
|
||||
@@ -580,9 +578,8 @@ def main():
|
||||
config = update_config(model, config)
|
||||
save(
|
||||
args.mlx_path,
|
||||
model_path,
|
||||
args.model,
|
||||
model,
|
||||
tokenizer,
|
||||
config,
|
||||
hf_repo=hf_repo,
|
||||
)
|
||||
|
||||
+41
-88
@@ -9,7 +9,7 @@ import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
import mlx.optimizers as optimizers
|
||||
import numpy as np
|
||||
from mlx.utils import tree_flatten, tree_map
|
||||
from mlx.utils import tree_map
|
||||
from tqdm import tqdm
|
||||
|
||||
from mlx_lm.tuner.datasets import load_dataset
|
||||
@@ -17,32 +17,12 @@ from mlx_lm.tuner.losses import kl_div_loss
|
||||
from mlx_lm.tuner.trainer import grad_checkpoint, iterate_batches
|
||||
from mlx_lm.tuner.utils import print_trainable_parameters
|
||||
from mlx_lm.utils import (
|
||||
fetch_from_hub,
|
||||
get_model_path,
|
||||
load,
|
||||
quantize_model,
|
||||
save,
|
||||
)
|
||||
|
||||
|
||||
class Catcher(nn.Module):
|
||||
def __init__(self, module):
|
||||
super().__init__()
|
||||
self.module = module
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
outputs = self.module(*args, **kwargs)
|
||||
self.outputs = outputs[0] if isinstance(outputs, tuple) else outputs
|
||||
return outputs
|
||||
|
||||
def __getattr__(self, key: str):
|
||||
if (value := self.get(key)) is not None:
|
||||
return value
|
||||
elif (m := self.get("module")) is not None:
|
||||
return getattr(m, key)
|
||||
else:
|
||||
super(nn.Module, self).__getattribute__(key)
|
||||
|
||||
|
||||
def dwq_quantize(
|
||||
model,
|
||||
q_model,
|
||||
@@ -51,10 +31,9 @@ def dwq_quantize(
|
||||
valid_data,
|
||||
batch_size: int = 2,
|
||||
max_seq_length: int = 2048,
|
||||
activation_layer_step: float = 0.25,
|
||||
activation_loss_weight: float = 1.0,
|
||||
dtype: mx.Dtype = mx.bfloat16,
|
||||
gradient_checkpoint: bool = False,
|
||||
temperature: float = 2.0,
|
||||
):
|
||||
group = mx.distributed.init()
|
||||
world_size = group.size()
|
||||
@@ -65,52 +44,35 @@ def dwq_quantize(
|
||||
tqdm.write(*args, **kwargs)
|
||||
|
||||
def unfreeze(_, m):
|
||||
if hasattr(m, "bits") and hasattr(m, "group_size") and m.mode == "affine":
|
||||
if (
|
||||
hasattr(m, "bits")
|
||||
and hasattr(m, "group_size")
|
||||
and m.mode == "affine"
|
||||
and m.bits < 8
|
||||
):
|
||||
m.unfreeze(keys=["scales", "biases"], recurse=False)
|
||||
|
||||
q_model.train()
|
||||
q_model.apply_to_modules(unfreeze)
|
||||
print_trainable_parameters(q_model)
|
||||
|
||||
layer_id_step = max(int(activation_layer_step * len(model.layers)), 1)
|
||||
layer_ids = list(range(len(model.layers)))[layer_id_step::layer_id_step]
|
||||
|
||||
for lid in layer_ids:
|
||||
model.layers[lid] = Catcher(model.layers[lid])
|
||||
q_model.layers[lid] = Catcher(q_model.layers[lid])
|
||||
|
||||
if gradient_checkpoint:
|
||||
grad_checkpoint(q_model.layers[0])
|
||||
|
||||
def forward(model, inputs):
|
||||
logits = model(inputs)
|
||||
extra_targets = [
|
||||
model.layers[lid].outputs.astype(mx.float32) for lid in layer_ids
|
||||
]
|
||||
for lid in layer_ids:
|
||||
model.layers[lid].outputs = None
|
||||
return logits, extra_targets
|
||||
scale = 1 / temperature
|
||||
|
||||
def loss_fn(params, x, targets, extra_targets, lengths):
|
||||
def loss_fn(params, x, targets, lengths):
|
||||
q_model.update(tree_map(lambda x: x.astype(dtype), params))
|
||||
logits, q_extra_targets = forward(q_model, x)
|
||||
losses = kl_div_loss(logits, targets)
|
||||
logits = q_model(x)
|
||||
losses = kl_div_loss(scale * logits, scale * targets)
|
||||
mask = mx.arange(1, 1 + targets.shape[1]) < lengths[:, 1:]
|
||||
ntoks = mask.sum()
|
||||
kl_loss = (mask * losses).sum() / ntoks
|
||||
act_loss = mx.stack(
|
||||
[
|
||||
(mask * (qe - e).abs().mean(axis=-1)).sum() / ntoks
|
||||
for qe, e in zip(q_extra_targets, extra_targets)
|
||||
]
|
||||
)
|
||||
act_loss = act_loss.mean()
|
||||
loss = kl_loss + activation_loss_weight * act_loss
|
||||
return loss, ntoks, kl_loss, act_loss
|
||||
loss = (mask * losses).sum() / ntoks
|
||||
return loss, ntoks
|
||||
|
||||
def step(inputs, targets, extra_targets, lengths, params):
|
||||
(loss, ntoks, *_), grads = mx.value_and_grad(loss_fn)(
|
||||
params, inputs, targets, extra_targets, lengths
|
||||
def step(inputs, targets, lengths, params):
|
||||
(loss, ntoks), grads = mx.value_and_grad(loss_fn)(
|
||||
params, inputs, targets, lengths
|
||||
)
|
||||
grads = nn.average_gradients(grads)
|
||||
params = opt.apply_gradients(grads, params)
|
||||
@@ -118,36 +80,24 @@ def dwq_quantize(
|
||||
|
||||
def validate(params, it):
|
||||
v_loss = 0.0
|
||||
v_kl_loss = 0.0
|
||||
v_act_loss = 0.0
|
||||
v_tokens = 0
|
||||
for it, (batch, lengths) in tqdm(
|
||||
enumerate(iterate_batches(valid_data, batch_size, max_seq_length)),
|
||||
for batch, lengths in tqdm(
|
||||
iterate_batches(valid_data, batch_size, max_seq_length),
|
||||
total=len(valid_data) // batch_size,
|
||||
desc="Computing validation loss",
|
||||
leave=False,
|
||||
):
|
||||
batch = batch[:, :-1]
|
||||
targets, extra_targets = forward(model, batch)
|
||||
mx.eval(targets, extra_targets)
|
||||
loss, ntoks, kl_loss, act_loss = loss_fn(
|
||||
params, batch, targets, extra_targets, lengths
|
||||
)
|
||||
targets = model(batch)
|
||||
mx.eval(targets)
|
||||
loss, ntoks = loss_fn(params, batch, targets, lengths)
|
||||
mx.eval(loss, ntoks)
|
||||
loss = mx.distributed.all_sum(loss, stream=mx.cpu).item() / world_size
|
||||
kl_loss = mx.distributed.all_sum(kl_loss, stream=mx.cpu).item() / world_size
|
||||
act_loss = (
|
||||
mx.distributed.all_sum(act_loss, stream=mx.cpu).item() / world_size
|
||||
)
|
||||
ntoks = mx.distributed.all_sum(ntoks, stream=mx.cpu).item()
|
||||
v_tokens += ntoks
|
||||
v_loss += loss * ntoks
|
||||
v_kl_loss += kl_loss * ntoks
|
||||
v_act_loss += act_loss * ntoks
|
||||
loss = v_loss / v_tokens
|
||||
kl_loss = v_kl_loss / v_tokens
|
||||
act_loss = v_act_loss / v_tokens
|
||||
rprint(f"Validation: {it=}, {loss=:.3f}, {kl_loss=:.3f}, {act_loss=:.3f}")
|
||||
rprint(f"Validation: {it=}, {loss=:.3f}")
|
||||
return loss
|
||||
|
||||
# Accumulate learned weights in higher precision
|
||||
@@ -172,9 +122,9 @@ def dwq_quantize(
|
||||
)
|
||||
):
|
||||
batch = batch[:, :-1]
|
||||
targets, extra_targets = forward(model, batch)
|
||||
mx.eval(targets, extra_targets)
|
||||
loss, ntoks, params = step(batch, targets, extra_targets, lengths, params)
|
||||
targets = model(batch)
|
||||
mx.eval(targets)
|
||||
loss, ntoks, params = step(batch, targets, lengths, params)
|
||||
mx.eval(loss, params)
|
||||
loss = mx.distributed.all_sum(loss, stream=mx.cpu).item() / world_size
|
||||
ntoks = mx.distributed.all_sum(ntoks, stream=mx.cpu).item()
|
||||
@@ -206,8 +156,6 @@ def dwq_quantize(
|
||||
)
|
||||
|
||||
q_model.update(tree_map(lambda x: x.astype(dtype), params))
|
||||
for lid in layer_ids:
|
||||
q_model.layers[lid] = q_model.layers[lid].module
|
||||
|
||||
|
||||
def load_data(
|
||||
@@ -273,7 +221,7 @@ def main():
|
||||
default=2048,
|
||||
help="Number of samples to use for training.",
|
||||
)
|
||||
parser.add_argument("--max-seq-length", type=int, default=2049)
|
||||
parser.add_argument("--max-seq-length", type=int, default=1025)
|
||||
parser.add_argument("--seed", type=int, default=123)
|
||||
parser.add_argument("--learning-rate", type=float, default=1e-6)
|
||||
parser.add_argument("--batch-size", type=int, default=4)
|
||||
@@ -299,9 +247,10 @@ def main():
|
||||
np.random.seed(args.seed)
|
||||
mx.random.seed(args.seed)
|
||||
|
||||
model_path, hf_repo = get_model_path(args.model, revision=None)
|
||||
model, config, tokenizer = fetch_from_hub(
|
||||
model_path, lazy=True, trust_remote_code=True
|
||||
model, tokenizer, config = load(
|
||||
args.model,
|
||||
lazy=True,
|
||||
return_config=True,
|
||||
)
|
||||
|
||||
train_data, valid_data = load_data(
|
||||
@@ -309,9 +258,10 @@ def main():
|
||||
)
|
||||
|
||||
if args.quantized_model is not None:
|
||||
q_model_path, _ = get_model_path(args.quantized_model, revision=None)
|
||||
q_model, config, _ = fetch_from_hub(
|
||||
q_model_path, lazy=True, trust_remote_code=True
|
||||
q_model, tokenizer, config = load(
|
||||
args.quantized_model,
|
||||
lazy=True,
|
||||
return_config=True,
|
||||
)
|
||||
if "quantization" not in config:
|
||||
raise ValueError("Quantized model must already be quantized.")
|
||||
@@ -324,6 +274,10 @@ def main():
|
||||
bits=args.bits,
|
||||
)
|
||||
|
||||
if mx.metal.is_available():
|
||||
max_rec_size = mx.metal.device_info()["max_recommended_working_set_size"]
|
||||
mx.set_wired_limit(max_rec_size)
|
||||
|
||||
opt = optimizers.Adam(learning_rate=args.learning_rate, bias_correction=True)
|
||||
dwq_quantize(
|
||||
model,
|
||||
@@ -337,9 +291,8 @@ def main():
|
||||
)
|
||||
save(
|
||||
args.mlx_path,
|
||||
model_path,
|
||||
args.model,
|
||||
q_model,
|
||||
tokenizer,
|
||||
config,
|
||||
hf_repo=hf_repo,
|
||||
)
|
||||
|
||||
@@ -16,8 +16,6 @@ from mlx_lm.tuner.losses import kl_div_loss
|
||||
from mlx_lm.tuner.trainer import grad_checkpoint
|
||||
from mlx_lm.utils import (
|
||||
compute_bits_per_weight,
|
||||
fetch_from_hub,
|
||||
get_model_path,
|
||||
load,
|
||||
quantize_model,
|
||||
save,
|
||||
@@ -187,9 +185,9 @@ def main():
|
||||
args = parser.parse_args()
|
||||
|
||||
group = mx.distributed.init()
|
||||
model, tokenizer, config = load(args.model, return_config=True)
|
||||
|
||||
if args.sensitivities is None:
|
||||
model, tokenizer = load(args.model)
|
||||
mx.random.seed(args.seed)
|
||||
data = load_data(tokenizer, num_samples=-1, sequence_length=512)
|
||||
|
||||
@@ -211,8 +209,6 @@ def main():
|
||||
sensitivities = json.load(fid)
|
||||
|
||||
sensitivities = dict(sensitivities)
|
||||
model_path, hf_repo = get_model_path(args.model, revision=None)
|
||||
model, config, tokenizer = fetch_from_hub(model_path, lazy=True)
|
||||
mx.random.seed(args.seed)
|
||||
data = load_data(tokenizer, num_samples=-1, sequence_length=512)
|
||||
|
||||
@@ -251,11 +247,10 @@ def main():
|
||||
|
||||
save(
|
||||
args.mlx_path,
|
||||
model_path,
|
||||
args.model,
|
||||
model,
|
||||
tokenizer,
|
||||
config,
|
||||
hf_repo=hf_repo,
|
||||
)
|
||||
print(f"Peak memory used: {mx.get_peak_memory() / 1000**3:.3f}GB")
|
||||
|
||||
|
||||
@@ -18,8 +18,7 @@ from mlx_lm.models.switch_layers import QuantizedSwitchLinear, SwitchLinear
|
||||
from mlx_lm.quant.utils import load_data
|
||||
from mlx_lm.utils import (
|
||||
compute_bits_per_weight,
|
||||
fetch_from_hub,
|
||||
get_model_path,
|
||||
load,
|
||||
save,
|
||||
)
|
||||
|
||||
@@ -202,8 +201,7 @@ def main():
|
||||
|
||||
mx.random.seed(args.seed)
|
||||
|
||||
model_path, hf_repo = get_model_path(args.model, revision=None)
|
||||
model, config, tokenizer = fetch_from_hub(model_path, lazy=True)
|
||||
model, tokenizer, config = load(args.model, lazy=True, return_config=True)
|
||||
calibration_data = load_data(tokenizer, args.num_samples, args.sequence_length)
|
||||
|
||||
model, config["quantization"] = gptq_quantize(
|
||||
@@ -220,11 +218,10 @@ def main():
|
||||
|
||||
save(
|
||||
args.mlx_path,
|
||||
model_path,
|
||||
args.model,
|
||||
model,
|
||||
tokenizer,
|
||||
config,
|
||||
hf_repo=hf_repo,
|
||||
)
|
||||
|
||||
|
||||
|
||||
+6
-65
@@ -279,8 +279,6 @@ class APIHandler(BaseHTTPRequestHandler):
|
||||
endpoints = {
|
||||
"/v1/completions": self.handle_text_completions,
|
||||
"/v1/chat/completions": self.handle_chat_completions,
|
||||
"/responses": self.handle_responses,
|
||||
"/v1/responses": self.handle_responses,
|
||||
"/chat/completions": self.handle_chat_completions,
|
||||
}
|
||||
|
||||
@@ -330,7 +328,6 @@ class APIHandler(BaseHTTPRequestHandler):
|
||||
self.max_tokens = self.body.get(
|
||||
"max_tokens", self.model_provider.cli_args.max_tokens
|
||||
)
|
||||
|
||||
self.temperature = self.body.get(
|
||||
"temperature", self.model_provider.cli_args.temp
|
||||
)
|
||||
@@ -495,18 +492,6 @@ class APIHandler(BaseHTTPRequestHandler):
|
||||
"id": None,
|
||||
}
|
||||
|
||||
if self.stream and self.object_type == "response" and finish_reason is None:
|
||||
return {
|
||||
"type": "response.output_text.delta",
|
||||
"delta": text,
|
||||
# TODO, these need valid values
|
||||
"sequence_number": None,
|
||||
"item_id": None,
|
||||
"output_index": 1,
|
||||
"content_index": 0,
|
||||
"logprobs": [],
|
||||
}
|
||||
|
||||
# Static response
|
||||
response = {
|
||||
"id": self.request_id,
|
||||
@@ -514,27 +499,13 @@ class APIHandler(BaseHTTPRequestHandler):
|
||||
"object": self.object_type,
|
||||
"model": self.requested_model,
|
||||
"created": self.created,
|
||||
}
|
||||
|
||||
if self.object_type == "response":
|
||||
response["output"] = [
|
||||
"choices": [
|
||||
{
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"content": [{"text": text, "type": "output_text"}],
|
||||
}
|
||||
]
|
||||
if self.stream:
|
||||
return {"response": response, "type": "response.completed"}
|
||||
|
||||
return response
|
||||
|
||||
response["choices"] = [
|
||||
{
|
||||
"index": 0,
|
||||
"finish_reason": finish_reason,
|
||||
},
|
||||
]
|
||||
"index": 0,
|
||||
"finish_reason": finish_reason,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
if token_logprobs or top_logprobs or tokens:
|
||||
response["choices"][0]["logprobs"] = {
|
||||
@@ -893,36 +864,6 @@ class APIHandler(BaseHTTPRequestHandler):
|
||||
|
||||
return prompt
|
||||
|
||||
def handle_responses(self) -> List[int]:
|
||||
body = self.body
|
||||
system_prompt = body.get("instructions")
|
||||
prompt = body["input"]
|
||||
tools = body.get("tools")
|
||||
messages = []
|
||||
if system_prompt:
|
||||
messages = [{"role": "system", "content": system_prompt}]
|
||||
if isinstance(prompt, list):
|
||||
for message in prompt:
|
||||
content = message["content"]
|
||||
if isinstance(content, list):
|
||||
if len(content) != 1 or content[0]["type"] != "input_text":
|
||||
raise ValueError("Unsupported content type.")
|
||||
message["content"] = content[0]["text"]
|
||||
messages.append(message)
|
||||
else:
|
||||
messages.append({"role": "user", "content": prompt})
|
||||
|
||||
# Determine response type
|
||||
self.request_id = f"resp_{uuid.uuid4()}"
|
||||
self.object_type = "response"
|
||||
prompt = self.tokenizer.apply_chat_template(
|
||||
messages,
|
||||
tools=tools,
|
||||
add_generation_prompt=True,
|
||||
**self.model_provider.cli_args.chat_template_args,
|
||||
)
|
||||
return prompt
|
||||
|
||||
def handle_text_completions(self) -> List[int]:
|
||||
"""
|
||||
Handle a text completion request.
|
||||
|
||||
+15
-9
@@ -176,19 +176,25 @@ class LoRASwitchLinear(nn.Module):
|
||||
self.lora_a = mx.random.uniform(
|
||||
low=-scale,
|
||||
high=scale,
|
||||
shape=(r * num_experts, input_dims),
|
||||
shape=(num_experts, r, input_dims),
|
||||
)
|
||||
self.lora_b = mx.zeros(shape=(num_experts, output_dims, r))
|
||||
self.num_experts = num_experts
|
||||
|
||||
def __call__(self, x, indices):
|
||||
shape = x.shape[:-3] + (self.num_experts, -1)
|
||||
|
||||
y = self.linear(x, indices)
|
||||
z = (self.dropout(x) @ self.lora_a.T).reshape(shape)
|
||||
z = mx.take_along_axis(z, indices[..., None], axis=-2)
|
||||
z = z[..., None, :] @ self.lora_b[indices].swapaxes(-2, -1)
|
||||
|
||||
def __call__(self, x, indices, sorted_indices=False):
|
||||
y = self.linear(x, indices, sorted_indices=sorted_indices)
|
||||
z = mx.gather_mm(
|
||||
self.dropout(x),
|
||||
self.lora_a.swapaxes(-1, -2),
|
||||
rhs_indices=indices,
|
||||
sorted_indices=sorted_indices,
|
||||
)
|
||||
z = mx.gather_mm(
|
||||
z,
|
||||
self.lora_b.swapaxes(-1, -2),
|
||||
rhs_indices=indices,
|
||||
sorted_indices=sorted_indices,
|
||||
)
|
||||
return y + (self.scale * z).astype(x.dtype)
|
||||
|
||||
|
||||
|
||||
+32
-12
@@ -10,7 +10,7 @@ import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
import numpy as np
|
||||
from mlx.nn.utils import average_gradients
|
||||
from mlx.utils import tree_flatten
|
||||
from mlx.utils import tree_flatten, tree_map
|
||||
from tqdm import tqdm
|
||||
|
||||
from .callbacks import TrainingCallback
|
||||
@@ -64,6 +64,12 @@ class TrainingArgs:
|
||||
default=False,
|
||||
metadata={"help": "Use gradient checkpointing to reduce memory use."},
|
||||
)
|
||||
grad_accumulation_steps: int = field(
|
||||
default=1,
|
||||
metadata={
|
||||
"help": "Number of steps to accumulate gradients before applying an optimizer update."
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def default_loss(model, batch, lengths):
|
||||
@@ -209,22 +215,29 @@ def train(
|
||||
if args.grad_checkpoint:
|
||||
grad_checkpoint(model.layers[0])
|
||||
|
||||
loss_value_and_grad = nn.value_and_grad(model, loss)
|
||||
|
||||
grad_accum_steps = args.grad_accumulation_steps
|
||||
if grad_accum_steps < 1:
|
||||
raise ValueError("grad_accumulation_steps must be at least 1")
|
||||
|
||||
state = [model.state, optimizer.state, mx.random.state]
|
||||
|
||||
@partial(mx.compile, inputs=state, outputs=state)
|
||||
def step(batch):
|
||||
# Forward and backward pass
|
||||
def step(batch, prev_grad, do_update):
|
||||
(lvalue, toks), grad = loss_value_and_grad(model, *batch)
|
||||
|
||||
# All reduce the gradients if running in distributed mode
|
||||
grad = average_gradients(grad)
|
||||
if prev_grad is not None:
|
||||
grad = tree_map(lambda x, y: x + y, grad, prev_grad)
|
||||
|
||||
# Model update
|
||||
optimizer.update(model, grad)
|
||||
if do_update:
|
||||
grad = average_gradients(grad)
|
||||
if grad_accum_steps > 1:
|
||||
grad = tree_map(lambda x: x / grad_accum_steps, grad)
|
||||
optimizer.update(model, grad)
|
||||
grad = None
|
||||
|
||||
return lvalue, toks
|
||||
|
||||
loss_value_and_grad = nn.value_and_grad(model, loss)
|
||||
return lvalue, toks, grad
|
||||
|
||||
model.train()
|
||||
losses = 0
|
||||
@@ -232,6 +245,8 @@ def train(
|
||||
steps = 0
|
||||
trained_tokens = 0
|
||||
train_time = 0
|
||||
grad_accum = None
|
||||
|
||||
# Main training loop
|
||||
for it, batch in zip(
|
||||
range(1, args.iters + 1),
|
||||
@@ -276,11 +291,16 @@ def train(
|
||||
|
||||
tic = time.perf_counter()
|
||||
|
||||
lvalue, toks = step(batch)
|
||||
lvalue, toks, grad_accum = step(
|
||||
batch,
|
||||
grad_accum,
|
||||
it % grad_accum_steps == 0,
|
||||
)
|
||||
|
||||
losses += lvalue
|
||||
n_tokens += toks
|
||||
steps += 1
|
||||
mx.eval(state, losses, n_tokens)
|
||||
mx.eval(state, losses, n_tokens, grad_accum)
|
||||
train_time += time.perf_counter() - tic
|
||||
|
||||
# Report training loss if needed
|
||||
|
||||
+18
-135
@@ -7,7 +7,7 @@ from typing import Dict
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
import mlx.optimizers as opt
|
||||
from mlx.utils import tree_flatten, tree_unflatten
|
||||
from mlx.utils import tree_flatten, tree_map_with_path, tree_unflatten
|
||||
|
||||
from ..models.switch_layers import QuantizedSwitchLinear, SwitchLinear
|
||||
from .dora import DoRAEmbedding, DoRALinear
|
||||
@@ -81,140 +81,23 @@ def linear_to_lora_layers(
|
||||
dropout=config["dropout"],
|
||||
)
|
||||
|
||||
keys = config.get("keys", None)
|
||||
if keys is not None:
|
||||
keys = set(keys)
|
||||
elif model.model_type in {
|
||||
"mistral",
|
||||
"mistral3",
|
||||
"llama",
|
||||
"llama4_text",
|
||||
"lfm2",
|
||||
"phi",
|
||||
"mixtral",
|
||||
"nemotron",
|
||||
"stablelm",
|
||||
"hunyuan",
|
||||
"qwen2",
|
||||
"qwen2_moe",
|
||||
"qwen3",
|
||||
"qwen3_moe",
|
||||
"phimoe",
|
||||
"gemma",
|
||||
"gemma2",
|
||||
"gemma3",
|
||||
"gemma3_text",
|
||||
"granite",
|
||||
"helium",
|
||||
"starcoder2",
|
||||
"cohere",
|
||||
"cohere2",
|
||||
"minicpm",
|
||||
"minicpm3",
|
||||
"minicpm4",
|
||||
"deepseek",
|
||||
"olmo2",
|
||||
"olmoe",
|
||||
"internlm3",
|
||||
"glm4",
|
||||
"glm",
|
||||
"mimo",
|
||||
"ernie4_5",
|
||||
"dots1",
|
||||
"smollm3",
|
||||
"exaone4",
|
||||
"hunyuan_v1_dense",
|
||||
"gpt_oss",
|
||||
"ernie4_5_moe",
|
||||
"granitemoe",
|
||||
"longcat_flash",
|
||||
"seed_oss",
|
||||
"apertus",
|
||||
"qwen3_next",
|
||||
"Klear",
|
||||
"lille-130m",
|
||||
}:
|
||||
keys = {"self_attn.q_proj", "self_attn.v_proj"}
|
||||
if model.model_type in ["mixtral", "phimoe"]:
|
||||
keys.add("block_sparse_moe.gate")
|
||||
if model.model_type in ["qwen2_moe", "qwen3_next"]:
|
||||
keys.add("mlp.gate")
|
||||
keys.add("mlp.shared_expert_gate")
|
||||
if model.model_type in ["olmoe", "qwen3_moe", "dots1", "Klear"]:
|
||||
keys.add("mlp.gate")
|
||||
if model.model_type in ["longcat_flash"]:
|
||||
keys.add("mlp.router.classifier")
|
||||
if model.model_type == "lille-130m":
|
||||
keys.add("attention.qkv_proj")
|
||||
keys.add("attention.out_proj")
|
||||
keys.add("feed_forward.gate_proj")
|
||||
keys.add("feed_forward.up_proj")
|
||||
keys.add("feed_forward.down_proj")
|
||||
elif model.model_type == "qwen3_next":
|
||||
keys.add("linear_attn.in_proj_qkvz")
|
||||
keys.add("linear_attn.out_proj")
|
||||
keys.add("linear_attn.in_proj_ba")
|
||||
keys.add("linear_attn.dt_bias")
|
||||
keys.add("self_attn.q_proj")
|
||||
keys.add("self_attn.k_proj")
|
||||
keys.add("self_attn.v_proj")
|
||||
keys.add("self_attn.o_proj")
|
||||
elif model.model_type == "gpt_bigcode":
|
||||
keys = {"attn.c_attn"}
|
||||
elif model.model_type == "gpt2":
|
||||
keys = {"attn.c_attn"}
|
||||
elif model.model_type == "gpt_neox":
|
||||
keys = {"attention.query_key_value"}
|
||||
elif model.model_type == "olmo":
|
||||
keys = {"att_proj"}
|
||||
elif model.model_type == "openelm":
|
||||
keys = {"attn.qkv_proj"}
|
||||
elif model.model_type == "phi3":
|
||||
keys = {"self_attn.qkv_proj"}
|
||||
elif model.model_type == "phi-msft":
|
||||
keys = {"mixer.Wqkv", "moe.gate"}
|
||||
elif model.model_type == "dbrx":
|
||||
keys = {"norm_attn_norm.attn.Wqkv", "ffn.router.layer"}
|
||||
elif model.model_type == "internlm2":
|
||||
keys = {"attention.wqkv", "attention.wo"}
|
||||
elif model.model_type in {
|
||||
"deepseek_v2",
|
||||
"deepseek_v3",
|
||||
"longcat_flash",
|
||||
"minicpm3",
|
||||
}:
|
||||
keys = {
|
||||
"self_attn.q_proj",
|
||||
"self_attn.q_a_proj",
|
||||
"self_attn.q_b_proj",
|
||||
"self_attn.kv_a_proj_with_mqa",
|
||||
"self_attn.kv_b_proj",
|
||||
}
|
||||
elif model.model_type == "mamba":
|
||||
keys = {
|
||||
"mixer.in_proj",
|
||||
"mixer.x_proj",
|
||||
"mixer.dt_proj",
|
||||
"mixer.out_proj",
|
||||
}
|
||||
elif model.model_type == "mamba2":
|
||||
keys = {
|
||||
"mixer.in_proj",
|
||||
"mixer.out_proj",
|
||||
}
|
||||
elif model.model_type == "exaone":
|
||||
keys = {"attn.attention.q_proj", "attn.attention.v_proj"}
|
||||
elif model.model_type == "bailing_moe":
|
||||
keys = {"attention.query_key_value", "attention.dense"}
|
||||
elif model.model_type == "nemotron_h":
|
||||
keys.add("mixer.in_proj")
|
||||
keys.add("mixer.out_proj")
|
||||
keys.add("mixer.q_proj")
|
||||
keys.add("mixer.k_proj")
|
||||
keys.add("mixer.v_proj")
|
||||
keys.add("mixer.o_proj")
|
||||
else:
|
||||
raise ValueError(f"Lora does not support {model.model_type}")
|
||||
if (keys := config.get("keys", None)) is None:
|
||||
keys = set()
|
||||
|
||||
def get_keys_for_lora(p, m):
|
||||
types = (
|
||||
nn.Linear,
|
||||
nn.QuantizedLinear,
|
||||
SwitchLinear,
|
||||
QuantizedSwitchLinear,
|
||||
nn.Embedding,
|
||||
nn.QuantizedEmbedding,
|
||||
)
|
||||
if hasattr(m, "to_lora") or isinstance(m, types):
|
||||
keys.add(p)
|
||||
|
||||
for l in model.layers:
|
||||
l.apply_to_modules(get_keys_for_lora)
|
||||
|
||||
for l in model.layers[-max(num_layers, 0) :]:
|
||||
lora_layers = [(k, to_lora(m)) for k, m in l.named_modules() if k in keys]
|
||||
|
||||
+54
-45
@@ -42,6 +42,7 @@ from .tuner.utils import get_total_parameters, load_adapters
|
||||
# Constants
|
||||
MODEL_REMAPPING = {
|
||||
"mistral": "llama",
|
||||
"llava": "mistral3",
|
||||
"phi-msft": "phixtral",
|
||||
"falcon_mamba": "mamba",
|
||||
"kimi_k2": "deepseek_v3",
|
||||
@@ -81,9 +82,7 @@ def compute_bits_per_weight(model):
|
||||
return model_bytes * 8 / model_params
|
||||
|
||||
|
||||
def get_model_path(
|
||||
path_or_hf_repo: str, revision: Optional[str] = None
|
||||
) -> Tuple[Path, Optional[str]]:
|
||||
def _download(path_or_hf_repo: str, revision: Optional[str] = None) -> Path:
|
||||
"""
|
||||
Ensures the model is available locally. If the path does not exist locally,
|
||||
it is downloaded from the Hugging Face Hub.
|
||||
@@ -93,12 +92,11 @@ def get_model_path(
|
||||
revision (str, optional): A revision id which can be a branch name, a tag, or a commit hash.
|
||||
|
||||
Returns:
|
||||
Tuple[Path, str]: A tuple containing the local file path and the Hugging Face repo ID.
|
||||
Path: The local file path.
|
||||
"""
|
||||
model_path = Path(path_or_hf_repo)
|
||||
|
||||
if not model_path.exists():
|
||||
hf_path = path_or_hf_repo
|
||||
model_path = Path(
|
||||
snapshot_download(
|
||||
path_or_hf_repo,
|
||||
@@ -116,16 +114,12 @@ def get_model_path(
|
||||
],
|
||||
)
|
||||
)
|
||||
else:
|
||||
from huggingface_hub import ModelCard
|
||||
|
||||
card_path = model_path / "README.md"
|
||||
if card_path.is_file():
|
||||
card = ModelCard.load(card_path)
|
||||
hf_path = card.data.base_model
|
||||
else:
|
||||
hf_path = None
|
||||
return model_path, hf_path
|
||||
return model_path
|
||||
|
||||
|
||||
def hf_repo_to_path(hf_repo):
|
||||
return Path(snapshot_download(hf_repo, local_files_only=True))
|
||||
|
||||
|
||||
def load_config(model_path: Path) -> dict:
|
||||
@@ -237,7 +231,12 @@ def load(
|
||||
model_config={},
|
||||
adapter_path: Optional[str] = None,
|
||||
lazy: bool = False,
|
||||
) -> Tuple[nn.Module, TokenizerWrapper]:
|
||||
return_config: bool = False,
|
||||
revision: str = None,
|
||||
) -> Union[
|
||||
Tuple[nn.Module, TokenizerWrapper],
|
||||
Tuple[nn.Module, TokenizerWrapper, Dict[str, Any]],
|
||||
]:
|
||||
"""
|
||||
Load the model and tokenizer from a given path or a huggingface repository.
|
||||
|
||||
@@ -252,16 +251,19 @@ def load(
|
||||
lazy (bool): If ``False`` eval the model parameters to make sure they are
|
||||
loaded in memory before returning, otherwise they will be loaded
|
||||
when needed. Default: ``False``
|
||||
return_config (bool: If ``True`` return the model config as the last item..
|
||||
revision (str, optional): A revision id which can be a branch name, a tag, or a commit hash.
|
||||
Returns:
|
||||
Tuple[nn.Module, TokenizerWrapper]: A tuple containing the loaded model and tokenizer.
|
||||
Union[Tuple[nn.Module, TokenizerWrapper], Tuple[nn.Module, TokenizerWrapper, Dict[str, Any]]]:
|
||||
A tuple containing the loaded model, tokenizer and, if requested, the model config.
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If config file or safetensors are not found.
|
||||
ValueError: If model class or args class are not found.
|
||||
"""
|
||||
model_path, _ = get_model_path(path_or_hf_repo)
|
||||
model_path = _download(path_or_hf_repo, revision=revision)
|
||||
|
||||
model, config = load_model(model_path, lazy)
|
||||
model, config = load_model(model_path, lazy, model_config=model_config)
|
||||
if adapter_path is not None:
|
||||
model = load_adapters(model, adapter_path)
|
||||
model.eval()
|
||||
@@ -269,19 +271,10 @@ def load(
|
||||
model_path, tokenizer_config, eos_token_ids=config.get("eos_token_id", None)
|
||||
)
|
||||
|
||||
return model, tokenizer
|
||||
|
||||
|
||||
def fetch_from_hub(
|
||||
model_path: Path, lazy: bool = False, trust_remote_code: bool = False
|
||||
) -> Tuple[nn.Module, dict, PreTrainedTokenizer]:
|
||||
model, config = load_model(model_path, lazy)
|
||||
tokenizer = load_tokenizer(
|
||||
model_path,
|
||||
eos_token_ids=config.get("eos_token_id", None),
|
||||
tokenizer_config_extra={"trust_remote_code": trust_remote_code},
|
||||
)
|
||||
return model, config, tokenizer
|
||||
if return_config:
|
||||
return model, tokenizer, config
|
||||
else:
|
||||
return model, tokenizer
|
||||
|
||||
|
||||
def make_shards(weights: dict, max_file_size_gb: int = MAX_FILE_SIZE_GB) -> list:
|
||||
@@ -308,24 +301,28 @@ def make_shards(weights: dict, max_file_size_gb: int = MAX_FILE_SIZE_GB) -> list
|
||||
return shards
|
||||
|
||||
|
||||
def create_model_card(path: Union[str, Path], hf_path: Union[str, Path]):
|
||||
def create_model_card(path: Union[str, Path], hf_path: Union[str, Path, None]):
|
||||
"""
|
||||
Uploads the model to Hugging Face hub.
|
||||
|
||||
Args:
|
||||
path (Union[str, Path]): Local path to the model.
|
||||
hf_path (Union[str, Path]): Path to the original Hugging Face model.
|
||||
hf_path (Union[str, Path, None]): Path to the original Hugging Face model.
|
||||
"""
|
||||
from huggingface_hub import ModelCard
|
||||
from huggingface_hub import ModelCard, ModelCardData
|
||||
|
||||
card = ModelCard.load(hf_path)
|
||||
if hf_path is None:
|
||||
card = ModelCard.from_template(ModelCardData(language="en"))
|
||||
else:
|
||||
card = ModelCard.load(hf_path)
|
||||
card.data.library_name = "mlx"
|
||||
card.data.pipeline_tag = "text-generation"
|
||||
if card.data.tags is None:
|
||||
card.data.tags = ["mlx"]
|
||||
elif "mlx" not in card.data.tags:
|
||||
card.data.tags += ["mlx"]
|
||||
card.data.base_model = str(hf_path)
|
||||
if hf_path is not None:
|
||||
card.data.base_model = str(hf_path)
|
||||
card.text = ""
|
||||
card.save(os.path.join(path, "README.md"))
|
||||
|
||||
@@ -345,15 +342,22 @@ def upload_to_hub(path: str, upload_repo: str):
|
||||
logging.set_verbosity_info()
|
||||
card_path = Path(path) / "README.md"
|
||||
card = ModelCard.load(card_path)
|
||||
hf_path = card.data.base_model
|
||||
card.text = dedent(
|
||||
f"""
|
||||
# {upload_repo}
|
||||
|
||||
hf_path = card.data.base_model
|
||||
|
||||
if hf_path is not None:
|
||||
provenance = f"""
|
||||
This model [{upload_repo}](https://huggingface.co/{upload_repo}) was
|
||||
converted to MLX format from [{hf_path}](https://huggingface.co/{hf_path})
|
||||
using mlx-lm version **{__version__}**.
|
||||
"""
|
||||
else:
|
||||
provenance = ""
|
||||
|
||||
card.text = dedent(
|
||||
f"""
|
||||
# {upload_repo}
|
||||
{provenance}
|
||||
## Use with mlx
|
||||
|
||||
```bash
|
||||
@@ -544,14 +548,20 @@ def save_config(
|
||||
|
||||
def save(
|
||||
dst_path: Union[str, Path],
|
||||
src_path: Union[str, Path],
|
||||
src_path_or_repo: Union[str, Path],
|
||||
model: nn.Module,
|
||||
tokenizer: TokenizerWrapper,
|
||||
config: Dict[str, Any],
|
||||
hf_repo: Optional[str] = None,
|
||||
donate_model: bool = True,
|
||||
):
|
||||
src_path = Path(src_path)
|
||||
|
||||
src_path = Path(src_path_or_repo)
|
||||
if not src_path.exists():
|
||||
hf_repo = src_path_or_repo
|
||||
src_path = hf_repo_to_path(hf_repo)
|
||||
else:
|
||||
hf_repo = None
|
||||
|
||||
dst_path = Path(dst_path)
|
||||
save_model(dst_path, model, donate_model=True)
|
||||
save_config(config, config_path=dst_path / "config.json")
|
||||
@@ -561,8 +571,7 @@ def save(
|
||||
for file in glob.glob(str(src_path / p)):
|
||||
shutil.copy(file, dst_path)
|
||||
|
||||
if hf_repo is not None:
|
||||
create_model_card(dst_path, hf_repo)
|
||||
create_model_card(dst_path, hf_repo)
|
||||
|
||||
|
||||
def common_prefix_len(list1, list2):
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
mlx>=0.29.1
|
||||
numpy
|
||||
transformers>=4.39.3
|
||||
protobuf
|
||||
pyyaml
|
||||
jinja2
|
||||
@@ -6,12 +6,12 @@ from pathlib import Path
|
||||
from setuptools import setup
|
||||
|
||||
package_dir = Path(__file__).parent / "mlx_lm"
|
||||
with open("requirements.txt") as fid:
|
||||
requirements = [l.strip() for l in fid.readlines()]
|
||||
|
||||
sys.path.append(str(package_dir))
|
||||
|
||||
from _version import __version__
|
||||
|
||||
MIN_MLX_VERSION = "0.29.2"
|
||||
|
||||
setup(
|
||||
name="mlx-lm",
|
||||
version=__version__,
|
||||
@@ -23,13 +23,22 @@ setup(
|
||||
author="MLX Contributors",
|
||||
url="https://github.com/ml-explore/mlx-lm",
|
||||
license="MIT",
|
||||
install_requires=requirements,
|
||||
install_requires=[
|
||||
f"mlx>={MIN_MLX_VERSION}; platform_system == 'Darwin'",
|
||||
"numpy",
|
||||
"transformers>=4.39.3",
|
||||
"protobuf",
|
||||
"pyyaml",
|
||||
"jinja2",
|
||||
],
|
||||
packages=["mlx_lm", "mlx_lm.models", "mlx_lm.quant", "mlx_lm.tuner"],
|
||||
python_requires=">=3.8",
|
||||
extras_require={
|
||||
"test": ["datasets", "lm-eval"],
|
||||
"train": ["datasets", "tqdm"],
|
||||
"evaluate": ["lm-eval", "tqdm"],
|
||||
"cuda": [f"mlx[cuda]>={MIN_MLX_VERSION}"],
|
||||
"cpu": [f"mlx[cpu]>={MIN_MLX_VERSION}"],
|
||||
},
|
||||
entry_points={
|
||||
"console_scripts": [
|
||||
|
||||
+23
-16
@@ -28,13 +28,6 @@ def swapped_with_identity(obj, func):
|
||||
|
||||
|
||||
class TestLora(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.capturedOutput = StringIO()
|
||||
sys.stdout = self.capturedOutput
|
||||
|
||||
def tearDown(self):
|
||||
sys.stdout = sys.__stdout__
|
||||
|
||||
def test_llama(self):
|
||||
from mlx_lm.models import llama
|
||||
|
||||
@@ -48,7 +41,6 @@ class TestLora(unittest.TestCase):
|
||||
vocab_size=10_000,
|
||||
tie_word_embeddings=False,
|
||||
)
|
||||
|
||||
lora_layers = 4
|
||||
|
||||
def check_config(params, expected_trainable_parameters=None):
|
||||
@@ -68,10 +60,13 @@ class TestLora(unittest.TestCase):
|
||||
self.assertEqual(trainable_params, expected_trainable_parameters)
|
||||
|
||||
params = {"rank": 8, "dropout": 0.0, "scale": 10.0}
|
||||
check_config(params)
|
||||
nparams = (
|
||||
args.hidden_size * 2 * 4 + (args.intermediate_size + args.hidden_size) * 3
|
||||
) * lora_layers
|
||||
check_config(params, expected_trainable_parameters=nparams * params["rank"])
|
||||
|
||||
params["rank"] = 1
|
||||
check_config(params)
|
||||
check_config(params, expected_trainable_parameters=nparams * params["rank"])
|
||||
|
||||
params["keys"] = ["self_attn.k_proj"]
|
||||
check_config(params)
|
||||
@@ -191,7 +186,7 @@ class TestDora(unittest.TestCase):
|
||||
|
||||
dora_layers = 4
|
||||
|
||||
def check_config(params):
|
||||
def check_config(params, expected_params=None):
|
||||
n_keys = 2
|
||||
if "keys" in params:
|
||||
n_keys = len(params["keys"])
|
||||
@@ -201,17 +196,29 @@ class TestDora(unittest.TestCase):
|
||||
trainable_params = sum(
|
||||
v.size for _, v in tree_flatten(model.trainable_parameters())
|
||||
)
|
||||
self.assertEqual(
|
||||
trainable_params,
|
||||
expected_params = expected_params or (
|
||||
dora_layers
|
||||
* (params["rank"] * hidden_size * 2 * n_keys + n_keys * hidden_size),
|
||||
* (params["rank"] * hidden_size * 2 * n_keys + n_keys * hidden_size)
|
||||
)
|
||||
self.assertEqual(trainable_params, expected_params)
|
||||
|
||||
params = {"rank": 8, "alpha": 16, "dropout": 0.0, "scale": 10.0}
|
||||
check_config(params)
|
||||
nparams = (
|
||||
args.hidden_size * 2 * 4 + (args.intermediate_size + args.hidden_size) * 3
|
||||
)
|
||||
nparams = (
|
||||
nparams * params["rank"] + 5 * args.hidden_size + 2 * args.intermediate_size
|
||||
) * dora_layers
|
||||
check_config(params, expected_params=nparams)
|
||||
|
||||
params["rank"] = 1
|
||||
check_config(params)
|
||||
nparams = (
|
||||
args.hidden_size * 2 * 4 + (args.intermediate_size + args.hidden_size) * 3
|
||||
)
|
||||
nparams = (
|
||||
nparams * params["rank"] + 5 * args.hidden_size + 2 * args.intermediate_size
|
||||
) * dora_layers
|
||||
check_config(params, expected_params=nparams * params["rank"])
|
||||
|
||||
params["keys"] = ["self_attn.k_proj"]
|
||||
check_config(params)
|
||||
|
||||
@@ -11,6 +11,7 @@ from mlx_lm.generate import (
|
||||
generate,
|
||||
stream_generate,
|
||||
)
|
||||
from mlx_lm.models.cache import RotatingKVCache
|
||||
from mlx_lm.sample_utils import make_logits_processors, make_sampler
|
||||
from mlx_lm.utils import load
|
||||
|
||||
@@ -301,6 +302,56 @@ class TestGenerate(unittest.TestCase):
|
||||
batch_tokens = batch_responses[uids[e]]
|
||||
self.assertEqual(tokens, batch_tokens)
|
||||
|
||||
def test_batch_sliding_window(self):
|
||||
prompts = [
|
||||
"Write a story about Einstein",
|
||||
"Hi",
|
||||
"What time is it?",
|
||||
"How tall is Mt Everest?",
|
||||
]
|
||||
prompts = [
|
||||
self.tokenizer.apply_chat_template(
|
||||
[{"role": "user", "content": p}],
|
||||
tokenize=True,
|
||||
add_generation_prompt=True,
|
||||
)
|
||||
for p in prompts
|
||||
]
|
||||
|
||||
self.model.make_cache = lambda: [
|
||||
RotatingKVCache(max_size=4) for _ in self.model.layers
|
||||
]
|
||||
batch_gen = BatchGenerator(
|
||||
self.model,
|
||||
stop_tokens=self.tokenizer.eos_token_ids,
|
||||
max_tokens=10,
|
||||
prefill_batch_size=1,
|
||||
prefill_step_size=8,
|
||||
completion_batch_size=2,
|
||||
)
|
||||
uids = batch_gen.insert(prompts)
|
||||
batch_responses = {uid: [] for uid in uids}
|
||||
while responses := batch_gen.next():
|
||||
for r in responses:
|
||||
batch_responses[r.uid].append(r.logprobs)
|
||||
|
||||
for e, uid in enumerate(uids):
|
||||
for i, response in enumerate(
|
||||
stream_generate(
|
||||
self.model,
|
||||
self.tokenizer,
|
||||
prompts[e],
|
||||
max_tokens=10,
|
||||
)
|
||||
):
|
||||
batch_logprobs = batch_responses[uid][i]
|
||||
logprobs = response.logprobs
|
||||
self.assertTrue(
|
||||
mx.allclose(batch_logprobs, logprobs, rtol=1e-4, atol=1e-4)
|
||||
)
|
||||
|
||||
del self.model.make_cache
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
+209
-7
@@ -37,7 +37,7 @@ class TestModels(unittest.TestCase):
|
||||
|
||||
def test_rotating_kv_cache(self):
|
||||
b, h, d = 1, 2, 32
|
||||
cache = RotatingKVCache(max_size=8, step=4)
|
||||
cache = RotatingKVCache(max_size=8)
|
||||
|
||||
k = mx.random.uniform(shape=(b, h, 2, d))
|
||||
v = mx.random.uniform(shape=(b, h, 2, d))
|
||||
@@ -70,7 +70,7 @@ class TestModels(unittest.TestCase):
|
||||
idx %= 8
|
||||
|
||||
# Try with nonzero keep
|
||||
cache = RotatingKVCache(max_size=8, step=4, keep=2)
|
||||
cache = RotatingKVCache(max_size=8, keep=2)
|
||||
|
||||
# Check a large update
|
||||
k = mx.random.uniform(shape=(b, h, 20, d))
|
||||
@@ -98,7 +98,7 @@ class TestModels(unittest.TestCase):
|
||||
# alternating prompt/prefill with generation
|
||||
d = 4
|
||||
h = 2
|
||||
cache = RotatingKVCache(max_size=18, step=4)
|
||||
cache = RotatingKVCache(max_size=18)
|
||||
|
||||
x = mx.random.uniform(shape=(1, h, 8, d))
|
||||
k, v = cache.update_and_fetch(x, x)
|
||||
@@ -175,6 +175,49 @@ class TestModels(unittest.TestCase):
|
||||
sums = mask.sum(axis=1)
|
||||
self.assertTrue(mx.array_equal(sums, expected_sums))
|
||||
|
||||
def test_llama_model_sliding_attention(self):
|
||||
from mlx_lm.models import llama
|
||||
|
||||
args = llama.ModelArgs(
|
||||
model_type="llama",
|
||||
hidden_size=64,
|
||||
num_hidden_layers=4,
|
||||
intermediate_size=256,
|
||||
num_attention_heads=8,
|
||||
num_key_value_heads=4,
|
||||
rms_norm_eps=1e-5,
|
||||
vocab_size=128,
|
||||
sliding_window=4,
|
||||
layer_types=[
|
||||
"full_attention",
|
||||
"sliding_attention",
|
||||
"sliding_attention",
|
||||
"full_attention",
|
||||
],
|
||||
tie_word_embeddings=False,
|
||||
rope_theta=10000.0,
|
||||
)
|
||||
model = llama.Model(args)
|
||||
|
||||
tokens = mx.array([[1, 2, 3, 4, 5]], dtype=mx.int32)
|
||||
out = model(tokens)
|
||||
mx.eval(out)
|
||||
self.assertEqual(out.shape, (1, 5, args.vocab_size))
|
||||
|
||||
caches = model.make_cache()
|
||||
self.assertIsInstance(caches[0], KVCache)
|
||||
self.assertIsInstance(caches[1], RotatingKVCache)
|
||||
self.assertIsInstance(caches[2], RotatingKVCache)
|
||||
self.assertIsInstance(caches[3], KVCache)
|
||||
|
||||
caches = model.make_cache()
|
||||
step = model(tokens[:, :2], cache=caches)
|
||||
mx.eval(step)
|
||||
step = model(tokens[:, 2:3], cache=caches)
|
||||
mx.eval(step)
|
||||
self.assertEqual(caches[0].offset, 3)
|
||||
self.assertEqual(caches[1].offset, 3)
|
||||
|
||||
def test_rope(self):
|
||||
rope = rope_utils.initialize_rope(32, base=100, traditional=False)
|
||||
self.assertTrue(isinstance(rope, nn.RoPE))
|
||||
@@ -304,6 +347,35 @@ class TestModels(unittest.TestCase):
|
||||
model, args.model_type, args.vocab_size, args.num_hidden_layers
|
||||
)
|
||||
|
||||
def test_lfm2_moe(self):
|
||||
from mlx_lm.models import lfm2_moe
|
||||
|
||||
args = lfm2_moe.ModelArgs(
|
||||
model_type="lfm2_moe",
|
||||
hidden_size=1024,
|
||||
intermediate_size=7168,
|
||||
num_hidden_layers=4,
|
||||
num_attention_heads=4,
|
||||
num_key_value_heads=2,
|
||||
norm_eps=1e-5,
|
||||
vocab_size=10_000,
|
||||
full_attn_idxs=[0, 1, 2],
|
||||
rope_theta=10000,
|
||||
max_position_embeddings=1000,
|
||||
conv_bias=True,
|
||||
conv_L_cache=3,
|
||||
moe_intermediate_size=1792,
|
||||
num_dense_layers=2,
|
||||
num_experts=4,
|
||||
num_experts_per_tok=2,
|
||||
norm_topk_prob=True,
|
||||
use_expert_bias=True,
|
||||
)
|
||||
model = lfm2_moe.Model(args)
|
||||
self.model_test_runner(
|
||||
model, args.model_type, args.vocab_size, args.num_hidden_layers
|
||||
)
|
||||
|
||||
def test_bitnet(self):
|
||||
from mlx_lm.models import bitnet
|
||||
|
||||
@@ -666,6 +738,19 @@ class TestModels(unittest.TestCase):
|
||||
model, args.model_type, args.vocab_size, args.num_hidden_layers
|
||||
)
|
||||
|
||||
def test_falcon_h1(self):
|
||||
from mlx_lm.models import falcon_h1
|
||||
|
||||
args = falcon_h1.ModelArgs(
|
||||
model_type="falcon_h1",
|
||||
num_hidden_layers=12,
|
||||
vocab_size=10000,
|
||||
)
|
||||
model = falcon_h1.Model(args)
|
||||
self.model_test_runner(
|
||||
model, args.model_type, args.vocab_size, args.num_hidden_layers
|
||||
)
|
||||
|
||||
def test_gpt2(self):
|
||||
from mlx_lm.models import gpt2
|
||||
|
||||
@@ -1650,6 +1735,50 @@ class TestModels(unittest.TestCase):
|
||||
"num_hidden_layers": 4,
|
||||
"vocab_size": 1000,
|
||||
},
|
||||
{
|
||||
"model_type": "qwen3_vl_moe",
|
||||
"text_config": {
|
||||
"model_type": "qwen3_moe",
|
||||
"hidden_size": 128,
|
||||
"num_hidden_layers": 4,
|
||||
"intermediate_size": 256,
|
||||
"num_attention_heads": 4,
|
||||
"num_key_value_heads": 2,
|
||||
"rms_norm_eps": 1e-5,
|
||||
"head_dim": 32,
|
||||
"vocab_size": 1000,
|
||||
"decoder_sparse_step": 1,
|
||||
"mlp_only_layers": [],
|
||||
"num_experts_per_tok": 2,
|
||||
"num_experts": 4,
|
||||
"moe_intermediate_size": 128,
|
||||
"rope_theta": 1000,
|
||||
"max_position_embeddings": 1000,
|
||||
"tie_word_embeddings": False,
|
||||
"norm_topk_prob": True,
|
||||
},
|
||||
"num_hidden_layers": 4,
|
||||
"vocab_size": 1000,
|
||||
},
|
||||
{
|
||||
"model_type": "qwen3_vl",
|
||||
"text_config": {
|
||||
"model_type": "qwen3",
|
||||
"hidden_size": 128,
|
||||
"num_hidden_layers": 4,
|
||||
"intermediate_size": 256,
|
||||
"num_attention_heads": 4,
|
||||
"num_key_value_heads": 2,
|
||||
"rms_norm_eps": 1e-5,
|
||||
"vocab_size": 1000,
|
||||
"head_dim": 32,
|
||||
"max_position_embeddings": 1000,
|
||||
"tie_word_embeddings": False,
|
||||
"rope_theta": 1000,
|
||||
},
|
||||
"num_hidden_layers": 4,
|
||||
"vocab_size": 1000,
|
||||
},
|
||||
{
|
||||
"model_type": "seed_oss",
|
||||
"hidden_size": 128,
|
||||
@@ -1768,6 +1897,48 @@ class TestModels(unittest.TestCase):
|
||||
"time_step_limit": (0.01, 10),
|
||||
"time_step_rank": "auto",
|
||||
},
|
||||
{
|
||||
"model_type": "olmo3",
|
||||
"num_heads": 8,
|
||||
"head_dim": 16,
|
||||
"vocab_size": 1000,
|
||||
"hidden_size": 128,
|
||||
"intermediate_size": 128,
|
||||
"num_attention_heads": 8,
|
||||
"rope_theta": 1000,
|
||||
"num_hidden_layers": 8,
|
||||
"rms_norm_eps": 1e-4,
|
||||
"sliding_window": 128,
|
||||
"tie_word_embeddings": True,
|
||||
"max_position_embeddings": 1000,
|
||||
},
|
||||
{
|
||||
"model_type": "jamba",
|
||||
"hidden_size": 128,
|
||||
"intermediate_size": 128,
|
||||
"num_hidden_layers": 8,
|
||||
"num_attention_heads": 4,
|
||||
"num_key_value_heads": 2,
|
||||
"attn_layer_offset": 1,
|
||||
"attn_layer_period": 2,
|
||||
"expert_layer_offset": 1,
|
||||
"expert_layer_period": 2,
|
||||
"mamba_d_conv": 4,
|
||||
"mamba_d_state": 128,
|
||||
"mamba_expand": 128,
|
||||
"num_experts": 4,
|
||||
"num_experts_per_tok": 2,
|
||||
"rms_norm_eps": 1e-5,
|
||||
"max_position_embeddings": 1000,
|
||||
"vocab_size": 1000,
|
||||
},
|
||||
{
|
||||
"model_type": "nanochat",
|
||||
"hidden_size": 1280,
|
||||
"num_hidden_layers": 20,
|
||||
"vocab_size": 32,
|
||||
"intermediate_size": 128,
|
||||
},
|
||||
]
|
||||
for config in test_configs:
|
||||
model_type = config["model_type"]
|
||||
@@ -1849,9 +2020,9 @@ class TestModels(unittest.TestCase):
|
||||
self.assertTrue(mx.allclose(out_state, out_state_m, atol=1e-4, rtol=1e-4))
|
||||
|
||||
def test_gated_delta(self):
|
||||
mx.random.seed(0)
|
||||
for B in [1, 2]:
|
||||
for T in [1, 2]:
|
||||
B = 1
|
||||
Hk = 16
|
||||
Hv = 32
|
||||
Dk = 128
|
||||
@@ -1860,14 +2031,45 @@ class TestModels(unittest.TestCase):
|
||||
q = mx.random.normal(shape=(B, T, Hk, Dk))
|
||||
k = mx.random.normal(shape=(B, T, Hk, Dk))
|
||||
v = mx.random.normal(shape=(B, T, Hv, Dv))
|
||||
g = mx.random.normal(shape=(B, T, Hv))
|
||||
beta = mx.random.normal(shape=(B, T, Hv))
|
||||
g = mx.random.uniform(shape=(B, T, Hv))
|
||||
beta = mx.random.uniform(shape=(B, T, Hv))
|
||||
state = mx.random.normal(shape=(B, Hv, Dk, Dv))
|
||||
|
||||
y_op, st_op = gated_delta_ops(q, k, v, g, beta, state)
|
||||
y_c, st_c = gated_delta_kernel(q, k, v, g, beta, state)
|
||||
self.assertTrue(mx.allclose(y_op, y_c, rtol=1e-4, atol=1e-4))
|
||||
self.assertTrue(mx.allclose(st_op, st_c, rtol=1e-4, atol=1e-3))
|
||||
self.assertTrue(mx.allclose(st_op, st_c, rtol=1e-4, atol=1e-4))
|
||||
|
||||
def test_gated_delta_masked(self):
|
||||
B = 1
|
||||
T = 3
|
||||
Hk = 16
|
||||
Hv = 32
|
||||
Dk = 128
|
||||
Dv = 128
|
||||
|
||||
mx.random.seed(0)
|
||||
q = mx.random.normal(shape=(B, T, Hk, Dk))
|
||||
k = mx.random.normal(shape=(B, T, Hk, Dk))
|
||||
v = mx.random.normal(shape=(B, T, Hv, Dv))
|
||||
g = mx.random.normal(shape=(B, T, Hv))
|
||||
mask = mx.array([[False, True, True]])
|
||||
beta = mx.random.normal(shape=(B, T, Hv))
|
||||
state = mx.random.normal(shape=(B, Hv, Dk, Dv))
|
||||
|
||||
y_gt, st_gt = gated_delta_ops(
|
||||
q[:, 1:],
|
||||
k[:, 1:],
|
||||
v[:, 1:],
|
||||
g[:, 1:],
|
||||
beta[:, 1:],
|
||||
state,
|
||||
)
|
||||
for fn in [gated_delta_ops, gated_delta_kernel]:
|
||||
y, st = fn(q, k, v, g, beta, state, mask)
|
||||
y = y[:, 1:]
|
||||
self.assertTrue(mx.allclose(y, y_gt, rtol=1e-4, atol=1e-4))
|
||||
self.assertTrue(mx.allclose(st, st_gt, rtol=1e-4, atol=1e-3))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
+113
-5
@@ -11,6 +11,7 @@ from mlx_lm.generate import generate_step
|
||||
from mlx_lm.models.base import create_attention_mask, create_causal_mask
|
||||
from mlx_lm.models.cache import (
|
||||
BatchKVCache,
|
||||
BatchRotatingKVCache,
|
||||
CacheList,
|
||||
ChunkedKVCache,
|
||||
KVCache,
|
||||
@@ -391,7 +392,7 @@ class TestPromptCache(unittest.TestCase):
|
||||
kv = mx.zeros((1, 1, 10, 32))
|
||||
cache.update_and_fetch(kv, kv)
|
||||
mask = cache.make_mask(3, window_size=5)
|
||||
self.assertEqual(mask.shape, (3, 11))
|
||||
self.assertEqual(mask.shape, (3, 10))
|
||||
self.assertTrue(mx.all(mask.sum(axis=-1) == 5))
|
||||
for i in range(3):
|
||||
s = 11 - 3 + i
|
||||
@@ -405,7 +406,7 @@ class TestPromptCache(unittest.TestCase):
|
||||
self.assertEqual(mask, None)
|
||||
|
||||
mask = cache.make_mask(1, window_size=5)
|
||||
self.assertEqual(mask.squeeze(1).tolist(), [True] + [False] * 3 + [True] * 4)
|
||||
self.assertEqual(mask.tolist(), [True] + [False] * 3 + [True] * 4)
|
||||
cmask = create_attention_mask(mx.zeros((1, 1, 32)), cache, window_size=5)
|
||||
self.assertTrue(mx.array_equal(cmask, mask))
|
||||
|
||||
@@ -413,9 +414,7 @@ class TestPromptCache(unittest.TestCase):
|
||||
cache.update_and_fetch(kv, kv)
|
||||
|
||||
mask = cache.make_mask(1, window_size=5)
|
||||
self.assertEqual(
|
||||
mask.squeeze(1).tolist(), [True] * 2 + [False] * 3 + [True] * 3
|
||||
)
|
||||
self.assertEqual(mask.tolist(), [True] * 2 + [False] * 3 + [True] * 3)
|
||||
cmask = create_attention_mask(mx.zeros((1, 1, 32)), cache, window_size=5)
|
||||
self.assertTrue(mx.array_equal(cmask, mask))
|
||||
|
||||
@@ -460,6 +459,115 @@ class TestPromptCache(unittest.TestCase):
|
||||
self.assertEqual(cache_a.offset.tolist(), [6, 7, 6, 1, 4])
|
||||
self.assertEqual(cache_a.left_padding.tolist(), [2, 1, 2, 7, 4])
|
||||
|
||||
def test_batch_rotating_kv_cache(self):
|
||||
cache = BatchRotatingKVCache(max_size=4, left_padding=[2, 0])
|
||||
mask = cache.make_mask(4)
|
||||
self.assertFalse(mx.any(mask[0, 0, 0, :]))
|
||||
self.assertTrue(
|
||||
mx.array_equal(mask[1, 0, 0, :], mx.array([True, False, False, False]))
|
||||
)
|
||||
|
||||
# Batch update works
|
||||
k, v = mx.zeros((2, 1, 4, 8)), mx.zeros((2, 1, 4, 8))
|
||||
k, v = cache.update_and_fetch(k, v)
|
||||
|
||||
mask = cache.make_mask(4)
|
||||
k, v = mx.zeros((2, 1, 4, 8)), mx.zeros((2, 1, 4, 8))
|
||||
k, v = cache.update_and_fetch(k, v)
|
||||
self.assertEqual(mask.shape[-2:], (4, k.shape[2]))
|
||||
self.assertEqual(
|
||||
mask[0, 0, 0, :].tolist(), [False, True, True, True, False, False, False]
|
||||
)
|
||||
|
||||
# Single query update works
|
||||
cache = BatchRotatingKVCache(max_size=4, left_padding=[2, 0])
|
||||
k, v = mx.zeros((2, 1, 4, 8)), mx.zeros((2, 1, 4, 8))
|
||||
k, v = cache.update_and_fetch(k, v)
|
||||
|
||||
mask = cache.make_mask(1)
|
||||
k, v = mx.zeros((2, 1, 1, 8)), mx.zeros((2, 1, 1, 8))
|
||||
|
||||
k, v = cache.update_and_fetch(k, v)
|
||||
self.assertEqual(mask.shape[-2:], (1, k.shape[2]))
|
||||
self.assertEqual(mask[0, 0, 0].tolist(), [True, False, True, True])
|
||||
self.assertEqual(mask[1, 0, 0].tolist(), [True, True, True, True])
|
||||
|
||||
# Check filtering
|
||||
cache = BatchRotatingKVCache(max_size=4, left_padding=[2, 0, 3])
|
||||
k, v = mx.zeros((3, 1, 3, 8)), mx.zeros((3, 1, 3, 8))
|
||||
cache.update_and_fetch(k, v)
|
||||
cache.filter(mx.array([1]))
|
||||
self.assertEqual(cache.keys.shape, (1, 1, 3, 8))
|
||||
|
||||
# Check extend
|
||||
cache = BatchRotatingKVCache(max_size=4, left_padding=[2, 1])
|
||||
other = BatchRotatingKVCache(max_size=4, left_padding=[2, 2])
|
||||
k, v = mx.zeros((2, 1, 5, 8)), mx.zeros((2, 1, 5, 8))
|
||||
cache.update_and_fetch(k, v)
|
||||
other.update_and_fetch(k, v)
|
||||
k, v = mx.zeros((2, 1, 1, 8)), mx.zeros((2, 1, 1, 8))
|
||||
cache.update_and_fetch(k, v)
|
||||
cache.extend(other)
|
||||
|
||||
# Check mask when going from prompt -> extend -> prompt
|
||||
cache = BatchRotatingKVCache(max_size=8, left_padding=[4])
|
||||
k, v = mx.zeros((1, 1, 8, 8)), mx.zeros((1, 1, 8, 8))
|
||||
cache.update_and_fetch(k, v)
|
||||
|
||||
mask = cache.make_mask(1)
|
||||
self.assertEqual(
|
||||
mask.squeeze().tolist(), [True, False, False, False, True, True, True, True]
|
||||
)
|
||||
|
||||
k, v = mx.zeros((1, 1, 1, 8)), mx.zeros((1, 1, 1, 8))
|
||||
cache.update_and_fetch(k, v)
|
||||
|
||||
mask = cache.make_mask(2)
|
||||
expected = mx.array(
|
||||
[
|
||||
[False, False, False, True, True, True, True, True, False],
|
||||
[False, False, False, True, True, True, True, True, True],
|
||||
]
|
||||
)
|
||||
self.assertTrue(mx.array_equal(mask.squeeze(), expected))
|
||||
|
||||
def test_save_load_batch_caches(self):
|
||||
cache_file = os.path.join(self.test_dir, "prompt_cache.safetensors")
|
||||
|
||||
cache = [
|
||||
MambaCache(left_padding=[1, 2]),
|
||||
BatchKVCache(left_padding=[1, 2]),
|
||||
BatchRotatingKVCache(max_size=10, left_padding=[1, 2]),
|
||||
]
|
||||
for c in cache:
|
||||
if isinstance(c, MambaCache):
|
||||
c[0] = mx.random.uniform(shape=(4, 4, 4))
|
||||
c[1] = mx.random.uniform(shape=(4, 4, 4))
|
||||
else:
|
||||
x = mx.random.uniform(shape=(4, 4, 7, 4))
|
||||
y = mx.random.uniform(shape=(4, 4, 7, 4))
|
||||
c.update_and_fetch(x, y)
|
||||
|
||||
save_prompt_cache(cache_file, cache)
|
||||
loaded_cache = load_prompt_cache(cache_file)
|
||||
left_padding = mx.array([1, 2])
|
||||
for c, lc in zip(cache, loaded_cache):
|
||||
self.assertTrue(mx.array_equal(c.left_padding, left_padding))
|
||||
|
||||
def test_rotating_cache_updates(self):
|
||||
cache = RotatingKVCache(max_size=8)
|
||||
k = v = mx.zeros((1, 1, 10, 1))
|
||||
cache.update_and_fetch(k, v)
|
||||
|
||||
for _ in range(3):
|
||||
k = v = mx.zeros((1, 1, 1, 1))
|
||||
cache.update_and_fetch(k, v)
|
||||
|
||||
k = v = mx.zeros((1, 1, 3, 1))
|
||||
k, v = cache.update_and_fetch(k, v)
|
||||
self.assertEqual(k.shape[2], 10)
|
||||
self.assertEqual(v.shape[2], 10)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -27,7 +27,10 @@ class TestUtils(unittest.TestCase):
|
||||
cls.test_dir_fid.cleanup()
|
||||
|
||||
def test_load(self):
|
||||
from mlx_lm.models.qwen2 import Model as Qwen2Model
|
||||
|
||||
model, _ = utils.load(HF_MODEL_PATH)
|
||||
self.assertIsInstance(model, Qwen2Model)
|
||||
|
||||
model_lazy, _ = utils.load(HF_MODEL_PATH, lazy=True)
|
||||
|
||||
@@ -91,6 +94,35 @@ class TestUtils(unittest.TestCase):
|
||||
self.assertEqual(model.layers[0].mlp.up_proj.scales.dtype, mx.bfloat16)
|
||||
self.assertEqual(model.layers[-1].mlp.up_proj.scales.dtype, mx.bfloat16)
|
||||
|
||||
def test_load_model_with_custom_get_classes(self):
|
||||
class CustomQwenModel(nn.Module):
|
||||
def __init__(self, args):
|
||||
super().__init__()
|
||||
self.config = args
|
||||
self.custom_attribute = "This is a custom model"
|
||||
|
||||
def load_weights(self, weights, **kwargs):
|
||||
self.qwenWeights = weights
|
||||
|
||||
class CustomQwenConfig:
|
||||
@classmethod
|
||||
def from_dict(cls, config):
|
||||
instance = cls()
|
||||
for k, v in config.items():
|
||||
setattr(instance, k, v)
|
||||
return instance
|
||||
|
||||
def custom_get_classes(config):
|
||||
return CustomQwenModel, CustomQwenConfig
|
||||
|
||||
model_path = utils.hf_repo_to_path(HF_MODEL_PATH)
|
||||
model, _ = utils.load_model(model_path, get_model_classes=custom_get_classes)
|
||||
|
||||
self.assertIsInstance(model, CustomQwenModel)
|
||||
self.assertTrue(hasattr(model, "custom_attribute"))
|
||||
self.assertEqual(model.custom_attribute, "This is a custom model")
|
||||
self.assertTrue(hasattr(model, "qwenWeights"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
import mlx.nn as nn
|
||||
|
||||
from mlx_lm.models.qwen2 import Model as Qwen2Model
|
||||
from mlx_lm.utils import get_model_path, load_model
|
||||
|
||||
HF_MODEL_PATH = "mlx-community/Qwen1.5-0.5B-Chat-4bit"
|
||||
|
||||
|
||||
class TestLoadModelCustomGetClasses(unittest.TestCase):
|
||||
|
||||
def test_load_model_with_custom_get_classes(self):
|
||||
class CustomQwenModel(nn.Module):
|
||||
def __init__(self, args):
|
||||
super().__init__()
|
||||
self.config = args
|
||||
self.custom_attribute = "This is a custom model"
|
||||
|
||||
def load_weights(self, weights, **kwargs):
|
||||
self.qwenWeights = weights
|
||||
|
||||
class CustomQwenConfig:
|
||||
@classmethod
|
||||
def from_dict(cls, config):
|
||||
instance = cls()
|
||||
for k, v in config.items():
|
||||
setattr(instance, k, v)
|
||||
return instance
|
||||
|
||||
def custom_get_classes(config):
|
||||
return CustomQwenModel, CustomQwenConfig
|
||||
|
||||
model_path, _ = get_model_path(HF_MODEL_PATH)
|
||||
model, _ = load_model(model_path, get_model_classes=custom_get_classes)
|
||||
|
||||
self.assertIsInstance(model, CustomQwenModel)
|
||||
self.assertTrue(hasattr(model, "custom_attribute"))
|
||||
self.assertEqual(model.custom_attribute, "This is a custom model")
|
||||
self.assertTrue(hasattr(model, "qwenWeights"))
|
||||
|
||||
def test_load_model_with_default_get_classes(self):
|
||||
model_path, _ = get_model_path(HF_MODEL_PATH)
|
||||
model, _ = load_model(model_path)
|
||||
|
||||
self.assertIsInstance(model, Qwen2Model)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user