Adds a committed .gitattributes so Windows and Linux checkouts converge on LF for all text files, overriding each developer's local core.autocrlf. Renormalizes existing blobs (server.py, dashboard.html, etc.) that had CRLF baked in, clearing the repo-wide phantom "modified" churn. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1052 lines
33 KiB
Python
1052 lines
33 KiB
Python
"""US-012 tests for the real PyTorch node backend."""
|
|
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import sys
|
|
import threading
|
|
import time
|
|
import types
|
|
import urllib.request
|
|
|
|
import pytest
|
|
|
|
from meshnet_node.model_backend import (
|
|
InsufficientVRAMError,
|
|
PartialModelLoadUnsupported,
|
|
TensorPayload,
|
|
TorchModelShard,
|
|
_call_layer,
|
|
_checkpoint_tensor_name_for_model,
|
|
_load_partial_model_from_snapshot,
|
|
_should_partial_materialize_shard,
|
|
_decoder_attention_mask,
|
|
_int_tensor_header,
|
|
build_quantization_config,
|
|
validate_quantization,
|
|
)
|
|
from meshnet_node.torch_server import TorchNodeServer
|
|
|
|
|
|
class _FakeBackend:
|
|
model_id = "fake-model"
|
|
total_layers = 12
|
|
is_head = True
|
|
is_tail = False
|
|
|
|
def encode_prompt(self, prompt: str) -> TensorPayload:
|
|
assert prompt == "The capital of France is"
|
|
return TensorPayload(
|
|
body=b"\x00" * (1 * 6 * 8 * 2),
|
|
shape=[1, 6, 8],
|
|
attention_mask_header=None,
|
|
position_ids_header=None,
|
|
)
|
|
|
|
def forward_bytes(self, body, shape, attention_mask_header, position_ids_header, start_layer=None):
|
|
assert shape == [1, 6, 8]
|
|
return TensorPayload(
|
|
body=body,
|
|
shape=shape,
|
|
attention_mask_header=attention_mask_header,
|
|
position_ids_header=position_ids_header,
|
|
)
|
|
|
|
|
|
class _FakeTailBackend(_FakeBackend):
|
|
is_head = False
|
|
is_tail = True
|
|
|
|
def forward_bytes(self, body, shape, attention_mask_header, position_ids_header, start_layer=None):
|
|
assert len(body) == 1 * 6 * 8 * 2
|
|
return " Paris"
|
|
|
|
|
|
class _FakeFullBackend(_FakeBackend):
|
|
is_head = True
|
|
is_tail = True
|
|
|
|
def generate_text(
|
|
self,
|
|
messages: list[dict],
|
|
max_new_tokens: int = 16,
|
|
temperature: float = 1.0,
|
|
top_p: float = 1.0,
|
|
) -> str:
|
|
assert messages == [{"role": "user", "content": "What is 7 times 8?"}]
|
|
assert max_new_tokens == 7
|
|
assert temperature == 1.0
|
|
assert top_p == 1.0
|
|
return "56"
|
|
|
|
def count_prompt_tokens(self, messages: list[dict]) -> int:
|
|
assert messages == [{"role": "user", "content": "What is 7 times 8?"}]
|
|
return 8
|
|
|
|
def count_text_tokens(self, text: str) -> int:
|
|
assert text == "56"
|
|
return 1
|
|
|
|
|
|
class _FakeChatTokenizer:
|
|
eos_token = ""
|
|
|
|
def apply_chat_template(self, messages, add_generation_prompt=True, tokenize=False):
|
|
assert add_generation_prompt is True
|
|
assert tokenize is False
|
|
return "debug prompt"
|
|
|
|
|
|
class _FakePipelineHeadBackend(_FakeBackend):
|
|
tokenizer = _FakeChatTokenizer()
|
|
|
|
def encode_prompt(self, prompt: str) -> TensorPayload:
|
|
assert prompt.startswith("debug prompt")
|
|
return TensorPayload(
|
|
body=b"\x00" * (1 * 6 * 8 * 2),
|
|
shape=[1, 6, 8],
|
|
attention_mask_header=None,
|
|
position_ids_header=None,
|
|
)
|
|
|
|
|
|
class _FakePipelineTailBackend(_FakeTailBackend):
|
|
def __init__(self) -> None:
|
|
self.start_layers: list[int | None] = []
|
|
|
|
def forward_bytes(self, body, shape, attention_mask_header, position_ids_header, start_layer=None):
|
|
self.start_layers.append(start_layer)
|
|
assert len(body) == 1 * 6 * 8 * 2
|
|
return " token"
|
|
|
|
|
|
class _BlockingStreamingTailBackend(_FakeTailBackend):
|
|
def __init__(self, second_token_release: threading.Event) -> None:
|
|
self._release = second_token_release
|
|
self.calls = 0
|
|
|
|
def forward_bytes(self, body, shape, attention_mask_header, position_ids_header, start_layer=None):
|
|
self.calls += 1
|
|
if self.calls == 1:
|
|
return " first"
|
|
self._release.wait(timeout=3.0)
|
|
return " second"
|
|
|
|
|
|
def test_quantization_flag_validation():
|
|
assert validate_quantization("bfloat16") == "bfloat16"
|
|
assert validate_quantization("int8") == "int8"
|
|
assert validate_quantization("nf4") == "nf4"
|
|
with pytest.raises(ValueError, match="quantization"):
|
|
validate_quantization("float32")
|
|
|
|
|
|
def test_node_package_declares_torch_dependency():
|
|
pyproject = Path("packages/node/pyproject.toml").read_text(encoding="utf-8")
|
|
|
|
assert '"torch>=' in pyproject
|
|
|
|
|
|
def test_bitsandbytes_configs_are_created_lazily(monkeypatch):
|
|
calls = []
|
|
|
|
class FakeBitsAndBytesConfig:
|
|
def __init__(self, **kwargs):
|
|
calls.append(kwargs)
|
|
|
|
monkeypatch.setitem(sys.modules, "torch", types.SimpleNamespace(bfloat16="bf16"))
|
|
monkeypatch.setitem(
|
|
sys.modules,
|
|
"transformers",
|
|
types.SimpleNamespace(BitsAndBytesConfig=FakeBitsAndBytesConfig),
|
|
)
|
|
|
|
assert build_quantization_config("bfloat16") is None
|
|
build_quantization_config("int8")
|
|
build_quantization_config("nf4")
|
|
|
|
assert calls == [
|
|
{"load_in_8bit": True},
|
|
{
|
|
"load_in_4bit": True,
|
|
"bnb_4bit_quant_type": "nf4",
|
|
"bnb_4bit_compute_dtype": "bf16",
|
|
},
|
|
]
|
|
|
|
|
|
def test_head_forward_accepts_text_prompt_and_returns_bfloat16_activations():
|
|
node = TorchNodeServer(backend=_FakeBackend())
|
|
port = node.start()
|
|
try:
|
|
payload = json.dumps({"prompt": "The capital of France is"}).encode()
|
|
req = urllib.request.Request(
|
|
f"http://127.0.0.1:{port}/forward",
|
|
data=payload,
|
|
headers={"Content-Type": "application/json"},
|
|
method="POST",
|
|
)
|
|
with urllib.request.urlopen(req, timeout=5) as resp:
|
|
body = resp.read()
|
|
headers = {key.lower(): value for key, value in resp.headers.items()}
|
|
|
|
assert len(body) == 1 * 6 * 8 * 2
|
|
assert headers["x-meshnet-shape"] == "1,6,8"
|
|
assert headers["x-meshnet-dtype"] == "bfloat16"
|
|
assert headers["x-meshnet-wire"] == "2"
|
|
finally:
|
|
node.stop()
|
|
|
|
|
|
def test_tail_forward_returns_text_completion_from_binary_activations():
|
|
node = TorchNodeServer(backend=_FakeTailBackend())
|
|
port = node.start()
|
|
try:
|
|
req = urllib.request.Request(
|
|
f"http://127.0.0.1:{port}/forward",
|
|
data=b"\x00" * (1 * 6 * 8 * 2),
|
|
headers={
|
|
"Content-Type": "application/octet-stream",
|
|
"X-Meshnet-Shape": "1,6,8",
|
|
"X-Meshnet-Dtype": "bfloat16",
|
|
"X-Meshnet-Session": "session-1",
|
|
"X-Meshnet-Chunk-Index": "0",
|
|
"X-Meshnet-Chunk-Total": "1",
|
|
"X-Meshnet-Hop-Index": "1",
|
|
},
|
|
method="POST",
|
|
)
|
|
with urllib.request.urlopen(req, timeout=5) as resp:
|
|
body = json.loads(resp.read())
|
|
|
|
assert body == {"text": " Paris"}
|
|
assert node.received_activations
|
|
assert node.forward_chunk_count == 1
|
|
finally:
|
|
node.stop()
|
|
|
|
|
|
def test_full_model_chat_completion_uses_generation_not_single_token_decode(capsys):
|
|
node = TorchNodeServer(backend=_FakeFullBackend())
|
|
port = node.start()
|
|
try:
|
|
payload = json.dumps({
|
|
"model": "fake-model",
|
|
"messages": [{"role": "user", "content": "What is 7 times 8?"}],
|
|
"max_tokens": 7,
|
|
}).encode()
|
|
req = urllib.request.Request(
|
|
f"http://127.0.0.1:{port}/v1/chat/completions",
|
|
data=payload,
|
|
headers={
|
|
"Content-Type": "application/json",
|
|
"X-Meshnet-Request-Id": "req-test-123",
|
|
},
|
|
method="POST",
|
|
)
|
|
with urllib.request.urlopen(req, timeout=5) as resp:
|
|
body = json.loads(resp.read())
|
|
|
|
assert body["choices"][0]["message"]["content"] == "56"
|
|
assert body["usage"] == {"prompt_tokens": 8, "completion_tokens": 1, "total_tokens": 9}
|
|
finally:
|
|
node.stop()
|
|
|
|
out = capsys.readouterr().out
|
|
assert " [node] processing chat model='fake-model' stream=False max_tokens=7 request_id=req-test-123" in out
|
|
assert " [node] chat complete tokens=1 elapsed_s=" in out
|
|
|
|
|
|
def test_pipeline_hop_logs_are_suppressed_without_debug(capsys):
|
|
tail_backend = _FakePipelineTailBackend()
|
|
head = TorchNodeServer(backend=_FakePipelineHeadBackend(), tracker_mode=True)
|
|
tail = TorchNodeServer(backend=tail_backend)
|
|
head_port = head.start()
|
|
tail_port = tail.start()
|
|
try:
|
|
payload = json.dumps({
|
|
"model": "fake-model",
|
|
"messages": [{"role": "user", "content": "hello"}],
|
|
"max_tokens": 1,
|
|
}).encode()
|
|
req = urllib.request.Request(
|
|
f"http://127.0.0.1:{head_port}/v1/chat/completions",
|
|
data=payload,
|
|
headers={
|
|
"Content-Type": "application/json",
|
|
"X-Meshnet-Route": json.dumps([
|
|
{"endpoint": f"http://127.0.0.1:{tail_port}", "start_layer": 22},
|
|
]),
|
|
},
|
|
method="POST",
|
|
)
|
|
with urllib.request.urlopen(req, timeout=5) as resp:
|
|
body = json.loads(resp.read())
|
|
finally:
|
|
head.stop()
|
|
tail.stop()
|
|
|
|
out = capsys.readouterr().out
|
|
assert body["choices"][0]["message"]["content"] == " token"
|
|
assert tail_backend.start_layers == [22]
|
|
assert "pipeline hop 0:" not in out
|
|
assert "pipeline hop 0 returned text" not in out
|
|
|
|
|
|
def test_pipeline_hop_logs_are_enabled_with_debug(capsys):
|
|
head = TorchNodeServer(backend=_FakePipelineHeadBackend(), tracker_mode=True, debug=True)
|
|
tail = TorchNodeServer(backend=_FakePipelineTailBackend())
|
|
head_port = head.start()
|
|
tail_port = tail.start()
|
|
try:
|
|
payload = json.dumps({
|
|
"model": "fake-model",
|
|
"messages": [{"role": "user", "content": "hello"}],
|
|
"max_tokens": 1,
|
|
}).encode()
|
|
req = urllib.request.Request(
|
|
f"http://127.0.0.1:{head_port}/v1/chat/completions",
|
|
data=payload,
|
|
headers={
|
|
"Content-Type": "application/json",
|
|
"X-Meshnet-Route": json.dumps([
|
|
{"endpoint": f"http://127.0.0.1:{tail_port}", "start_layer": 22},
|
|
]),
|
|
},
|
|
method="POST",
|
|
)
|
|
with urllib.request.urlopen(req, timeout=5) as resp:
|
|
json.loads(resp.read())
|
|
finally:
|
|
head.stop()
|
|
tail.stop()
|
|
|
|
out = capsys.readouterr().out
|
|
assert f" [node] pipeline hop 0: http://127.0.0.1:{tail_port} start_layer=22" in out
|
|
assert " [node] pipeline hop 0 returned text=' token'" in out
|
|
|
|
|
|
def test_split_shard_chat_streams_each_generated_token_incrementally():
|
|
release_second = threading.Event()
|
|
head = TorchNodeServer(backend=_FakePipelineHeadBackend(), tracker_mode=True)
|
|
tail = TorchNodeServer(backend=_BlockingStreamingTailBackend(release_second))
|
|
head_port = head.start()
|
|
tail_port = tail.start()
|
|
response = None
|
|
try:
|
|
payload = json.dumps({
|
|
"model": "fake-model",
|
|
"messages": [{"role": "user", "content": "hello"}],
|
|
"stream": True,
|
|
"max_tokens": 2,
|
|
}).encode()
|
|
req = urllib.request.Request(
|
|
f"http://127.0.0.1:{head_port}/v1/chat/completions",
|
|
data=payload,
|
|
headers={
|
|
"Content-Type": "application/json",
|
|
"X-Meshnet-Route": json.dumps([
|
|
{"endpoint": f"http://127.0.0.1:{tail_port}", "start_layer": 22},
|
|
]),
|
|
},
|
|
method="POST",
|
|
)
|
|
response = urllib.request.urlopen(req, timeout=5)
|
|
|
|
first_token_line = ""
|
|
deadline = time.time() + 2.0
|
|
while time.time() < deadline:
|
|
line = response.readline().decode()
|
|
if '"content": " first"' in line:
|
|
first_token_line = line
|
|
break
|
|
|
|
assert first_token_line
|
|
assert not release_second.is_set()
|
|
release_second.set()
|
|
rest = response.read().decode()
|
|
finally:
|
|
release_second.set()
|
|
if response is not None:
|
|
response.close()
|
|
head.stop()
|
|
tail.stop()
|
|
|
|
assert '"content": " second"' in rest
|
|
assert "data: [DONE]" in rest
|
|
|
|
|
|
def test_current_requests_snapshot_while_generating():
|
|
release_second = threading.Event()
|
|
head = TorchNodeServer(backend=_FakePipelineHeadBackend(), tracker_mode=True)
|
|
tail = TorchNodeServer(backend=_BlockingStreamingTailBackend(release_second))
|
|
head_port = head.start()
|
|
tail_port = tail.start()
|
|
response = None
|
|
try:
|
|
payload = json.dumps({
|
|
"model": "fake-model",
|
|
"messages": [{"role": "user", "content": "hello"}],
|
|
"stream": True,
|
|
"max_tokens": 2,
|
|
}).encode()
|
|
req = urllib.request.Request(
|
|
f"http://127.0.0.1:{head_port}/v1/chat/completions",
|
|
data=payload,
|
|
headers={
|
|
"Content-Type": "application/json",
|
|
"X-Meshnet-Request-Id": "req-live-1",
|
|
"X-Meshnet-Route": json.dumps([
|
|
{"endpoint": f"http://127.0.0.1:{tail_port}", "start_layer": 22},
|
|
]),
|
|
},
|
|
method="POST",
|
|
)
|
|
response = urllib.request.urlopen(req, timeout=5)
|
|
deadline = time.time() + 2.0
|
|
while time.time() < deadline:
|
|
live = head.current_requests
|
|
if live and live[0]["request_id"] == "req-live-1" and live[0]["tokens"] >= 1:
|
|
break
|
|
time.sleep(0.02)
|
|
assert head.current_requests
|
|
snap = head.current_requests[0]
|
|
assert snap["request_id"] == "req-live-1"
|
|
assert snap["tokens"] >= 1
|
|
assert snap["tokens_per_sec"] >= 0
|
|
assert snap["routing_complete"] is True
|
|
release_second.set()
|
|
response.read()
|
|
finally:
|
|
release_second.set()
|
|
if response is not None:
|
|
response.close()
|
|
head.stop()
|
|
tail.stop()
|
|
|
|
assert head.current_requests == []
|
|
|
|
|
|
def test_distributed_generating_log_includes_tps(capsys):
|
|
head = TorchNodeServer(backend=_FakePipelineHeadBackend(), tracker_mode=True)
|
|
tail = TorchNodeServer(backend=_FakePipelineTailBackend())
|
|
head_port = head.start()
|
|
tail_port = tail.start()
|
|
try:
|
|
payload = json.dumps({
|
|
"model": "fake-model",
|
|
"messages": [{"role": "user", "content": "hello"}],
|
|
"max_tokens": 1,
|
|
}).encode()
|
|
req = urllib.request.Request(
|
|
f"http://127.0.0.1:{head_port}/v1/chat/completions",
|
|
data=payload,
|
|
headers={
|
|
"Content-Type": "application/json",
|
|
"X-Meshnet-Route": json.dumps([
|
|
{"endpoint": f"http://127.0.0.1:{tail_port}", "start_layer": 22},
|
|
]),
|
|
},
|
|
method="POST",
|
|
)
|
|
with urllib.request.urlopen(req, timeout=5) as resp:
|
|
json.loads(resp.read())
|
|
finally:
|
|
head.stop()
|
|
tail.stop()
|
|
|
|
out = capsys.readouterr().out
|
|
assert "generating step=1/1" in out
|
|
assert " tps=" in out
|
|
assert "generation complete tokens=1" in out
|
|
assert out.count("generating step=1/1") == 1
|
|
|
|
|
|
def test_int_tensor_header_serializes_torch_tensors():
|
|
torch = pytest.importorskip("torch")
|
|
|
|
header = _int_tensor_header(torch.tensor([[1, 2, 3]], dtype=torch.long))
|
|
|
|
assert header.startswith("1,3:")
|
|
|
|
|
|
def test_decoder_attention_mask_is_causal_float_mask():
|
|
torch = pytest.importorskip("torch")
|
|
|
|
hidden_states = torch.zeros((1, 3, 8), dtype=torch.bfloat16)
|
|
mask = _decoder_attention_mask(torch.ones((1, 3), dtype=torch.long), hidden_states, torch)
|
|
|
|
assert mask.shape == (1, 1, 3, 3)
|
|
assert mask.dtype == torch.bfloat16
|
|
assert mask[0, 0, 0, 1] < 0
|
|
assert mask[0, 0, 2, 0] == 0
|
|
|
|
|
|
def test_call_layer_passes_rotary_position_embeddings():
|
|
class NeedsPositionEmbeddings:
|
|
def __call__(self, hidden_states, **kwargs):
|
|
assert kwargs["position_embeddings"] == "rotary"
|
|
return hidden_states
|
|
|
|
assert _call_layer(
|
|
NeedsPositionEmbeddings(),
|
|
"hidden",
|
|
attention_mask=None,
|
|
position_ids="positions",
|
|
position_embeddings="rotary",
|
|
) == "hidden"
|
|
|
|
|
|
def test_partial_materialize_guard_requires_local_non_full_non_quantized_snapshot(tmp_path):
|
|
snapshot_dir = tmp_path / "snapshot"
|
|
snapshot_dir.mkdir()
|
|
(snapshot_dir / "config.json").write_text("{}")
|
|
(snapshot_dir / "model.safetensors.index.json").write_text('{"weight_map": {}}')
|
|
|
|
assert _should_partial_materialize_shard(
|
|
str(snapshot_dir),
|
|
4,
|
|
7,
|
|
total_layers_hint=40,
|
|
uses_quantized_weights=False,
|
|
) is True
|
|
assert _should_partial_materialize_shard(
|
|
str(snapshot_dir),
|
|
0,
|
|
39,
|
|
total_layers_hint=40,
|
|
uses_quantized_weights=False,
|
|
) is True
|
|
assert _should_partial_materialize_shard(
|
|
str(snapshot_dir),
|
|
4,
|
|
7,
|
|
total_layers_hint=40,
|
|
uses_quantized_weights=True,
|
|
) is False
|
|
assert _should_partial_materialize_shard(
|
|
"repo/model",
|
|
4,
|
|
7,
|
|
total_layers_hint=40,
|
|
uses_quantized_weights=False,
|
|
) is False
|
|
|
|
|
|
def test_checkpoint_tensor_name_remapped_for_text_only_causal_lm():
|
|
class TextOnlyModel:
|
|
def __init__(self):
|
|
self.model = types.SimpleNamespace(layers=[])
|
|
|
|
model = TextOnlyModel()
|
|
assert _checkpoint_tensor_name_for_model(
|
|
model,
|
|
"model.language_model.layers.0.mlp.gate.weight",
|
|
) == "model.layers.0.mlp.gate.weight"
|
|
assert _checkpoint_tensor_name_for_model(
|
|
model,
|
|
"model.language_model.embed_tokens.weight",
|
|
) == "model.embed_tokens.weight"
|
|
|
|
|
|
def test_checkpoint_tensor_name_kept_for_multimodal_backbone():
|
|
class MultimodalModel:
|
|
def __init__(self):
|
|
self.model = types.SimpleNamespace(language_model=types.SimpleNamespace())
|
|
|
|
model = MultimodalModel()
|
|
name = "model.language_model.layers.0.mlp.gate.weight"
|
|
assert _checkpoint_tensor_name_for_model(model, name) == name
|
|
|
|
|
|
def test_partial_snapshot_loader_remaps_language_model_checkpoint_keys(tmp_path):
|
|
snapshot_dir = tmp_path / "snapshot"
|
|
snapshot_dir.mkdir()
|
|
(snapshot_dir / "config.json").write_text(json.dumps({
|
|
"text_config": {"num_hidden_layers": 3},
|
|
}))
|
|
(snapshot_dir / "model.safetensors.index.json").write_text(json.dumps({
|
|
"weight_map": {
|
|
"model.language_model.layers.1.self_attn.q_proj.weight": "shard-2.safetensors",
|
|
}
|
|
}))
|
|
(snapshot_dir / "shard-2.safetensors").write_bytes(b"stub")
|
|
|
|
class FakeModule:
|
|
def __init__(self):
|
|
self.to_calls = []
|
|
|
|
def to(self, device):
|
|
self.to_calls.append(device)
|
|
return self
|
|
|
|
class FakeModel:
|
|
def __init__(self):
|
|
self.model = types.SimpleNamespace(
|
|
layers=[FakeModule(), FakeModule(), FakeModule()],
|
|
rotary_emb=FakeModule(),
|
|
)
|
|
|
|
def tie_weights(self):
|
|
pass
|
|
|
|
class AutoConfigStub:
|
|
@staticmethod
|
|
def from_pretrained(model_id):
|
|
return types.SimpleNamespace(
|
|
text_config=types.SimpleNamespace(num_hidden_layers=3),
|
|
get_text_config=lambda: types.SimpleNamespace(num_hidden_layers=3),
|
|
)
|
|
|
|
class AutoModelStub:
|
|
@staticmethod
|
|
def from_config(cfg, torch_dtype=None):
|
|
return FakeModel()
|
|
|
|
set_calls = []
|
|
|
|
def fake_set_tensor(module, tensor_name, device, value=None, dtype=None):
|
|
set_calls.append(tensor_name)
|
|
|
|
class FakeSafeOpen:
|
|
def __init__(self, filename, framework, device):
|
|
self.filename = Path(filename).name
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc, tb):
|
|
return False
|
|
|
|
def get_tensor(self, tensor_name):
|
|
return tensor_name
|
|
|
|
class UnusedContext:
|
|
def __enter__(self):
|
|
return None
|
|
|
|
def __exit__(self, exc_type, exc, tb):
|
|
return False
|
|
|
|
_load_partial_model_from_snapshot(
|
|
AutoConfigStub,
|
|
AutoModelStub,
|
|
types.SimpleNamespace(),
|
|
str(snapshot_dir),
|
|
1,
|
|
1,
|
|
"bf16",
|
|
"cpu:0",
|
|
init_empty_weights_fn=UnusedContext,
|
|
set_tensor_fn=fake_set_tensor,
|
|
safe_open_fn=FakeSafeOpen,
|
|
)
|
|
|
|
assert set_calls == ["model.layers.1.self_attn.q_proj.weight"]
|
|
|
|
|
|
def test_partial_snapshot_loader_skips_tensors_absent_from_causal_lm(tmp_path):
|
|
# Multimodal/MTP checkpoints (Qwen3.5/3.6-MoE) carry mtp.* and model.visual.*
|
|
# tensors that the text-only CausalLM never builds — they must be skipped,
|
|
# not assigned (assignment raises AttributeError: 'mtp' / 'visual').
|
|
snapshot_dir = tmp_path / "snapshot"
|
|
snapshot_dir.mkdir()
|
|
(snapshot_dir / "config.json").write_text(json.dumps({
|
|
"text_config": {"num_hidden_layers": 3},
|
|
}))
|
|
(snapshot_dir / "model.safetensors.index.json").write_text(json.dumps({
|
|
"weight_map": {
|
|
"model.language_model.layers.1.self_attn.q_proj.weight": "shard-2.safetensors",
|
|
"mtp.layers.1.input_layernorm.weight": "shard-2.safetensors",
|
|
"model.visual.blocks.1.attn.qkv.weight": "shard-2.safetensors",
|
|
}
|
|
}))
|
|
(snapshot_dir / "shard-2.safetensors").write_bytes(b"stub")
|
|
|
|
class FakeModule:
|
|
def to(self, device):
|
|
return self
|
|
|
|
class FakeModel:
|
|
def __init__(self):
|
|
self.model = types.SimpleNamespace(
|
|
layers=[FakeModule(), FakeModule(), FakeModule()],
|
|
rotary_emb=FakeModule(),
|
|
)
|
|
|
|
def tie_weights(self):
|
|
pass
|
|
|
|
def state_dict(self):
|
|
return {"model.layers.1.self_attn.q_proj.weight": None}
|
|
|
|
class AutoConfigStub:
|
|
@staticmethod
|
|
def from_pretrained(model_id):
|
|
return types.SimpleNamespace(
|
|
text_config=types.SimpleNamespace(num_hidden_layers=3),
|
|
get_text_config=lambda: types.SimpleNamespace(num_hidden_layers=3),
|
|
)
|
|
|
|
class AutoModelStub:
|
|
@staticmethod
|
|
def from_config(cfg, torch_dtype=None):
|
|
return FakeModel()
|
|
|
|
set_calls = []
|
|
|
|
def fake_set_tensor(module, tensor_name, device, value=None, dtype=None):
|
|
set_calls.append(tensor_name)
|
|
|
|
class FakeSafeOpen:
|
|
def __init__(self, filename, framework, device):
|
|
pass
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc, tb):
|
|
return False
|
|
|
|
def get_tensor(self, tensor_name):
|
|
return tensor_name
|
|
|
|
class UnusedContext:
|
|
def __enter__(self):
|
|
return None
|
|
|
|
def __exit__(self, exc_type, exc, tb):
|
|
return False
|
|
|
|
_load_partial_model_from_snapshot(
|
|
AutoConfigStub,
|
|
AutoModelStub,
|
|
types.SimpleNamespace(),
|
|
str(snapshot_dir),
|
|
1,
|
|
1,
|
|
"bf16",
|
|
"cpu:0",
|
|
init_empty_weights_fn=UnusedContext,
|
|
set_tensor_fn=fake_set_tensor,
|
|
safe_open_fn=FakeSafeOpen,
|
|
)
|
|
|
|
assert set_calls == ["model.layers.1.self_attn.q_proj.weight"]
|
|
|
|
|
|
def test_partial_snapshot_loader_materializes_only_assigned_tensors(tmp_path):
|
|
snapshot_dir = tmp_path / "snapshot"
|
|
snapshot_dir.mkdir()
|
|
(snapshot_dir / "config.json").write_text("{}")
|
|
(snapshot_dir / "model.safetensors.index.json").write_text(json.dumps({
|
|
"weight_map": {
|
|
"model.embed_tokens.weight": "shard-1.safetensors",
|
|
"model.layers.0.self_attn.q_proj.weight": "shard-1.safetensors",
|
|
"model.layers.1.self_attn.q_proj.weight": "shard-2.safetensors",
|
|
"model.layers.2.self_attn.q_proj.weight": "shard-3.safetensors",
|
|
"model.norm.weight": "shard-3.safetensors",
|
|
"lm_head.weight": "shard-3.safetensors",
|
|
}
|
|
}))
|
|
for rel in ("shard-1.safetensors", "shard-2.safetensors", "shard-3.safetensors"):
|
|
(snapshot_dir / rel).write_bytes(b"stub")
|
|
|
|
class FakeModule:
|
|
def __init__(self, name):
|
|
self.name = name
|
|
self.to_calls = []
|
|
|
|
def to(self, device):
|
|
self.to_calls.append(device)
|
|
return self
|
|
|
|
class FakeModel:
|
|
def __init__(self):
|
|
self.model = types.SimpleNamespace(
|
|
embed_tokens=FakeModule("embed"),
|
|
layers=[FakeModule("layer0"), FakeModule("layer1"), FakeModule("layer2")],
|
|
rotary_emb=FakeModule("rotary"),
|
|
norm=FakeModule("norm"),
|
|
)
|
|
self.lm_head = FakeModule("lm_head")
|
|
self.tie_weights_called = 0
|
|
|
|
def tie_weights(self):
|
|
self.tie_weights_called += 1
|
|
|
|
class AutoConfigStub:
|
|
@staticmethod
|
|
def from_pretrained(model_id):
|
|
assert model_id == str(snapshot_dir)
|
|
return types.SimpleNamespace(num_hidden_layers=3)
|
|
|
|
class AutoModelStub:
|
|
@staticmethod
|
|
def from_config(cfg, torch_dtype=None):
|
|
assert cfg.num_hidden_layers == 3
|
|
assert torch_dtype == "bf16"
|
|
return FakeModel()
|
|
|
|
class EmptyWeights:
|
|
def __init__(self):
|
|
self.entered = 0
|
|
self.exited = 0
|
|
|
|
def __call__(self):
|
|
return self
|
|
|
|
def __enter__(self):
|
|
self.entered += 1
|
|
return None
|
|
|
|
def __exit__(self, exc_type, exc, tb):
|
|
self.exited += 1
|
|
return False
|
|
|
|
init_empty_weights = EmptyWeights()
|
|
set_calls = []
|
|
|
|
def fake_set_tensor(module, tensor_name, device, value=None, dtype=None):
|
|
set_calls.append((tensor_name, device, value, dtype))
|
|
|
|
tensors = {
|
|
"shard-1.safetensors": {
|
|
"model.embed_tokens.weight": "embed",
|
|
"model.layers.0.self_attn.q_proj.weight": "layer0",
|
|
},
|
|
"shard-2.safetensors": {
|
|
"model.layers.1.self_attn.q_proj.weight": "layer1",
|
|
},
|
|
"shard-3.safetensors": {
|
|
"model.layers.2.self_attn.q_proj.weight": "layer2",
|
|
"model.norm.weight": "norm",
|
|
"lm_head.weight": "lm_head",
|
|
},
|
|
}
|
|
|
|
class FakeSafeOpen:
|
|
def __init__(self, filename, framework, device):
|
|
assert framework == "pt"
|
|
assert device == "cpu"
|
|
self.filename = Path(filename).name
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc, tb):
|
|
return False
|
|
|
|
def get_tensor(self, tensor_name):
|
|
return tensors[self.filename][tensor_name]
|
|
|
|
model = _load_partial_model_from_snapshot(
|
|
AutoConfigStub,
|
|
AutoModelStub,
|
|
types.SimpleNamespace(),
|
|
str(snapshot_dir),
|
|
1,
|
|
1,
|
|
"bf16",
|
|
"cpu:0",
|
|
init_empty_weights_fn=init_empty_weights,
|
|
set_tensor_fn=fake_set_tensor,
|
|
safe_open_fn=FakeSafeOpen,
|
|
)
|
|
|
|
assert init_empty_weights.entered == 1
|
|
assert init_empty_weights.exited == 1
|
|
assert model.tie_weights_called == 1
|
|
assert [call[0] for call in set_calls] == ["model.layers.1.self_attn.q_proj.weight"]
|
|
assert model.model.layers[1].to_calls == ["cpu:0"]
|
|
assert model.model.layers[0].to_calls == []
|
|
assert model.model.layers[2].to_calls == []
|
|
assert model.model.embed_tokens.to_calls == []
|
|
assert model.model.norm.to_calls == []
|
|
assert model.lm_head.to_calls == []
|
|
assert model.model.rotary_emb.to_calls == ["cpu:0"]
|
|
|
|
|
|
def test_partial_snapshot_loader_requires_known_layer_count(tmp_path):
|
|
snapshot_dir = tmp_path / "snapshot"
|
|
snapshot_dir.mkdir()
|
|
(snapshot_dir / "config.json").write_text("{}")
|
|
(snapshot_dir / "model.safetensors.index.json").write_text(json.dumps({
|
|
"weight_map": {"model.layers.0.self_attn.q_proj.weight": "shard.safetensors"}
|
|
}))
|
|
(snapshot_dir / "shard.safetensors").write_bytes(b"stub")
|
|
|
|
class AutoConfigStub:
|
|
@staticmethod
|
|
def from_pretrained(model_id):
|
|
return types.SimpleNamespace()
|
|
|
|
class AutoModelStub:
|
|
@staticmethod
|
|
def from_config(cfg, torch_dtype=None):
|
|
raise AssertionError("from_config should not run without a known layer count")
|
|
|
|
class UnusedContext:
|
|
def __enter__(self):
|
|
return None
|
|
|
|
def __exit__(self, exc_type, exc, tb):
|
|
return False
|
|
|
|
with pytest.raises(PartialModelLoadUnsupported, match="num_hidden_layers"):
|
|
_load_partial_model_from_snapshot(
|
|
AutoConfigStub,
|
|
AutoModelStub,
|
|
types.SimpleNamespace(),
|
|
str(snapshot_dir),
|
|
0,
|
|
0,
|
|
"bf16",
|
|
"cpu:0",
|
|
init_empty_weights_fn=lambda: UnusedContext(),
|
|
set_tensor_fn=lambda *args, **kwargs: None,
|
|
safe_open_fn=lambda *args, **kwargs: None,
|
|
)
|
|
|
|
|
|
def test_torch_model_shard_prefers_partial_loader_for_local_snapshot(tmp_path, monkeypatch):
|
|
import meshnet_node.model_backend as backend
|
|
|
|
snapshot_dir = tmp_path / "snapshot"
|
|
snapshot_dir.mkdir()
|
|
(snapshot_dir / "config.json").write_text("{}")
|
|
(snapshot_dir / "model.safetensors.index.json").write_text('{"weight_map": {}}')
|
|
|
|
class FakeModel:
|
|
def __init__(self):
|
|
self.model = types.SimpleNamespace(
|
|
layers=[object(), object(), object()],
|
|
embed_tokens=object(),
|
|
)
|
|
self.config = types.SimpleNamespace(hidden_size=8)
|
|
self.eval_called = 0
|
|
|
|
def eval(self):
|
|
self.eval_called += 1
|
|
|
|
fake_model = FakeModel()
|
|
partial_calls = []
|
|
|
|
class AutoConfigStub:
|
|
@staticmethod
|
|
def from_pretrained(model_id, cache_dir=None):
|
|
return types.SimpleNamespace(num_hidden_layers=3, text_config=types.SimpleNamespace(dtype="torch.bfloat16"))
|
|
|
|
class AutoModelStub:
|
|
@staticmethod
|
|
def from_pretrained(*args, **kwargs):
|
|
raise AssertionError("full model load should not run for partial local shards")
|
|
|
|
class AutoTokenizerStub:
|
|
@staticmethod
|
|
def from_pretrained(model_id, cache_dir=None):
|
|
assert model_id == str(snapshot_dir)
|
|
return types.SimpleNamespace()
|
|
|
|
monkeypatch.setitem(
|
|
sys.modules,
|
|
"torch",
|
|
types.SimpleNamespace(
|
|
cuda=types.SimpleNamespace(is_available=lambda: False),
|
|
device=lambda value: value,
|
|
bfloat16="bf16",
|
|
),
|
|
)
|
|
monkeypatch.setitem(
|
|
sys.modules,
|
|
"transformers",
|
|
types.SimpleNamespace(
|
|
AutoConfig=AutoConfigStub,
|
|
AutoModelForCausalLM=AutoModelStub,
|
|
AutoTokenizer=AutoTokenizerStub,
|
|
),
|
|
)
|
|
monkeypatch.setattr(
|
|
backend,
|
|
"_load_partial_model_from_snapshot",
|
|
lambda *args, **kwargs: partial_calls.append((args, kwargs)) or fake_model,
|
|
)
|
|
|
|
shard = TorchModelShard(
|
|
"repo/model",
|
|
1,
|
|
1,
|
|
quantization="auto",
|
|
cache_dir=snapshot_dir,
|
|
)
|
|
|
|
assert len(partial_calls) == 1
|
|
assert shard.model is fake_model
|
|
assert fake_model.eval_called == 1
|
|
assert shard.total_layers == 3
|
|
assert shard.is_head is False
|
|
assert shard.is_tail is False
|
|
|
|
|
|
@pytest.mark.integration
|
|
def test_two_node_gpt2_completion_is_deterministic():
|
|
if os.environ.get("CI"):
|
|
pytest.skip("GPT-2 integration test is skipped in CI")
|
|
torch = pytest.importorskip("torch")
|
|
pytest.importorskip("transformers")
|
|
pytest.importorskip("safetensors")
|
|
pytest.importorskip("accelerate")
|
|
pytest.importorskip("bitsandbytes")
|
|
if not torch.cuda.is_available():
|
|
pytest.skip("GPT-2 integration test requires a CUDA GPU")
|
|
|
|
head = TorchNodeServer(
|
|
model_id="openai-community/gpt2",
|
|
shard_start=0,
|
|
shard_end=6,
|
|
quantization="bfloat16",
|
|
)
|
|
tail = TorchNodeServer(
|
|
model_id="openai-community/gpt2",
|
|
shard_start=6,
|
|
shard_end=12,
|
|
quantization="bfloat16",
|
|
)
|
|
head_port = head.start()
|
|
tail_port = tail.start()
|
|
try:
|
|
prompt_req = urllib.request.Request(
|
|
f"http://127.0.0.1:{head_port}/forward",
|
|
data=json.dumps({"prompt": "The capital of France is"}).encode(),
|
|
headers={"Content-Type": "application/json"},
|
|
method="POST",
|
|
)
|
|
with urllib.request.urlopen(prompt_req, timeout=60) as resp:
|
|
activation = resp.read()
|
|
head_headers = resp.headers
|
|
|
|
tail_req = urllib.request.Request(
|
|
f"http://127.0.0.1:{tail_port}/forward",
|
|
data=activation,
|
|
headers={
|
|
"Content-Type": "application/octet-stream",
|
|
"X-Meshnet-Shape": head_headers["X-Meshnet-Shape"],
|
|
"X-Meshnet-Dtype": head_headers["X-Meshnet-Dtype"],
|
|
"X-Meshnet-Session": "gpt2-session",
|
|
"X-Meshnet-Chunk-Index": "0",
|
|
"X-Meshnet-Chunk-Total": "1",
|
|
"X-Meshnet-Hop-Index": "1",
|
|
"X-Meshnet-Attn-Mask": head_headers["X-Meshnet-Attn-Mask"],
|
|
"X-Meshnet-Position-Ids": head_headers["X-Meshnet-Position-Ids"],
|
|
},
|
|
method="POST",
|
|
)
|
|
with urllib.request.urlopen(tail_req, timeout=60) as resp:
|
|
body = json.loads(resp.read())
|
|
|
|
assert body["text"].strip()
|
|
assert body["text"] == " Paris"
|
|
finally:
|
|
head.stop()
|
|
tail.stop()
|