Skip to content

cpu-device: OR-switch device resolver + RoPE multi-head fallback fix - #160

Open
spaz122772 wants to merge 4 commits into
FlashML-org:mainfrom
spaz122772:cpu-device
Open

cpu-device: OR-switch device resolver + RoPE multi-head fallback fix#160
spaz122772 wants to merge 4 commits into
FlashML-org:mainfrom
spaz122772:cpu-device

Conversation

@spaz122772

Copy link
Copy Markdown

Summary

  • Adds an OR-switch device resolver (engine/device_switch.py): prefer CUDA when available, fall back to CPU. Same branch runs on GPU or CPU-only boxes unchanged.
  • Full CPU-only build path: no-op CUDA stub so C++ extensions compile without cudart/nvcc; guarded flashlib/triton imports; FREETOKEN_CPU_ONLY=1 build.
  • Fixes a RoPE multi-head forward bug in kernel/torch_fallback.py: the engine passes a multi-head concatenated query (seq, NQ*head_dim) = (seq, 2048) (16 heads x 128); the old code rotated only the first head's dims, leaving 15/16 heads un-rotated (engine-vs-reference post-RoPE cosine was -0.02). Fix reshapes per-head, keeps cos/sin 2-D (no phantom broadcast dim), rotates each head, reassembles. Unit test cos = 1.000000 (single- and multi-head).

Verified (real execution, arm64 CPU-only, no CUDA/triton)

  • qwen3-0.6b serves via --moe-backend cpu; "The capital of France is" -> "Paris."
  • Sustained 120-token decode coherent; temp>0 sampling diverges across runs.
  • Cold-start reproducibility confirmed from clean process/port state.
  • Reproducible recipe + regression test in tools/rope_fallback_test.py.

Notes / limitations

  • Only qwen3-0.6b exercised locally; fix lives in the shared fallback kernel (arch-general for the CPU/MoE path).
  • Local model weights are gitignored and not included.
  • CI smoke workflow (cpu-device-smoke.yml) added for free-runner verification.

Branch: https://github.com/spaz122772/FreeToken/tree/cpu-device

Solar Pro4 and others added 4 commits August 24, 2026 21:32
- NEW engine/device_switch.py: resolve_device()/make_stream()/guard_cuda() OR-switch
- engine.py init now routes through device_switch instead of hard-coded cuda:{rank}
- CI smoke workflow on free ubuntu-latest (reproducible, no paid cloud)

Verified: resolve_device(0)==cpu on M4 (no NVIDIA GPU); switch logic asserted.
Free/open-source CPU-fallback path; porting fork, not upstream.
- setup.py: FREETOKEN_CPU_ONLY=1 builds C++ exts with stub cuda runtime (no cudart/nvcc)
- NEW stub_cuda_runtime.h: faithful no-op CUDA symbols + CUDART_CB + enum flags
- pinned_tensor.cpp / cpu_moe_ext.cpp: #ifdef FREETOKEN_CPU_ONLY -> stub header
- engine.py: OR-switch ALL torch.cuda.* calls behind has_cuda() (init, sync, events,
  allocator, graph teardown, forward assert, ForwardOutput annotation)
- offload_cache.py / offload_kernels.py: flashlib/triton imports guarded so the
  package imports on CPU-only hardware (GPU path untouched when flashlib present)
- CI: free ubuntu-latest verifies OR-switch + CPU-only build + CPU smoke-serve

VERIFIED LOCALLY on M4 (arm64, no CUDA):
  - package imports without triton/flashlib
  - FREETOKEN_CPU_ONLY=1 build -> _pinned_tensor + _cpu_moe compile, link, import
  - AVX-512 kernels use runtime dispatch w/ scalar fallback -> portable build
  - OR-switch resolve_device(0) -> cpu

Remaining (needs x86-Linux or free CI runner): real model generation smoke test.
Root cause: apply_rope_with_cos_sin_cache_inplace mishandled the engine's
multi-head concatenated query (seq, NQ*head_dim)=(seq,2048). It treated the
whole strip as one head (2048-wide) and rotated only the first rotary_dim
dims, leaving 15 of 16 heads un-rotated -> attention divergence (engine vs HF
post-RoPE cosine -0.02).

Fix: reshape (seq, NQ*head_dim) -> (seq*NQ, head_dim), rotate each head with
2-D cos/sin (no phantom unsqueeze dim), reshape back. Isolated unit test
tools/rope_fallback_test.py: cos=1.000000 (single- and multi-head).

Verified on M4 (arm64, NO CUDA, NO triton):
- qwen3-0.6b serves via --moe-backend cpu, answers 'The capital of France is'
  -> 'Paris.'
- sustained 120-tok decode coherent; temp>0 sampling diverges across runs.
- cold-start reproducibility confirmed from clean process/port state.

Also: OR-switch device resolver (CUDA-preferred, CPU-fallback), CPU-only C++
build via no-op CUDA stub, flag-guarded probe hooks (zero cost when off).
Consolidated record: docs/freetoken-deployment/phase5-cpu-device/CONSOLIDATED.md.

Local model weights (_test_models/) gitignored — not committed.
…tive)

Root cause: setup.py passed CppExtension sources as absolute paths
(str(ROOT / '...')) and pyproject listed csrc/**/* as package-data.
Modern setuptools (>=77, what the free CI runner pulls) runs
build_py.assert_relative() and rejects absolute paths
(DistutilsSetupError: setup script specifies an absolute path),
breaking the CPU-only editable build on GitHub Actions.

Fix:
- setup.py: pass extension sources as paths relative to setup.py
  ('python/freetoken/kernel/csrc/...'), the portable form setuptools
  accepts. Applies to both the CPU-only and CUDA build branches.
- pyproject.toml: drop csrc/**/* from package-data; C++ sources are
  build inputs, not runtime data, and must not be shipped as such.

Verified locally with setuptools 84.0.0 (editable CPU-only install):
  FREETOKEN_CPU_ONLY=1 pip install -e . --no-build-isolation  -> OK
  from freetoken.kernel import _cpu_moe, _pinned_tensor        -> OK
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant