diff --git a/.gitignore b/.gitignore index cdf3cfcf89..c36bc5d962 100644 --- a/.gitignore +++ b/.gitignore @@ -84,5 +84,16 @@ __pycache__/ .*.swp .DS_Store +# Linux TP protocol and ROCm ownership harnesses +tests/test_tp_linux +ds4-kernel-v41-tp-attention +ds4-kernel-v41-tp-moe +ds4-kernel-v41-tp-gates + +/ds4-kernel-v41-tp-mmq + +/ds4-kernel-v41-tp-down + +/ds4-kernel-v41-tp-bind-failure /tests/test_qwen4_conv_parallel /tests/test_q8_prefill_variants diff --git a/Makefile b/Makefile index ce60429a81..a90af4d590 100644 --- a/Makefile +++ b/Makefile @@ -166,7 +166,7 @@ check-mxfp4-half-lut: test-mxfp4-metal: check-mxfp4-half-lut tests/test_mxfp4_metal ./tests/test_mxfp4_metal -tests/test_metal_moe_prefill.o: tests/test_metal_moe_prefill.c ds4_gpu.h +tests/test_metal_moe_prefill.o: tests/test_metal_moe_prefill.c ds4_gpu.h ds4_deepseek41_gpu.h ds4_gpu_tp.h $(CC) $(CFLAGS) -fno-fast-math -I. -c -o $@ $< tests/test_metal_moe_prefill: tests/test_metal_moe_prefill.o $(CORE_OBJS) @@ -206,7 +206,7 @@ tests/test_q8_prefill_variants: tests/test_q8_prefill_variants.o $(CORE_OBJS) test-q8-prefill-variants: tests/test_q8_prefill_variants ./tests/test_q8_prefill_variants -tests/test_metal_ssd_experts.o: tests/test_metal_ssd_experts.c ds4_gpu.h +tests/test_metal_ssd_experts.o: tests/test_metal_ssd_experts.c ds4_gpu.h ds4_deepseek41_gpu.h ds4_gpu_tp.h $(CC) $(CFLAGS) -fno-fast-math -I. -c -o $@ $< tests/test_metal_ssd_experts: tests/test_metal_ssd_experts.o $(CORE_OBJS) @@ -218,7 +218,7 @@ test-metal-ssd-experts: tests/test_metal_ssd_experts ./tests/test_metal_ssd_experts --q4 ./tests/test_metal_ssd_experts --mxfp4 -tests/test_metal_command_memory: tests/test_metal_command_memory.c ds4_gpu.h $(CORE_OBJS) +tests/test_metal_command_memory: tests/test_metal_command_memory.c ds4_gpu.h $(CORE_OBJS) ds4_deepseek41_gpu.h ds4_gpu_tp.h $(CC) $(CFLAGS) -I. -o $@ $< $(CORE_OBJS) $(METAL_LDLIBS) .PHONY: test-metal-command-memory @@ -229,7 +229,7 @@ test-metal-command-memory: tests/test_metal_command_memory MTL_DEBUG_LAYER=1 ./tests/test_metal_command_memory batch MTL_DEBUG_LAYER=1 ./tests/test_metal_command_memory big -tests/test_deepseek41_metal.o: tests/test_deepseek41_metal.c ds4_gpu.h ds4_deepseek41_gpu.h +tests/test_deepseek41_metal.o: tests/test_deepseek41_metal.c ds4_gpu.h ds4_deepseek41_gpu.h ds4_gpu_tp.h $(CC) $(filter-out -ffast-math,$(CFLAGS)) -I. -c -o $@ $< tests/test_deepseek41_metal: tests/test_deepseek41_metal.o $(CORE_OBJS) @@ -239,19 +239,19 @@ tests/test_deepseek41_metal: tests/test_deepseek41_metal.o $(CORE_OBJS) test-deepseek41-metal: tests/test_deepseek41_metal ./tests/test_deepseek41_metal -tests/test_deepseek41_graph.o: tests/test_deepseek41_graph.c ds4.c ds4_gpu.h ds4_engram.h +tests/test_deepseek41_graph.o: tests/test_deepseek41_graph.c ds4.c ds4_gpu.h ds4_engram.h ds4_deepseek41_gpu.h ds4_gpu_tp.h $(CC) $(filter-out -ffast-math,$(CFLAGS)) -Wno-unused-function -I. -c -o $@ $< tests/test_deepseek41_graph: tests/test_deepseek41_graph.o $(filter-out ds4.o,$(CORE_OBJS)) $(CC) $(filter-out -ffast-math,$(CFLAGS)) -o $@ $^ $(METAL_LDLIBS) -tests/test_deepseek41_prefill.o: tests/test_deepseek41_prefill.c ds4.c ds4_gpu.h ds4_engram.h +tests/test_deepseek41_prefill.o: tests/test_deepseek41_prefill.c ds4.c ds4_gpu.h ds4_engram.h ds4_deepseek41_gpu.h ds4_gpu_tp.h $(CC) $(filter-out -ffast-math,$(CFLAGS)) -Wno-unused-function -I. -c -o $@ $< tests/test_deepseek41_prefill: tests/test_deepseek41_prefill.o $(filter-out ds4.o,$(CORE_OBJS)) $(CC) $(filter-out -ffast-math,$(CFLAGS)) -o $@ $^ $(METAL_LDLIBS) -tests/test_metal_tp_bulk: tests/test_metal_tp_bulk.c ds4_gpu.h ds4_tp.h $(CORE_OBJS) +tests/test_metal_tp_bulk: tests/test_metal_tp_bulk.c ds4_gpu.h ds4_tp.h $(CORE_OBJS) ds4_deepseek41_gpu.h ds4_gpu_tp.h $(CC) $(filter-out -ffast-math,$(CFLAGS)) -I. -o $@ $< $(CORE_OBJS) $(METAL_LDLIBS) tests/test_deepseek41_cli.o: tests/test_deepseek41_cli.c ds4_cli.c ds4.h @@ -260,7 +260,7 @@ tests/test_deepseek41_cli.o: tests/test_deepseek41_cli.c ds4_cli.c ds4.h tests/test_deepseek41_cli: tests/test_deepseek41_cli.o ds4_help.o ds4_prompt_prefix.o linenoise.o ds4_gpu_args.o $(CORE_OBJS) $(CC) $(CFLAGS) -o $@ $^ $(METAL_LDLIBS) -tests/test_metal_dense_mpp.o: tests/test_metal_dense_mpp.c ds4_gpu.h +tests/test_metal_dense_mpp.o: tests/test_metal_dense_mpp.c ds4_gpu.h ds4_deepseek41_gpu.h ds4_gpu_tp.h $(CC) $(CFLAGS) -fno-fast-math -I. -c -o $@ $< tests/test_metal_dense_mpp: tests/test_metal_dense_mpp.o $(CORE_OBJS) @@ -312,7 +312,7 @@ cuda: strix-halo: $(MAKE) -B ds4 ds4-server ds4-bench ds4-eval ds4-agent \ - CORE_OBJS="ds4.o ds4_image.o ds4_distributed.o ds4_tp.o ds4_ssd.o ds4_rocm.o ds4_rocm_compat.o ds4_rocm_unavailable.o ds4_layer_pack.o $(ROCM_MMQ_OBJS)" \ + CORE_OBJS="ds4.o ds4_image.o ds4_distributed.o ds4_tp.o ds4_ssd.o ds4_rocm.o ds4_rocm_compat.o ds4_rocm_unavailable.o ds4_layer_pack.o ds4_engram.o $(ROCM_MMQ_OBJS)" \ CFLAGS="$(CFLAGS) $(ROCM_HOST_CFLAGS) -DDS4_ROCM_BUILD" \ DS4_LINK="$(HIPCC) $(ROCM_CFLAGS)" \ DS4_LINK_LIBS="$(ROCM_LDLIBS)" @@ -328,7 +328,7 @@ test-rocm: test-session-state \ tests/test_layer_pack tests/test_engine_mgpu_placement tests/test_gpu_args tests/test_prompt_prefix \ ds4 ds4-server ds4-bench ds4-agent \ - CORE_OBJS="ds4.o ds4_image.o ds4_distributed.o ds4_tp.o ds4_ssd.o ds4_rocm.o ds4_rocm_compat.o ds4_rocm_unavailable.o ds4_layer_pack.o $(ROCM_MMQ_OBJS)" \ + CORE_OBJS="ds4.o ds4_image.o ds4_distributed.o ds4_tp.o ds4_ssd.o ds4_rocm.o ds4_rocm_compat.o ds4_rocm_unavailable.o ds4_layer_pack.o ds4_engram.o $(ROCM_MMQ_OBJS)" \ CFLAGS="$(CFLAGS) $(ROCM_HOST_CFLAGS) -DDS4_ROCM_BUILD" \ DS4_LINK="$(HIPCC) $(ROCM_CFLAGS)" \ DS4_LINK_LIBS="$(ROCM_LDLIBS)" @@ -365,22 +365,22 @@ gguf-tools/quality-testing/score_official: gguf-tools/quality-testing/score_offi tests/test_cuda_q8_scratch.o: tests/test_cuda_q8_scratch.cu cuda/mmq/ds4_mmq.h $(NVCC) $(NVCCFLAGS) -std=c++17 -Icuda/mmq -c -o $@ $< -tests/test_cuda_ssd_cache.o: tests/test_cuda_ssd_cache.c ds4_gpu.h +tests/test_cuda_ssd_cache.o: tests/test_cuda_ssd_cache.c ds4_gpu.h ds4_deepseek41_gpu.h ds4_gpu_tp.h $(CC) $(QUALITY_CFLAGS) -D_GNU_SOURCE -I. -c -o $@ $< -tests/test_cuda_ssd_batch.o: tests/test_cuda_ssd_batch.c ds4_gpu.h +tests/test_cuda_ssd_batch.o: tests/test_cuda_ssd_batch.c ds4_gpu.h ds4_deepseek41_gpu.h ds4_gpu_tp.h $(CC) $(QUALITY_CFLAGS) -D_GNU_SOURCE -I. -c -o $@ $< tests/test_cuda_ssd_batch: tests/test_cuda_ssd_batch.o ds4_cuda.o ds4_image.o $(MMQ_OBJS) $(DS4_LINK) -o $@ $^ $(DS4_LINK_LIBS) -tests/test_cuda_tp.o: tests/test_cuda_tp.c ds4_gpu.h ds4_gpu_tp.h +tests/test_cuda_tp.o: tests/test_cuda_tp.c ds4_gpu.h ds4_gpu_tp.h ds4_deepseek41_gpu.h $(CC) $(QUALITY_CFLAGS) -D_GNU_SOURCE -I. -c -o $@ $< tests/test_cuda_tp: tests/test_cuda_tp.o ds4_cuda.o ds4_image.o $(MMQ_OBJS) $(DS4_LINK) -o $@ $^ $(DS4_LINK_LIBS) -tests/test_cuda_tp_repack.o: tests/test_cuda_tp_repack.cu ds4_gpu.h +tests/test_cuda_tp_repack.o: tests/test_cuda_tp_repack.cu ds4_gpu.h ds4_deepseek41_gpu.h ds4_gpu_tp.h $(NVCC) $(NVCCFLAGS) -std=c++17 -I. -c -o $@ $< tests/test_cuda_tp_repack: tests/test_cuda_tp_repack.o ds4_cuda.o ds4_image.o $(MMQ_OBJS) @@ -399,7 +399,7 @@ tests/test_cuda_ssd_cache: tests/test_cuda_ssd_cache.o ds4_cuda.o ds4_image.o $( test-cuda-ssd-cache: tests/test_cuda_ssd_cache ./tests/test_cuda_ssd_cache -tests/test_cuda_q8_rows.o: tests/test_cuda_q8_rows.c ds4_gpu.h +tests/test_cuda_q8_rows.o: tests/test_cuda_q8_rows.c ds4_gpu.h ds4_deepseek41_gpu.h ds4_gpu_tp.h $(CC) $(QUALITY_CFLAGS) -I. -c -o $@ $< tests/test_cuda_q8_rows: tests/test_cuda_q8_rows.o ds4_cuda.o ds4_image.o $(MMQ_OBJS) @@ -409,7 +409,7 @@ tests/test_cuda_q8_rows: tests/test_cuda_q8_rows.o ds4_cuda.o ds4_image.o $(MMQ_ test-cuda-q8-rows: tests/test_cuda_q8_rows ./tests/test_cuda_q8_rows -tests/test_cuda_reductions.o: tests/test_cuda_reductions.cu ds4_gpu.h +tests/test_cuda_reductions.o: tests/test_cuda_reductions.cu ds4_gpu.h ds4_deepseek41_gpu.h ds4_gpu_tp.h $(NVCC) $(NVCCFLAGS) -std=c++17 -I. -c -o $@ $< tests/test_cuda_reductions: tests/test_cuda_reductions.o ds4_cuda.o ds4_image.o $(MMQ_OBJS) @@ -419,7 +419,7 @@ tests/test_cuda_reductions: tests/test_cuda_reductions.o ds4_cuda.o ds4_image.o test-cuda-reductions: tests/test_cuda_reductions ./tests/test_cuda_reductions -tests/test_cuda_shared.o: tests/test_cuda_shared.cu ds4_gpu.h ds4_deepseek41_gpu.h +tests/test_cuda_shared.o: tests/test_cuda_shared.cu ds4_gpu.h ds4_deepseek41_gpu.h ds4_gpu_tp.h $(NVCC) $(NVCCFLAGS) -std=c++17 -I. -c -o $@ $< tests/test_cuda_shared: tests/test_cuda_shared.o ds4_cuda.o ds4_image.o $(MMQ_OBJS) @@ -429,7 +429,7 @@ tests/test_cuda_shared: tests/test_cuda_shared.o ds4_cuda.o ds4_image.o $(MMQ_OB test-cuda-shared: tests/test_cuda_shared ./tests/test_cuda_shared -tests/test_deepseek41_prefill.o: tests/test_deepseek41_prefill.c ds4.c ds4_gpu.h ds4_engram.h +tests/test_deepseek41_prefill.o: tests/test_deepseek41_prefill.c ds4.c ds4_gpu.h ds4_engram.h ds4_deepseek41_gpu.h ds4_gpu_tp.h $(CC) $(CFLAGS) -I. -c -o $@ $< tests/test_deepseek41_prefill: tests/test_deepseek41_prefill.o $(filter-out ds4.o,$(CORE_OBJS)) @@ -468,7 +468,7 @@ tests/test_mxfp4_cuda: tests/test_mxfp4_cuda.cu $(MMQ_OBJS) test-mxfp4-cuda: tests/test_mxfp4_cuda ./tests/test_mxfp4_cuda -tests/test_deepseek41_cuda.o: tests/test_deepseek41_metal.c ds4_gpu.h ds4_deepseek41_gpu.h +tests/test_deepseek41_cuda.o: tests/test_deepseek41_metal.c ds4_gpu.h ds4_deepseek41_gpu.h ds4_gpu_tp.h $(CC) $(QUALITY_CFLAGS) -D_GNU_SOURCE -I. -c -o $@ $< tests/test_deepseek41_cuda: tests/test_deepseek41_cuda.o ds4_cuda.o ds4_image.o $(MMQ_OBJS) @@ -507,7 +507,7 @@ ds4_cli.o: ds4_cli.c ds4.h ds4_ssd.h ds4_distributed.h ds4_help.h ds4_prompt_pre ds4_distributed.o: ds4_distributed.c ds4_distributed.h ds4.h ds4_ssd.h $(CC) $(CFLAGS) -c -o $@ ds4_distributed.c -ds4_tp.o: ds4_tp.c ds4_tp.h ds4.h ds4_ssd.h ds4_gpu.h ds4_gpu_tp.h +ds4_tp.o: ds4_tp.c ds4_tp_io.h ds4_tp_roce.h ds4_tp.h ds4.h ds4_ssd.h ds4_gpu.h ds4_gpu_tp.h ds4_deepseek41_gpu.h $(CC) $(CFLAGS) -c -o $@ ds4_tp.c ds4_help.o: ds4_help.c ds4_help.h @@ -546,7 +546,7 @@ ds4_test.o: tests/ds4_test.c ds4_server.c ds4.h ds4_ssd.h ds4_distributed.h ds4_ ds4_agent_test.o: tests/ds4_agent_test.c ds4_agent.c ds4.h ds4_ssd.h ds4_distributed.h ds4_tp.h ds4_help.h ds4_prompt_prefix.h ds4_kvstore.h ds4_web.h linenoise.h $(CC) $(CFLAGS) -Wno-unused-function -c -o $@ tests/ds4_agent_test.c -tests/cuda_long_context_smoke.o: tests/cuda_long_context_smoke.c ds4_gpu.h +tests/cuda_long_context_smoke.o: tests/cuda_long_context_smoke.c ds4_gpu.h ds4_deepseek41_gpu.h ds4_gpu_tp.h $(CC) $(CFLAGS) -I. -c -o $@ tests/cuda_long_context_smoke.c rax.o: rax.c rax.h rax_malloc.h @@ -555,7 +555,7 @@ rax.o: rax.c rax.h rax_malloc.h linenoise.o: linenoise.c linenoise.h $(CC) $(CFLAGS) -c -o $@ linenoise.c -ds4_cpu.o: ds4.c ds4.h ds4_ssd.h ds4_distributed.h ds4_gpu.h ds4_gpu_tp.h +ds4_cpu.o: ds4.c ds4.h ds4_ssd.h ds4_distributed.h ds4_gpu.h ds4_gpu_tp.h ds4_deepseek41_gpu.h $(CC) $(CFLAGS) -Wno-unused-function -DDS4_NO_GPU -c -o $@ ds4.c ds4_cli_cpu.o: ds4_cli.c ds4.h ds4_ssd.h ds4_distributed.h ds4_help.h ds4_prompt_prefix.h linenoise.h @@ -579,7 +579,7 @@ ds4_agent_cpu.o: ds4_agent.c ds4.h ds4_ssd.h ds4_distributed.h ds4_help.h ds4_pr ds4_metal.o: ds4_metal.m ds4_gpu.h ds4_gpu_tp.h ds4_deepseek41_gpu.h $(METAL_SRCS) $(CC) $(OBJCFLAGS) -c -o $@ ds4_metal.m -tests/test_glm53_kda.o: tests/test_glm53_kda.c ds4_gpu.h +tests/test_glm53_kda.o: tests/test_glm53_kda.c ds4_gpu.h ds4_deepseek41_gpu.h ds4_gpu_tp.h $(CC) $(CFLAGS) -I. -c -o $@ tests/test_glm53_kda.c tests/test_glm53_vision_engine.o: tests/test_glm53_vision_engine.c ds4.h ds4_image.h @@ -653,7 +653,7 @@ test-qwen4-vision: tests/test_qwen4_vision { echo "set DS4_QWEN4_SNAPSHOT, DS4_QWEN4_MMPROJ and DS4_QWEN4_IMAGE"; exit 1; } python3 tests/qwen4_vision_ref.py --snapshot "$(DS4_QWEN4_SNAPSHOT)" --mmproj "$(DS4_QWEN4_MMPROJ)" --image "$(DS4_QWEN4_IMAGE)" -tests/test_glm53_kda_rocm.o: tests/test_glm53_kda.c ds4_gpu.h +tests/test_glm53_kda_rocm.o: tests/test_glm53_kda.c ds4_gpu.h ds4_deepseek41_gpu.h ds4_gpu_tp.h $(CC) $(filter-out -ffast-math,$(CFLAGS)) $(ROCM_HOST_CFLAGS) -DDS4_ROCM_BUILD -I. -c -o $@ $< $(GLM53_KDA_ROCM_TEST): tests/test_glm53_kda_rocm.o ds4_rocm.o ds4_image.o $(ROCM_MMQ_OBJS) @@ -662,7 +662,7 @@ $(GLM53_KDA_ROCM_TEST): tests/test_glm53_kda_rocm.o ds4_rocm.o ds4_image.o $(ROC test-glm53-kda-rocm: $(GLM53_KDA_ROCM_TEST) ./$(GLM53_KDA_ROCM_TEST) -tests/test_glm_attention_rocm.o: tests/test_glm_attention.c ds4.h ds4_gpu.h ds4_linux_memory.h +tests/test_glm_attention_rocm.o: tests/test_glm_attention.c ds4.h ds4_gpu.h ds4_linux_memory.h ds4_deepseek41_gpu.h ds4_gpu_tp.h $(CC) $(filter-out -ffast-math,$(CFLAGS)) $(ROCM_HOST_CFLAGS) -DDS4_ROCM_BUILD -I. -c -o $@ $< tests/test_glm_attention_rocm: tests/test_glm_attention_rocm.o ds4_rocm.o ds4_image.o $(ROCM_MMQ_OBJS) @@ -685,7 +685,7 @@ test-linux-memory: tests/test_linux_memory test-rocm-memory: tests/test_rocm_memory ./tests/test_rocm_memory -tests/test_glm_attention.o: tests/test_glm_attention.c ds4.h ds4_gpu.h ds4_linux_memory.h +tests/test_glm_attention.o: tests/test_glm_attention.c ds4.h ds4_gpu.h ds4_linux_memory.h ds4_deepseek41_gpu.h ds4_gpu_tp.h $(CC) $(filter-out -ffast-math,$(CFLAGS)) -I. -c -o $@ $< ifeq ($(UNAME_S),Darwin) @@ -724,6 +724,33 @@ tests/test_deepseek41_gguf: tests/test_deepseek41_gguf.o ds4_engram.c $(filter-o test-deepseek41-gguf: tests/test_deepseek41_gguf ./tests/test_deepseek41_gguf +ifeq ($(UNAME_S),Linux) +.PHONY: test-deepseek41-memory +test-deepseek41-memory: tests/test_deepseek41_memory.c ds4.c ds4.h ds4_gpu.h ds4_linux_memory.h ds4_deepseek41_gpu.h ds4_gpu_tp.h + @set -eu; \ + test_bin=$$(mktemp "$${TMPDIR:-/tmp}/ds4-memory.XXXXXX"); \ + trap 'rm -f "$$test_bin"' EXIT; \ + $(CC) $(filter-out -ffast-math,$(CFLAGS)) -O0 -UNDEBUG -UDS4_NO_GPU -DDS4_ROCM_BUILD \ + -Wno-unused-function -ffunction-sections -fdata-sections -I. \ + tests/test_deepseek41_memory.c -Wl,--gc-sections $(LDLIBS) -o "$$test_bin"; \ + "$$test_bin" + +# Exercise the real accelerator span builders on sparse GGUFs without a GPU +# library, production object rebuild, or model allocation. Keep both branches. +.PHONY: test-deepseek41-cache-spans +test-deepseek41-cache-spans: tests/test_deepseek41_cache_spans.c ds4.c ds4.h ds4_gpu.h ds4_engram.h ds4_deepseek41_gpu.h ds4_gpu_tp.h + @set -eu; \ + test_dir=$$(mktemp -d "$${TMPDIR:-/tmp}/ds4-cache-spans.XXXXXX"); \ + trap 'rm -f "$$test_dir/generic" "$$test_dir/rocm"; rmdir "$$test_dir"' EXIT; \ + $(CC) $(filter-out -ffast-math,$(CFLAGS)) -O0 -UNDEBUG -UDS4_NO_GPU -UDS4_ROCM_BUILD \ + -Wno-unused-function -ffunction-sections -fdata-sections -I. \ + tests/test_deepseek41_cache_spans.c -Wl,--gc-sections $(LDLIBS) -o "$$test_dir/generic"; \ + "$$test_dir/generic"; \ + $(CC) $(filter-out -ffast-math,$(CFLAGS)) -O0 -UNDEBUG -UDS4_NO_GPU -DDS4_ROCM_BUILD \ + -Wno-unused-function -ffunction-sections -fdata-sections -I. \ + tests/test_deepseek41_cache_spans.c -Wl,--gc-sections $(LDLIBS) -o "$$test_dir/rocm"; \ + "$$test_dir/rocm" +endif tests/test_qwen4_ngrams.o: tests/test_qwen4_ngrams.c ds4.c ds4.h $(CC) $(filter-out -ffast-math,$(CFLAGS)) -Wno-unused-function -I. -c -o $@ $< @@ -783,7 +810,7 @@ cuda/mmq/mmvq.o: cuda/mmq/mmvq.cu cuda/mmq/mmvq.cuh cuda/mmq/common.cuh cuda/mmq cuda/mmq/ds4_repack.o: cuda/mmq/ds4_repack.cu cuda/mmq/ds4_repack.h $(NVCC) $(NVCCFLAGS) -std=c++17 -c -o $@ $< -ds4_rocm.o: ds4_rocm.cu ds4_rocm.h ds4_rocm_memory.h ds4_linux_memory.h ds4_gpu.h ds4_gpu_tp.h ds4_glm53_vision_gpu.cuh ds4_deepseek4_vision_gpu.cuh ds4_image.h ds4_iq2_tables_cuda.inc $(ROCM_SRCS) +ds4_rocm.o: ds4_rocm.cu ds4_rocm.h ds4_rocm_memory.h ds4_linux_memory.h ds4_gpu.h ds4_gpu_tp.h ds4_glm53_vision_gpu.cuh ds4_deepseek4_vision_gpu.cuh ds4_image.h ds4_iq2_tables_cuda.inc $(ROCM_SRCS) ds4_deepseek41_gpu.h $(HIPCC) $(ROCM_CFLAGS) -c -o $@ ds4_rocm.cu cuda/mmq/ds4_ggml_stubs.rocm.o: cuda/mmq/ds4_ggml_stubs.cu cuda/mmq/ds4_ggml_stubs.h cuda/mmq/common.cuh cuda/mmq/vendors/hip.h ds4_rocm_memory.h ds4_linux_memory.h @@ -798,19 +825,69 @@ cuda/mmq/quantize.rocm.o: cuda/mmq/quantize.cu cuda/mmq/quantize.cuh cuda/mmq/co cuda/mmq/mmid.rocm.o: cuda/mmq/mmid.cu cuda/mmq/mmid.cuh cuda/mmq/common.cuh cuda/mmq/ds4_ggml_stubs.h cuda/mmq/vendors/hip.h $(HIPCC) $(ROCM_MMQ_FLAGS) -c -o $@ $< -cuda/mmq/mmvq.rocm.o: cuda/mmq/mmvq.cu cuda/mmq/mmvq.cuh cuda/mmq/common.cuh cuda/mmq/ds4_ggml_stubs.h cuda/mmq/quantize.cuh cuda/mmq/vecdotq.cuh cuda/mmq/unary.cuh cuda/mmq/vendors/hip.h +cuda/mmq/mmvq.rocm.o: cuda/mmq/mmvq.cu cuda/mmq/mmvq.cuh cuda/mmq/common.cuh cuda/mmq/ds4_ggml_stubs.h cuda/mmq/quantize.cuh cuda/mmq/vecdotq.cuh cuda/mmq/unary.cuh cuda/mmq/vendors/hip.h cuda/mmq/mmq.cuh $(HIPCC) $(ROCM_MMQ_FLAGS) -c -o $@ $< cuda/mmq/d2r_stubs.rocm.o: cuda/mmq/test/d2r_stubs.cu cuda/mmq/ds4_mmq_d2r.cuh cuda/mmq/vendors/hip.h $(HIPCC) $(ROCM_MMQ_FLAGS) -c -o $@ $< -tests/test_mxfp4_rocm.o: tests/test_mxfp4_rocm.c ds4_gpu.h +tests/test_mxfp4_rocm.o: tests/test_mxfp4_rocm.c ds4_gpu.h ds4_deepseek41_gpu.h ds4_gpu_tp.h + $(CC) $(filter-out -ffast-math,$(CFLAGS)) $(ROCM_HOST_CFLAGS) -DDS4_ROCM_BUILD -I. -c -o $@ $< + +# Compile the CPU references without fast-math; link the production GPU objects. +tests/test_deepseek41_rocm.o: tests/test_deepseek41_rocm.c ds4_gpu.h ds4_deepseek41_gpu.h ds4_gpu_tp.h + $(CC) $(filter-out -ffast-math,$(CFLAGS)) -ffp-contract=off $(ROCM_HOST_CFLAGS) -DDS4_ROCM_BUILD -I. -c -o $@ $< + +ds4_image.rocm.o: ds4_image.c ds4_image.h third_party/iris/jpeg.h third_party/iris/png.h + $(CC) $(CFLAGS) $(ROCM_HOST_CFLAGS) -DDS4_ROCM_BUILD -c -o $@ $< + +tests/test_deepseek41_tp_rocm.o: tests/test_deepseek41_tp_rocm.c ds4_gpu.h ds4_deepseek41_gpu.h ds4_gpu_tp.h + $(CC) $(filter-out -ffast-math,$(CFLAGS)) -ffp-contract=off $(ROCM_HOST_CFLAGS) -DDS4_ROCM_BUILD -I. -c -o $@ $< + +tests/test_deepseek41_tp_moe_rocm.o: tests/test_deepseek41_tp_moe_rocm.c ds4_gpu.h ds4_deepseek41_gpu.h ds4_gpu_tp.h + $(CC) $(filter-out -ffast-math,$(CFLAGS)) -ffp-contract=off $(ROCM_HOST_CFLAGS) -DDS4_ROCM_BUILD -I. -c -o $@ $< + +ds4-kernel-v41-tp-moe: tests/test_deepseek41_tp_moe_rocm.o ds4_rocm.o ds4_image.rocm.o $(ROCM_MMQ_OBJS) + $(HIPCC) $(ROCM_CFLAGS) -o $@ $^ $(ROCM_LDLIBS) + +tests/test_rocm_tp_bind_failure.o: tests/test_rocm_tp_bind_failure.c ds4.c ds4.h ds4_gpu.h ds4_tp.h ds4_deepseek41_gpu.h ds4_gpu_tp.h + $(CC) $(CFLAGS) $(ROCM_HOST_CFLAGS) -DDS4_ROCM_BUILD -ffunction-sections -fdata-sections -I. -c -o $@ $< + +ds4-kernel-v41-tp-bind-failure: tests/test_rocm_tp_bind_failure.o ds4_image.o ds4_distributed.o ds4_tp.o ds4_ssd.o ds4_rocm.o ds4_rocm_compat.o ds4_rocm_unavailable.o ds4_layer_pack.o ds4_engram.o $(ROCM_MMQ_OBJS) + $(HIPCC) $(ROCM_CFLAGS) -Wl,--gc-sections -Wl,--wrap=ds4_gpu_tensor_alloc_coherent -o $@ $^ $(ROCM_LDLIBS) + +tests/test_rocm_tp_gates.o: tests/test_rocm_tp_gates.c ds4_gpu.h ds4_tp.c ds4_tp_io.h ds4_tp_roce.h ds4_tp.h ds4.h ds4_deepseek41_gpu.h ds4_gpu_tp.h + $(CC) $(CFLAGS) $(ROCM_HOST_CFLAGS) -DDS4_ROCM_BUILD -ffunction-sections -fdata-sections -I. -c -o $@ $< + +ds4-kernel-v41-tp-gates: tests/test_rocm_tp_gates.o ds4_rocm.o ds4_image.rocm.o $(ROCM_MMQ_OBJS) + $(HIPCC) $(ROCM_CFLAGS) -Wl,--gc-sections -o $@ $^ $(ROCM_LDLIBS) + +tests/test_deepseek41_tp_mmq_rocm.o: tests/test_deepseek41_tp_mmq_rocm.c ds4_gpu.h ds4_deepseek41_gpu.h ds4_gpu_tp.h $(CC) $(filter-out -ffast-math,$(CFLAGS)) $(ROCM_HOST_CFLAGS) -DDS4_ROCM_BUILD -I. -c -o $@ $< +ds4-kernel-v41-tp-mmq: tests/test_deepseek41_tp_mmq_rocm.o ds4_rocm.o ds4_image.rocm.o $(ROCM_MMQ_OBJS) + $(HIPCC) $(ROCM_CFLAGS) -o $@ $^ $(ROCM_LDLIBS) + +tests/test_deepseek41_tp_down_rocm.o: tests/test_deepseek41_tp_down_rocm.c ds4_gpu.h ds4_deepseek41_gpu.h ds4_gpu_tp.h + $(CC) $(filter-out -ffast-math,$(CFLAGS)) -ffp-contract=off $(ROCM_HOST_CFLAGS) -DDS4_ROCM_BUILD -I. -c -o $@ $< + +ds4-kernel-v41-tp-down: tests/test_deepseek41_tp_down_rocm.o ds4_rocm.o ds4_image.rocm.o $(ROCM_MMQ_OBJS) + $(HIPCC) $(ROCM_CFLAGS) -o $@ $^ $(ROCM_LDLIBS) + +ds4-kernel-v41-tp-attention: tests/test_deepseek41_tp_rocm.o ds4_rocm.o ds4_image.rocm.o $(ROCM_MMQ_OBJS) + $(HIPCC) $(ROCM_CFLAGS) -o $@ $^ $(ROCM_LDLIBS) + +ds4-kernel-v41: tests/test_deepseek41_rocm.o ds4_rocm.o ds4_image.rocm.o $(ROCM_MMQ_OBJS) + $(HIPCC) $(ROCM_CFLAGS) -o $@ $^ $(ROCM_LDLIBS) + +.PHONY: test-deepseek41-rocm +test-deepseek41-rocm: ds4-kernel-v41 + ./ds4-kernel-v41 + tests/test_mxfp4_rocm: tests/test_mxfp4_rocm.o ds4_rocm.o ds4_image.o $(ROCM_MMQ_OBJS) $(HIPCC) $(ROCM_CFLAGS) -o $@ $^ $(ROCM_LDLIBS) -tests/bench_mxfp4_rocm.o: tests/bench_mxfp4_rocm.c ds4_gpu.h +tests/bench_mxfp4_rocm.o: tests/bench_mxfp4_rocm.c ds4_gpu.h ds4_deepseek41_gpu.h ds4_gpu_tp.h $(CC) $(filter-out -ffast-math,$(CFLAGS)) $(ROCM_HOST_CFLAGS) -DDS4_ROCM_BUILD -I. -c -o $@ $< tests/bench_mxfp4_rocm: tests/bench_mxfp4_rocm.o ds4_rocm.o ds4_image.o $(ROCM_MMQ_OBJS) @@ -819,7 +896,7 @@ tests/bench_mxfp4_rocm: tests/bench_mxfp4_rocm.o ds4_rocm.o ds4_image.o $(ROCM_M test-mxfp4-rocm: tests/test_mxfp4_rocm ./tests/test_mxfp4_rocm -ds4_rocm_compat.o: ds4_rocm_compat.cu ds4_gpu.h ds4_gpu_tp.h ds4_gpu_mgpu.h ds4_gpu_args.h ds4_rocm_memory.h ds4_linux_memory.h +ds4_rocm_compat.o: ds4_rocm_compat.cu ds4_gpu.h ds4_gpu_tp.h ds4_gpu_mgpu.h ds4_gpu_args.h ds4_rocm_memory.h ds4_linux_memory.h ds4_deepseek41_gpu.h $(HIPCC) $(ROCM_CFLAGS) -c -o $@ ds4_rocm_compat.cu ds4_rocm_unavailable.o: ds4_rocm_unavailable.cu @@ -840,7 +917,7 @@ tests/test_gpu_args.o: tests/test_gpu_args.c ds4_gpu_args.h ds4_gpu_mgpu.h tests/test_gpu_args: tests/test_gpu_args.o ds4_gpu_args_cpu.o $(CC) $(CFLAGS) -o $@ $^ $(LDLIBS) -ds4_cpu_test_hooks.o: ds4.c ds4.h ds4_image.h ds4_gpu.h ds4_gpu_mgpu.h ds4_layer_pack.h +ds4_cpu_test_hooks.o: ds4.c ds4.h ds4_image.h ds4_gpu.h ds4_gpu_mgpu.h ds4_layer_pack.h ds4_deepseek41_gpu.h ds4_gpu_tp.h $(CC) $(CFLAGS) -Wno-unused-function -DDS4_NO_GPU -DDS4_TEST_HOOKS -c -o $@ ds4.c tests/test_engine_mgpu_placement.o: tests/test_engine_mgpu_placement.c ds4.h ds4_gpu_mgpu.h ds4_layer_pack.h @@ -855,13 +932,13 @@ tests/test_sampling.o: tests/test_sampling.c ds4.h tests/test_sampling: tests/test_sampling.o ds4_cpu_test_hooks.o ds4_image.o ds4_distributed.o ds4_tp.o ds4_ssd.o ds4_layer_pack.o $(CC) $(CFLAGS) -o $@ $^ $(LDLIBS) -tests/test_session_state.o: tests/test_session_state.c ds4.c ds4.h ds4_gpu.h ds4_image.h ds4_tp.h +tests/test_session_state.o: tests/test_session_state.c ds4.c ds4.h ds4_gpu.h ds4_image.h ds4_tp.h ds4_deepseek41_gpu.h ds4_gpu_tp.h $(CC) $(CFLAGS) -Wno-unused-function -DDS4_NO_GPU -I. -c -o $@ $< tests/test_session_state: tests/test_session_state.o $(filter-out ds4_cpu.o,$(CPU_CORE_OBJS)) $(CC) $(CFLAGS) -o $@ $^ $(LDLIBS) -tests/test_session_state_gpu.o: tests/test_session_state.c ds4.c ds4.h ds4_gpu.h ds4_image.h ds4_tp.h +tests/test_session_state_gpu.o: tests/test_session_state.c ds4.c ds4.h ds4_gpu.h ds4_image.h ds4_tp.h ds4_deepseek41_gpu.h ds4_gpu_tp.h $(CC) $(CFLAGS) -Wno-unused-function -I. -c -o $@ $< tests/test_session_state_gpu: tests/test_session_state_gpu.o $(filter-out ds4.o,$(CORE_OBJS)) @@ -877,6 +954,15 @@ tests/test_tp_commands.o: tests/test_tp_commands.c ds4_tp.c ds4_tp.h ds4.h ds4_g tests/test_tp_commands: tests/test_tp_commands.o $(filter-out ds4_tp.o,$(CPU_CORE_OBJS)) $(CC) $(CFLAGS) -o $@ $^ $(LDLIBS) +ifeq ($(UNAME_S),Linux) +tests/test_tp_linux: tests/test_tp_linux.c ds4_tp.c ds4_tp_io.h ds4_tp_roce.h ds4_tp.h ds4.h + $(CC) $(CFLAGS) -ffunction-sections -fdata-sections -o $@ $< -Wl,--gc-sections -pthread -lm + +.PHONY: test-tp-linux +test-tp-linux: tests/test_tp_linux + ./tests/test_tp_linux +endif + tests/test_tp_rdma.o: tests/test_tp_rdma.c ds4_tp.c ds4_tp.h ds4.h ds4_gpu_tp.h $(CC) $(CFLAGS) -I. -c -o $@ $< @@ -903,25 +989,25 @@ test-session-state: tests/test_session_state tests/test_tp_commands tests/test_t ./tests/test_tp_tcp ifneq ($(UNAME_S),Darwin) -tests/test_gpu_xdev.o: tests/test_gpu_xdev.c ds4_gpu.h ds4_gpu_mgpu.h +tests/test_gpu_xdev.o: tests/test_gpu_xdev.c ds4_gpu.h ds4_gpu_mgpu.h ds4_deepseek41_gpu.h ds4_gpu_tp.h $(CC) $(CFLAGS) -I. -I$(CUDA_HOME)/include -c -o $@ $< tests/test_gpu_xdev: tests/test_gpu_xdev.o ds4_cuda.o ds4_image.o $(MMQ_OBJS) $(NVCC) $(NVCCFLAGS) -o $@ $^ $(CUDA_LDLIBS) -tests/test_gpu_model_cache.o: tests/test_gpu_model_cache.c ds4_gpu.h +tests/test_gpu_model_cache.o: tests/test_gpu_model_cache.c ds4_gpu.h ds4_deepseek41_gpu.h ds4_gpu_tp.h $(CC) $(CFLAGS) -I. -I$(CUDA_HOME)/include -c -o $@ $< tests/test_gpu_model_cache: tests/test_gpu_model_cache.o ds4_cuda.o ds4_image.o $(MMQ_OBJS) $(NVCC) $(NVCCFLAGS) -o $@ $^ $(CUDA_LDLIBS) -tests/test_gpu_lookup_cache_strict.o: tests/test_gpu_lookup_cache_strict.c ds4_gpu.h ds4_gpu_mgpu.h +tests/test_gpu_lookup_cache_strict.o: tests/test_gpu_lookup_cache_strict.c ds4_gpu.h ds4_gpu_mgpu.h ds4_deepseek41_gpu.h ds4_gpu_tp.h $(CC) $(CFLAGS) -I. -I$(CUDA_HOME)/include -c -o $@ $< tests/test_gpu_lookup_cache_strict: tests/test_gpu_lookup_cache_strict.o ds4_cuda.o ds4_image.o $(MMQ_OBJS) $(NVCC) $(NVCCFLAGS) -o $@ $^ $(CUDA_LDLIBS) -ds4_cuda_test_hooks.o: ds4.c ds4.h ds4_gpu.h ds4_gpu_mgpu.h ds4_gpu_tp.h ds4_layer_pack.h +ds4_cuda_test_hooks.o: ds4.c ds4.h ds4_gpu.h ds4_gpu_mgpu.h ds4_gpu_tp.h ds4_layer_pack.h ds4_deepseek41_gpu.h $(CC) $(CFLAGS) -Wno-unused-function -DDS4_TEST_HOOKS -I$(CUDA_HOME)/include -c -o $@ ds4.c tests/test_engine_mgpu_refusal.o: tests/test_engine_mgpu_refusal.c ds4.h ds4_gpu_mgpu.h @@ -1064,6 +1150,7 @@ clean: rm -f tests/test_metal_ssd_experts rm -f tests/test_metal_command_memory rm -f tests/test_deepseek41_metal + rm -f ds4-kernel-v41 rm -f tests/test_deepseek41_cuda rm -f tests/test_cuda_q8_rows rm -f tests/test_cuda_reductions @@ -1083,7 +1170,8 @@ clean: rm -f tests/test_linux_memory tests/test_rocm_memory rm -f tests/test_glm_attention tests/test_glm_attention_rocm rm -f tests/test_ssd_cache tests/test_engram - rm -f tests/test_session_state tests/test_session_state_gpu tests/test_tp_commands + rm -f tests/test_session_state tests/test_session_state_gpu tests/test_tp_commands tests/test_tp_linux + rm -f ds4-kernel-v41-tp-bind-failure ds4-kernel-v41-tp-attention ds4-kernel-v41-tp-moe ds4-kernel-v41-tp-gates ds4-kernel-v41-tp-mmq ds4-kernel-v41-tp-down rm -f tests/test_tp_rdma tests/test_tp_link tests/test_tp_tcp rm -f tests/test_metal_tp_spec rm -f tests/test_metal_tp_cancel diff --git a/QA_BEFORE_RELEASES.md b/QA_BEFORE_RELEASES.md index 3410ec08bb..335cf66f00 100644 --- a/QA_BEFORE_RELEASES.md +++ b/QA_BEFORE_RELEASES.md @@ -1102,6 +1102,8 @@ clients. and streaming decode. Repeat across OpenAI chat, Responses, Anthropic, and completions. Abandoned work must stop at the next backend-safe boundary, and a valid request after each cancellation must complete normally. +- For V4.1 concurrent serving, run `tests/test_server_batching.py --pairs 8 --workers 4 --output batching.json` with the server defaults, then repeat with `--stream --cancel-first 4` and `--same-prompt --case long-greedy`. Cover resident and SSD experts, cold and restored disk checkpoints, and a separately recorded `--mixed-prefill-quantum 2048` control. Preserve full responses and inspect actual slot overlap and chunk boundaries. Fixed-size diagnostic controls do not substitute for testing the defaults. Fixture groups named "batch" in official scoring do not test concurrent requests. +- Set time limits from observed model loading, prefill, generation and queue duration. If a test is still progressing when its limit expires, preserve the attempt, increase or remove the inadequate limit and complete it; investigate a stalled test. Do not count partial assertions as a completed suite. For the vision-cache test, use `--timeout` and `--thinking-tokens` when the model needs more time or reasoning tokens, recording the settings and checking that answers finish normally. - For the repeatable chat-completions cancellation and slot-reuse gate, run `python3 tests/test_server_batching.py --url http://127.0.0.1:8000 --pairs 2 --workers 4 --case short-sampled --max-tokens 12 --cancel-first 4`. Then run @@ -1664,6 +1666,39 @@ Exclude loading. Only the first row is initial prefill; later rows are appends. --step-mul 2 --ctx-alloc 36864 --gen-tokens 128 --show-output --csv RESULT.csv ``` +## DeepSeek V4.1 Flash (ROCm/gfx1151) + +- ROCm 10.0, Strix Halo `gfx1151`, calibrated `DeepSeek-V4.1-Flash-Q2.gguf` (365,713,686,528 bytes; saved SHA-256 `1ce6a8f8806205c13330d7ca287bd198331dc5ca35ccc5d8a9a92a188a6f6f42`). Text/vision, resident experts, SSD streaming and two-machine inference; Engram stays disk-backed. +- Official scoring: 115 cases / 3,954 teacher-forced targets per ROCm resident and two-machine panel, default arithmetic, no DSpark or `--quality`. CUDA is the recorded `a04f46f` GB10 reference with a 76 GiB SSD cache; it predates later CUDA SSD prefill changes. Metal values are published same-Q2 aggregates. CUDA and Metal figures are recorded references; this ROCm panel did not rerun them. + +| Official panel | Targets | ROCm resident NLL | ROCm two-rank NLL | CUDA reference NLL | +|---|---:|---:|---:|---:| +| General 100 | 2,994 | 0.368267385 | 0.362317121 | 0.363135483 | +| Batched 12 | 768 | 0.521719404 | 0.525356473 | 0.521390812 | +| Selected 8K; 8,197 prompt tokens | 64 | 0.133878180 | 0.138695106 | 0.116477286 | +| Selected 16K; 16,389 prompt tokens | 64 | 0.488504024 | 0.497056311 | 0.478793408 | +| Selected 64K; 65,541 prompt tokens | 64 | 0.439256315 | 0.448364408 | 0.437359191 | + +| General 100 backend/mode | NLL | Target-logprob MAE | Recorded API top1 agreement | +|---|---:|---:|---:| +| ROCm resident | 0.368267385 | 0.230736441 | 2705/2994 | +| ROCm two-rank | 0.362317121 | 0.223234795 | 2704/2994 | +| CUDA SSD | 0.363135483 | 0.223383502 | 2696/2994 | +| Published Metal resident batching | 0.365680596 | — | 2705/2994 | +| Published Metal scalar/SSD | 0.364576009 | 0.227531809 | 2697/2994 | + +- Probability results are mixed. Resident general NLL is 0.71% above published Metal resident and 1.41% above CUDA. The selected long cases each contain only 64 targets; neither these scores nor passing practical checks establish universal generation or distribution equivalence. +- Fixed-length greedy literary continuations can become repetitive; the native benchmark checks timing and completion, not prose quality. Coding, tool-use and image checks use their own prompts and validators. +- Practical checks pass in resident and two-machine modes: nine executable coding checks with a 36,992-token prompt, seven real tool-use checks after a 9,509-token prompt and a 24-token cached append, plus 28 sampled checks. Sampled settings: temperature 0.6, top-p 0.95, seed 1729, min-p/top-k 0; thinking and DSpark off. +- State: 69 exact comparisons and 138 full logit vectors per resident/two-machine panel. Covers interleaved sessions, snapshot restore, rewind, corrupt snapshot rejection and three cancellation/recovery modes, at lengths from 31 through 16,383 tokens. Current transport measurements and complete-output checks are listed in [CLUSTERING_ROCM.md](docs/CLUSTERING_ROCM.md#measured-performance). +- Vision: six resident and six RoCE image cases pass, plus focused SSD photo/screenshot checks; 168 full vectors and 112 complete states in total. Image facts, image changes and exact replay are checked. Restoring an image snapshot into a new session and every image over every transport were not tested. Image inputs are correctness checks, not timing inputs. +- SSD: 92 GiB cache, fresh 16K/64K with 69,632 allocated context and 512/128 outputs; complete frontiers and printed continuations match resident execution. Memory/swap results and exact commands are in [STRIX_HALO.md](docs/STRIX_HALO.md#ssd-performance). +- Kernel checks cover production layouts, full outputs, independent FP64 references, tail rows, canaries, selection ties and nonfinite inputs. The V4.1 split-key decode attention path uses FP32 throughout; matrix instructions remain in the prefill paths. FP16 matrix paths accumulate in FP32; Engram packing checks representability and retains the FP32 fallback. Numerical differences are assessed with the official and practical results above. +- ROCm/gfx1151/V4.1 guards preserve other model/backend paths. Shared CPU/CUDA/Metal code was checked with preprocessing comparisons; five frontends build/link/help checks and relevant memory/cache/Engram units pass. No fresh physical GLM, CUDA or Metal regression run is claimed. +- Exactly two machines, each with its assigned experts in RAM; no cluster SSD streaming or DSpark. RoCE uses system-RAM staging. Plain TCP over USB4 Ethernet also works. Long-running production endurance was not tested. +- Performance, hardware/SSD/profile details and commands: [single-node SSD](docs/STRIX_HALO.md#deepseek-v41-flash), [TCP/RoCE](docs/CLUSTERING_ROCM.md#measured-performance). + + ### CUDA SSD Streaming V4.1 CUDA starts with text inference on a single DGX Spark. Test on diff --git a/README.md b/README.md index 5c3dbf923e..6e5cf24e5d 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ language models on consumer hardware (that is, hardware that people can actually own). To reach this goal, we are building a small native inference engine optimized first for **DeepSeek V4 Flash** (including the experimental vision model), -**DeepSeek V4.1 Flash** (Metal, and text inference on CUDA), +**DeepSeek V4.1 Flash** (Metal, CUDA text inference, and ROCm on Strix Halo), and additionally **GLM 5.2 and 5.3**, **GLM 5.3 Flash** and **DeepSeek V4 PRO**, and **Qwen3.8 Flash Next** (Metal and CUDA). The code is self-contained and deliberately narrow, not a general GGUF runner: you need to use the @@ -165,10 +165,9 @@ requirements. DeepSeek Vision Experimental uses a different checkpoint from Flash 0731; GLM 5.3 Flash and Qwen3.8 Flash Next add vision to the same text model through a separate encoder. -DeepSeek V4.1 Flash text and vision run on Metal; text also runs on a DGX Spark. -Q2 runs with SSD streaming on one 128 GB Mac or Spark, or resident across two -Macs or two Sparks using RDMA. Q4 needs SSD streaming or a 512 GB Mac. -Engram tables remain on disk in every mode, so use a fast +DeepSeek V4.1 Flash text and vision run on Metal and ROCm on Strix Halo (`gfx1151`); CUDA supports text on DGX Spark. ROCm Q2 supports resident weights, SSD streaming and two-machine resident clustering. See [ROCm qualification](QA_BEFORE_RELEASES.md#deepseek-v41-flash-rocmgfx1151) and [clustering setup](docs/CLUSTERING_ROCM.md). + +Q2 runs with SSD streaming on one 128 GB Mac or Spark, or resident across two Macs or two Sparks using RDMA. Q4 needs SSD streaming or a 512 GB Mac. Engram tables remain on disk in every mode, so use a fast local SSD. See the [model guide](docs/MODELS.md#deepseek-v41-flash) for downloads and setup. diff --git a/cuda/mmq/ds4_mmq.cu b/cuda/mmq/ds4_mmq.cu index 5e9e5ceef5..11fd32b6b3 100644 --- a/cuda/mmq/ds4_mmq.cu +++ b/cuda/mmq/ds4_mmq.cu @@ -26,6 +26,29 @@ #include "mmid.cuh" #include "ds4_mmq_d2r.cuh" +#if defined(GGML_USE_HIP) +// One small producer on the existing stream. Thread0 scans384 bounds;128 +// threads fill all575 slots and then their disjoint active expert records. +static __global__ void ds4_mmq_compact_produce(const int32_t *bounds, ds4_mmq_compact::List *out) { + __shared__ uint32_t offsets[ds4_mmq_compact::experts + 1]; + __shared__ uint32_t status; + const uint32_t t = threadIdx.x; + for (uint32_t i = t; i < ds4_mmq_compact::capacity; i += blockDim.x) + out->entries[i] = {UINT32_MAX, UINT32_MAX}; + if (t == 0) { + status = ds4_mmq_compact::prefix(bounds, offsets); + out->status = status; + out->count = status ? 0 : offsets[ds4_mmq_compact::experts]; + } + __syncthreads(); + if (status) return; + for (uint32_t e = t; e < ds4_mmq_compact::experts; e += blockDim.x) + for (uint32_t i = offsets[e]; i < offsets[e + 1]; i++) + out->entries[i] = {e, i - offsets[e]}; +} + +#endif + #include #include #include @@ -1337,6 +1360,11 @@ int ds4_mmq_moe_pair_impl( const int64_t s01 = (int64_t)K / blck; const int64_t s02 = (int64_t)M * s01; +#if defined(GGML_USE_HIP) + // Declared first so the list remains owned through both projections. + ggml_cuda_pool_alloc compact_alloc; + ds4_mmq_compact::List *compact_list = nullptr; +#endif ggml_cuda_pool_alloc ids_src1_alloc; ggml_cuda_pool_alloc ids_dst_alloc; ggml_cuda_pool_alloc expert_bounds_alloc; @@ -1472,6 +1500,23 @@ int ds4_mmq_moe_pair_impl( ? (int64_t)n_tokens : ne_get_rows; +#if defined(GGML_USE_HIP) + const bool compact_eligible = + type == GGML_TYPE_IQ2_XXS && cc == GGML_CUDA_CC_OFFSET_AMD + 0x1151 && + n_tokens == 2048 && n_experts == 384 && n_expert_used == 6 && K == 5120 && M == 2304 && + !direct_gateup_q8 && !fused_down && !persistent_pair_maps && !xa_soa && !xb_soa && + !use_stream_k && stream == nullptr && routed_ncols_max == 2048 && + get_mmq_y_host(cc) == 64 && get_mmq_x_max_host(cc) == 64 && + ggml_cuda_info().devices[dev].warp_size == 32 && mmq_get_nwarps_host(cc,32) == 4; + if (compact_eligible) { + compact_list = compact_alloc.alloc(ctx->pool(),1); + ds4_mmq_compact_produce<<<1,128,0,stream>>>(expert_bounds,compact_list); + err = cudaGetLastError(); + if (err != cudaSuccess) return -102; + } + +#endif + /* The materialized path stream-frees gate/up Q8_1 before allocating the * down Q8_1. The direct path needs both simultaneously, but writes down * Q8_1 into caller-owned gate scratch instead of growing the CUDA pool. */ @@ -1722,6 +1767,10 @@ int ds4_mmq_moe_pair_impl( /*x_soa=*/xa_soa, /*soa_blocks=*/soa_blocks, }; +#if defined(GGML_USE_HIP) + args.compact_list = compact_list; +#endif + { ds4_mmq_nvtx_scope stage( diff --git a/cuda/mmq/mmq.cuh b/cuda/mmq/mmq.cuh index b0a6d63334..12ac5611fa 100644 --- a/cuda/mmq/mmq.cuh +++ b/cuda/mmq/mmq.cuh @@ -7,6 +7,38 @@ #include #include +#if defined(GGML_USE_HIP) +#include +#include +#if defined(__HIPCC__) +#define DS4_MMQ_COMPACT_HD __host__ __device__ +#else +#define DS4_MMQ_COMPACT_HD +#endif +namespace ds4_mmq_compact { +constexpr uint32_t experts = 384, routes = 2048 * 6, tile = 64; +constexpr uint32_t capacity = (routes + tile - 1) / tile + experts - 1; +struct Entry { uint32_t expert, column_tile; }; +struct List { uint32_t count, status; Entry entries[capacity]; }; +static_assert(capacity == 575 && sizeof(List) == 4608, "fixed complete list extent"); +// Exact shared host/device prefix routine; bounded counts never overflow. +DS4_MMQ_COMPACT_HD inline uint32_t prefix(const int32_t *bounds, uint32_t *out) { + out[0] = 0; + if (bounds[0] != 0 || bounds[experts] != (int32_t)routes) return 1; + for (uint32_t e = 0; e < experts; e++) { + const int32_t lo = bounds[e], hi = bounds[e + 1]; + if (lo < 0 || hi < lo || hi > (int32_t)routes || hi - lo > 2048) return 2; + const uint32_t n = ((uint32_t)(hi - lo) + tile - 1) / tile; + out[e + 1] = out[e] + n; + if (out[e + 1] > capacity) return 3; + } + return 0; +} +} +#undef DS4_MMQ_COMPACT_HD + +#endif + using namespace ggml_cuda_mma; #define MMQ_DP4A_MAX_BATCH_SIZE 64 // Max. batch size to use for dp4a MMQ kernels when FP16 tensor cores are available. @@ -4238,6 +4270,112 @@ static __global__ void mul_mat_q_stream_k_fixup( } } +#if defined(GGML_USE_HIP) +template +#if defined(RDNA4) || defined(RDNA3) || defined(RDNA2) || defined(CDNA) || defined(GCN) + __launch_bounds__(ggml_cuda_get_physical_warp_size()*mmq_get_nwarps_device(), 2) +#endif // defined(RDNA4) || defined(RDNA3) || defined(RDNA2) || defined(CDNA) || defined(GCN) +static __global__ void ds4_mmq_compact_mul_mat_q( + const char * __restrict__ x, const int * __restrict__ y, const int32_t * __restrict__ ids_dst, + const int32_t * __restrict__ expert_bounds, float * __restrict__ dst, float * __restrict__ tmp_fixup, + const uint3 blocks_per_ne00, const int nrows_x, const int ncols_dst, const int stride_row_x, const int ncols_y, const int stride_col_dst, + const uint3 channel_ratio, const uint3 nchannels_y, const int stride_channel_x, const int stride_channel_y, const int stride_channel_dst, + const uint3 sample_ratio, const uint3 nsamples_y, const int stride_sample_x, const int stride_sample_y, const int stride_sample_dst, + const uint3 ntx, const char * __restrict__ x_soa, const int64_t soa_blocks, const ds4_mmq_compact::List * __restrict__ compact) { + static_assert(type==GGML_TYPE_IQ2_XXS && mmq_x==64,"gfx1151 compact tile"); + if (compact->status || blockIdx.y >= compact->count) return; + const ds4_mmq_compact::Entry compact_entry=compact->entries[blockIdx.y]; + + // Skip unused template specializations for faster compilation: + if (mmq_x > get_mmq_x_max_device() || mmq_x % mmq_get_granularity_device(mmq_x) != 0) { + NO_DEVICE_CODE; + return; + } + + constexpr int nwarps = mmq_get_nwarps_device(); + constexpr int warp_size = ggml_cuda_get_physical_warp_size(); + + constexpr int mmq_y = get_mmq_y_device(); + + // Initialize the ids for writing back data with just the index. + // For regular matrix multiplications this is never changed. + // For MoE the correct indices are loaded from ids_dst. + extern __shared__ int ids_dst_shared[]; // Stored at beginning of shared memory. +#pragma unroll + for (int j0 = 0; j0 < mmq_x; j0 += nwarps*warp_size) { + const int j = j0 + threadIdx.y*warp_size + threadIdx.x; + + if (j0 + nwarps*warp_size > mmq_x && j >= mmq_x) { + break; + } + + ids_dst_shared[j] = j; + } + __syncthreads(); + + // This gfx1151-only admission uses conventional tiling. + { + const int wt = 0; + const int zt = compact_entry.expert; + const int jt = compact_entry.column_tile; + const int it = blockIdx.x; + + // Defaults for regular matrix multiplication: + int col_low = 0; + int col_high = ncols_dst; + int col_diff = ncols_dst; + int offset_y = wt*stride_sample_y + zt*stride_channel_y; + int offset_dst = wt*stride_sample_dst + zt*stride_channel_dst + jt*mmq_x*stride_col_dst; + + if (ids_dst) { + col_low = expert_bounds[zt + 0]; + col_high = expert_bounds[zt + 1]; + col_diff = col_high - col_low; + + offset_y = 0; + offset_dst = 0; + + if (jt*mmq_x >= col_diff) { + return; + } + + // __syncthreads(); // There is no previous tile that could cause a race condition. +#pragma unroll + for (int j0 = 0; j0 < mmq_x; j0 += nwarps*warp_size) { + const int j = j0 + threadIdx.y*warp_size + threadIdx.x; + + if (j0 + nwarps*warp_size > mmq_x && j >= mmq_x) { + break; + } + + // ds4 (S1.1a): the final column tile of an expert is partial + // when col_diff % mmq_x != 0; reading all mmq_x lanes over-reads + // ids_dst past col_high (OOB for the last expert -- confirmed by + // compute-sanitizer memcheck). These lanes are masked out of + // write-back (tile_y_max_j), so clamp the read to valid columns. + const int j_col = jt*mmq_x + j; + ids_dst_shared[j] = j_col < col_diff ? ids_dst[col_low + j_col] : 0; + } + __syncthreads(); + } + + offset_y += (col_low + jt*mmq_x)*(sizeof(block_q8_1_mmq)/sizeof(int)); + offset_dst += it*mmq_y; + + const int tile_x_max_i = nrows_x - it*mmq_y - 1; + const int tile_y_max_j = col_diff - jt*mmq_x - 1; + + const int offset_x = fastdiv(wt, sample_ratio)*stride_sample_x + fastdiv(zt, channel_ratio)*stride_channel_x + it*mmq_y*stride_row_x; + + constexpr bool fixup = false; + mul_mat_q_process_tile + (x, offset_x, y + offset_y, ids_dst_shared, dst + offset_dst, tmp_fixup, stride_row_x, ncols_y, stride_col_dst, + tile_x_max_i, tile_y_max_j, 0, blocks_per_ne00.z, x_soa, soa_blocks); + return; + } +} +#endif + struct mmq_args { const char * x; ggml_type type_x; const int * y; const int32_t * ids_dst; const int32_t * expert_bounds; float * dst; int64_t ncols_x; int64_t nrows_x; int64_t ncols_dst; int64_t stride_row_x; int64_t ncols_y; int64_t nrows_dst; @@ -4249,6 +4387,10 @@ struct mmq_args { // ignored; soa_blocks = pair count (Q2_K) or block count (IQ2_XXS). // Trailing fields so existing aggregate initializers value-init them. const char * x_soa; int64_t soa_blocks; +#if defined(GGML_USE_HIP) + const ds4_mmq_compact::List * compact_list = nullptr; +#endif + }; template @@ -4294,6 +4436,26 @@ static void launch_mul_mat_q(ggml_backend_cuda_context & ctx, const mmq_args & a const uint3 channel_ratio_fd = init_fastdiv_values(channel_ratio); const uint3 sample_ratio_fd = init_fastdiv_values(sample_ratio); +#if defined(GGML_USE_HIP) + if (args.compact_list) { + if constexpr (type == GGML_TYPE_IQ2_XXS && mmq_x == 64) { + GGML_ASSERT(cc == GGML_CUDA_CC_OFFSET_AMD+0x1151 && mmq_y==64 && + args.nrows_x==2304 && args.ncols_x==5120 && args.ncols_max==2048 && + args.nchannels_x==384 && args.nchannels_y==384 && args.nsamples_y==1 && + args.ids_dst && args.expert_bounds && !args.use_stream_k && !args.x_soa); + CUDA_SET_SHARED_MEMORY_LIMIT((ds4_mmq_compact_mul_mat_q), nbytes_shared); + ds4_mmq_compact_mul_mat_q<<>> + (args.x, args.y, args.ids_dst, args.expert_bounds, args.dst, nullptr, + blocks_per_ne00_fd, args.nrows_x, args.ncols_dst, args.stride_row_x, args.ncols_y, args.nrows_dst, + channel_ratio_fd, nchannels_y_fd, args.stride_channel_x, args.stride_channel_y, args.stride_channel_dst, + sample_ratio_fd, nsamples_y_fd, args.stride_sample_x, args.stride_sample_y, args.stride_sample_dst, + ntx_fd, args.x_soa, args.soa_blocks, args.compact_list); + } else { GGML_ASSERT(false && "unexpected compact tile selection"); } + return; + } + +#endif + if (!args.use_stream_k) { if (args.nrows_x % mmq_y == 0) { constexpr bool need_check = false; diff --git a/docs/CLUSTERING_ROCM.md b/docs/CLUSTERING_ROCM.md new file mode 100644 index 0000000000..dd5a298deb --- /dev/null +++ b/docs/CLUSTERING_ROCM.md @@ -0,0 +1,184 @@ +# DeepSeek V4.1: two-machine ROCm cluster + +- Two ROCm/gfx1151 machines; tested with 128 GB RAM each. +- Same engine revision and `DeepSeek-V4.1-Flash-Q2.gguf` on both. Each keeps the full GGUF; each machine loads approximately 80.6 GiB of weights into RAM. Engram stays on disk. +- Exactly two machines: one coordinator and one worker. They share attention computation and split the experts equally. +- Cluster mode requires the assigned experts to fit in RAM. SSD expert streaming, DSpark and splitting by `--layers` are not supported. +- Both transports require a reachable TCP control address. Use a trusted network: peer traffic has no authentication or encryption. +- Build both peers with `make strix-halo ROCM_ARCH=gfx1151` after installing any required RoCE headers. +- Run from the engine build directory. Set these variables in **both** terminals; `MODEL` may differ between machines: + +```bash +MODEL=/absolute/path/DeepSeek-V4.1-Flash-Q2.gguf +COORD=10.99.0.1 # Coordinator's address on the selected link +CTX=16384 +``` + +## TCP over Ethernet or USB4 + +- Working Ethernet/IP connection; coordinator TCP port 9911 reachable from the worker. USB4 Ethernet (`thunderbolt_net`) works with the same commands: set `COORD` to the coordinator's USB IP address. +- No RDMA packages required. + +```bash +# Coordinator +./ds4-server --rocm -m "$MODEL" --ctx "$CTX" \ + --tensor-parallel --role coordinator --listen "$COORD" 9911 \ + --transport tcp --batched-session 1 --host 127.0.0.1 --port 8080 + +# Worker, in its own terminal +./ds4 --rocm -m "$MODEL" --ctx "$CTX" \ + --tensor-parallel --role worker --coordinator "$COORD" 9911 \ + --transport tcp +``` + +## RoCE + +- Both hosts need RoCE-capable Ethernet adapters, a working driver and an active Ethernet verbs port. Ordinary Ethernet alone is insufficient. +- Install the runtime/provider packages where inference runs; development headers are needed when building the engine. Package names: [Fedora](https://packages.fedoraproject.org/pkgs/rdma-core/), [Ubuntu](https://packages.ubuntu.com/source/jammy/rdma-core). + +```bash +# Fedora, both inference/build environments +sudo dnf install libibverbs libibverbs-utils rdma-core-devel iproute + +# Ubuntu/Debian alternative +sudo apt install rdma-core libibverbs1 ibverbs-providers ibverbs-utils libibverbs-dev iproute2 +``` + +```bash +# Both hosts/environments: inspect local device, port and GIDs +ibv_devices +ibv_devinfo -v +rdma link +ulimit -l # Locked-memory allowance; at least 16 MiB for RoCE staging +DEV=rocep194s0 # Replace with this host's verbs device +PORT=1 +for file in /sys/class/infiniband/"$DEV"/ports/"$PORT"/gids/*; do + idx=${file##*/} + printf '%s %s %s %s\n' "$idx" "$(cat "$file")" \ + "$(cat /sys/class/infiniband/"$DEV"/ports/"$PORT"/gid_attrs/types/"$idx")" \ + "$(cat /sys/class/infiniband/"$DEV"/ports/"$PORT"/gid_attrs/ndevs/"$idx")" +done +GID=1 # Choose this host's nonzero RoCE v2 GID for the cabled NIC/IP +``` + +- `rdma link` comes from [Fedora iproute](https://packages.fedoraproject.org/pkgs/iproute/iproute/fedora-rawhide.html) or [Ubuntu iproute2](https://packages.ubuntu.com/jammy/all/iproute2/filelist). +- Device names and GID indexes may differ between hosts. `ibv_devinfo` must show an active port with Ethernet link layer. +- The selected device's `/dev/infiniband/uverbs*` must be accessible. Containers also need its device access, userspace provider and adequate memlock allowance; packages alone do not configure the host NIC. +- Set `COORD` to the coordinator's address on the RoCE Ethernet link. + +```bash +# Coordinator +./ds4-server --rocm -m "$MODEL" --ctx "$CTX" \ + --tensor-parallel --role coordinator --listen "$COORD" 9911 \ + --transport rdma --rdma-device "$DEV" --rdma-port "$PORT" --rdma-gid-index "$GID" \ + --batched-session 1 --host 127.0.0.1 --port 8080 + +# Worker +./ds4 --rocm -m "$MODEL" --ctx "$CTX" \ + --tensor-parallel --role worker --coordinator "$COORD" 9911 \ + --transport rdma --rdma-device "$DEV" --rdma-port "$PORT" --rdma-gid-index "$GID" +``` + +- RoCE transfers use buffers in system RAM. GPU-direct transfers are not implemented; RCCL is not required. +- Explicit `tcp` or `rdma` fails if unavailable. `auto` negotiates configured RoCE, then TCP at connection setup; no mid-generation fallback. + +## Vision and first request + +- Add `--vision /absolute/path/DeepSeek-V4.1-Flash-Vision.gguf` to **both** commands. Keep `--ctx` equal on both. +- The HTTP API runs only on the coordinator. Test port 8080 after startup; do not send HTTP to peer port 9911. + +```bash +curl http://127.0.0.1:8080/v1/chat/completions \ + -H 'Content-Type: application/json' \ + -d '{"model":"deepseek-v4.1-flash","messages":[{"role":"user","content":"Say hello."}],"temperature":0,"max_tokens":64,"thinking":false}' +``` + + +## Measured performance + +- Two Framework Desktop systems, 128 GB each, 16-core Strix Halo / `gfx1151`; coordinator Ryzen AI Max+ 395, worker engineering sample `100-000001243-50_Y`. +- Model drives: coordinator SK hynix PC711 1 TB (PCIe 3.0 ×4, ext4); worker Kingston FURY Renegade 2 TB (`SFYRD2000G`, PCIe 4.0 ×4, btrfs). Engram remains disk-backed. +- TCP/RoCE: Intel E810-C QSFP NICs, 100 Gb/s link, MTU 9000. Coordinator NIC negotiated PCIe 3.0 ×4; worker PCIe 4.0 ×4. +- Existing boot settings include `pci=realloc pcie_aspm=off`, in addition to the [GPU-visible memory settings](STRIX_HALO.md#gpu-visible-memory). Their individual performance effect was not isolated. +- Linux `7.2.5-100.fc43.x86_64`, ROCm 10.0 SDK (`10.0.0-4`, HIP `7.15.26333`). TuneD `accelerator-performance`, fans at maximum speed on both machines. +- Same Q2 file, 69,632 allocated context, fresh full prefix, fixed greedy outputs, no DSpark or images. Native `ds4-bench`; startup and a 256-token/128-output warmup excluded. One run per cell; values are **prefill / decode tokens/s**. + +| Prompt tokens | Generated tokens | TCP, 100 GbE | RoCE RC, 100 GbE | +|---:|---:|---:|---:| +| 1,024 | 128 | 123.54 / 15.42 | 123.80 / 15.94 | +| 16,384 | 512 | 412.94 / 15.59 | 410.54 / 15.94 | +| 65,536 | 128 | 432.83 / 14.96 | 431.85 / 15.33 | + +- A second RoCE 16K/512 run measured **408.25 / 15.92** prefill/decode tokens/s. +- All 129,280 frontier logits and complete printed continuations match across transports at each depth. No OOM; minimum usable RAM in the qualification panel: at least 32.9 GiB. Host zram swap-out was nonzero; these are not zero-swap or cold-cache measurements. +- RoCE device logs and hardware send counters confirm RDMA payloads on both peers. Its TCP control connection is intentional; similar decode rates do not indicate TCP fallback. +- V4.1 CED uses about 8B active parameters/token in prefill and 16B in decode. Full prefixes exercise the decoder-suffix optimization; short appends can follow a different schedule. [Architecture](https://huggingface.co/deepseek-ai/DeepSeek-V4.1-Flash/blob/df42c109f1defefcbfcedbe7d905718a12266e40/README.md?code=true). +- Results apply to these drives, NIC attachment, profile. Other network adapters have not been tested. Long-running production use has not been tested. + +### Appending to an existing prompt + +Separately recorded measurements of the unchanged prefill path: same hardware, allocation and warmup; one live session, no generation between frontiers. Values time only the newly appended tokens, in tokens/s. + +| Existing → final tokens | Added tokens | TCP, 100 GbE | RoCE | +|---:|---:|---:|---:| +| 4,096 → 8,192 | 4,096 | 219.61 | 218.78 | +| 8,192 → 16,384 | 8,192 | 359.70 | 355.86 | +| 57,344 → 65,536 | 8,192 | 314.92 | 313.65 | + +### Reproduce the table + +Build the engine on both machines. On the coordinator, build the included TP-only warmup adapter; it links the existing engine objects and leaves `ds4-bench` untouched: + +```bash +make strix-halo ROCM_ARCH=gfx1151 +bash speed-bench/build-rocm-v41-warmup.sh # Coordinator only + +tuned-adm active # Expect accelerator-performance during the workload +tuned-adm verify # Verify the applied profile +``` + +Set this in both terminals after the relevant device setup above: + +```bash +MODEL=/absolute/path/DeepSeek-V4.1-Flash-Q2.gguf +COORD=10.99.0.1 # Coordinator address on the selected link +TRANSPORT=rdma # tcp or rdma +DEV=rocep194s0 # This host's active verbs device +GID=1 # This host's matching RoCE v2 GID +LINK=(--tensor-parallel --transport "$TRANSPORT") +case "$TRANSPORT" in + rdma) LINK+=(--rdma-device "$DEV" --rdma-port 1 --rdma-gid-index "$GID") ;; +esac +``` + +```bash +# Coordinator: 16K/512; use DEPTH=1024 or 65536 with GEN=128 for the other rows. +DEPTH=16384 +GEN=512 +./ds4-bench-warm --backend rocm -m "$MODEL" \ + --prompt-file speed-bench/promessi_sposi.txt \ + --ctx-start "$DEPTH" --ctx-max "$DEPTH" --ctx-alloc 69632 \ + --gen-tokens "$GEN" --show-output --csv "tp-$TRANSPORT-$DEPTH.csv" \ + --dump-frontier-logits-dir "frontiers-$TRANSPORT-$DEPTH" \ + --role coordinator --listen "$COORD" 19475 "${LINK[@]}" + +# Worker: start for each coordinator run. +./ds4 --rocm -m "$MODEL" --ctx 69632 \ + --role worker --coordinator "$COORD" 19475 "${LINK[@]}" +``` + +- The adapter warms 256 prefix tokens and 128 decode steps, then creates a fresh session before the unchanged native measured loop. A separate short process is not the same warmup procedure. +- Preserve the CSV, full frontier files, printed continuation, revision/build flags, model identity, active power profile and swap/OOM counters. Verify the active power profile before comparing timings; no image-conditioned prefill timings. + +For the append table, keep the worker command and replace the coordinator command with: + +```bash +# 4K → 8K → 16K, no generated tokens between appends +./ds4-bench-warm --backend rocm -m "$MODEL" \ + --prompt-file speed-bench/promessi_sposi.txt \ + --ctx-start 4096 --ctx-max 16384 --step-mul 2 --ctx-alloc 69632 \ + --gen-tokens 0 --csv "append-$TRANSPORT.csv" \ + --dump-frontier-logits-dir "append-frontiers-$TRANSPORT" \ + --role coordinator --listen "$COORD" 19475 "${LINK[@]}" +# For 56K → 64K: --ctx-start 57344 --ctx-max 65536 --step-mul 1 --step-incr 8192 +``` diff --git a/docs/MODELS.md b/docs/MODELS.md index eebc77d5a6..177dc0428a 100644 --- a/docs/MODELS.md +++ b/docs/MODELS.md @@ -45,10 +45,7 @@ To build weights rather than download them, see [GGUF tools](../gguf-tools/READM ## DeepSeek V4.1 Flash -V4.1 Flash text and vision inference work on Metal. CUDA supports text with -Q2 SSD streaming on one Spark or resident shards across two Sparks. It needs its own -GGUF, tokenizer and inference graph; V4 Flash weights and DSpark support files -are not interchangeable with it. +V4.1 Flash text and vision inference work on Metal and ROCm on Strix Halo (`gfx1151`). ROCm Q2 supports resident expert weights, SSD streaming and two-machine resident clustering; see [Strix Halo setup](STRIX_HALO.md#deepseek-v41-flash) and [clustering commands](CLUSTERING_ROCM.md). CUDA supports text with Q2 SSD streaming on one Spark or resident shards across two Sparks. V4.1 needs its own GGUF, tokenizer and inference graph; V4 Flash weights and DSpark support files are not interchangeable with it. | Target | File size | Main weights | | --- | ---: | ---: | @@ -93,9 +90,7 @@ Resident and TP inference also batch continued prefills automatically. For concurrent serving, see [session batching](SERVER.md#multiple-sessions). Each slot needs its own context memory; start with `--ctx 4096` before -increasing both context and slot count. CUDA Q2 SSD mode batches up to eight -decode rows; CUDA network TP currently serves sessions in order. DSpark, -pipeline execution and ROCm are not implemented for V4.1; vision requires Metal. +increasing both context and slot count. CUDA Q2 SSD mode batches up to eight decode rows; CUDA network TP currently serves sessions in order. DSpark and pipeline execution are unsupported for V4.1. ROCm network TP requires resident expert shards; SSD streaming is single-node only. Scalar, batched and tensor-parallel execution are not numerically identical. Q4 batched prefill shows a small probability-score loss on the short official @@ -111,8 +106,9 @@ For images, download the matching encoder and add it to the same command: --ssd-streaming --vision gguf/DeepSeek-V4.1-Flash-Vision.gguf ``` -Vision works with SSD streaming, full residency and two-Mac TP. Pass the encoder -on both TP ranks. Use `/read image.png` in `ds4`, `view_image` in `ds4-agent`, +On Metal, vision works with SSD streaming, full residency and two-Mac TP. ROCm vision is qualified with resident expert weights and SSD streaming. Pass the encoder on both TP ranks. + +Use `/read image.png` in `ds4`, `view_image` in `ds4-agent`, or the [server image API](SERVER.md#images). V4 Flash vision encoders do not work with V4.1. See [conversion](../gguf-tools/README.md#convert-deepseek-v41-flash) to build the GGUFs from safetensors. @@ -201,7 +197,7 @@ Directional steering is supported for GLM 5.3, not GLM 5.2. PNG and JPEG input works in the CLI, native agent, and HTTP server on Metal, single-GPU CUDA, and ROCm. The encoder must match the model. -V4.1 Flash vision is currently Metal-only; its setup is [above](#deepseek-v41-flash). +V4.1 Flash vision supports Metal and ROCm on Strix Halo; CUDA V4.1 vision is not implemented. Setup is [above](#deepseek-v41-flash). ### DeepSeek Flash Vision Experimental diff --git a/docs/STRIX_HALO.md b/docs/STRIX_HALO.md index 8055ac9d84..5c4faf35d5 100644 --- a/docs/STRIX_HALO.md +++ b/docs/STRIX_HALO.md @@ -33,21 +33,13 @@ the container. Do not mix header versions as a general workaround. ## GPU-visible memory -Check the memory pool reported by `rocminfo`. Some 128 GB configurations expose -only about 62 GB to the GPU, which is insufficient for resident Flash Q2 plus -runtime buffers. Firmware and kernel GTT/TTM settings control this limit. - -The native reference setup used these memory parameters: +Check the GPU-visible memory pool reported by `rocminfo`. Some 128 GB systems expose only about 62 GiB to the GPU. The tested 128 GB Fedora Linux Strix Halo system, running a recent kernel and ROCm 10.0, used these boot parameters: ```text -amdgpu.gttsize=126976 ttm.pages_limit=32505856 ttm.page_pool_size=32505856 +amd_iommu=off amdgpu.gttsize=126976 ttm.pages_limit=32505856 ``` -They are a system-specific starting point, not an allocation budget for -DwarfStar. Preserve existing boot options and consult your kernel's settings -before changing them. Keep RAM available for the OS even when the GPU can -address most of it. Do not disable the IOMMU merely to copy another host's -configuration; doing so changes device isolation. +The GTT/TTM settings expose about 124 GiB to the GPU. An SSD expert-cache request such as `92GB` is fitted to that GPU-visible limit as well as available system RAM; a stock ~62 GiB pool can therefore yield a much smaller cache. `amd_iommu=off` was part of the tested setup, but is not required for GTT sizing and disables DMA isolation. Keep RAM available for the OS. See the [host configuration guide](https://strix-halo-toolboxes.com/#config) for Fedora, Ubuntu/Debian, and systemd-boot instructions. ## Build and run Flash @@ -62,6 +54,71 @@ larger mixed and Q4 models have substantially higher memory requirements. Flash's ROCm resident and pipeline paths should not be confused with the GLM SSD-streaming path. +## DeepSeek V4.1 Flash + +- ROCm 10.0 supports calibrated V4.1 Flash Q2 text/vision, resident experts, SSD streaming and [two-machine TCP/RoCE](CLUSTERING_ROCM.md). Engram remains disk-backed in every mode. +- Tested SSD configuration: 128 GB Framework Desktop, 16-core Strix Halo engineering sample `100-000001243-50_Y`, Radeon `gfx1151`; Kingston FURY Renegade 2 TB (`SFYRD2000G`, PCIe 4.0 ×4, btrfs) holds the model. +- Linux `7.2.5-100.fc43.x86_64`, ROCm SDK `10.0.0-4` / HIP `7.15.26333`; TuneD **`accelerator-performance`**, fans at maximum speed. Existing boot flags: the [GTT/TTM settings above](#gpu-visible-memory), plus `pci=realloc pcie_aspm=off`; their individual effects were not isolated. + +### SSD performance + +Native `ds4-bench`, full fresh text prefix, greedy decoding, no DSpark or images; 92 GiB expert/staging cache. One run per row, startup and a separate GPU readiness warmup excluded; **tokens/s**: + +| Prompt tokens | Allocated context | Generated tokens | Prefill | Decode | +|---:|---:|---:|---:|---:| +| 16,384 | 69,632 | 512 | 301.23 | 9.73 | +| 65,536 | 69,632 | 128 | 350.03 | 9.08 | + +- All 129,280 frontier logits and complete printed continuations match the corresponding resident runs. Minimum usable RAM: 12.25 GiB; no OOM. Host zram swap-out pages in table order: 0, 0. No cold-cache claim; other qualification runs recorded nonzero host swap. +- The tuned Engram matrix path requires hipBLASLt 100401, revision `8d1ae90e`; other library versions retain the existing fallback and may have different prefill performance. +- Actual prompts reach 65,536 tokens; populated 256K was not tested. Cache admission depends on available RAM, context and sessions; images may need a smaller cache. The GPU-visible limit shares system RAM and is not a cache budget. +- Six resident image/state cases and two focused SSD cases (photo and screenshot) pass on this source. Official probability results are mixed; see [quality and limitations](../QA_BEFORE_RELEASES.md#deepseek-v41-flash-rocmgfx1151). No image-conditioned prefill timing is included. + +### Run text or vision + +```bash +make strix-halo ROCM_ARCH=gfx1151 +./download_model.sh ds41f-q2 +./download_model.sh ds41f-vision +MODEL=gguf/DeepSeek-V4.1-Flash-Q2.gguf +VISION=gguf/DeepSeek-V4.1-Flash-Vision.gguf + +# CLI, text +./ds4 --rocm -m "$MODEL" --ssd-streaming \ + --ssd-streaming-cache-experts 92GB --ctx 69632 + +# HTTP server, text and images; --vision takes the matching sidecar. +./ds4-server --rocm -m "$MODEL" --vision "$VISION" \ + --ssd-streaming --ssd-streaming-cache-experts 92GB --ctx 69632 \ + --batched-session 1 --host 127.0.0.1 --port 8080 +``` + +For a machine with sufficient RAM for resident experts, omit both SSD options. Keep `--vision` for image requests and set `--ctx` to the required allocation. See [image request examples](MODELS.md#vision). + +### Reproduce SSD measurements + +Run one configuration per process; preserve the CSV, full frontier files and printed output. The timing input is the repository's `speed-bench/promessi_sposi.txt`. + +```bash +tuned-adm active # Expect accelerator-performance during the workload +tuned-adm verify +MODEL=/absolute/path/DeepSeek-V4.1-Flash-Q2.gguf +DEPTH=16384 +ALLOC=69632 +GEN=512 +# Other row: DEPTH=65536 ALLOC=69632 GEN=128 + +DS4_METAL_CB_TIMES=1 ./ds4-bench --backend rocm -m "$MODEL" \ + --ssd-streaming --ssd-streaming-cache-experts 92GB \ + --prompt-file speed-bench/promessi_sposi.txt \ + --ctx-start "$DEPTH" --ctx-max "$DEPTH" --ctx-alloc "$ALLOC" \ + --gen-tokens "$GEN" --show-output --csv "ssd-$DEPTH-$GEN.csv" \ + --dump-frontier-logits-dir "ssd-frontiers-$DEPTH-$GEN" +``` + +- `DS4_METAL_CB_TIMES` is scoped to this command and prints the measured prefill time window on ROCm too. No tuning override is needed. +- Check the active power profile during the measurement; save revision/build flags, model filename/size and existing provenance, cache/KV configuration, actual prompt/output counts, and memory/swap/OOM counters. Do not substitute HTTP timings for this native table. + ## GLM 5.3 Flash The reference Q2 setup uses SSD streaming to leave room for its graph and KV diff --git a/download_model.sh b/download_model.sh index 0e39d055ac..d6a8ab9593 100755 --- a/download_model.sh +++ b/download_model.sh @@ -130,8 +130,10 @@ Targets: ds41f-q2 DeepSeek V4.1 Flash calibrated Q2, about 341 GiB on disk. Main weights - occupy 152 GiB; Engram tables stay on disk. Metal only: use SSD streaming + occupy 152 GiB; Engram tables stay on disk. Metal supports SSD streaming on one 128 GB Mac, tensor parallelism on two, or a larger resident Mac. + ROCm gfx1151 supports SSD streaming on a 128 GB Strix Halo or resident + inference with more GPU-visible memory; see docs/STRIX_HALO.md. ds41f-vision Matching V4.1 Flash vision encoder, about 0.9 GiB. Add --vision FILE diff --git a/ds4.c b/ds4.c index 7437625e01..03d6a4fa7a 100644 --- a/ds4.c +++ b/ds4.c @@ -47,7 +47,7 @@ #include "ds4_image.h" #include "ds4_engram.h" #include "ds4_tp.h" -#if !defined(DS4_NO_GPU) && !defined(DS4_ROCM_BUILD) +#if !defined(DS4_NO_GPU) #define DS4_HAS_DEEPSEEK41_GPU 1 #endif #if !defined(DS4_NO_GPU) && !defined(DS4_ROCM_BUILD) @@ -74,7 +74,7 @@ uint64_t ds4_test_mixed_native_count(void) { #endif /* TP context for the verify-block RDMA window (set with the gate callbacks). */ -#if !defined(DS4_NO_GPU) && defined(__APPLE__) +#if defined(DS4_HAS_DEEPSEEK41_GPU) && (defined(__APPLE__) || defined(DS4_ROCM_BUILD)) static ds4_tp *g_tp_block_ctx; #endif @@ -2755,6 +2755,14 @@ static void parse_tensors(ds4_model *m, ds4_cursor *c) { } } +#ifdef DS4_ROCM_BUILD +static int model_engram_table_index(const ds4_tensor *t) { + return ds4_streq(t->name, "blk.1.engram_embd.weight") ? 0 : + ds4_streq(t->name, "blk.14.engram_embd.weight") ? 1 : -1; +} + +#endif + /* Engram is deliberately outside the weight mapping, not merely absent from * a residency list. Startup warming and any future weight-view code must not * turn its 189 GiB of random-access rows into a resident model allocation. */ @@ -2795,6 +2803,28 @@ static void model_unmap_engram(ds4_model *m) { m->size = start; } +#ifdef DS4_ROCM_BUILD +/* The GGUF descriptors retain these validated file extents after their pages + * are unmapped. Accelerator startup must skip only these disk-only tables; + * every other tensor must still fit the addressable weight mapping. */ +static DS4_MAYBE_UNUSED bool model_tensor_is_disk_only_engram( + const ds4_model *m, const ds4_tensor *t) { + if (m->file_size <= m->size || t->abs_offset < m->size || + model_engram_table_index(t) < 0 || t->type != DS4_TENSOR_I8 || + t->ndim != 2 || t->dim[0] != DS4_ENGRAM_ROW_BYTES || + !t->dim[1] || t->dim[1] > UINT32_MAX || + t->bytes != t->dim[1] * DS4_ENGRAM_ROW_BYTES || + t->abs_offset > m->file_size || t->bytes > m->file_size - t->abs_offset) + return false; + ds4_str arch = {0}, encoding = {0}; + return model_get_string(m, "general.architecture", &arch) && + ds4_streq(arch, "deepseek41") && + model_get_string(m, "deepseek41.engram.encoding", &encoding) && + ds4_streq(encoding, "e4m3_e8m0_32_row264"); +} + +#endif + /* Like V4.1 Engram, the n-gram table must trail the resident weights. This * prevents warming or a future whole-model GPU view from faulting it in. */ static void model_unmap_qwen_ngrams(ds4_model *m, const char *path) { @@ -3344,7 +3374,6 @@ static uint64_t accelerator_cuda_preload_span_bytes(void) { return mb * 1048576ull; } -#ifndef DS4_ROCM_BUILD static bool accelerator_span_filter_contains(uint64_t off, uint64_t bytes, const uint64_t *span_offsets, @@ -3361,13 +3390,22 @@ static bool accelerator_span_filter_contains(uint64_t off, } return false; } -#endif +#ifdef DS4_ROCM_BUILD +static bool accelerator_prepare_model_tensor_spans_with_cache(const ds4_model *m, + const uint64_t *span_offsets, + const uint64_t *span_sizes, + uint32_t span_count, + uint64_t *prepared_out, + int (*cache_range)(const void *, uint64_t, + uint64_t, uint64_t, const char *)) { +#else static bool accelerator_prepare_model_tensor_spans(const ds4_model *m, const uint64_t *span_offsets, const uint64_t *span_sizes, uint32_t span_count, uint64_t *prepared_out) { +#endif uint64_t cap = m->n_tensors; if (cap == 0) { if (prepared_out) *prepared_out = 0; @@ -3387,10 +3425,14 @@ static bool accelerator_prepare_model_tensor_spans(const ds4_model *m, for (uint64_t i = 0; i < m->n_tensors; i++) { const ds4_tensor *t = &m->tensors[i]; if (t->bytes == 0) continue; +#ifdef DS4_ROCM_BUILD + if (model_tensor_is_disk_only_engram(m, t)) continue; +#else if (t == m->ngram_tensor) continue; if (DS4_MODEL_FAMILY == DS4_MODEL_FAMILY_DEEPSEEK41 && (ds4_streq(t->name, "blk.1.engram_embd.weight") || ds4_streq(t->name, "blk.14.engram_embd.weight"))) continue; +#endif if (t->abs_offset > m->size || t->bytes > m->size - t->abs_offset) { free(spans); return false; @@ -3400,6 +3442,13 @@ static bool accelerator_prepare_model_tensor_spans(const ds4_model *m, continue; } #endif +#ifdef DS4_ROCM_BUILD + if (!accelerator_span_filter_contains(t->abs_offset, t->bytes, + span_offsets, span_sizes, span_count)) continue; + spans[nspan++] = (accelerator_tensor_span){ + .off = t->abs_offset, .end = t->abs_offset + t->bytes, + }; +#else /* Expert sharding owns tensor slices, not necessarily whole tensors. */ for (uint32_t j = 0; j < (span_count ? span_count : 1u); j++) { uint64_t off = t->abs_offset, end = off + t->bytes; @@ -3422,6 +3471,7 @@ static bool accelerator_prepare_model_tensor_spans(const ds4_model *m, } spans[nspan++] = (accelerator_tensor_span){.off = off, .end = end}; } +#endif } if (nspan == 0) { free(spans); @@ -3463,7 +3513,11 @@ static bool accelerator_prepare_model_tensor_spans(const ds4_model *m, } char label[96]; snprintf(label, sizeof(label), "tensor-span:%" PRIu64, merged); +#ifdef DS4_ROCM_BUILD + if (cache_range(m->map, m->size, off, end - off, label) == 0) { +#else if (ds4_gpu_cache_model_range(m->map, m->size, off, end - off, label) == 0) { +#endif if (tty) fputc('\n', stderr); fprintf(stderr, "ds4: accelerator failed to prepare model tensor span %" PRIu64 @@ -3498,6 +3552,18 @@ static bool accelerator_prepare_model_tensor_spans(const ds4_model *m, return true; } +#ifdef DS4_ROCM_BUILD +static bool accelerator_prepare_model_tensor_spans(const ds4_model *m, + const uint64_t *span_offsets, + const uint64_t *span_sizes, + uint32_t span_count, + uint64_t *prepared_out) { + return accelerator_prepare_model_tensor_spans_with_cache(m, span_offsets, + span_sizes, span_count, prepared_out, ds4_gpu_cache_model_range); +} + +#endif + #ifndef DS4_ROCM_BUILD static bool accelerator_cache_q8_tensors(const ds4_model *m, const uint64_t *span_offsets, @@ -3528,7 +3594,11 @@ static bool accelerator_cache_model_tensors(ds4_backend backend, const ds4_model *m, const uint64_t *span_offsets, const uint64_t *span_sizes, - uint32_t span_count) { + uint32_t span_count +#ifdef DS4_ROCM_BUILD + , bool exact_resident +#endif + ) { if (backend != DS4_BACKEND_CUDA) return true; if (!m || !m->map || m->size == 0) return false; #ifndef DS4_ROCM_BUILD @@ -3539,9 +3609,23 @@ static bool accelerator_cache_model_tensors(ds4_backend backend, const double t0 = now_sec(); uint64_t prepared = 0; +#ifdef DS4_ROCM_BUILD + const bool prepared_ok = exact_resident ? + accelerator_prepare_model_tensor_spans_with_cache(m, span_offsets, span_sizes, + span_count, &prepared, ds4_gpu_cache_model_range_exact) : + accelerator_prepare_model_tensor_spans(m, span_offsets, span_sizes, span_count, &prepared); +#else if (!accelerator_prepare_model_tensor_spans(m, span_offsets, span_sizes, span_count, &prepared)) { return false; } +#endif +#ifdef DS4_ROCM_BUILD + if (!prepared_ok) return false; +#endif +#ifdef DS4_ROCM_BUILD + if (exact_resident && !ds4_gpu_release_model_upload_staging(m->map, m->size)) + return false; +#endif #ifndef DS4_ROCM_BUILD if (!m->ngram_tensor && !accelerator_cache_q8_tensors(m, span_offsets, span_sizes, span_count)) return false; #endif @@ -3564,12 +3648,19 @@ static bool accelerator_cache_model_tensors(ds4_backend backend, const ds4_model *m, const uint64_t *span_offsets, const uint64_t *span_sizes, - uint32_t span_count) { + uint32_t span_count +#ifdef DS4_ROCM_BUILD + , bool exact_resident +#endif + ) { (void)backend; (void)m; (void)span_offsets; (void)span_sizes; (void)span_count; +#ifdef DS4_ROCM_BUILD + (void)exact_resident; +#endif return true; } #endif @@ -40083,6 +40174,15 @@ bool ds4_tokens_starts_with(const ds4_tokens *tokens, const ds4_tokens *prefix) */ #define DS41_PREFILL_CAP 8192u #define DS41_INDEX_BATCH 32u +#ifdef DS4_ROCM_BUILD +/* Linux readers publish into host staging; only the consumed chunk is uploaded. */ +#define DS41_ENGRAM_GPU_PREFETCH_ROWS(g) 0u +/* The ROCm packed-index capability is false; no dispatch consumes this view. */ +#define DS41_INDEX_PACKED_WORDS(g) 0u +#else +#define DS41_ENGRAM_GPU_PREFETCH_ROWS(g) (g->carry_cap ? g->carry_cap : g->prefill_cap) +#define DS41_INDEX_PACKED_WORDS(g) ds4_gpu_dsv41_indexer_packed_bytes(g->ctx, g->prefill_cap) / 4u +#endif #define DS41_CARRY_ROWS(X) \ X(residual, DS4_N_HC * DS4_N_EMBD, DS4_V41_CARRY_BF16) \ X(pre, DS4_N_HC, DS4_V41_CARRY_F32) X(ffn_split, 24, DS4_V41_CARRY_F32) \ @@ -40123,12 +40223,25 @@ typedef struct { #undef DS41_ROW_FIELD } ds41_prefill_row; +#ifdef DS4_ROCM_BUILD +static uint32_t ds41_prefill_logical_limit(uint32_t ctx) { +#else static uint32_t ds41_prefill_limit(uint32_t ctx) { +#endif const uint32_t limit = ctx < 8192u || getenv("DS4_METAL_DISABLE_V41_WIDE_CHUNK") ? 2048u : (ctx < 16384u || getenv("DS4_METAL_DISABLE_V41_8K_CHUNK")) ? 4096u : DS41_PREFILL_CAP; return ctx < limit ? ctx : limit; } +#ifdef DS4_ROCM_BUILD +static uint32_t ds41_prefill_limit(uint32_t ctx) { + const uint32_t limit = ds41_prefill_logical_limit(ctx); + /* Bound ROCm workspace memory independently of the causal sweep. + * Full-context admission still includes model, carry and runtime storage. */ + return limit < 2048u ? limit : 2048u; +} +#endif + static uint32_t ds41_carry_words(uint32_t width, uint32_t format, bool compact) { if (!compact || format == DS4_V41_CARRY_F32) return width; return format == DS4_V41_CARRY_BF16 ? (width + 1u) / 2u : (width + 31u) / 32u; @@ -40142,13 +40255,25 @@ static uint32_t ds41_carry_cap(uint32_t ctx) { uint64_t cap = (UINT64_C(3) << 30) / row_bytes; if (cap > 32768u) cap = 32768u; if (cap > ctx) cap = ctx; +#ifdef DS4_ROCM_BUILD + const uint32_t chunk = ds41_prefill_logical_limit(ctx); +#else const uint32_t chunk = ds41_prefill_limit(ctx); +#endif if (!chunk) return 0; /* Keep the causal sweep boundary independent of the encoder tile size. */ cap -= cap % 2048u; return cap > chunk ? (uint32_t)cap : 0; } +#ifdef DS4_ROCM_BUILD +static uint32_t ds41_engram_host_capacity(uint32_t prefill, uint32_t carry) { + const uint32_t rows = carry ? carry : prefill; + /* The disabled pipeline joins before consuming: retain its full buffer. */ + return getenv("DS4_METAL_DISABLE_V41_ENGRAM_PIPELINE") || rows <= 4096u ? rows : 4096u; +} +#endif + #define DS41_SCRATCH(X) \ X(image_text_mask, (g->prefill_cap + 3u) / 4u) \ X(residual, DS4_N_HC * DS4_N_EMBD) \ @@ -40163,7 +40288,7 @@ static uint32_t ds41_carry_cap(uint32_t ctx) { X(index_q, DS4_N_INDEXER_HEAD * DS4_N_INDEXER_HEAD_DIM) \ X(index_k, DS4_N_INDEXER_HEAD_DIM) X(index_weights, DS4_N_INDEXER_HEAD) \ X(index_scores, DS41_INDEX_BATCH * g->ctx) X(selected_comp, DS4_N_INDEXER_TOP_K) \ - X(index_packed, ds4_gpu_dsv41_indexer_packed_bytes(g->ctx, g->prefill_cap) / 4u) \ + X(index_packed, DS41_INDEX_PACKED_WORDS(g)) \ X(selected_kv, DS4_N_INDEXER_TOP_K * DS4_N_HEAD_DIM) \ X(block_scores, (g->ctx + 7u) / 8u) X(block_selected, 2048) \ X(block_mask, (g->ctx + 7u) / 8u) \ @@ -40176,7 +40301,7 @@ static uint32_t ds41_carry_cap(uint32_t ctx) { X(shared_gate, DS4_N_FF_EXP) X(shared_up, DS4_N_FF_EXP) \ X(shared_mid, DS4_N_FF_EXP) X(shared, DS4_N_EMBD) \ X(engram_rows, DS4_ENGRAM_COLS * DS4_ENGRAM_DIM) \ - X(engram_prefetch, (g->carry_cap ? g->carry_cap : g->prefill_cap) * DS4_ENGRAM_COLS * DS4_ENGRAM_DIM) \ + X(engram_prefetch, DS41_ENGRAM_GPU_PREFETCH_ROWS(g) * DS4_ENGRAM_COLS * DS4_ENGRAM_DIM) \ X(engram_kv, (DS4_N_HC + 1u) * DS4_N_EMBD) X(logits, DS4_N_VOCAB) typedef struct { @@ -40193,6 +40318,13 @@ typedef struct { ds4_engram_history history; uint32_t *token_map; uint32_t (*prefill_ids)[2][DS4_ENGRAM_COLS]; +#ifdef DS4_ROCM_BUILD + ds4_gpu_dsv41_hc_plan *hc_sgemm; + float *host_engram_rows; + uint32_t host_engram_capacity; + int32_t *host_selected_comp; + uint8_t *host_text_mask; +#endif ds4_engram_table table[2]; float rows[2][DS4_ENGRAM_COLS * DS4_ENGRAM_DIM]; ds4_gpu_tensor *window[40]; @@ -40217,6 +40349,10 @@ static bool ds41_read_array(const ds4_model *m, const char *key, uint32_t type, static void ds41_graph_free(ds41_gpu_graph *g) { if (!g) return; +#ifdef DS4_ROCM_BUILD + ds4_gpu_dsv41_hc_plan_free(g->hc_sgemm); + g->hc_sgemm = NULL; +#endif #if !defined(__APPLE__) && !defined(DS4_ROCM_BUILD) ds4_gpu_decode_graphs_invalidate(); #endif @@ -40250,6 +40386,11 @@ static void ds41_graph_free(ds41_gpu_graph *g) { free(g->token_map); free(g->prefill_ids); free(g->rows_view); +#ifdef DS4_ROCM_BUILD + free(g->host_engram_rows); + free(g->host_selected_comp); + free(g->host_text_mask); +#endif ds4_gpu_tensor_free(g->prefill_tokens); memset(g, 0, sizeof(*g)); g->table[0].fd = g->table[1].fd = -1; @@ -40280,11 +40421,23 @@ static uint64_t ds41_graph_bytes(uint32_t ctx) { floats += (uint64_t)g->prefill_cap * 512u; if (g->carry_cap > g->prefill_cap) floats += (uint64_t)(g->carry_cap - g->prefill_cap) * 2u * DS4_ENGRAM_COLS; +#ifdef DS4_ROCM_BUILD + floats += (uint64_t)ds41_engram_host_capacity(g->prefill_cap, g->carry_cap) * + DS4_ENGRAM_COLS * DS4_ENGRAM_DIM; + floats += (uint64_t)g->prefill_cap * DS4_N_INDEXER_TOP_K + + (g->prefill_cap + 3u) / 4u; +#endif +#ifdef DS4_ROCM_BUILD + /* ROCm reuses its runtime scratch pool, covered by the separate 2 GiB + * runtime/I/O allowance. It owns neither Metal workspace below. */ + const uint64_t packed = 0, sort = 0; +#else /* Expert-major matrix kernels use one bounded packed activation buffer. */ const uint64_t packed = g->prefill_cap >= 512 ? (g->prefill_cap > 4096u ? UINT64_C(512) : UINT64_C(256)) * 1024 * 1024 : 0; /* The index sorter owns two full-width merge buffers outside the graph. */ const uint64_t sort = (uint64_t)DS41_INDEX_BATCH * ctx * 2u * sizeof(uint32_t); +#endif return floats * sizeof(float) + sizeof(*g) + (uint64_t)DS4_N_VOCAB * 4u + packed + sort; } @@ -40326,6 +40479,15 @@ static DS4_MAYBE_UNUSED bool ds41_graph_alloc(ds41_gpu_graph *g, const ds4_model g->prefill_ids = malloc((size_t)(g->carry_cap ? g->carry_cap : g->prefill_cap) * sizeof(*g->prefill_ids)); g->rows_view = calloc(g->prefill_cap, sizeof(*g->rows_view)); +#ifdef DS4_ROCM_BUILD + g->host_engram_capacity = ds41_engram_host_capacity(g->prefill_cap, g->carry_cap); + g->host_engram_rows = malloc((size_t)g->host_engram_capacity * + DS4_ENGRAM_COLS * DS4_ENGRAM_DIM * sizeof(float)); + g->host_selected_comp = malloc((size_t)g->prefill_cap * DS4_N_INDEXER_TOP_K * sizeof(int32_t)); + g->host_text_mask = malloc(g->prefill_cap); + if (!g->host_engram_rows || !g->host_selected_comp || !g->host_text_mask) goto fail; + memset(g->host_selected_comp, 0xff, (size_t)g->prefill_cap * DS4_N_INDEXER_TOP_K * sizeof(int32_t)); +#endif g->prefill_tokens = ds4_gpu_tensor_alloc((uint64_t)g->prefill_cap * sizeof(int32_t)); if (!g->token_map || !g->prefill_ids || !g->rows_view || !g->prefill_tokens) goto fail; g->engram.token_map = g->token_map; @@ -40368,16 +40530,26 @@ static DS4_MAYBE_UNUSED bool ds41_graph_alloc(ds41_gpu_graph *g, const ds4_model g->previous_score[i] = ds4_gpu_tensor_alloc(512u * sizeof(float)); if (!g->compressed[i] || !g->index_cache[i] || !g->previous_kv[i] || !g->previous_score[i]) goto fail; } +#ifdef DS4_ROCM_BUILD +#define DS41_ALLOC(name, count) \ + if (!(g->name = ds4_gpu_tensor_alloc((uint64_t)(count) * sizeof(float)))) goto fail; +#else #define DS41_ALLOC(name, count) \ if (!(g->name = (!strcmp(#name, "engram_prefetch") || !strcmp(#name, "image_text_mask") ? \ ds4_gpu_tensor_alloc_managed : ds4_gpu_tensor_alloc)( \ (uint64_t)((count) != 0 ? (count) : 1u) * sizeof(float)))) goto fail; +#endif DS41_SCRATCH(DS41_ALLOC) #undef DS41_ALLOC +#ifdef DS4_ROCM_BUILD +#define DS41_BATCH_ALLOC(name, count) \ + if (!(g->batch.name = ds4_gpu_tensor_alloc((uint64_t)(count) * g->prefill_cap * 4u))) goto fail; +#else #define DS41_BATCH_ALLOC(name, count) \ if (!(g->batch.name = (!strcmp(#name, "engram_rows") || !strcmp(#name, "selected_comp") ? \ ds4_gpu_tensor_alloc_managed : ds4_gpu_tensor_alloc)( \ (uint64_t)(count) * g->prefill_cap * 4u))) goto fail; +#endif DS41_PREFILL_STORAGE(DS41_BATCH_ALLOC) /* Engram projection ends before HC normalization. HC normalization * precedes Q projection or follows attention; routed expert outputs are @@ -40432,6 +40604,16 @@ static bool ds41_bf16(ds4_gpu_tensor *x, uint32_t width) { static bool ds41_matmul(ds4_gpu_tensor *out, const ds4_model *m, const ds4_tensor *weight, const ds4_gpu_tensor *in, bool round) { +#ifdef DS4_ROCM_BUILD + if (weight->type == DS4_TENSOR_F16 || weight->type == DS4_TENSOR_Q8_0) { + const bool ok = weight->type == DS4_TENSOR_F16 ? + ds4_gpu_dsv41_projection_rows(out, m->map, m->size, weight->abs_offset, + (uint32_t)weight->dim[0], (uint32_t)weight->dim[1], 1u, in) : + ds4_gpu_dsv41_q8_projection_rows(out, m->map, m->size, weight->abs_offset, + (uint32_t)weight->dim[0], (uint32_t)weight->dim[1], 1u, in); + return ok && (!round || ds41_bf16(out, (uint32_t)weight->dim[1])); + } +#endif return metal_graph_matmul_plain_tensor(out, m, weight, weight->dim[0], weight->dim[1], in, 1) && (!round || ds41_bf16(out, (uint32_t)weight->dim[1])); } @@ -40441,10 +40623,22 @@ static bool ds41_matmul_batch(ds4_gpu_tensor *out, const ds4_model *m, uint32_t count, bool round) { const uint32_t width = (uint32_t)weight->dim[0], outputs = (uint32_t)weight->dim[1]; bool ok; +#ifdef DS4_ROCM_BUILD + /* Full-tile Q8 WMMA uses F16 operands internally. Preserve the graph's + * requested output rounding after either projection. */ + if (weight->type == DS4_TENSOR_F16 || weight->type == DS4_TENSOR_Q8_0) { + ok = weight->type == DS4_TENSOR_F16 ? + ds4_gpu_dsv41_projection_rows(out, m->map, m->size, + weight->abs_offset, width, outputs, count, in) : + ds4_gpu_dsv41_q8_projection_rows(out, m->map, m->size, + weight->abs_offset, width, outputs, count, in); + return ok && (!round || ds4_gpu_dsv41_quantize(out, outputs, count, DS4_V41_BF16)); + } +#endif /* Small decode batches retain scalar reductions before BF16 and sparse * routing boundaries. Preserve Metal's separate vocabulary-head dispatch. */ if (count >= 2 && count <= DS4_TP_BATCH_MAX_ROWS && -#ifdef __APPLE__ +#if defined(__APPLE__) || defined(DS4_ROCM_BUILD) outputs != DS4_N_VOCAB && #endif weight->type == DS4_TENSOR_Q8_0) { @@ -40553,7 +40747,11 @@ static bool ds41_sum_partial(ds41_gpu_graph *g, ds4_gpu_tensor *x, !ds4_gpu_tp_gate_encode(il, gate)) return false; ds4_gpu_tensor *first = g->tp_rank ? g->tp_in[slot] : g->tp_out[slot]; ds4_gpu_tensor *second = g->tp_rank ? g->tp_out[slot] : g->tp_in[slot]; +#ifdef DS4_ROCM_BUILD + return ds4_gpu_tp_add_tensor(x, first, second, DS4_N_EMBD) != 0; +#else return ds4_gpu_add_tensor(x, first, second, DS4_N_EMBD) != 0; +#endif } static bool ds41_norm(ds4_gpu_tensor *out, const ds4_gpu_tensor *in, @@ -40571,10 +40769,21 @@ static bool ds41_sum_partial_batch(ds41_gpu_graph *g, ds4_gpu_tensor *x, ds4_gpu_tensor *peer = g->batch.q; const uint64_t bytes = (uint64_t)count * DS4_N_EMBD * sizeof(float); if (!ds4_gpu_tp_big_gate_encode(il, count, x, peer, bytes)) return false; +#ifdef DS4_ROCM_BUILD + return ds4_gpu_tp_add_tensor(x, g->tp_rank ? peer : x, + g->tp_rank ? x : peer, count * DS4_N_EMBD) != 0; +#else return ds4_gpu_add_tensor(x, g->tp_rank ? peer : x, g->tp_rank ? x : peer, count * DS4_N_EMBD) != 0; +#endif } +#ifdef DS4_ROCM_BUILD +static bool ds41_tp_failed(const ds41_gpu_graph *g) { + return g->tp_world == 2u && ds4_gpu_tp_failed(); +} +#endif + static bool ds41_rope(ds4_gpu_tensor *x, uint32_t heads, uint32_t width, uint32_t il, uint32_t pos, bool inverse) { return ds4_gpu_dsv41_rope(x, width, heads, 1, pos, @@ -40608,6 +40817,20 @@ static bool ds41_attention_low(ds41_gpu_graph *g, const ds4_model *m, static bool ds41_attention_output(ds41_gpu_graph *g, const ds4_model *m, const ds4_layer_weights *l) { +#ifdef DS4_ROCM_BUILD + if (g->tp_world == 2u) + return l->attn_output_a->type == DS4_TENSOR_Q8_0 && + l->attn_output_b->type == DS4_TENSOR_Q8_0 && + ds4_gpu_dsv41_attention_output_tp_batch(g->block, g->low, m->map, m->size, + l->attn_output_a->abs_offset, l->attn_output_b->abs_offset, + g->heads, 1u, g->tp_rank); + if (l->attn_output_b->type == DS4_TENSOR_Q8_0) + return + ds4_gpu_dsv41_attention_output_batch(g->block, g->low, m->map, m->size, + l->attn_output_a->abs_offset, l->attn_output_b->abs_offset, g->heads, 1u); + return g->tp_world == 1u && ds41_attention_low(g, m, l) && + ds41_matmul(g->block, m, l->attn_output_b, g->low, false); +#else const uint32_t groups = DS4_N_OUT_GROUP / g->tp_world; if (!ds41_attention_low(g, m, l)) return false; return g->tp_world == 2 ? @@ -40615,6 +40838,7 @@ static bool ds41_attention_output(ds41_gpu_graph *g, const ds4_model *m, 8192, (uint64_t)g->tp_rank * groups * 1024u, (uint64_t)groups * 1024u, DS4_N_EMBD, g->low, 0) : ds41_matmul(g->block, m, l->attn_output_b, g->low, false); +#endif } static bool ds41_attention_publish(ds41_gpu_graph *g, const ds4_model *m, @@ -40653,6 +40877,13 @@ static bool ds41_attention_candidates(ds41_gpu_graph *g, uint32_t il) { if (n_comp && ds41_index_source(il)) { if (il == 20) { const uint32_t blocks = (n_comp + 7u) / 8u, top = blocks < 2048u ? blocks : 2048u; +#ifdef DS4_ROCM_BUILD + /* Selecting every block publishes an all-zero mask. Scratch scores + * and ordered block IDs have no readers after this mask is built. */ + if (blocks <= 2048u && !getenv("DS4_METAL_DISABLE_V41_BATCH_TOPK")) { + if (!ds4_gpu_tensor_fill_f32(g->block_mask, 0.0f, blocks)) return false; + } else +#endif if (!ds4_gpu_dsv41_candidate_blocks(g->block_scores, g->index_scores, n_comp, 1, pos, ratio) || !ds4_gpu_indexer_topk_tensor(g->block_selected, g->block_scores, blocks, 1, top) || !ds4_gpu_dsv4_topk_mask_tensor(g->block_mask, g->block_selected, blocks, 1, top)) return false; @@ -40666,8 +40897,21 @@ static bool ds41_attention_pick(ds41_gpu_graph *g, uint32_t il) { const uint32_t ratio = ds4_layer_compress_ratio(il); const uint32_t n_comp = ratio ? (g->pos + 1u) / ratio : 0; const uint32_t top = n_comp < DS4_N_INDEXER_TOP_K ? n_comp : DS4_N_INDEXER_TOP_K; +#ifndef DS4_ROCM_BUILD return ds41_attention_candidates(g, il) && (!n_comp || !ds41_index_source(il) || ds4_gpu_indexer_topk_tensor(g->selected_comp, g->index_scores, n_comp, 1, top)); +#else + if (!ds41_attention_candidates(g, il)) return false; + if (!n_comp || !ds41_index_source(il)) return true; +#ifdef DS4_ROCM_BUILD + if (n_comp > 1u && n_comp < DS4_N_INDEXER_TOP_K && + !getenv("DS4_METAL_DISABLE_V41_BATCH_TOPK")) { + return ds4_gpu_dsv41_indexer_topk_batch(g->selected_comp, g->index_scores, + n_comp, 1u, g->pos, ratio); + } +#endif + return ds4_gpu_indexer_topk_tensor(g->selected_comp, g->index_scores, n_comp, 1, top); +#endif } static bool ds41_attention_select_published(ds41_gpu_graph *g, const ds4_model *m, @@ -40680,7 +40924,10 @@ static bool ds41_attention_select_published(ds41_gpu_graph *g, const ds4_model * !ds41_rope(g->index_q, DS4_N_INDEXER_HEAD, 128, il, pos, false) || !ds4_gpu_dsv41_quantize(g->index_q, 128, DS4_N_INDEXER_HEAD, DS4_V41_FP4_E8M0) || !ds41_matmul(g->index_weights, m, l->indexer_proj, g->norm, true) || -#ifdef __APPLE__ +#if defined(DS4_ROCM_BUILD) + !ds4_gpu_dsv41_indexer_scores_one(g->index_scores, g->index_q, g->index_weights, + g->index_cache[owner], n_comp)) return false; +#elif defined(__APPLE__) !ds4_gpu_glm_indexer_score_one_tensor(g->index_scores, g->index_q, g->index_weights, g->index_cache[owner], n_comp, DS4_N_INDEXER_HEAD, 128, 1.0f / 64.0f, false)) return false; #else @@ -40739,6 +40986,45 @@ static bool ds41_attention(ds41_gpu_graph *g, const ds4_model *m, ds41_bf16(g->block, DS4_N_EMBD); } +#ifdef DS4_ROCM_BUILD +/* The first ROCm streaming path uses one uniform Q2 expert size class. */ +static bool ds41_stream_table(const ds4_model *m, const ds4_layer_weights *l, + uint32_t il, ds4_gpu_stream_expert_table *table) { + if (!m || !l || !table || il >= DS4_N_LAYER || + DS4_MODEL_FAMILY != DS4_MODEL_FAMILY_DEEPSEEK41 || + DS4_N_EXPERT != 384u || DS4_N_EXPERT_USED != 6u || + !l->ffn_gate_exps || !l->ffn_up_exps || !l->ffn_down_exps) return false; + const ds4_tensor *t[] = {l->ffn_gate_exps, l->ffn_up_exps, l->ffn_down_exps}; + for (uint32_t i = 0; i < 3; i++) { + uint64_t bytes = 0; + if (t[i]->type != (i == 2 ? DS4_TENSOR_Q2_K : DS4_TENSOR_IQ2_XXS) || + t[i]->ndim != 3 || t[i]->dim[0] != (i == 2 ? DS4_N_FF_EXP : DS4_N_EMBD) || + t[i]->dim[1] != (i == 2 ? DS4_N_EMBD : DS4_N_FF_EXP) || + t[i]->dim[2] != DS4_N_EXPERT || + t[i]->elements != (uint64_t)DS4_N_EMBD * DS4_N_FF_EXP * DS4_N_EXPERT || + !tensor_nbytes(t[i]->type, t[i]->elements, &bytes) || + bytes != t[i]->bytes || + t[i]->abs_offset > m->size || bytes > m->size - t[i]->abs_offset) return false; + } + uint64_t gate = 0, down = 0; + if (!streaming_layer_gate_down_expert_bytes(l, &gate, &down)) return false; + *table = graph_stream_expert_table_make(m, l, il, gate, down); + return true; +} + +static bool ds41_stream_selected_begin(ds41_gpu_graph *g, const ds4_model *m, + const ds4_layer_weights *l, uint32_t il) { + ds4_gpu_stream_expert_table table; + int32_t selected[6]; + if (!ds41_stream_table(m, l, il, &table) || + !ds4_gpu_tensor_read(g->selected, 0, selected, sizeof(selected))) return false; + for (uint32_t i = 0; i < 6; i++) + if (selected[i] < 0 || (uint32_t)selected[i] >= DS4_N_EXPERT) return false; + return ds4_gpu_routed_moe_set_selected_override(selected, 6) && + ds4_gpu_stream_expert_cache_begin_selected_load(&table, selected, 6); +} +#endif + static bool ds41_moe_partial(ds41_gpu_graph *g, const ds4_model *m, const ds4_layer_weights *l, uint32_t il, uint32_t token) { uint64_t gate_row = 0, down_row = 0; @@ -40754,6 +41040,11 @@ static bool ds41_moe_partial(ds41_gpu_graph *g, const ds4_model *m, m->map, m->size, bias->abs_offset, 0, 0, token, DS4_N_EXPERT, DS4_N_EXPERT_USED, DS4_EXPERT_WEIGHT_SCALE, 0, 0, true, false, g->route_logits)) return false; +#ifdef DS4_ROCM_BUILD + /* Start expert I/O before enqueuing the independent shared expert. The + * routed consumer joins the matching compact table and upload event. */ + if (g->streaming && !ds41_stream_selected_begin(g, m, l, il)) return false; +#endif const bool shared_here = !shared_owner || g->tp_rank == (il & 1u); bool shared_queued = false; #if !defined(__APPLE__) && !defined(DS4_ROCM_BUILD) @@ -40778,7 +41069,14 @@ static bool ds41_moe_partial(ds41_gpu_graph *g, const ds4_model *m, !ds41_bf16(g->shared_mid, DS4_N_FF_EXP) || !ds41_matmul(g->shared, m, l->ffn_down_shexp, g->shared_mid, true))) return false; bool routed_ok; -#ifndef __APPLE__ +#ifdef DS4_ROCM_BUILD + if (g->tp_world == 2u) { + routed_ok = ds4_gpu_dsv41_routed_moe_tp_tensor(routed, g->gate, g->up, g->mid, g->experts, + m->map, m->size, l->ffn_gate_exps->abs_offset, l->ffn_up_exps->abs_offset, + l->ffn_down_exps->abs_offset, g->selected, g->route_weights, + g->norm, 1u, g->tp_rank); + } else +#elif !defined(__APPLE__) if (g->tp_world == 2) { routed_ok = ds4_gpu_routed_moe_batch_owned_tensor(routed, g->gate, g->up, g->mid, g->experts, m->map, m->size, l->ffn_gate_exps->abs_offset, l->ffn_up_exps->abs_offset, @@ -40869,15 +41167,36 @@ static bool ds41_norm_batch(ds4_gpu_tensor *out, const ds4_gpu_tensor *in, ds4_gpu_dsv41_quantize(out, (uint32_t)weight->dim[0], count, DS4_V41_BF16); } -static bool ds41_hc_mix_batch(ds41_prefill_row *b, const ds4_model *m, +static bool ds41_hc_mix_batch( +#ifdef DS4_ROCM_BUILD + ds41_gpu_graph *g, +#endif + ds41_prefill_row *b, const ds4_model *m, const ds4_layer_weights *l, bool ffn, uint32_t count) { const ds4_tensor *fn = ffn ? l->hc_ffn_fn : l->hc_attn_fn; const ds4_tensor *scale = ffn ? l->hc_ffn_scale : l->hc_attn_scale; const ds4_tensor *base = ffn ? l->hc_ffn_base : l->hc_attn_base; const ds4_gpu_tensor *input = ffn ? b->after_attn : b->residual; +#ifdef DS4_ROCM_BUILD + bool projected; + if (fn->type == DS4_TENSOR_F16 && fn->dim[0] == 20480u && fn->dim[1] == 24u && count == 2048u) { + /* Preserve the existing RMS input and feed F32 mix directly to Sinkhorn. */ + if (!ds4_gpu_rms_norm_plain_rows_tensor(b->flat_norm, input, + DS4_N_HC * DS4_N_EMBD, count, DS4_RMS_EPS)) return false; + const int result = ds4_gpu_dsv41_hc_project(&g->hc_sgemm, + b->mix, m->map, m->size, fn->abs_offset, count, + b->flat_norm, g->batch.heads); + if (result < 0) return false; + projected = result > 0 || ds41_matmul_batch(b->mix, m, fn, b->flat_norm, count, false); + } else + projected = fn->type == DS4_TENSOR_F16 && count > DS4_TP_BATCH_MAX_ROWS ? +#else const bool projected = -#ifdef __APPLE__ +#endif +#if defined(__APPLE__) || defined(DS4_ROCM_BUILD) +#ifndef DS4_ROCM_BUILD fn->type == DS4_TENSOR_F16 && count > DS4_TP_BATCH_MAX_ROWS ? +#endif ds4_gpu_hc_rms_scale_project_f16_tensor(b->mix, b->flat_norm, m->map, m->size, fn->abs_offset, DS4_N_HC * DS4_N_EMBD, 24u, input, count, DS4_RMS_EPS) : @@ -40902,7 +41221,11 @@ static bool ds41_before_attention_batch(ds41_gpu_graph *g, ds41_prefill_row *b, DS4_N_EMBD, count, DS4_RMS_EPS)) return false; } - if (!ds41_hc_mix_batch(b, m, l, false, count)) return false; + if (!ds41_hc_mix_batch( +#ifdef DS4_ROCM_BUILD + g, +#endif + b, m, l, false, count)) return false; /* V4.1 consumes the preceding sublayer's mixer, not the newly computed one. */ const bool mixed = il ? ds4_gpu_hc_weighted_sum_split_tensor(b->x, b->residual, b->ffn_split, DS4_N_EMBD, DS4_N_HC) : @@ -40911,12 +41234,20 @@ static bool ds41_before_attention_batch(ds41_gpu_graph *g, ds41_prefill_row *b, ds41_norm_batch(b->norm, b->x, m, l->attn_norm, count); } -static bool ds41_after_attention_batch(ds41_prefill_row *b, const ds4_model *m, +static bool ds41_after_attention_batch( +#ifdef DS4_ROCM_BUILD + ds41_gpu_graph *g, +#endif + ds41_prefill_row *b, const ds4_model *m, const ds4_layer_weights *l, uint32_t count) { return ds4_gpu_hc_expand_split_tensor(b->after_attn, b->block, b->residual, b->attn_split, DS4_N_EMBD, DS4_N_HC) && ds4_gpu_dsv41_quantize(b->after_attn, DS4_N_EMBD * DS4_N_HC, count, DS4_V41_BF16) && - ds41_hc_mix_batch(b, m, l, true, count) && + ds41_hc_mix_batch( +#ifdef DS4_ROCM_BUILD + g, +#endif + b, m, l, true, count) && ds4_gpu_hc_weighted_sum_split_tensor(b->x, b->after_attn, b->attn_split, DS4_N_EMBD, DS4_N_HC) && ds4_gpu_dsv41_quantize(b->x, DS4_N_EMBD, count, DS4_V41_BF16) && ds41_norm_batch(b->norm, b->x, m, l->ffn_norm, count); @@ -41022,7 +41353,10 @@ static bool ds41_index_batch(ds41_gpu_graph *g, const ds4_model *m, q, weights, g->index_cache[owner], n_comp, rows, start + off, ratio)); ds4_gpu_tensor_free(weights); ds4_gpu_tensor_free(q); - const bool batch_topk = (start + off + 1u) / ratio >= 1024u && + const bool batch_topk = +#ifndef DS4_ROCM_BUILD + (start + off + 1u) / ratio >= 1024u && +#endif !getenv("DS4_METAL_DISABLE_V41_BATCH_TOPK"); for (uint32_t t = 0; ok && (!batch_topk || il >= 20u) && t < rows; t++) { row.pos = start + off + t; @@ -41073,7 +41407,11 @@ static bool ds41_attention_batch(ds41_gpu_graph *g, const ds4_model *m, ds41_gpu_graph row = *g; const bool batch_index = ds41_index_source(il) && !getenv("DS4_METAL_DISABLE_V41_BATCH_INDEX"); - const bool batch_publish = ds41_kv_source(il) && + const bool batch_publish = +#ifdef DS4_ROCM_BUILD + ratio == 2u && +#endif + ds41_kv_source(il) && #ifdef __APPLE__ ratio == 2u && #endif @@ -41221,6 +41559,42 @@ static bool ds41_route_batch(ds41_gpu_graph *g, const ds4_model *m, return true; } +#ifdef DS4_ROCM_BUILD +/* The CED encoder already replicates shared experts. Only defer that work; + * keep the routed rank0+rank1 reduction and final routed+shared addition. */ +static bool ds41_moe_batch_overlap(ds41_gpu_graph *g, const ds4_model *m, + const ds4_layer_weights *l, uint32_t il, + uint32_t count) { + ds41_prefill_row *b = &g->batch; + ds4_gpu_tensor *peer = b->q; + const uint64_t bytes = (uint64_t)count * DS4_N_EMBD * sizeof(float); + if (!ds41_matmul_batch(b->route_logits, m, l->ffn_gate_inp, b->norm, count, false) || + !ds41_route_batch(g, m, l, count) || + !ds4_gpu_dsv41_routed_moe_tp_tensor(b->routed, b->gate, b->up, b->mid, b->experts, + m->map, m->size, l->ffn_gate_exps->abs_offset, l->ffn_up_exps->abs_offset, + l->ffn_down_exps->abs_offset, b->selected, b->route_weights, + b->norm, count, g->tp_rank)) return false; + if (!ds4_gpu_tp_big_gate_begin(il, count, b->routed, peer, bytes)) { + ds4_gpu_tp_big_gate_abort(); + return false; + } + /* low/x hold shared scratch/output; block holds the routed partial and + * q is dead until the receive. norm survives both expert computations. */ + const bool ok = + ds41_matmul_batch(b->shared_gate, m, l->ffn_gate_shexp, b->norm, count, true) && + ds41_matmul_batch(b->shared_up, m, l->ffn_up_shexp, b->norm, count, true) && + ds4_gpu_swiglu_tensor(b->shared_mid, b->shared_gate, b->shared_up, + count * DS4_N_FF_EXP, DS4_SWIGLU_CLAMP_EXP, 1.0f) && + ds4_gpu_dsv41_quantize(b->shared_mid, DS4_N_FF_EXP, count, DS4_V41_BF16) && + ds41_matmul_batch(b->shared, m, l->ffn_down_shexp, b->shared_mid, count, true) && + ds4_gpu_tp_big_gate_join(il, count, peer, bytes) && + ds4_gpu_tp_add_tensor(b->routed, g->tp_rank ? peer : b->routed, + g->tp_rank ? b->routed : peer, count * DS4_N_EMBD); + if (!ok) ds4_gpu_tp_big_gate_abort(); + return ok; +} +#endif + static bool ds41_moe_batch(ds41_gpu_graph *g, const ds4_model *m, const ds4_layer_weights *l, uint32_t il, uint32_t count, bool shared_owner) { @@ -41238,7 +41612,13 @@ static bool ds41_moe_batch(ds41_gpu_graph *g, const ds4_model *m, ds4_gpu_dsv41_quantize(b->shared_mid, DS4_N_FF_EXP, count, DS4_V41_BF16) && ds41_matmul_batch(b->shared, m, l->ffn_down_shexp, b->shared_mid, count, true))) && ( -#ifndef __APPLE__ +#ifdef DS4_ROCM_BUILD + g->tp_world == 2u ? + ds4_gpu_dsv41_routed_moe_tp_tensor(b->routed, b->gate, b->up, b->mid, b->experts, + m->map, m->size, l->ffn_gate_exps->abs_offset, l->ffn_up_exps->abs_offset, + l->ffn_down_exps->abs_offset, b->selected, b->route_weights, + b->norm, count, g->tp_rank) : +#elif !defined(__APPLE__) g->tp_world == 2 ? ds4_gpu_routed_moe_batch_owned_tensor(b->routed, b->gate, b->up, b->mid, b->experts, m->map, m->size, l->ffn_gate_exps->abs_offset, l->ffn_up_exps->abs_offset, @@ -41277,8 +41657,16 @@ static DS4_MAYBE_UNUSED bool ds41_graph_step(ds41_gpu_graph *g, const ds4_model * layer mapped, using the same admitted reserve as layer-major prefill. */ const bool layer_resident = g->streaming && g->quality; if (layer_resident && !ds4_gpu_end_commands()) ok = false; - const bool queue_layers = g->tp_world == 2 && !g->imatrix && - !getenv("DS4_METAL_DISABLE_V41_TP_DECODE_QUEUE"); + const bool queue_layers = +#ifdef DS4_ROCM_BUILD + /* The resident default-stream path has stable model/scratch pointers. + * Keep the existing Engram overwrite and final-token drains below. */ + (g->tp_world == 1u && !g->streaming && !g->quality && !g->imatrix && + !g->image_count && !g_expert_profile.active && + !getenv("DS4_ROCM_DISABLE_V41_RESIDENT_QUEUE")) || +#endif + (g->tp_world == 2 && !g->imatrix && + !getenv("DS4_METAL_DISABLE_V41_TP_DECODE_QUEUE")); for (uint32_t il = 0; ok && il < DS4_N_LAYER; il++) { const ds4_layer_weights *l = &w->layer[il]; if (layer_resident) @@ -41299,7 +41687,11 @@ static DS4_MAYBE_UNUSED bool ds41_graph_step(ds41_gpu_graph *g, const ds4_model * 14, and before publishing the completed token to the CPU. */ const bool drain = !queue_layers || il == 13 || il + 1u == DS4_N_LAYER; if (drain && !ds4_gpu_end_commands()) ok = false; +#ifdef DS4_ROCM_BUILD + if (ds41_tp_failed(g)) ok = false; +#else if (g->tp_world == 2 && ds4_gpu_tp_failed()) ok = false; +#endif if (ok && g->imatrix) ok = imatrix_collect_tensor_batch(g->imatrix, g->norm, g->mid, g->selected, false, il, 1); @@ -41307,9 +41699,22 @@ static DS4_MAYBE_UNUSED bool ds41_graph_step(ds41_gpu_graph *g, const ds4_model if (ok && drain && !layer_resident && il + 1u < DS4_N_LAYER) ok = ds4_gpu_begin_commands() != 0; } +#ifdef DS4_ROCM_BUILD + /* ROCm commands_active() is always false. A failed resident layer can + * leave ordered work queued; drain it before invalidating the graph. */ + if (!ok && queue_layers && g->tp_world == 1u) + (void)ds4_gpu_end_commands(); +#endif if (ds4_gpu_commands_active() && !ds4_gpu_end_commands()) ok = false; if (layer_resident && !metal_graph_stream_map_decode_static_all(m, w)) ok = false; - if (g->tp_world == 2 && ds4_gpu_tp_failed()) ok = false; +#ifdef DS4_ROCM_BUILD + if (ds41_tp_failed(g)) ok = false; +#else + if (g->tp_world == 2 && ds4_gpu_tp_failed()) ok = false; +#endif +#ifdef DS4_ROCM_BUILD + if (g->streaming && !ds4_gpu_stream_expert_cache_quiesce()) ok = false; +#endif if (ok && logits) ok = ds41_graph_logits(g, m, w, logits); if (!ok) { g->valid = false; @@ -41324,12 +41729,20 @@ static DS4_MAYBE_UNUSED bool ds41_graph_step(ds41_gpu_graph *g, const ds4_model * routes take precedence over the rest of the prompt's popular experts. */ static bool ds41_prefill_seed(ds41_gpu_graph *g, const ds4_model *m, const ds4_layer_weights *l, uint32_t il, uint32_t count) { -#if defined(__APPLE__) && !defined(DS4_NO_GPU) +#if (defined(__APPLE__) || defined(DS4_ROCM_BUILD)) && !defined(DS4_NO_GPU) if (!g->streaming || getenv("DS4_METAL_DISABLE_STREAMING_PREFILL_CACHE_SEED")) return true; uint32_t target = ds4_gpu_stream_expert_cache_configured_count() / DS4_N_LAYER; if (target > DS4_N_EXPERT) target = DS4_N_EXPERT; if (!target) return true; +#ifdef DS4_ROCM_BUILD + int32_t selected_host[2048u * 6u]; + if (!count || count > g->prefill_cap || count > 2048u || DS4_N_EXPERT_USED != 6u || + !ds4_gpu_tensor_read(g->batch.selected, 0, selected_host, + (uint64_t)count * 6u * sizeof(int32_t))) return false; + const int32_t *selected = selected_host; +#else const int32_t *selected = ds4_gpu_tensor_contents(g->batch.selected); +#endif if (!selected) return false; uint32_t frequency[DS4_MAX_EXPERT] = {0}; const uint32_t recent = count < 32u ? count : 32u; @@ -41356,10 +41769,14 @@ static bool ds41_prefill_seed(ds41_gpu_graph *g, const ds4_model *m, const ds4_gpu_stream_expert_table table = graph_stream_expert_table_make(m, l, il, routed_expert_row_bytes(l->ffn_gate_exps) * DS4_N_FF_EXP, routed_expert_row_bytes(l->ffn_down_exps) * DS4_N_EMBD); +#ifdef DS4_ROCM_BUILD + return ds4_gpu_stream_expert_cache_seed_experts_gpu_copy(&table, experts, priority, n) != 0; +#else if (!ds4_gpu_begin_commands()) return false; const bool ok = ds4_gpu_stream_expert_cache_seed_experts_gpu_copy(&table, experts, priority, n) != 0; const bool ended = ds4_gpu_end_commands() != 0; return ended && ok; +#endif #else (void)g; (void)m; (void)l; (void)il; (void)count; return true; @@ -41379,7 +41796,7 @@ static uint32_t ds41_prefill_count(const ds41_gpu_graph *g, uint32_t remaining) if (!ds41_tp_batch_enabled(g) || g->imatrix || getenv("DS4_METAL_DISABLE_V41_LAYER_PREFILL")) return 1; uint32_t minimum = 256u; -#ifdef __APPLE__ +#if defined(__APPLE__) || defined(DS4_ROCM_BUILD) minimum = g->tp_world == 2 && !getenv("DS4_METAL_DISABLE_V41_TP_SMALL_PREFILL") ? 32u : 256u; #endif @@ -41588,6 +42005,9 @@ typedef struct { const uint32_t *ids; float *out; uint32_t count, ready; +#ifdef DS4_ROCM_BUILD + uint32_t ring_rows, consumed; +#endif bool active, ok, cancel, done; } ds41_engram_prefetch; @@ -41596,14 +42016,32 @@ static void *ds41_engram_prefetch_read(void *arg) { p->ok = true; for (uint32_t off = 0; off < p->count; off += 2048u) { const uint32_t count = p->count - off < 2048u ? p->count - off : 2048u; +#ifdef DS4_ROCM_BUILD + uint32_t dst = off; + if (p->ring_rows) { + /* Each slot remains owned by its consumer until its synchronous + * H2D copy returns. Cancellation also releases a blocked reader. */ + while (off >= p->ring_rows && + __atomic_load_n(&p->consumed, __ATOMIC_ACQUIRE) < off - p->ring_rows + 2048u && + !__atomic_load_n(&p->cancel, __ATOMIC_RELAXED)) { + const struct timespec pause = {.tv_nsec = 1000000}; + nanosleep(&pause, NULL); + } + dst = off % p->ring_rows; + } +#endif if (__atomic_load_n(&p->cancel, __ATOMIC_RELAXED) || !ds4_engram_read_batch(p->table, p->ids + (size_t)off * 2u * DS4_ENGRAM_COLS, count, 2u * DS4_ENGRAM_COLS, +#ifdef DS4_ROCM_BUILD + p->out + (size_t)dst * DS4_ENGRAM_COLS * DS4_ENGRAM_DIM)) { +#else p->out + (size_t)off * DS4_ENGRAM_COLS * DS4_ENGRAM_DIM)) { +#endif p->ok = false; break; } - /* Publish only completed rows; later disk reads use disjoint memory. */ + /* Publish only completed rows; live consumers own disjoint memory. */ __atomic_store_n(&p->ready, off + count, __ATOMIC_RELEASE); } __atomic_store_n(&p->done, true, __ATOMIC_RELEASE); @@ -41632,15 +42070,121 @@ static bool ds41_engram_prefetch_join(ds41_engram_prefetch *p, bool cancel) { } static bool ds41_engram_prefetch_start(ds41_engram_prefetch *p, ds41_gpu_graph *g, - uint32_t table, uint32_t count) { + uint32_t table, uint32_t count +#ifdef DS4_ROCM_BUILD + , bool pipeline +#endif + ) { +#ifdef DS4_ROCM_BUILD + if (p->active || table >= 2u || !count || + count > (g->carry_cap ? g->carry_cap : g->prefill_cap) || + !g->host_engram_capacity) return false; + /* A diagnostic toggled after graph allocation must not overrun a ring + * or join a blocked producer. Fall back to synchronous tile reads. */ + const bool ring = count > g->host_engram_capacity; + if (ring && (!pipeline || g->host_engram_capacity != 4096u || g->prefill_cap != 2048u)) + return false; +#endif *p = (ds41_engram_prefetch){.table = &g->table[table], .count = count, - .ids = g->prefill_ids[0][table], .out = ds4_gpu_tensor_contents(g->engram_prefetch)}; + .ids = g->prefill_ids[0][table], +#ifdef DS4_ROCM_BUILD + .out = g->host_engram_rows, .ring_rows = ring ? g->host_engram_capacity : 0}; +#else + .out = ds4_gpu_tensor_contents(g->engram_prefetch)}; +#endif if (!p->out || pthread_create(&p->thread, NULL, ds41_engram_prefetch_read, p)) return false; p->active = true; return true; } +#ifdef DS4_ROCM_BUILD +typedef struct { + pthread_t thread; + bool active, ok; + ds4_gpu_dsv41_stream_layer_plan plan; +} ds41_stream_layer_load; + +static void *ds41_stream_layer_read(void *arg) { + ds41_stream_layer_load *job = (ds41_stream_layer_load *)arg; + job->ok = ds4_gpu_dsv41_stream_load_layer(&job->plan) != 0; + return NULL; +} + +/* One CPU loader owns the alternate expert slot. No graph may outlive its + * loader; a failed pthread join cannot safely release the model beneath it. */ +static bool ds41_stream_layer_join(ds41_stream_layer_load *job) { + if (!job->active) return true; + if (pthread_join(job->thread, NULL)) ds4_die("cannot join V4.1 expert reader safely"); + const bool ok = job->ok; + memset(job, 0, sizeof(*job)); + return ok; +} + +static bool ds41_stream_layer_start(ds41_stream_layer_load *job, + const ds4_model *m, const ds4_weights *w, uint32_t il) { + ds4_gpu_stream_expert_table table; + if (!job || job->active || il >= DS4_N_LAYER || !ds41_stream_table(m, &w->layer[il], il, &table)) return false; + memset(job, 0, sizeof(*job)); + /* Dynamic cache ownership remains on this thread until D2D finishes. */ + if (!ds4_gpu_dsv41_stream_prepare_layer(&table, &job->plan)) return false; + const int rc = pthread_create(&job->thread, NULL, ds41_stream_layer_read, job); + if (rc) { + if (!ds4_gpu_dsv41_stream_cancel_layer(&job->plan)) + ds4_die("cannot cancel unstarted V4.1 expert reader plan"); + memset(job, 0, sizeof(*job)); + fprintf(stderr, "ds4: failed to start V4.1 expert reader: %s\n", strerror(rc)); + return false; + } + job->active = true; + return true; +} + +static bool ds41_stream_sweep_start(ds41_stream_layer_load *job, + const ds4_model *m, const ds4_weights *w) { + return ds4_gpu_stream_expert_cache_quiesce() && ds41_stream_layer_start(job, m, w, 0); +} + +static bool ds41_stream_sweep_finish(ds41_stream_layer_load *job) { + bool ok = ds41_stream_layer_join(job); + if (!ds4_gpu_stream_expert_cache_quiesce()) ok = false; + return ok; +} + +static bool ds41_stream_layer_enter(ds41_stream_layer_load *job, + const ds4_model *m, const ds4_weights *w, + uint32_t il, uint32_t executed_layers) { + if (!job->active || job->plan.table.model_map != m->map || job->plan.table.layer != il || + !ds41_stream_layer_join(job)) return false; + return il + 1u == executed_layers || ds41_stream_layer_start(job, m, w, il + 1u); +} + +static bool ds41_stream_layer_leave(const ds4_model *m, const ds4_weights *w, uint32_t il) { + ds4_gpu_stream_expert_table table; + return ds41_stream_table(m, &w->layer[il], il, &table) && + ds4_gpu_stream_expert_cache_note_layer_consumed(&table); +} + +static bool ds41_engram_prefetch_upload(ds41_engram_prefetch *p, ds4_gpu_tensor *out, + uint32_t off, uint32_t count) { + if (!p->out || !count || count > 2048u || off % 2048u || + off > p->count || count > p->count - off || + __atomic_load_n(&p->ready, __ATOMIC_ACQUIRE) < off + count || + (p->ring_rows && __atomic_load_n(&p->consumed, __ATOMIC_RELAXED) != off)) return false; + const uint32_t src = p->ring_rows ? off % p->ring_rows : off; + const uint64_t row = (uint64_t)DS4_ENGRAM_COLS * DS4_ENGRAM_DIM; + /* ROCm tensor_write uses host-synchronous hipMemcpy(H2D), not an async + * command buffer. Success ends every GPU access to this host slot. */ + if (!ds4_gpu_tensor_write(out, 0, p->out + (uint64_t)src * row, + (uint64_t)count * row * sizeof(float))) { + if (!ds4_gpu_synchronize()) ds4_die("cannot drain V4.1 Engram upload safely"); + return false; + } + if (p->ring_rows) __atomic_store_n(&p->consumed, off + count, __ATOMIC_RELEASE); + return true; +} +#endif + static uint32_t ds41_encoder_chunk_cap(const ds41_gpu_graph *g, uint32_t count) { if (count < 8192u && g->prefill_cap > 2048u) return 2048u; /* Keep the decoder suffix optimization for 8k prompts. */ @@ -41672,6 +42216,9 @@ static bool ds41_graph_prefill_sweep(ds41_gpu_graph *g, const ds4_model *m, const bool decoder_suffix = wide && total_count >= 8192u && !getenv("DS4_METAL_DISABLE_V41_DECODER_SUFFIX"); if ((encoder_only || resume_encoder) && !decoder_suffix) return false; +#ifdef DS4_ROCM_BUILD + if (g->streaming && g->quality) return false; +#endif uint32_t (*ids)[2][DS4_ENGRAM_COLS] = g->prefill_ids; ds4_engram_history next_history = g->history; if (!ds41_hash_tokens(g, &next_history, tokens, total_count, &ids[0][0][0])) @@ -41689,15 +42236,26 @@ static bool ds41_graph_prefill_sweep(ds41_gpu_graph *g, const ds4_model *m, const bool pipeline_engram = overlap_engram && !getenv("DS4_METAL_DISABLE_V41_ENGRAM_PIPELINE"); bool engram_prefetched = overlap_engram && - ds41_engram_prefetch_start(&engram_prefetch, g, 0, total_count); + ds41_engram_prefetch_start(&engram_prefetch, g, 0, total_count +#ifdef DS4_ROCM_BUILD + , pipeline_engram +#endif + ); +#ifdef DS4_ROCM_BUILD + ds41_stream_layer_load prepare = {0}; + bool ok = !g->streaming || ds41_stream_sweep_start(&prepare, m, w); +#else bool ok = !g->streaming || metal_graph_stream_map_token(m, w); metal_graph_stream_prepare_slot prepare = {0}; +#endif for (uint32_t il = 0; ok && il < DS4_N_LAYER; il++) { if (cancel && cancel(cancel_ud)) { ok = false; break; } if (encoder_only && il == 20u) { /* Publish every encoder key, but leave the decoder invalid until * the last sweep rebuilds its exact 2541-token dependency suffix. */ +#ifndef DS4_ROCM_BUILD if (g->streaming) ok = metal_graph_stream_map_layer_decode(m, w, il); +#endif if (ok) ok = ds41_decoder_prepare(g, m, &w->layer[il], il, initial_start, 0, total_count, true, batch_hc, batch_attention, cancel, cancel_ud); break; @@ -41705,16 +42263,24 @@ static bool ds41_graph_prefill_sweep(ds41_gpu_graph *g, const ds4_model *m, /* Layer 1 no longer reads the prefix buffer. Fill it for layer 14 * while the intervening encoder layers run. The table stays on disk. */ if (il == 2u) engram_prefetched = overlap_engram && - ds41_engram_prefetch_start(&engram_prefetch, g, 1, total_count); + ds41_engram_prefetch_start(&engram_prefetch, g, 1, total_count +#ifdef DS4_ROCM_BUILD + , pipeline_engram +#endif + ); const double t0 = profile ? now_sec() : 0; if (g->streaming) { -#ifdef __APPLE__ +#ifdef DS4_ROCM_BUILD + ok = ds41_stream_layer_enter(&prepare, m, w, il, encoder_only ? 20u : DS4_N_LAYER); +#elif defined(__APPLE__) const uint32_t first_count = total_count < encoder_chunk ? total_count : encoder_chunk; if (!g->encoder_resident || il >= 20) ok = metal_graph_stream_prepare_join_layer(NULL, m, w, il, first_count, false, true, false, false, &prepare, 1); #endif +#ifndef DS4_ROCM_BUILD if (ok) ok = metal_graph_stream_map_layer(m, w, il); +#endif #if !defined(__APPLE__) && !defined(DS4_ROCM_BUILD) /* Below 2K, unused expert reads outweigh the overlap. */ if (ok && total_count >= 2048u) { @@ -41738,6 +42304,9 @@ static bool ds41_graph_prefill_sweep(ds41_gpu_graph *g, const ds4_model *m, false, true, false, false, &prepare, 1); #endif } +#ifdef DS4_ROCM_BUILD + const bool stream_layer_ready = g->streaming && ok; +#endif const double t_map = profile ? now_sec() : 0; uint32_t first = 0; if (ok && decoder_suffix && il >= 20u) { @@ -41769,18 +42338,28 @@ static bool ds41_graph_prefill_sweep(ds41_gpu_graph *g, const ds4_model *m, if (ok) ok = ds4_gpu_tensor_write(g->prefill_tokens, 0, tokens + off, (uint64_t)count * sizeof(int32_t)); if (ok && g->image_count) { +#ifdef DS4_ROCM_BUILD + ds41_text_mask(g, start, count, g->host_text_mask); + ok = ds4_gpu_tensor_write(g->image_text_mask, 0, g->host_text_mask, count) != 0; +#else uint8_t *mask = ds4_gpu_tensor_contents(g->image_text_mask); ok = mask != NULL; if (ok) ds41_text_mask(g, start, count, mask); +#endif } if (ok && !il && batch_core) { +#ifdef DS4_ROCM_BUILD + ok = ds4_gpu_tensor_write(g->batch.selected_comp, 0, g->host_selected_comp, + (uint64_t)count * DS4_N_INDEXER_TOP_K * sizeof(int32_t)) != 0; +#else void *selection = ds4_gpu_tensor_contents(g->batch.selected_comp); if (!selection) ok = false; else memset(selection, 0xff, (size_t)count * DS4_N_INDEXER_TOP_K * sizeof(int32_t)); +#endif } if (ok) ok = ds4_gpu_begin_commands() != 0; if (!il) { -#ifndef __APPLE__ +#if !defined(__APPLE__) && !defined(DS4_ROCM_BUILD) if (ok && !g->image_count) { float *pre = calloc((size_t)count * DS4_N_HC, sizeof(float)); if (!pre) ok = false; @@ -41815,12 +42394,28 @@ static bool ds41_graph_prefill_sweep(ds41_gpu_graph *g, const ds4_model *m, ds41_engram_prefetch_wait(&engram_prefetch, off + count, cancel, cancel_ud) : ds41_engram_prefetch_join(&engram_prefetch, false); } else if (getenv("DS4_METAL_DISABLE_V41_BATCH_ENGRAM")) { +#ifdef DS4_ROCM_BUILD + for (uint32_t t = 0; ok && t < count; t++) { + ok = ds4_engram_read(&g->table[engram], ids[off + t][engram], DS4_ENGRAM_COLS, + g->rows[engram]) && + ds4_gpu_tensor_write(g->rows_view[t].engram_rows, 0, + g->rows[engram], sizeof(g->rows[engram])); + } +#else for (uint32_t t = 0; ok && t < count; t++) ok = ds4_engram_read(&g->table[engram], ids[off + t][engram], DS4_ENGRAM_COLS, ds4_gpu_tensor_contents(g->rows_view[t].engram_rows)); +#endif } else { +#ifdef DS4_ROCM_BUILD + ok = ds4_engram_read_batch(&g->table[engram], ids[off][engram], count, + 2u * DS4_ENGRAM_COLS, g->host_engram_rows) && + ds4_gpu_tensor_write(g->batch.engram_rows, 0, g->host_engram_rows, + (uint64_t)count * DS4_ENGRAM_COLS * DS4_ENGRAM_DIM * sizeof(float)); +#else ok = ds4_engram_read_batch(&g->table[engram], ids[off][engram], count, 2u * DS4_ENGRAM_COLS, ds4_gpu_tensor_contents(g->batch.engram_rows)); +#endif } } const double t_engram = profile ? now_sec() : 0; @@ -41837,9 +42432,13 @@ static bool ds41_graph_prefill_sweep(ds41_gpu_graph *g, const ds4_model *m, } while (0) if (ok) ok = ds4_gpu_begin_commands() != 0; if (ok && engram_prefetched && ds41_engram_layer(il)) { +#ifdef DS4_ROCM_BUILD + ok = ds41_engram_prefetch_upload(&engram_prefetch, g->batch.engram_rows, off, count); +#else const uint64_t bytes = (uint64_t)DS4_ENGRAM_COLS * DS4_ENGRAM_DIM * sizeof(float); ok = ds4_gpu_tensor_copy(g->batch.engram_rows, 0, g->engram_prefetch, off * bytes, count * bytes) != 0; +#endif } if (ok && batch_hc) ok = ds41_before_attention_batch(g, &active, m, &w->layer[il], il, count); @@ -41872,7 +42471,8 @@ static bool ds41_graph_prefill_sweep(ds41_gpu_graph *g, const ds4_model *m, g->batch.heads, count, g->tp_rank) && ds41_sum_partial_batch(g, g->batch.block, il, count) && ds4_gpu_dsv41_quantize(g->batch.block, DS4_N_EMBD, count, DS4_V41_BF16); - } else if (ok && l->attn_output_b->type == DS4_TENSOR_Q8_0) { + } else + if (ok && l->attn_output_b->type == DS4_TENSOR_Q8_0) { ok = ds4_gpu_dsv41_attention_output_batch(g->batch.block, g->batch.low, m->map, m->size, l->attn_output_a->abs_offset, l->attn_output_b->abs_offset, g->batch.heads, count) && @@ -41887,7 +42487,11 @@ static bool ds41_graph_prefill_sweep(ds41_gpu_graph *g, const ds4_model *m, g->batch.low, count, true); } DS41_STAGE("attention output"); - if (ok && batch_hc) ok = ds41_after_attention_batch(&active, m, l, count); + if (ok && batch_hc) ok = ds41_after_attention_batch( +#ifdef DS4_ROCM_BUILD + g, +#endif + &active, m, l, count); DS41_STAGE("hc/ffn norm"); for (uint32_t t = 0; ok && !batch_hc && t < count; t++) { row.pos = start + t; @@ -41900,6 +42504,12 @@ static bool ds41_graph_prefill_sweep(ds41_gpu_graph *g, const ds4_model *m, } } if (ok && batch_moe) { +#ifdef DS4_ROCM_BUILD + if (il < 20u && g->tp_world == 2u && count >= 32u && count <= 2048u && + ds4_gpu_tp_big_gate_overlap_supported()) + ok = ds41_moe_batch_overlap(g, m, &w->layer[il], il, count); + else +#endif ok = ds41_moe_batch(g, m, &w->layer[il], il, count, false); DS41_STAGE("shared/routed ffn"); if (ok && batch_hc) { @@ -41924,7 +42534,11 @@ static bool ds41_graph_prefill_sweep(ds41_gpu_graph *g, const ds4_model *m, } const double t_encoded = profile ? now_sec() : 0; if (ds4_gpu_commands_active() && !ds4_gpu_end_commands()) ok = false; - if (g->tp_world == 2 && ds4_gpu_tp_failed()) ok = false; +#ifdef DS4_ROCM_BUILD + if (ds41_tp_failed(g)) ok = false; +#else + if (g->tp_world == 2 && ds4_gpu_tp_failed()) ok = false; +#endif const double t_done = profile ? now_sec() : 0; if (ok && !encoder_only && off + count == total_count) ok = ds41_prefill_seed(g, m, &w->layer[il], il, count); @@ -41952,13 +42566,21 @@ static bool ds41_graph_prefill_sweep(ds41_gpu_graph *g, const ds4_model *m, row.pre = g->rows_view[count - 1u].ffn_split; } } +#ifdef DS4_ROCM_BUILD + if (stream_layer_ready && !ds41_stream_layer_leave(m, w, il)) ok = false; +#endif } if (!ds41_engram_prefetch_join(&engram_prefetch, !ok)) ok = false; +#ifdef DS4_ROCM_BUILD + if (g->streaming && !ds41_stream_sweep_finish(&prepare)) ok = false; + if (g->streaming && !ok) (void)ds4_gpu_synchronize(); +#else #if !defined(__APPLE__) && !defined(DS4_ROCM_BUILD) if (g->streaming) ds4_gpu_stream_expert_cache_prefetch_finish(true); #endif if (!metal_graph_stream_prepare_join_all(&prepare, 1)) ok = false; if (g->streaming && !metal_graph_stream_map_decode_static_all(m, w)) ok = false; +#endif if (ok && !encoder_only) { ok = ds4_gpu_begin_commands() && ds4_gpu_tensor_copy(g->residual, 0, row.residual, 0, (uint64_t)DS4_N_HC * DS4_N_EMBD * 4u) && @@ -41977,6 +42599,7 @@ static bool ds41_graph_prefill(ds41_gpu_graph *g, const ds4_model *m, return ds41_graph_prefill_sweep(g, m, w, tokens, count, progress, progress_ud, total, cancel, cancel_ud, false, false); } +#if !defined(DS4_ROCM_BUILD) static ds41_gpu_graph *ds41_batch_workspace(ds41_gpu_graph *const *graphs, int count) { if (!graphs || count < 2 || count > DS4_TP_BATCH_MAX_ROWS) return NULL; ds41_gpu_graph *largest = NULL; @@ -42097,7 +42720,11 @@ static DS4_MAYBE_UNUSED bool ds41_graph_step_batch(ds41_gpu_graph *const *graphs } if (ok) ok = ds41_sum_partial_batch(g, active.block, il, rows); if (ok) ok = ds4_gpu_dsv41_quantize(active.block, DS4_N_EMBD, rows, DS4_V41_BF16) && - ds41_after_attention_batch(&active, model, l, rows) && + ds41_after_attention_batch( +#ifdef DS4_ROCM_BUILD + g, +#endif + &active, model, l, rows) && ds41_moe_batch(g, model, l, il, rows, shared_owner) && (shared_owner ? ds4_gpu_tensor_copy(active.block, 0, active.routed, 0, (uint64_t)rows * DS4_N_EMBD * sizeof(float)) : @@ -42168,6 +42795,9 @@ static bool ds41_graph_short_prefill(ds41_gpu_graph *g, const ds4_model *m, if (!ok) g->valid = false; return ok; } +#endif /* Metal native session batching */ +#undef DS41_ENGRAM_GPU_PREFETCH_ROWS +#undef DS41_INDEX_PACKED_WORDS #undef DS41_PREFILL_ROWS #undef DS41_PREFILL_ALIASES #undef DS41_PREFILL_STORAGE @@ -42231,6 +42861,10 @@ typedef enum { struct ds4_engine { char *model_path; uint64_t ds41_session_bytes; +#ifdef DS4_ROCM_BUILD + uint64_t ds41_host_memory_baseline; + bool ds41_model_loaded, ds41_stream_slots_ready; +#endif ds4_model model; ds4_model mtp_model; ds4_model vision_model; @@ -62402,14 +63036,21 @@ static int ds41_save_payload(ds4_session *s, FILE *fp, char *err, size_t errlen) return rc; } +static bool ds41_payload_header_valid(ds4_session *s, const uint32_t *h, + uint64_t remaining) { + ds41_gpu_graph *g = &s->ds41_graph; + const uint32_t pos = h[7]; + return s->ds41_graph_ready && pos && pos < g->ctx && pos < h[2] && h[2] <= 1048576u && + h[3] == 1 && h[4] == 128 && h[5] == 128 && h[6] == h[2] + 1u && + h[8] == 40 && h[9] == 512 && h[10] == 128 && h[11] == DS4_N_VOCAB && + h[12] == 0x413431u && remaining == ds41_payload_body_bytes(g, pos); +} + static int ds41_load_payload(ds4_session *s, FILE *fp, const uint32_t *h, uint64_t remaining, char *err, size_t errlen) { ds41_gpu_graph *g = &s->ds41_graph; const uint32_t pos = h[7]; - if (!s->ds41_graph_ready || !pos || pos >= g->ctx || pos >= h[2] || h[2] > 1048576u || - h[3] != 1 || h[4] != 128 || h[5] != 128 || h[6] != h[2] + 1u || - h[8] != 40 || h[9] != 512 || h[10] != 128 || h[11] != DS4_N_VOCAB || - h[12] != 0x413431u || remaining != ds41_payload_body_bytes(g, pos)) { + if (!ds41_payload_header_valid(s, h, remaining)) { payload_set_err(err, errlen, "invalid V4.1 snapshot dimensions or size"); return 1; } @@ -63206,6 +63847,48 @@ int ds4_session_load_payload(ds4_session *s, FILE *fp, uint64_t payload_bytes, c payload_set_err(err, errlen, "unsupported session payload version"); return 1; } +#if defined(DS4_ROCM_BUILD) && defined(DS4_HAS_DEEPSEEK41_GPU) + if (s->engine && s->engine->tp.active && ds4_session_is_ds41(s)) { + /* V4.1 KV projections and compression state are replicated: ownership + * begins at attention heads and experts, after the KV input. Restore + * the complete frontier on both ranks instead of rebuilding tokens, + * which loses image embeddings and changes decode-vs-prefill history. + * Image identity retains the existing same-session authentication + * contract of the local payload API. No borrowed image pointers live + * in a checkpoint, and restore does not clear that identity. */ + if (!ds41_payload_header_valid(s, h, remaining)) { + payload_set_err(err, errlen, "invalid V4.1 TP snapshot dimensions or size"); + return 1; + } + if (ds4_session_tp_leader(s)) { + const off_t body = ftello(fp); + const off_t header_bytes = (off_t)sizeof(h); + if (body < header_bytes || fseeko(fp, body - header_bytes, SEEK_SET)) { + payload_set_err(err, errlen, "V4.1 TP restore requires a seekable checkpoint"); + return 1; + } + if (!ds4_tp_send_restore_payload(s->engine->tp.ctx, s->tp_session_id, + fp, payload_bytes, err, errlen)) { + ds4_session_invalidate(s); + return 1; + } + if (fseeko(fp, body, SEEK_SET)) { + ds4_tp_mark_failed(s->engine->tp.ctx); + ds4_session_invalidate(s); + payload_set_err(err, errlen, "cannot reread the local TP checkpoint"); + return 1; + } + } + const int rc = ds41_load_payload(s, fp, h, remaining, err, errlen); + if (rc && ds4_session_tp_leader(s)) { + /* The worker has already committed this payload. A local read or + * GPU failure must prevent any further gate from using the pair. */ + ds4_tp_mark_failed(s->engine->tp.ctx); + ds4_session_invalidate(s); + } + return rc; + } +#endif if (s->engine && s->engine->tp.active) { /* A local payload cannot restore another rank's caches. Keep the exact * saved tokens, consume the payload (leaving trailers readable), and @@ -64053,7 +64736,7 @@ static int ds4_engine_collect_sequential_imatrix( return 1; } g.imatrix = &collector; -#ifdef __APPLE__ +#if defined(DS4_HAS_DEEPSEEK41_GPU) && (defined(__APPLE__) || defined(DS4_ROCM_BUILD)) d.imatrix = &collector; #endif @@ -68360,6 +69043,48 @@ static bool ds4_glm_streaming_resident_prefix_bytes( return true; } +#if defined(DS4_ROCM_BUILD) && defined(DS4_HAS_DEEPSEEK41_GPU) +static bool ds41_stream_cache_configure(ds4_engine *e) { + uint64_t expert = 0; + for (uint32_t il = 0; il < DS4_N_LAYER; il++) { + ds4_gpu_stream_expert_table table; + if (!ds41_stream_table(&e->model, &e->weights.layer[il], il, &table)) { + fprintf(stderr, "ds4: V4.1 ROCm SSD streaming requires uniform IQ2_XXS/Q2_K experts (layer %u)\n", il); + return false; + } + const uint64_t bytes = 2u * table.gate_expert_bytes + table.down_expert_bytes; + if (expert && expert != bytes) return false; + expert = bytes; + } + const uint64_t slots = 2u * DS4_N_EXPERT * expert; + const uint64_t requested = e->ssd_streaming_cache_bytes; + const uint32_t maximum = DS4_N_LAYER * DS4_N_EXPERT; + uint64_t count = e->ssd_streaming_cache_experts; + if (requested) { + if (requested < slots || (requested - slots) / expert < DS4_N_EXPERT_USED) { + fprintf(stderr, "ds4: V4.1 SSD cache target must cover two complete prefill layers plus six experts (%.2f GiB)\n", + ds4_bytes_to_gib(slots + DS4_N_EXPERT_USED * expert)); + return false; + } + count = (requested - slots) / expert; + } + if (count > maximum) count = maximum; + if (count < DS4_N_EXPERT_USED) { + fprintf(stderr, "ds4: V4.1 SSD streaming needs at least six dynamic expert slots\n"); + return false; + } + e->ssd_streaming_cache_experts = (uint32_t)count; + e->ssd_streaming_cache_bytes = count * expert; + e->ssd_streaming_prefill_headroom_bytes = slots; + e->ssd_streaming_full_layer_bytes = 0; + e->ssd_streaming_full_layers = 0; + fprintf(stderr, "ds4: V4.1 SSD expert storage %.2f GiB = %.2f GiB two-layer staging + %.2f GiB dynamic (%u experts)\n", + ds4_bytes_to_gib(slots + count * expert), ds4_bytes_to_gib(slots), + ds4_bytes_to_gib(count * expert), (uint32_t)count); + return true; +} +#endif + static bool ds4_engine_configure_streaming_cache_budget(ds4_engine *e) { g_glm_streaming_full_resident_start = 0; g_glm_streaming_full_resident_layers = 0; @@ -68374,6 +69099,10 @@ static bool ds4_engine_configure_streaming_cache_budget(ds4_engine *e) { return true; #else if (!e || !e->ssd_streaming) return true; +#if defined(DS4_ROCM_BUILD) && defined(DS4_HAS_DEEPSEEK41_GPU) + if (DS4_MODEL_FAMILY == DS4_MODEL_FAMILY_DEEPSEEK41) + return ds41_stream_cache_configure(e); +#endif const bool glm_full_layer_streaming = DS4_MODEL_FAMILY == DS4_MODEL_FAMILY_GLM_DSA && @@ -70363,27 +71092,70 @@ static bool engine_warm_full_model(const ds4_engine_options *opt) { } #ifdef DS4_HAS_DEEPSEEK41_GPU +#ifdef DS4_ROCM_BUILD +static uint64_t ds41_rocm_host_reserve_bytes(uint64_t host) { + /* Match the ROCm resident GLM policy: usable host RAM already excludes CMA. + * The one-eighth reserve rejects viable resident vision sessions and + * limits SSD cache capacity despite usable host memory. Keep an + * explicit OS reserve and the separate two-GiB runtime allowance. */ + const uint64_t minimum = UINT64_C(8) << 30; + const uint64_t reserve = host / 16u; + return reserve > minimum ? reserve : minimum; +} + +static uint64_t ds41_rocm_stream_reserve_bytes(uint64_t host) { + /* Graphs and staging have their own admission charges. Keep the same OS + * reserve plus the entire transient allowance during lazy cache growth; + * the generic 16-GiB allocator floor otherwise strands admitted slots. */ + return ds4_add_sat_u64(ds41_rocm_host_reserve_bytes(host), UINT64_C(2) << 30); +} +#endif static bool ds41_memory_admit_for_host(ds4_engine *e, uint64_t graph_bytes, bool fit_cache, uint64_t host, uint64_t recommended) { const uint64_t gib = UINT64_C(1073741824); uint64_t budget = host / 8u * 7u; +#ifdef DS4_ROCM_BUILD + { + const uint64_t reserve = ds41_rocm_host_reserve_bytes(host); + budget = host > reserve ? host - reserve : 0; + } +#endif if (!host || !recommended) { fprintf(stderr, "ds4: cannot determine a safe V4.1 memory budget\n"); return false; } +#ifdef DS4_ROCM_BUILD + const uint64_t host_budget = budget; +#endif if (budget > recommended) budget = recommended; uint64_t weights = g_tp_shard_model_bytes ? g_tp_shard_model_bytes : e->model.size; if (e->ssd_streaming && !weights_streaming_non_routed_bytes(&e->weights, &weights)) return false; weights = ds4_add_sat_u64(weights, e->vision_model.size); const uint64_t fixed = ds4_add_sat_u64(weights, +#ifdef DS4_ROCM_BUILD + ds4_add_sat_u64(graph_bytes, ds4_add_sat_u64(2u * gib, e->ssd_streaming_prefill_headroom_bytes))); +#else ds4_add_sat_u64(graph_bytes, 2u * gib + e->ssd_streaming_prefill_headroom_bytes)); +#endif uint64_t expert = 0; if (e->ssd_streaming && !ds4_streaming_routed_expert_bytes(&e->weights, &expert)) return false; +#ifdef DS4_ROCM_BUILD + uint32_t minimum_experts = 1; + if (e->ssd_streaming) minimum_experts = DS4_N_EXPERT_USED; + if (fixed >= budget || (expert && (budget - fixed) / expert < minimum_experts)) { +#else if (fixed >= budget || (expert && budget - fixed < expert)) { +#endif +#ifdef DS4_ROCM_BUILD + fprintf(stderr, "ds4: V4.1 needs %.2f GiB before any dynamic cache, including context/runtime buffers; " + "safe ROCm budget %.2f GiB. Use a smaller context.\n", + ds4_bytes_to_gib(fixed), ds4_bytes_to_gib(budget)); +#else fprintf(stderr, "ds4: V4.1 needs %.2f GiB before the expert cache; safe budget %.2f GiB. " "Use --ssd-streaming or a smaller context.\n", ds4_bytes_to_gib(fixed), ds4_bytes_to_gib(budget)); +#endif return false; } if (expert && e->ssd_streaming_cache_experts > (budget - fixed) / expert) { @@ -70392,15 +71164,83 @@ static bool ds41_memory_admit_for_host(ds4_engine *e, uint64_t graph_bytes, return false; } const uint32_t count = (uint32_t)((budget - fixed) / expert); +#ifdef DS4_ROCM_BUILD + fprintf(stderr, + "ds4: V4.1 SSD cache fitted %.2f -> %.2f GiB total " + "(dynamic %u -> %u slots) after %.2f GiB fixed " + "weights/context/staging/runtime; limited by %.2f GiB %s budget\n", + ds4_bytes_to_gib(e->ssd_streaming_cache_bytes + + e->ssd_streaming_prefill_headroom_bytes), + ds4_bytes_to_gib((uint64_t)count * expert + + e->ssd_streaming_prefill_headroom_bytes), + e->ssd_streaming_cache_experts, count, + ds4_bytes_to_gib(fixed), ds4_bytes_to_gib(budget), + recommended < host_budget ? "GPU working-set" : "host-RAM"); +#else fprintf(stderr, "ds4: V4.1 SSD cache fitted from %u to %u experts for context/runtime headroom\n", e->ssd_streaming_cache_experts, count); +#endif e->ssd_streaming_cache_experts = count; e->ssd_streaming_cache_bytes = (uint64_t)count * expert; } return true; } +#ifdef DS4_ROCM_BUILD +static bool ds41_stream_unallocated_bytes(const ds4_engine *e, + const ds4_gpu_stream_expert_memory *memory, + uint64_t *bytes) { + const uint64_t transient = UINT64_C(2) << 30; + const uint64_t actual_transient = ds4_add_sat_u64(memory->selected_bytes, memory->pinned_bytes); + if (memory->dynamic_bytes > e->ssd_streaming_cache_bytes || + memory->layer_bytes > e->ssd_streaming_prefill_headroom_bytes || + actual_transient > transient) return false; + *bytes = ds4_add_sat_u64(e->ssd_streaming_cache_bytes - memory->dynamic_bytes, + ds4_add_sat_u64(e->ssd_streaming_prefill_headroom_bytes - memory->layer_bytes, + transient - actual_transient)); + return true; +} +#endif + static bool ds41_memory_admit(ds4_engine *e, uint64_t graph_bytes, bool fit_cache) { +#ifdef DS4_ROCM_BUILD + uint64_t available = 0; + if (!ds4_linux_nonmovable_memory(&available) || !e->ds41_host_memory_baseline) { + fprintf(stderr, "ds4: cannot determine a safe V4.1 ROCm memory budget\n"); + return false; + } + /* Charge the model once against pre-upload availability. CMA is excluded; VRAM/GTT are not added to host RAM on a unified-memory device. */ + if (!ds41_memory_admit_for_host(e, graph_bytes, fit_cache, + e->ds41_host_memory_baseline, ds4_gpu_recommended_working_set_size())) return false; + const uint64_t additional = graph_bytes > e->ds41_session_bytes ? + graph_bytes - e->ds41_session_bytes : 0; + uint64_t not_loaded = e->startup_model_span_bytes ? 0 : + (g_tp_shard_model_bytes ? g_tp_shard_model_bytes : e->model.size); + uint64_t remaining_buffers = UINT64_C(2) << 30; + if (e->ssd_streaming) { + ds4_gpu_stream_expert_memory memory; + if (!weights_streaming_non_routed_bytes(&e->weights, ¬_loaded) || + !ds4_gpu_stream_expert_cache_get_memory(&memory) || + !ds41_stream_unallocated_bytes(e, &memory, &remaining_buffers)) { + fprintf(stderr, "ds4: V4.1 SSD allocation exceeds its admitted cache/staging plan\n"); + return false; + } + if (e->ds41_model_loaded) not_loaded = 0; + } + /* The sidecar upload follows startup admission and preserves the primary + * cache. Charge its remaining allocation until that auxiliary map exists. */ + if (e->vision_ready && !e->vision_map_ready) + not_loaded = ds4_add_sat_u64(not_loaded, e->vision_model.size); + const uint64_t required = ds4_add_sat_u64(not_loaded, ds4_add_sat_u64(additional, + ds4_add_sat_u64(ds41_rocm_host_reserve_bytes(e->ds41_host_memory_baseline), remaining_buffers))); + if (required >= available) { + fprintf(stderr, "ds4: V4.1 ROCm needs %.2f GiB additional usable memory including reserves; " + "only %.2f GiB is available\n", ds4_bytes_to_gib(required), + ds4_bytes_to_gib(available)); + return false; + } + return true; +#else uint64_t host = glm_graph_host_memory_bytes(); #ifdef __linux__ const long pages = sysconf(_SC_PHYS_PAGES), page_size = sysconf(_SC_PAGESIZE); @@ -70409,6 +71249,7 @@ static bool ds41_memory_admit(ds4_engine *e, uint64_t graph_bytes, bool fit_cach #endif return ds41_memory_admit_for_host(e, graph_bytes, fit_cache, host, ds4_gpu_recommended_working_set_size()); +#endif } #endif @@ -70429,6 +71270,7 @@ static int ds4_engine_open_internal(ds4_engine **out, #if defined(DS4_ROCM_BUILD) && !defined(DS4_NO_GPU) g_glm_rocm_guard_available_baseline = 0; (void)ds4_linux_nonmovable_memory(&g_glm_rocm_guard_available_baseline); + e->ds41_host_memory_baseline = g_glm_rocm_guard_available_baseline; #endif e->model.fd = -1; e->mtp_model.fd = -1; @@ -70555,13 +71397,27 @@ static int ds4_engine_open_internal(ds4_engine **out, if (graph_backend) ds4_linux_graph_backend_set_oom_score(opt->backend); model_open(&e->model, opt->model_path, graph_backend, !opt->inspect_only); if (DS4_MODEL_FAMILY == DS4_MODEL_FAMILY_DEEPSEEK41 && - opt->vision_path && opt->vision_path[0] && e->backend != DS4_BACKEND_METAL) { + opt->vision_path && opt->vision_path[0] && e->backend != DS4_BACKEND_METAL +#if defined(DS4_ROCM_BUILD) && !defined(DS4_NO_GPU) + && e->backend != DS4_BACKEND_CUDA +#endif + ) { +#ifdef DS4_ROCM_BUILD + fprintf(stderr, "ds4: V4.1 vision requires Metal or ROCm\n"); +#else fprintf(stderr, "ds4: V4.1 vision requires Metal\n"); +#endif ds4_engine_close(e); *out = NULL; return 1; } config_validate_model(&e->model); +#ifdef DS4_ROCM_BUILD + if (opt->tp.role != DS4_TP_NONE && DS4_MODEL_FAMILY != DS4_MODEL_FAMILY_DEEPSEEK41) { + fprintf(stderr, "ds4: ROCm network tensor parallelism currently requires V4.1 Flash\n"); + ds4_engine_close(e); *out = NULL; return 1; + } +#endif if (ds4_model_is_qwen4() && !opt->inspect_only) { const bool backend_ok = #ifdef DS4_HAS_QWEN4_GPU @@ -70582,12 +71438,25 @@ static int ds4_engine_open_internal(ds4_engine **out, } } if (DS4_MODEL_FAMILY == DS4_MODEL_FAMILY_DEEPSEEK41 && !opt->inspect_only) { +#ifdef DS4_ROCM_BUILD + const bool backend_supported = e->backend == DS4_BACKEND_METAL +#if defined(DS4_ROCM_BUILD) && !defined(DS4_NO_GPU) + || (e->backend == DS4_BACKEND_CUDA && + (!opt->ssd_streaming || (!opt->quality && !opt->ssd_streaming_preload_experts && + !opt->ssd_streaming_full_layers)) && + (opt->tp.role == DS4_TP_NONE || !opt->ssd_streaming) && !opt->cuda_tensor_parallel && + (!gpu_cfg || gpu_cfg->n_gpus <= 1)) +#endif + ; + const bool supported = backend_supported && +#else const bool supported = (e->backend == DS4_BACKEND_METAL || #if defined(DS4_HAS_DEEPSEEK41_GPU) && !defined(__APPLE__) (e->backend == DS4_BACKEND_CUDA && !opt->cuda_tensor_parallel && (!gpu_cfg || gpu_cfg->n_gpus <= 1)) || #endif false) && +#endif opt->distributed.role == DS4_DISTRIBUTED_NONE && !load_slice && !opt->dspark && !opt->glm_mtp && !opt->first_token_test && !opt->metal_graph_test && @@ -70595,8 +71464,13 @@ static int ds4_engine_open_internal(ds4_engine **out, (!opt->directional_steering_file || !opt->directional_steering_file[0]) && e->power_percent == 100 && opt->context_size <= 1048576; if (!supported) { +#ifdef DS4_ROCM_BUILD + fprintf(stderr, "ds4: V4.1 ROCm supports resident network TP; SSD TP, DSpark, steering, " + "legacy diagnostics and SSD quality/full-layer/preload modes are unsupported\n"); +#else fprintf(stderr, "ds4: V4.1 requires Metal or single-GPU CUDA per rank (optional network tensor parallelism); " "DSpark, steering and legacy diagnostics are not supported (maximum context 1048576)\n"); +#endif ds4_engine_close(e); *out = NULL; return 1; @@ -70714,6 +71588,21 @@ static int ds4_engine_open_internal(ds4_engine **out, load_layer_end, load_output, load_output_optional); +#ifdef DS4_ROCM_BUILD + if (opt->tp.role != DS4_TP_NONE) { + for (uint32_t il = 0; il < DS4_N_LAYER; ++il) { + const ds4_layer_weights *l = &e->weights.layer[il]; + if (l->ffn_gate_exps->type != DS4_TENSOR_IQ2_XXS || + l->ffn_up_exps->type != DS4_TENSOR_IQ2_XXS || + l->ffn_down_exps->type != DS4_TENSOR_Q2_K || + l->attn_output_a->type != DS4_TENSOR_Q8_0 || + l->attn_output_b->type != DS4_TENSOR_Q8_0) { + fprintf(stderr, "ds4: V4.1 ROCm TP requires IQ2_XXS gate/up, Q2_K down and Q8_0 attention output weights (layer %u)\n", il); + ds4_engine_close(e); *out = NULL; return 1; + } + } + } +#endif if (e->model.ngram_tensor) { fprintf(stderr, "ds4: Qwen BF16 n-grams: %.2f GiB, disk reads only\n", (double)e->model.ngram_tensor->bytes / (1024.0 * 1024.0 * 1024.0)); @@ -70795,7 +71684,9 @@ static int ds4_engine_open_internal(ds4_engine **out, *out = NULL; return 1; } +#ifndef DS4_ROCM_BUILD if (engine_warm_full_model(opt)) model_warm_weights(&e->model); +#endif } #endif if (ds4_model_is_qwen4()) { @@ -70820,7 +71711,11 @@ static int ds4_engine_open_internal(ds4_engine **out, (void)ds4_gpu_set_model_fd_for_map(e->model.fd, e->model.map); ready = accelerator_cache_model_tensors(e->backend, &e->model, - NULL, NULL, 0); + NULL, NULL, 0 +#ifdef DS4_ROCM_BUILD + , false +#endif + ); } if (!ready) { fprintf(stderr, "ds4: Qwen3.8 test model map setup failed\n"); @@ -70969,6 +71864,16 @@ static int ds4_engine_open_internal(ds4_engine **out, if (safe_cache_bytes != 0 && e->ssd_streaming_cache_bytes > safe_cache_bytes) { e->ssd_streaming_cache_bytes = safe_cache_bytes; +#if defined(DS4_ROCM_BUILD) && defined(DS4_HAS_DEEPSEEK41_GPU) + if (DS4_MODEL_FAMILY == DS4_MODEL_FAMILY_DEEPSEEK41) { + fprintf(stderr, + "ds4: ROCm V4.1 SSD cache request %.2f GiB capped to %.2f GiB " + "by GPU recommended working-set budget " + "(7/8 allowance minus context/graph buffers)\n", + ds4_bytes_to_gib(requested_cache_bytes), + ds4_bytes_to_gib(safe_cache_bytes)); + } else +#endif fprintf(stderr, "ds4: %s SSD streaming cache budget %.2f GiB capped to %.2f GiB " "to stay below the graph working-set pressure budget\n", @@ -71167,7 +72072,13 @@ static int ds4_engine_open_internal(ds4_engine **out, * GPU budget, so let the residency set pin them — that is what makes * the shard actually resident. Without the sysctl, fall back to lazy * faulting (slow but functional). */ - if (e->backend == DS4_BACKEND_METAL && tp_shard && glm_graph_wired_limit_bytes() == 0) { + if ( +#ifdef DS4_ROCM_BUILD + graph_backend +#else + e->backend == DS4_BACKEND_METAL +#endif + && tp_shard && glm_graph_wired_limit_bytes() == 0) { fprintf(stderr, "ds4: iogpu.wired_limit_mb is 0 -- TP expert shard will page " "lazily; raise it (e.g. sudo sysctl iogpu.wired_limit_mb=120000) " @@ -71241,6 +72152,15 @@ static int ds4_engine_open_internal(ds4_engine **out, ds4_gpu_set_quality(e->quality); ds4_gpu_set_glm_model(DS4_MODEL_FAMILY == DS4_MODEL_FAMILY_GLM_DSA); ds4_gpu_set_ssd_streaming(e->ssd_streaming); +#ifdef DS4_ROCM_BUILD + ds4_gpu_set_deepseek41_model(DS4_MODEL_FAMILY == DS4_MODEL_FAMILY_DEEPSEEK41); + if (DS4_MODEL_FAMILY == DS4_MODEL_FAMILY_DEEPSEEK41 && e->ssd_streaming) { + const uint64_t reserve = ds41_rocm_stream_reserve_bytes(e->ds41_host_memory_baseline); + ds4_gpu_set_streaming_free_reserve(reserve); + fprintf(stderr, "ds4: V4.1 ROCm SSD allocator reserve %.2f GiB (OS + transients)\n", + ds4_bytes_to_gib(reserve)); + } +#endif if (!ds4_engine_configure_streaming_auto_cache(e, opt->context_size)) { ds4_engine_close(e); *out = NULL; @@ -71484,12 +72404,26 @@ static int ds4_engine_open_internal(ds4_engine **out, spans.len, (double)span_bytes / 1073741824.0); } - model_map_ok = ds4_gpu_set_model_map_spans(e->model.map, - e->model.size, - load_offsets, - load_sizes, - load_span_count, - spans.max_tensor_bytes); +#ifdef DS4_ROCM_BUILD + if (e->backend == DS4_BACKEND_CUDA && + DS4_MODEL_FAMILY == DS4_MODEL_FAMILY_DEEPSEEK41) { + /* The following tensor-cache preparation merges these static + * spans. Register now and upload that final layout once. */ + model_map_ok = ds4_gpu_set_model_map(e->model.map, e->model.size); + } else +#endif +#ifdef DS4_ROCM_BUILD + { +#endif + model_map_ok = ds4_gpu_set_model_map_spans(e->model.map, + e->model.size, + load_offsets, + load_sizes, + load_span_count, + spans.max_tensor_bytes); +#ifdef DS4_ROCM_BUILD + } +#endif free(spans.v); } else if (load_slice) { const bool map_output = @@ -71599,7 +72533,11 @@ static int ds4_engine_open_internal(ds4_engine **out, *out = NULL; return 1; } - if (tp_shard && e->backend == DS4_BACKEND_METAL) { + if (tp_shard +#ifndef DS4_ROCM_BUILD + && e->backend == DS4_BACKEND_METAL +#endif + ) { model_warm_weights_sharded(&e->model, &e->weights, tp_shard_rank); } @@ -71643,9 +72581,18 @@ static int ds4_engine_open_internal(ds4_engine **out, return 1; } (void)ds4_gpu_set_model_fd_for_map(e->model.fd, e->model.map); +#ifdef DS4_ROCM_BUILD + const bool exact_v41_resident = + DS4_MODEL_FAMILY == DS4_MODEL_FAMILY_DEEPSEEK41 && !e->ssd_streaming && + !load_slice && !tp_shard && !support_model_runtime_ready; +#endif if (!accelerator_cache_model_tensors(e->backend, &e->model, load_offsets, load_sizes, - load_span_count)) { + load_span_count +#ifdef DS4_ROCM_BUILD + , exact_v41_resident +#endif + )) { fprintf(stderr, "ds4: %s failed to prepare optional model cache\n", ds4_backend_name(e->backend)); free(load_offsets); @@ -71656,12 +72603,54 @@ static int ds4_engine_open_internal(ds4_engine **out, } free(load_offsets); free(load_sizes); +#if defined(DS4_ROCM_BUILD) && defined(DS4_HAS_DEEPSEEK41_GPU) + if (DS4_MODEL_FAMILY == DS4_MODEL_FAMILY_DEEPSEEK41) { + e->ds41_model_loaded = true; + if (e->ssd_streaming) { + /* Static weights are copied and no expert loader has started. + * Return the idle primary upload pool before budget admission. */ + if (e->backend == DS4_BACKEND_CUDA && !load_slice && !tp_shard && + !support_model_runtime_ready && + !ds4_gpu_release_model_upload_staging(e->model.map, e->model.size)) { + fprintf(stderr, "ds4: V4.1 SSD failed to release startup upload staging\n"); + ds4_engine_close(e); + *out = NULL; + return 1; + } + ds4_gpu_stream_expert_table even, odd; + const uint32_t ctx = opt->context_size > 0 ? (uint32_t)opt->context_size : 4096u; + const uint32_t sessions = e->placement_session_count_hint > 0 ? + (uint32_t)e->placement_session_count_hint : 1u; + if (!ds41_memory_admit(e, ds4_mul_sat_u64(ds41_graph_bytes(ctx), sessions), false) || + !ds41_stream_table(&e->model, &e->weights.layer[0], 0, &even) || + !ds41_stream_table(&e->model, &e->weights.layer[1], 1, &odd) || + !ds4_gpu_stream_expert_cache_quiesce() || + !ds4_gpu_stream_expert_cache_reserve_layers(&even, &odd)) { + fprintf(stderr, "ds4: V4.1 SSD failed to reserve both admitted expert layers\n"); + ds4_engine_close(e); + *out = NULL; + return 1; + } + e->ds41_stream_slots_ready = true; + fprintf(stderr, "ds4: V4.1 SSD prepared %.2f GiB static weights and %.2f GiB expert staging; " + "%.2f GiB dynamic cache, %.2f GiB graph, 2.00 GiB runtime/I/O allowance\n", + ds4_bytes_to_gib(e->startup_model_span_bytes), + ds4_bytes_to_gib(e->ssd_streaming_prefill_headroom_bytes), + ds4_bytes_to_gib(e->ssd_streaming_cache_bytes), + ds4_bytes_to_gib(ds4_mul_sat_u64(ds41_graph_bytes(ctx), sessions))); + } + } +#endif /* Also apply explicit optional Q8 preload settings to the runtime * support model when loaded. */ if (support_model_runtime_ready) { (void)ds4_gpu_set_model_fd_for_map(e->mtp_model.fd, e->mtp_model.map); if (!accelerator_cache_model_tensors(e->backend, &e->mtp_model, - NULL, NULL, 0)) { + NULL, NULL, 0 +#ifdef DS4_ROCM_BUILD + , false +#endif + )) { fprintf(stderr, "ds4: %s failed to prepare optional support model cache\n", ds4_backend_name(e->backend)); ds4_engine_close(e); @@ -72470,7 +73459,11 @@ int ds4_engine_tp_bind(ds4_engine *e, struct ds4_tp *tp, char *err, size_t errle #else if (!e || !tp || (e->backend != DS4_BACKEND_METAL && (e->backend != DS4_BACKEND_CUDA || DS4_MODEL_FAMILY != DS4_MODEL_FAMILY_DEEPSEEK41))) { +#ifdef DS4_ROCM_BUILD + snprintf(err, errlen, "tensor parallelism requires Metal or V4.1 ROCm"); +#else snprintf(err, errlen, "tensor parallelism requires Metal or V4.1 CUDA"); +#endif return 0; } if (e->tp.ctx) { @@ -72481,8 +73474,20 @@ int ds4_engine_tp_bind(ds4_engine *e, struct ds4_tp *tp, char *err, size_t errle const uint64_t vec_bytes = (uint64_t)DS4_N_EMBD * sizeof(float); const uint64_t slab_bytes = ds4_tp_slab_bytes((uint32_t)DS4_N_LAYER, (uint32_t)DS4_N_EMBD); e->tp.ctx = tp; +#ifdef DS4_ROCM_BUILD + e->tp.slab = ds4_gpu_tensor_alloc_coherent(slab_bytes); + if (!e->tp.slab) { + e->tp.ctx = NULL; + /* Cleanup owns subsequent allocations through the slab lifetime. */ + snprintf(err, errlen, "tp: slab allocation failed (%llu bytes)", + (unsigned long long)slab_bytes); + return 0; + } + e->tp.zero_vec = ds4_gpu_tensor_alloc_coherent(vec_bytes); +#else e->tp.slab = ds4_gpu_tensor_alloc(slab_bytes); e->tp.zero_vec = ds4_gpu_tensor_alloc(vec_bytes); +#endif e->tp.out_views = calloc(slots, sizeof(*e->tp.out_views)); e->tp.in_views = calloc(slots, sizeof(*e->tp.in_views)); e->tp.batch_out_views = calloc((size_t)DS4_N_LAYER, sizeof(*e->tp.batch_out_views)); @@ -72497,10 +73502,13 @@ int ds4_engine_tp_bind(ds4_engine *e, struct ds4_tp *tp, char *err, size_t errle goto fail; } void *network_slab; +#ifndef DS4_ROCM_BUILD if (e->backend == DS4_BACKEND_CUDA) { e->tp.host_slab = calloc(1, (size_t)slab_bytes); network_slab = e->tp.host_slab; - } else { + } else +#endif + { network_slab = ds4_gpu_tensor_contents(e->tp.slab); } if (!network_slab || !ds4_gpu_tensor_fill_f32(e->tp.slab, 0, slab_bytes / 4u) || @@ -72541,20 +73549,26 @@ int ds4_engine_tp_bind(ds4_engine *e, struct ds4_tp *tp, char *err, size_t errle goto fail; } ds4_gpu_tp_set_batch_exchange(ds4_engine_tp_batch_exchange); -#ifdef __APPLE__ +#if defined(__APPLE__) || defined(DS4_ROCM_BUILD) g_tp_block_ctx = tp; #endif ds4_gpu_tp_set_big_exchange(ds4_engine_tp_big_exchange); /* Reuse the existing half-logit frames for V4.1 on CUDA as well. */ - e->tp.vocab_split = DS4_MODEL_FAMILY == DS4_MODEL_FAMILY_DEEPSEEK4 || - (DS4_MODEL_FAMILY == DS4_MODEL_FAMILY_DEEPSEEK41 && e->backend == DS4_BACKEND_CUDA); + e->tp.vocab_split = DS4_MODEL_FAMILY == DS4_MODEL_FAMILY_DEEPSEEK4 +#ifdef DS4_ROCM_BUILD + || DS4_MODEL_FAMILY == DS4_MODEL_FAMILY_DEEPSEEK41 +#endif +#ifndef DS4_ROCM_BUILD + || (DS4_MODEL_FAMILY == DS4_MODEL_FAMILY_DEEPSEEK41 && e->backend == DS4_BACKEND_CUDA) +#endif + ; e->tp.ctx = tp; e->tp.rank = ds4_tp_rank(tp); e->tp.eval_seq = 0; e->tp.active = true; ds4_log(stderr, DS4_LOG_OK, "tensor parallelism bound: rank %d, 50/50 expert split, %s transport", - e->tp.rank, ds4_tp_is_rdma(tp) ? "rdma" : "tcp"); + e->tp.rank, ds4_tp_transport_name(tp)); return 1; fail: ds4_engine_tp_unbind(e); @@ -72567,7 +73581,7 @@ void ds4_engine_tp_unbind(ds4_engine *e) { if (!e || !e->tp.ctx) return; ds4_gpu_tp_shutdown(); ds4_tp_detach_slab(e->tp.ctx); -#ifdef __APPLE__ +#if defined(__APPLE__) || defined(DS4_ROCM_BUILD) g_tp_block_ctx = NULL; #endif const uint32_t slots = (uint32_t)DS4_N_LAYER * DS4_TP_GATES_PER_LAYER; @@ -72604,6 +73618,12 @@ bool ds4_engine_is_deepseek41(ds4_engine *e) { void ds4_engine_close(ds4_engine *e) { if (!e) return; +#if defined(DS4_ROCM_BUILD) && defined(DS4_HAS_DEEPSEEK41_GPU) + if (e->metal_ready && e->ssd_streaming && DS4_MODEL_FAMILY == DS4_MODEL_FAMILY_DEEPSEEK41) { + (void)ds4_gpu_stream_expert_cache_quiesce(); + (void)ds4_gpu_stream_expert_cache_release_layer_cache(); + } +#endif ds4_engine_tp_unbind(e); ds4_expert_profile_close(); weights_free(&e->weights); @@ -72813,6 +73833,12 @@ int ds4_session_create(ds4_session **out, ds4_engine *e, int ctx_size) { s->ctx_size = ctx_size; #ifdef DS4_HAS_DEEPSEEK41_GPU if (DS4_MODEL_FAMILY == DS4_MODEL_FAMILY_DEEPSEEK41) { +#ifdef DS4_ROCM_BUILD + if (e->ssd_streaming && !e->ds41_stream_slots_ready) { + free(s); + return 1; + } +#endif if (ctx_size > 1048576 || !ds41_memory_admit(e, ds4_add_sat_u64(e->ds41_session_bytes, ds41_graph_bytes((uint32_t)ctx_size)), false) || @@ -72828,6 +73854,25 @@ int ds4_session_create(ds4_session **out, ds4_engine *e, int ctx_size) { s->ds41_graph.tp_rank = (uint32_t)e->tp.rank; s->ds41_graph.tp_out = e->tp.out_views; s->ds41_graph.tp_in = e->tp.in_views; +#ifdef DS4_ROCM_BUILD + /* TP projections pack half-width rows in the full-size scratch + * allocations. Scalar fallback views must use that packed stride. */ + ds41_gpu_graph *g = &s->ds41_graph; + for (uint32_t i = 0; i < g->prefill_cap; ++i) { +#define DS41_TP_ROW_VIEW(name, width) do { \ + ds4_gpu_tensor_free(g->rows_view[i].name); \ + g->rows_view[i].name = ds4_gpu_tensor_view(g->batch.name, \ + (uint64_t)i * (width) * sizeof(float), (uint64_t)(width) * sizeof(float)); \ + if (!g->rows_view[i].name) { \ + ds41_graph_free(g); free(s); return 1; \ + } \ + } while (0) + DS41_TP_ROW_VIEW(q, DS4_N_HEAD / 2u * DS4_N_HEAD_DIM); + DS41_TP_ROW_VIEW(heads, DS4_N_HEAD / 2u * DS4_N_HEAD_DIM); + DS41_TP_ROW_VIEW(low, DS4_N_OUT_GROUP / 2u * DS4_N_LORA_O); +#undef DS41_TP_ROW_VIEW + } +#endif if (e->tp.vocab_split) { const uint64_t half_bytes = (uint64_t)DS4_N_VOCAB / 2u * sizeof(float); s->ds41_graph.tp_logits_half = ds4_gpu_tensor_view(s->ds41_graph.logits, @@ -75385,8 +76430,12 @@ static int ds4_session_sync_internal(ds4_session *s, const ds4_tokens *prompt, c const uint32_t remaining = (uint32_t)(prompt->len - i); if (g->encoder_resident && remaining < 768u) ds41_encoder_release(g, &e->model, &encoder); +#ifdef DS4_ROCM_BUILD + const uint32_t short_count = 0; +#else const uint32_t short_count = decoder_pending ? 0u : ds41_short_prefill_count(g, &e->weights, remaining); +#endif const uint32_t count = short_count ? short_count : g->encoder_resident ? (remaining - 512u < g->prefill_cap ? remaining - 512u : g->prefill_cap) : ds41_prefill_count(g, remaining); @@ -75397,8 +76446,11 @@ static int ds4_session_sync_internal(ds4_session *s, const ds4_tokens *prompt, c /* Never add an encoder sweep just to defer the decoder. A short * remainder finishes the pending decoder here, then runs normally. */ const bool layer_major = count > 1u; - const bool ok = short_count ? + const bool ok = +#ifndef DS4_ROCM_BUILD + short_count ? ds41_graph_short_prefill(g, &e->model, &e->weights, prompt->v + i, count) : +#endif layer_major ? (defer_decoder || decoder_pending ? ds41_graph_prefill_sweep(g, &e->model, &e->weights, prompt->v + i, count, @@ -78129,6 +79181,10 @@ static bool glm53_graph_encode_native_session_batch( #ifdef DS4_HAS_DEEPSEEK41_GPU static bool ds41_sessions_batch_supported(ds4_decode_item *items, int count, const ds4_engine *e) { +#ifdef DS4_ROCM_BUILD + (void)items; (void)count; (void)e; + return false; +#else if (!items || count < 2 || count > DS4_TP_BATCH_MAX_ROWS || !e || e->support_kind != DS4_SUPPORT_NONE) return false; @@ -78150,6 +79206,7 @@ static bool ds41_sessions_batch_supported(ds4_decode_item *items, int count, graphs[i] = &s->ds41_graph; } return ds41_batch_workspace(graphs, count) != NULL; +#endif } #endif @@ -79418,7 +80475,7 @@ static int ds4_sessions_eval_batch_native( const bool native_qkv = native_shared && metal_graph_native_session_batch_qkv_supported(items, count, e); if (native_ds41) { -#ifdef DS4_HAS_DEEPSEEK41_GPU +#if defined(DS4_HAS_DEEPSEEK41_GPU) && !defined(DS4_ROCM_BUILD) ds41_gpu_graph *graphs[DS4_TP_BATCH_MAX_ROWS]; int tokens[DS4_TP_BATCH_MAX_ROWS]; for (uint32_t i = 0; i < prefill_rows; i++) { @@ -80098,7 +81155,11 @@ int ds4_sessions_eval_batch(ds4_decode_item *items, int count, } #ifndef DS4_NO_GPU - if (e->backend == DS4_BACKEND_CUDA) { + if (e->backend == DS4_BACKEND_CUDA +#ifdef DS4_ROCM_BUILD + && !ds4_session_is_ds41(first) +#endif + ) { return ds4_sessions_eval_batch_cuda(items, count, err, errlen); } if (ds4_sessions_eval_batch_metal_supported(items, count, e)) { @@ -80173,7 +81234,11 @@ int ds4_sessions_eval_batch_with_prefill( return ds4_sessions_eval_batch_native(items, count, prefill_session->engine, prefill_session, prefill_prompt, err, errlen); #endif - if (prefill_session->engine->backend == DS4_BACKEND_CUDA) { + if (prefill_session->engine->backend == DS4_BACKEND_CUDA +#ifdef DS4_ROCM_BUILD + && !ds4_session_is_ds41(prefill_session) +#endif + ) { return ds4_sessions_eval_batch_with_prefill_cuda( items, count, prefill_session, prefill_prompt, err, errlen); } diff --git a/ds4.h b/ds4.h index 82798d6781..2d059b4207 100644 --- a/ds4.h +++ b/ds4.h @@ -122,6 +122,7 @@ typedef struct { int leader_port; ds4_tp_transport transport; const char *rdma_device; + int rdma_port; /* Linux RoCE port; zero selects port 1 */ int rdma_gid_index; bool rdma_gid_index_set; bool glm_token_prefill; diff --git a/ds4_deepseek41_gpu.h b/ds4_deepseek41_gpu.h index 39f11699f8..1f37725dd7 100644 --- a/ds4_deepseek41_gpu.h +++ b/ds4_deepseek41_gpu.h @@ -23,7 +23,7 @@ typedef enum { } ds4_v41_activation_format; int ds4_gpu_dsv41_quantize(ds4_gpu_tensor *x, uint32_t width, uint32_t rows, ds4_v41_activation_format format); -#if !defined(__APPLE__) && !defined(DS4_ROCM_BUILD) && !defined(DS4_NO_GPU) +#if !defined(__APPLE__) && !defined(DS4_ROCM_BUILD) && !defined(__HIP_PLATFORM_AMD__) && !defined(DS4_NO_GPU) /* CUDA scalar Q8 shared expert. 1: queued; 0: unsupported, no work queued; * -1: failure. After 1, keep the input/output tensors alive and unchanged * until join, which orders the result before subsequent main-stream work. @@ -80,6 +80,11 @@ int ds4_gpu_dsv41_candidate_filter(ds4_gpu_tensor *scores, uint32_t start, uint32_t ratio); /* Causal index scores over ratio-1/2 compressed keys, without an extra cast * of the already quantized FP4 queries/keys. Scores have source_rows stride. */ +#ifdef DS4_ROCM_BUILD +int ds4_gpu_dsv41_indexer_scores_one(ds4_gpu_tensor *scores, + const ds4_gpu_tensor *q, const ds4_gpu_tensor *weights, + const ds4_gpu_tensor *keys, uint32_t source_rows); +#endif int ds4_gpu_dsv41_indexer_scores_batch(ds4_gpu_tensor *scores, const ds4_gpu_tensor *q, const ds4_gpu_tensor *weights, @@ -124,6 +129,51 @@ int ds4_gpu_dsv41_gather_kv(ds4_gpu_tensor *out, const ds4_gpu_tensor *source, const ds4_gpu_tensor *ids, uint32_t source_rows, uint32_t selected_rows); +#if defined(DS4_ROCM_BUILD) || defined(__HIP_PLATFORM_AMD__) +/* V4.1 IQ2_XXS/Q2_K resident rank reference: global routing IDs, one + * contiguous half of384 experts, and F32 partial output. */ +int ds4_gpu_dsv41_routed_moe_tp_tensor( + ds4_gpu_tensor *out, ds4_gpu_tensor *gate, ds4_gpu_tensor *up, + ds4_gpu_tensor *mid, ds4_gpu_tensor *scratch, + const void *model_map, uint64_t model_size, + uint64_t gate_offset, uint64_t up_offset, uint64_t down_offset, + const ds4_gpu_tensor *selected, const ds4_gpu_tensor *weights, + const ds4_gpu_tensor *x, uint32_t n_tokens, uint32_t tp_rank); +/* Bulk owned gate/up operator. Global routing is preserved; unowned output + * rows are zero. The caller retains the weighted activation/down boundary. */ +int ds4_gpu_dsv41_moe_tp_gate_up( + ds4_gpu_tensor *gate, ds4_gpu_tensor *up, + const void *model_map, uint64_t model_size, + uint64_t gate_offset, uint64_t up_offset, + const ds4_gpu_tensor *selected, const ds4_gpu_tensor *x, + uint32_t n_tokens, uint32_t tp_rank); +/* Owned Q2_K down projection with the inherited hot F16-mid and per-expert + * F16-output boundaries. Scratch retains six slots, zeroing unowned ones. */ +int ds4_gpu_dsv41_moe_tp_down( + ds4_gpu_tensor *out, ds4_gpu_tensor *scratch, + const ds4_gpu_tensor *mid, const ds4_gpu_tensor *selected, + const void *model_map, uint64_t model_size, uint64_t down_offset, + uint32_t n_tokens, uint32_t tp_rank); +/* HC2048 F32 SGEMM: 1 enqueued, 0 unsupported, -1 failure. + * Never fall back after failure. Full heads scratch remains live on stream0; + * free the plan before graph tensors. Input is already RMS-normalized; + * output remains F32 for Sinkhorn. */ +typedef struct ds4_gpu_dsv41_hc_plan ds4_gpu_dsv41_hc_plan; +int ds4_gpu_dsv41_hc_project(ds4_gpu_dsv41_hc_plan **plan, + ds4_gpu_tensor *out, const void *model_map, + uint64_t model_size, uint64_t weight_offset, + uint32_t rows, const ds4_gpu_tensor *input, + ds4_gpu_tensor *full_heads_scratch); +void ds4_gpu_dsv41_hc_plan_free(ds4_gpu_dsv41_hc_plan *plan); + +/* Preserve V4.1 activation formats while applying Q8 weights. */ +int ds4_gpu_dsv41_q8_projection_rows(ds4_gpu_tensor *out, + const void *model_map, uint64_t model_size, + uint64_t weight_offset, uint32_t width, + uint32_t outputs, uint32_t rows, + const ds4_gpu_tensor *in); +#endif + #ifdef __cplusplus } #endif diff --git a/ds4_deepseek4_vision_gpu.cuh b/ds4_deepseek4_vision_gpu.cuh index d84e492329..bbd3f0cc53 100644 --- a/ds4_deepseek4_vision_gpu.cuh +++ b/ds4_deepseek4_vision_gpu.cuh @@ -125,6 +125,37 @@ __global__ static void deepseek4_vision_swiglu_split_kernel( (gate / (1.0f + expf(-gate))) * up); } +#ifdef __HIP_PLATFORM_AMD__ +/* V4.1 rounds the biased attention projection before adding the residual. */ +__global__ static void deepseek41_vision_bias_residual_kernel( + float *x, + const uint16_t *bias, + const float *residual, + uint64_t count, + uint32_t width) { + const uint64_t i = (uint64_t)blockIdx.x * blockDim.x + threadIdx.x; + if (i >= count) return; + const float projected = deepseek4_vision_round_bf16_dev( + x[i] + glm53_vision_bf16(bias + i % width)); + x[i] = deepseek4_vision_round_bf16_dev(projected + residual[i]); +} + +__global__ static void deepseek41_vision_swiglu_split_kernel( + float *out, + const float *gate_up, + uint64_t count, + uint32_t width) { + const uint64_t i = (uint64_t)blockIdx.x * blockDim.x + threadIdx.x; + if (i >= count) return; + const uint32_t d = i % width; + const uint64_t source = (i / width) * width * 2u + d; + const float gate = gate_up[source]; + const float activated = deepseek4_vision_round_bf16_dev( + gate / (1.0f + expf(-gate))); + out[i] = deepseek4_vision_round_bf16_dev(activated * gate_up[source + width]); +} +#endif + __global__ static void deepseek4_vision_add_residual_kernel( float *x, const float *residual, @@ -195,6 +226,24 @@ extern "C" int ds4_gpu_deepseek4_vision_encode( const ds4_deepseek4_vision_weights *weights) { if (!out || !patches || !model_map || !weights || grid_h == 0u || grid_w == 0u || grid_h > UINT32_MAX / grid_w) return 0; +#ifdef __HIP_PLATFORM_AMD__ + /* ROCm includes the public weights struct, including projection_dim. CUDA + * retains the older4096-wide encoder and its historical private layout. */ + const uint32_t projection = weights->projection_dim ? weights->projection_dim : 4096u; + if (projection != 4096u && projection != 5120u) return 0; + if (projection == 5120u) { + const uint64_t llm_h = ((uint64_t)grid_h + 2u) / 3u; + const uint64_t llm_w = ((uint64_t)grid_w + 2u) / 3u; + if (llm_h * (llm_w + 1u) + 2u > 1024u) return 0; + } +#endif +#ifdef __HIP_PLATFORM_AMD__ +#define DS4_VISION_PROJECTION projection +#define DS4_VISION_ALIGNED aligned_projection +#else +#define DS4_VISION_PROJECTION 4096u +#define DS4_VISION_ALIGNED aligned4096 +#endif const uint32_t rows = grid_h * grid_w; const uint32_t aligned_rows = ((grid_h + 2u) / 3u) * ((grid_w + 2u) / 3u); @@ -202,7 +251,7 @@ extern "C" int ds4_gpu_deepseek4_vision_encode( const uint64_t row2816 = (uint64_t)rows * 2816u; const uint64_t row3072 = (uint64_t)rows * 3072u; const uint64_t row5632 = (uint64_t)rows * 5632u; - const uint64_t aligned4096 = (uint64_t)aligned_rows * 4096u; + const uint64_t DS4_VISION_ALIGNED = (uint64_t)aligned_rows * DS4_VISION_PROJECTION; const uint64_t aligned9216 = (uint64_t)aligned_rows * 9216u; if (row5632 > SIZE_MAX / sizeof(float) || aligned9216 > SIZE_MAX / sizeof(float)) return 0; @@ -229,13 +278,18 @@ extern "C" int ds4_gpu_deepseek4_vision_encode( DSV4_VISION_ALLOC(mlp_w1, row5632); DSV4_VISION_ALLOC(mlp_mid, row2816); DSV4_VISION_ALLOC(align_in, aligned9216); - DSV4_VISION_ALLOC(align_a, aligned4096); - DSV4_VISION_ALLOC(align_b, aligned4096); + DSV4_VISION_ALLOC(align_a, DS4_VISION_ALIGNED); + DSV4_VISION_ALLOC(align_b, DS4_VISION_ALIGNED); #undef DSV4_VISION_ALLOC if (!ds4_gpu_tensor_write( patch, 0, patches, (uint64_t)rows * 588u * sizeof(float)) || !ds4_gpu_begin_commands()) goto cleanup; +#ifdef __HIP_PLATFORM_AMD__ + ok = DS4_VISION_PROJECTION != 5120u || deepseek4_vision_round_tensor( + patch, (uint64_t)rows * 588u, "V4.1 vision patch input round"); + if (ok) +#endif ok = ds4_gpu_glm53_matmul_bf16( a, model_map, model_size, weights->patch_weight, 588u, 1024u, patch, rows); @@ -307,6 +361,15 @@ extern "C" int ds4_gpu_deepseek4_vision_encode( if (!bias) ok = 0; } if (ok) { +#ifdef __HIP_PLATFORM_AMD__ + if (DS4_VISION_PROJECTION == 5120u) { + deepseek41_vision_bias_residual_kernel<<< + (unsigned)((row1024 + 255u) / 256u), 256u, 0, + DS4_DEEPSEEK4_VISION_STREAM>>>( + (float *)tmp->ptr, bias, (const float *)cur->ptr, + row1024, 1024u); + } else +#endif glm53_vision_bias_kernel<<< (unsigned)((row1024 + 255u) / 256u), 256u, 0, DS4_DEEPSEEK4_VISION_STREAM>>>( @@ -340,6 +403,15 @@ extern "C" int ds4_gpu_deepseek4_vision_encode( if (ok) ok = deepseek4_vision_round_tensor( mlp_w1, row5632, "DeepSeek vision MLP input round"); if (ok) { +#ifdef __HIP_PLATFORM_AMD__ + if (DS4_VISION_PROJECTION == 5120u) { + deepseek41_vision_swiglu_split_kernel<<< + (unsigned)((row2816 + 255u) / 256u), 256u, 0, + DS4_DEEPSEEK4_VISION_STREAM>>>( + (float *)mlp_mid->ptr, (const float *)mlp_w1->ptr, + row2816, 2816u); + } else +#endif deepseek4_vision_swiglu_split_kernel<<< (unsigned)((row2816 + 255u) / 256u), 256u, 0, DS4_DEEPSEEK4_VISION_STREAM>>>( @@ -386,43 +458,43 @@ extern "C" int ds4_gpu_deepseek4_vision_encode( } if (ok) ok = ds4_gpu_glm53_matmul_bf16( align_a, model_map, model_size, weights->aligner_w1, - 9216u, 4096u, align_in, aligned_rows); + 9216u, DS4_VISION_PROJECTION, align_in, aligned_rows); if (ok) { bias = glm53_vision_weight( - model_map, model_size, weights->aligner_w1_bias, 4096u, + model_map, model_size, weights->aligner_w1_bias, DS4_VISION_PROJECTION, "DeepSeek vision aligner hidden bias"); if (!bias) ok = 0; } if (ok) { deepseek4_vision_gelu_bias_kernel<<< - (unsigned)((aligned4096 + 255u) / 256u), 256u, 0, + (unsigned)((DS4_VISION_ALIGNED + 255u) / 256u), 256u, 0, DS4_DEEPSEEK4_VISION_STREAM>>>( (float *)align_b->ptr, (const float *)align_a->ptr, - bias, aligned4096, 4096u); + bias, DS4_VISION_ALIGNED, DS4_VISION_PROJECTION); ok = glm53_vision_launch_ok("DeepSeek vision aligner GELU"); } if (ok) ok = ds4_gpu_glm53_matmul_bf16( align_a, model_map, model_size, weights->aligner_w2, - 4096u, 4096u, align_b, aligned_rows); + DS4_VISION_PROJECTION, DS4_VISION_PROJECTION, align_b, aligned_rows); if (ok) { bias = glm53_vision_weight( - model_map, model_size, weights->aligner_w2_bias, 4096u, + model_map, model_size, weights->aligner_w2_bias, DS4_VISION_PROJECTION, "DeepSeek vision aligner output bias"); if (!bias) ok = 0; } if (ok) { glm53_vision_bias_kernel<<< - (unsigned)((aligned4096 + 255u) / 256u), 256u, 0, + (unsigned)((DS4_VISION_ALIGNED + 255u) / 256u), 256u, 0, DS4_DEEPSEEK4_VISION_STREAM>>>( - (float *)align_a->ptr, bias, NULL, aligned4096, 4096u); + (float *)align_a->ptr, bias, NULL, DS4_VISION_ALIGNED, DS4_VISION_PROJECTION); ok = glm53_vision_launch_ok( "DeepSeek vision aligner output bias"); } if (ok) ok = deepseek4_vision_round_tensor( - align_a, aligned4096, "DeepSeek vision aligner output round"); + align_a, DS4_VISION_ALIGNED, "DeepSeek vision aligner output round"); if (ds4_gpu_end_commands() == 0) ok = 0; if (ok) ok = ds4_gpu_tensor_read( - align_a, 0, out, aligned4096 * sizeof(float)); + align_a, 0, out, DS4_VISION_ALIGNED * sizeof(float)); cleanup: ds4_gpu_tensor_free(align_b); @@ -442,3 +514,6 @@ cleanup: } #undef DS4_DEEPSEEK4_VISION_STREAM + +#undef DS4_VISION_PROJECTION +#undef DS4_VISION_ALIGNED diff --git a/ds4_engram.c b/ds4_engram.c index 8e4d8e2b23..3c9a970476 100644 --- a/ds4_engram.c +++ b/ds4_engram.c @@ -15,6 +15,9 @@ #include #else #include +#ifdef DS4_ROCM_BUILD +#include +#endif #endif bool ds4_engram_layout_valid(const ds4_engram_layout *l) { @@ -231,6 +234,40 @@ static void *read_batch_thread(void *context) { read_batch_part(reader->batch, reader->part); return NULL; } +#ifdef DS4_ROCM_BUILD + +static bool read_batch_pthreads(engram_batch *batch) { + pthread_t threads[ENGRAM_READERS - 1]; + engram_reader readers[ENGRAM_READERS - 1]; + size_t started = 0; + int create_error = 0; + batch->readers = ENGRAM_READERS; + /* The caller reads partition zero; bounded workers read the other fifteen. + * Partition boundaries and duplicate reuse match dispatch_apply_f exactly. */ + for (size_t part = 1; part < batch->readers; part++) { + readers[started] = (engram_reader){batch, part}; + create_error = pthread_create(&threads[started], NULL, + read_batch_thread, &readers[started]); + if (create_error) break; + started++; + } + if (!create_error) read_batch_part(batch, 0); + for (size_t i = 0; i < started; i++) { + const int error = pthread_join(threads[i], NULL); + if (error) { + /* Stack arguments, request rows and output cannot be released if a + * worker's termination is unproven. Do not return a live borrower. */ + fprintf(stderr, "ds4: cannot join Engram reader safely (%d)\n", error); + abort(); + } + } + if (create_error) { + errno = create_error; + return false; + } + return true; +} +#endif #endif bool ds4_engram_read_batch(const ds4_engram_table *t, const uint32_t *rows, @@ -274,6 +311,8 @@ bool ds4_engram_read_batch(const ds4_engram_table *t, const uint32_t *rows, #ifdef __APPLE__ dispatch_apply_f(batch.readers, dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), &batch, read_batch_part); +#elif defined(DS4_ROCM_BUILD) + if (!read_batch_pthreads(&batch)) { ok = false; break; } #else pthread_t threads[ENGRAM_READERS - 1]; engram_reader readers[ENGRAM_READERS - 1]; diff --git a/ds4_engram.h b/ds4_engram.h index 1cc9c3c719..90b3ed29a8 100644 --- a/ds4_engram.h +++ b/ds4_engram.h @@ -55,8 +55,9 @@ bool ds4_engram_read(const ds4_engram_table *table, const uint32_t *rows, size_t count, float *out); /* Read COLS rows per token, restoring token order after deduplicated disk reads. * Input stride is in row IDs; output is packed [token][COLS][DIM]. Temporary - * storage is bounded to 384 KiB, independent of the table and prefix size. - * On macOS, large batches use bounded concurrent pread readers. */ + * request storage is bounded to 384 KiB, independent of table/prefix size. + * On macOS and Linux ROCm, large batches use 16 concurrent pread readers; + * their bounded worker stacks are additional to request storage. */ bool ds4_engram_read_batch(const ds4_engram_table *table, const uint32_t *rows, size_t tokens, size_t stride, float *out); diff --git a/ds4_gpu.h b/ds4_gpu.h index a4c76a5d98..04ebb1d0e4 100644 --- a/ds4_gpu.h +++ b/ds4_gpu.h @@ -198,6 +198,12 @@ int ds4_gpu_set_aux_model_map_range(const void *model_map, uint64_t map_size); int ds4_gpu_set_model_map_spans(const void *model_map, uint64_t model_size, const uint64_t *offsets, const uint64_t *sizes, uint32_t count, uint64_t max_tensor_bytes); int ds4_gpu_cache_model_range(const void *model_map, uint64_t model_size, uint64_t offset, uint64_t bytes, const char *label); +#if defined(DS4_ROCM_BUILD) || defined(__HIP_PLATFORM_AMD__) +/* V4.1 resident startup: exact new arenas on gfx1151, registered primary file only. */ +int ds4_gpu_cache_model_range_exact(const void *model_map, uint64_t model_size, uint64_t offset, uint64_t bytes, const char *label); +/* Finish primary V4.1 startup uploads; later uploads recreate their staging pool. */ +int ds4_gpu_release_model_upload_staging(const void *model_map, uint64_t model_size); +#endif int ds4_gpu_cache_q8_f16_range(const void *model_map, uint64_t model_size, uint64_t offset, uint64_t bytes, uint64_t in_dim, uint64_t out_dim, const char *label); int ds4_gpu_q8_cache_suppressed(void); void ds4_gpu_set_q8_cache_suppressed(int suppressed); @@ -239,6 +245,13 @@ int ds4_gpu_should_use_managed_kv_cache(uint64_t kv_cache_bytes, uint64_t contex void ds4_gpu_set_quality(bool quality); void ds4_gpu_set_glm_model(bool enabled); void ds4_gpu_set_ssd_streaming(bool enabled); +#if defined(DS4_ROCM_BUILD) || defined(__HIP_PLATFORM_AMD__) +/* Called after set_ssd_streaming has released the preceding model caches. */ +void ds4_gpu_set_deepseek41_model(bool enabled); +/* Override the allocator reserve for a model with an explicit admission plan. + * set_ssd_streaming resets this to the ROCm default for each engine open. */ +void ds4_gpu_set_streaming_free_reserve(uint64_t bytes); +#endif void ds4_gpu_set_glm_streaming_prefill_full_layer(bool enabled); #ifdef __APPLE__ int ds4_gpu_device_is_pre_m5_apple_silicon(void); @@ -399,9 +412,37 @@ int ds4_gpu_stream_expert_cache_prepare_selected_batch( uint32_t n_tokens, uint32_t n_selected); #endif -#ifdef DS4_ROCM_BUILD +#if defined(DS4_ROCM_BUILD) || defined(__HIP_PLATFORM_AMD__) +typedef struct ds4_gpu_stream_expert_memory { + uint64_t dynamic_bytes, layer_bytes, selected_bytes, pinned_bytes; +} ds4_gpu_stream_expert_memory; +/* Call reserve/query/quiesce without a concurrent full-layer loader. Slab + * capacity includes free slots; it is not the current cached-entry count. */ +int ds4_gpu_stream_expert_cache_reserve_layers( + const ds4_gpu_stream_expert_table *even, + const ds4_gpu_stream_expert_table *odd); +int ds4_gpu_stream_expert_cache_note_layer_consumed( + const ds4_gpu_stream_expert_table *table); +int ds4_gpu_stream_expert_cache_quiesce(void); +int ds4_gpu_stream_expert_cache_get_memory(ds4_gpu_stream_expert_memory *out); int ds4_gpu_stream_expert_cache_load_layer( const ds4_gpu_stream_expert_table *table); +/* V4.1's single-owner sweep prepares on the graph thread after quiescing + * selected reads and joining its previous loader. All source copies finish + * before return; only this immutable descriptor crosses to the disk worker. + * Do not mutate/reuse a plan while its loader is running. */ +typedef struct ds4_gpu_dsv41_stream_layer_plan { + ds4_gpu_stream_expert_table table; + uint64_t generation; + uint64_t hits[6]; +} ds4_gpu_dsv41_stream_layer_plan; +int ds4_gpu_dsv41_stream_prepare_layer( + const ds4_gpu_stream_expert_table *table, + ds4_gpu_dsv41_stream_layer_plan *plan); +int ds4_gpu_dsv41_stream_load_layer( + const ds4_gpu_dsv41_stream_layer_plan *plan); +int ds4_gpu_dsv41_stream_cancel_layer( + const ds4_gpu_dsv41_stream_layer_plan *plan); int ds4_gpu_stream_expert_cache_seed_from_layer_selected( const ds4_gpu_stream_expert_table *table, const ds4_gpu_tensor *selected, @@ -416,8 +457,9 @@ int ds4_gpu_stream_expert_cache_seed_experts( const int32_t *expert_ids, const uint32_t *expert_priorities, uint32_t n_experts); -#ifdef __APPLE__ -/* Seed from mapped weights with blits appended to the active command buffer. */ +#if defined(__APPLE__) || defined(DS4_ROCM_BUILD) || defined(__HIP_PLATFORM_AMD__) +/* Metal appends mapped-weight blits to its command buffer; ROCm copies from + * a matching full-layer slot and completes those D2D copies before return. */ int ds4_gpu_stream_expert_cache_seed_experts_gpu_copy( const ds4_gpu_stream_expert_table *table, const int32_t *expert_ids, @@ -433,7 +475,6 @@ void ds4_gpu_print_memory_report(const char *label); void ds4_gpu_model_residency_skip(int skip); /* Submit one trivial command buffer (first-submission costs paid at load). */ int ds4_gpu_warm_command_queue(void); - /* Tensor-parallel sliced projections (Metal decode path only). * * ds4_gpu_matmul_q8_0_kslice_tensor computes a k-range partial matvec: diff --git a/ds4_gpu_tp.h b/ds4_gpu_tp.h index 3c0328d750..866286370d 100644 --- a/ds4_gpu_tp.h +++ b/ds4_gpu_tp.h @@ -12,7 +12,8 @@ extern "C" { /* Metal queues each gate on its service thread and orders GPU arrival/release * with events or flags. CUDA waits for its local stream and calls the exchange - * synchronously. Callbacks return nonzero on success; both ranks must issue + * synchronously. ROCm queues gates on its service thread using coherent + * arrival/release flags and a guarded GPU consumer. Callbacks return nonzero on success; both ranks must issue * the same gate sequence. Shutdown precedes transport/slab destruction. */ typedef int (*ds4_gpu_tp_exchange_fn)(void *ud, uint32_t layer, uint32_t gate, uint64_t seq); typedef int (*ds4_gpu_tp_batch_exchange_fn)(void *ud, uint32_t layer, @@ -51,6 +52,23 @@ void ds4_gpu_tp_keepalive_pause(int paused); * the output-projection partials at the big gate. */ void ds4_gpu_tp_set_attn_head_split(int enabled); +#if defined(DS4_ROCM_BUILD) || defined(__HIP_PLATFORM_AMD__) +/* Split the bulk arrival from its wait so independent compute may overlap. */ +int ds4_gpu_tp_big_gate_overlap_supported(void); +int ds4_gpu_tp_big_gate_begin(uint32_t layer, uint32_t rows, + const ds4_gpu_tensor *out_t, + ds4_gpu_tensor *in_t, uint64_t bytes); +int ds4_gpu_tp_big_gate_join(uint32_t layer, uint32_t rows, + ds4_gpu_tensor *in_t, uint64_t bytes); +/* Fail the gate and drain GPU users before releasing private scratch. */ +void ds4_gpu_tp_big_gate_abort(void); +/* Host-coherent slab allocation; views preserve host/device aliases. */ +ds4_gpu_tensor *ds4_gpu_tensor_alloc_coherent(uint64_t bytes); +/* Release the queue slot after reduction; skip peer data after failure. */ +int ds4_gpu_tp_add_tensor(ds4_gpu_tensor *out, const ds4_gpu_tensor *a, + const ds4_gpu_tensor *b, uint32_t n); +#endif + #ifdef __cplusplus } #endif diff --git a/ds4_help.c b/ds4_help.c index e514ac3c28..92df7c651d 100644 --- a/ds4_help.c +++ b/ds4_help.c @@ -255,6 +255,9 @@ static void print_distributed(FILE *fp, const help_colors *c) { fputc('\n', fp); opt(fp, c, "--tensor-parallel", "Switch --role/--listen/--coordinator to two-machine tensor parallelism."); opt(fp, c, "--transport auto|rdma|tcp", "Tensor gate transport. Default: auto"); +#if defined(__linux__) && defined(DS4_ROCM_BUILD) + opt(fp, c, "--rdma-port N", "Select the local active Ethernet verbs port. Default: 1"); +#endif opt(fp, c, "--rdma-device NAME", "Select a verbs device when auto-detection is ambiguous."); opt(fp, c, "--rdma-gid-index N", "Select the local verbs GID index."); opt(fp, c, "--tensor-parallel-token-prefill", "GLM diagnostic: prefill one token at a time for exact arithmetic."); diff --git a/ds4_rocm.cu b/ds4_rocm.cu index 566248edf0..85e9745b39 100644 --- a/ds4_rocm.cu +++ b/ds4_rocm.cu @@ -81,6 +81,7 @@ struct ds4_gpu_tensor { void *ptr; uint64_t bytes; int owner; + void *host_ptr = nullptr; /* coherent host allocation or view; otherwise NULL */ }; typedef struct { @@ -124,6 +125,8 @@ __device__ __constant__ static const int8_t cuda_mxfp4_values_x2[16] = { #include "ds4_iq2_tables_cuda.inc" +static int ds4_rocm_is_gfx1151(void); + #include "rocm/ds4_rocm_runtime.cuh" #include "rocm/ds4_rocm_common.cuh" @@ -171,21 +174,13 @@ extern "C" int ds4_gpu_dspark_gfx1151_fast_path(void) { #include "rocm/ds4_rocm_current_api_compat.cuh" +#include "rocm/ds4_rocm_v41.cuh" + #include "ds4_glm53_vision_gpu.cuh" #include "ds4_deepseek4_vision_gpu.cuh" #include "rocm/ds4_rocm_deepseek4_vision.cuh" -/* Tensor-parallel gates are Metal-only; stubs keep shared graph code - * linkable (TP option validation rejects non-Metal backends). */ -extern "C" int ds4_gpu_tp_gate_encode(uint32_t layer, uint32_t gate) { - (void)layer; (void)gate; - fprintf(stderr, DS4_GPU_LOG_PREFIX "tensor parallelism is Metal-only\n"); - return 0; -} - -extern "C" void ds4_gpu_tp_set_batch_exchange(ds4_gpu_tp_batch_exchange_fn fn) { - (void)fn; -} +#include "rocm/ds4_rocm_tp.cuh" extern "C" void ds4_gpu_tp_suspend_expert_sharding(int suspend) { (void)suspend; @@ -203,24 +198,6 @@ extern "C" void ds4_gpu_model_residency_skip(int skip) { (void)skip; } -extern "C" void ds4_gpu_tp_set_big_exchange(ds4_gpu_tp_big_exchange_fn fn) { - (void)fn; -} - -extern "C" int ds4_gpu_tp_big_gate_encode(uint32_t layer, uint32_t rows, - const ds4_gpu_tensor *out_t, - ds4_gpu_tensor *in_t, - uint64_t bytes) { - (void)layer; (void)rows; (void)out_t; (void)in_t; (void)bytes; - return 0; -} - -extern "C" int ds4_gpu_tp_batch_gate_encode(uint32_t layer, uint32_t rows) { - (void)layer; (void)rows; - fprintf(stderr, DS4_GPU_LOG_PREFIX "tensor parallelism is Metal-only\n"); - return 0; -} - extern "C" int ds4_gpu_matmul_q8_0_kslice_tensor( ds4_gpu_tensor *out, const void *model_map, uint64_t model_size, uint64_t weight_offset, uint64_t full_in_dim, uint64_t k_off, diff --git a/ds4_tp.c b/ds4_tp.c index 5edd905c68..0b54479f3e 100644 --- a/ds4_tp.c +++ b/ds4_tp.c @@ -21,7 +21,6 @@ #include #include #include -#include #include #include #include @@ -29,7 +28,20 @@ #include "ds4_tp.h" #include "ds4_gpu.h" -#if (defined(__APPLE__) || defined(__linux__)) && defined(__has_include) +#if defined(__linux__) && defined(DS4_ROCM_BUILD) +#define DS4_TP_LINUX 1 +#include "ds4_tp_io.h" +#include +#if !defined(DS4_TP_NO_ROCE) && defined(__has_include) +#if __has_include() +#include +#include +#define DS4_TP_HAVE_ROCE 1 +#endif +#endif +#endif + +#if !defined(DS4_TP_LINUX) && (defined(__APPLE__) || defined(__linux__)) && defined(__has_include) #if __has_include() #include #include @@ -39,8 +51,13 @@ #define DS4_TP_MAGIC UINT32_C(0x44533454) /* "DS4T" */ #define DS4_TP_BATCH_MAGIC UINT32_C(0x44533442) /* "DS4B" */ +#ifdef DS4_TP_LINUX +/* ROCm framing/checkpoints differ from CUDA/Metal wire14. */ +#define DS4_TP_PROTOCOL_VERSION 15u +#else /* V4.1 CUDA workers now return half-logit frames after successful work. */ #define DS4_TP_PROTOCOL_VERSION 14u +#endif #define DS4_TP_DEFAULT_TIMEOUT_SEC 300 /* Once both ranks enter a Metal gate, a live exchange normally completes in @@ -71,6 +88,9 @@ typedef struct { uint32_t gates_per_token; uint32_t pad; uint64_t gate_slot_mask[DS4_TP_GATE_MASK_WORDS]; +#ifdef DS4_TP_LINUX + uint64_t nonce; +#endif } ds4_tp_hello_fixed; typedef struct { @@ -185,6 +205,10 @@ struct ds4_tp { int control_fd; int data_fd; /* TCP fallback, headers, and verify gates */ bool rdma_active; +#ifdef DS4_TP_LINUX + struct ds4_tp_roce *roce; + uint64_t epoch; +#endif uint32_t peer_ctx; uint32_t n_layer; uint32_t n_embd; @@ -233,6 +257,10 @@ static void tp_set_err(char *err, size_t errlen, const char *fmt, ...) { va_end(ap); } +#ifdef DS4_TP_LINUX +#include "ds4_tp_roce.h" +#endif + static int tp_write_full(int fd, const void *buf, size_t len) { const char *p = buf; while (len) { @@ -478,6 +506,9 @@ void ds4_tp_usage(FILE *fp) { " --tensor-parallel-token-prefill\n" " GLM diagnostic: prefill one token at a time.\n" " --debug-hash Cross-check hidden state every n tokens.\n"); +#ifdef DS4_TP_LINUX + fprintf(fp, " --rdma-port <1..255> Linux RoCE port (default 1).\n"); +#endif } int ds4_tp_parse_cli_arg( @@ -516,6 +547,18 @@ int ds4_tp_parse_cli_arg( } opt->rdma_gid_index = (int)value; opt->rdma_gid_index_set = true; +#ifdef DS4_TP_LINUX + } else if (!strcmp(arg, "--rdma-port")) { + if (i + 1 >= argc) goto missing; + char *end = NULL; + errno = 0; + long value = strtol(argv[++i], &end, 10); + if (errno || !end || *end || value < 1 || value > 255) { + tp_set_err(err, errlen, "invalid --rdma-port %s", argv[i]); + return DS4_TP_CLI_ERROR; + } + opt->rdma_port = (int)value; +#endif } else if (!strcmp(arg, "--tensor-parallel-token-prefill")) { opt->glm_token_prefill = true; } else if (!strcmp(arg, "--debug-hash")) { @@ -604,7 +647,7 @@ int ds4_tp_validate_engine_options( { if (!ds4_tp_enabled(&opt->tp)) { if (opt->tp.requested || opt->tp.transport != DS4_TP_TRANSPORT_AUTO || - opt->tp.rdma_device || opt->tp.rdma_gid_index_set || + opt->tp.rdma_device || opt->tp.rdma_port || opt->tp.rdma_gid_index_set || opt->tp.glm_token_prefill || opt->tp.debug_hash != 0) { tp_set_err(err, errlen, "tensor-parallel options require --tensor-parallel and --role"); @@ -612,6 +655,18 @@ int ds4_tp_validate_engine_options( } return 1; } +#ifdef DS4_ROCM_BUILD + if (opt->backend != DS4_BACKEND_METAL +#ifdef DS4_TP_LINUX + && opt->backend != DS4_BACKEND_CUDA +#endif + ) { +#ifdef DS4_TP_LINUX + tp_set_err(err, errlen, "tensor parallelism requires Metal or V4.1 ROCm"); +#else + tp_set_err(err, errlen, "tensor parallelism requires the Metal backend"); +#endif +#else bool supported_backend = opt->backend == DS4_BACKEND_METAL; #if !defined(__APPLE__) && !defined(DS4_ROCM_BUILD) && !defined(DS4_NO_GPU) supported_backend |= opt->backend == DS4_BACKEND_CUDA; @@ -622,8 +677,17 @@ int ds4_tp_validate_engine_options( } if (opt->backend == DS4_BACKEND_CUDA && (opt->cuda_tensor_parallel || opt->ssd_streaming)) { tp_set_err(err, errlen, "network CUDA TP requires one GPU per rank and resident expert shards"); +#endif return 0; } +#ifdef DS4_TP_LINUX + if (opt->backend == DS4_BACKEND_CUDA && + (opt->ssd_streaming || opt->dspark || opt->glm_mtp || + (opt->mtp_path && opt->mtp_path[0]) || opt->cuda_tensor_parallel)) { + tp_set_err(err, errlen, "V4.1 ROCm network TP requires resident weights without speculative drafting or local multi-GPU TP"); + return 0; + } +#endif if (opt->distributed.role != DS4_DISTRIBUTED_NONE) { tp_set_err(err, errlen, "tensor parallelism and --role distributed modes are exclusive"); return 0; @@ -1921,6 +1985,22 @@ static void tp_rdma_close(ds4_tp *tp) { * Bring-up. * --------------------------------------------------------------------- */ +#ifdef DS4_TP_LINUX +/* Capability bits use the public transport enum; AUTO is a request, not a capability. */ +static int tp_linux_select(uint32_t request, uint32_t peer_request, + uint32_t caps, uint32_t peer_caps) { + if (request > DS4_TP_TRANSPORT_TCP || + peer_request > DS4_TP_TRANSPORT_TCP) return -1; + if (request && peer_request && request != peer_request) return -1; + uint32_t chosen = request ? request : peer_request; + uint32_t common = caps & peer_caps; + if (chosen) return common & (1u << chosen) ? (int)chosen : -1; + if (common & (1u << DS4_TP_TRANSPORT_RDMA)) return DS4_TP_TRANSPORT_RDMA; + return common & (1u << DS4_TP_TRANSPORT_TCP) ? DS4_TP_TRANSPORT_TCP : -1; +} + +#endif + static int tp_hello_exchange(ds4_tp *tp, const ds4_tp_identity *id, int rdma_ok, char *err, size_t errlen) { ds4_tp_hello_fixed mine = { @@ -1939,11 +2019,28 @@ static int tp_hello_exchange(ds4_tp *tp, const ds4_tp_identity *id, int rdma_ok, .gate_slot_step = id->gate_slot_step, .gates_per_token = id->gates_per_token, }; +#ifdef DS4_TP_LINUX + uint32_t caps = (1u << DS4_TP_TRANSPORT_TCP) | + (rdma_ok ? 1u << DS4_TP_TRANSPORT_RDMA : 0u); + mine.pad = (uint32_t)tp->opt.transport | (caps << 8); + ssize_t nonce_bytes; + do { nonce_bytes = getrandom(&mine.nonce, sizeof(mine.nonce), 0); } + while (nonce_bytes < 0 && errno == EINTR); + if (nonce_bytes != sizeof(mine.nonce)) { + tp_set_err(err, errlen, "tp connection nonce: %s", strerror(errno)); + return 0; + } +#endif memcpy(mine.gate_slot_mask, id->gate_slot_mask, sizeof(mine.gate_slot_mask)); ds4_tp_hello_fixed theirs; +#ifdef DS4_TP_LINUX + if (!ds4_tp_io_exchange(tp->control_fd, &mine, &theirs, + offsetof(ds4_tp_hello_fixed, nonce), tp->timeout_sec * 1000u, &tp->failed)) { +#else if (!tp_write_full(tp->control_fd, &mine, sizeof(mine)) || !tp_read_full(tp->control_fd, &theirs, sizeof(theirs))) { +#endif tp_set_err(err, errlen, "tp hello exchange failed"); return 0; } @@ -1956,6 +2053,13 @@ static int tp_hello_exchange(ds4_tp *tp, const ds4_tp_identity *id, int rdma_ok, theirs.version, DS4_TP_PROTOCOL_VERSION); return 0; } +#ifdef DS4_TP_LINUX + if (!ds4_tp_io_exchange(tp->control_fd, &mine.nonce, &theirs.nonce, + sizeof(mine.nonce), tp->timeout_sec * 1000u, &tp->failed)) { + tp_set_err(err, errlen, "tp nonce exchange failed"); + return 0; + } +#endif if (theirs.role == mine.role) { tp_set_err(err, errlen, "tp hello: both sides claim role %u", mine.role); return 0; @@ -1995,6 +2099,17 @@ static int tp_hello_exchange(ds4_tp *tp, const ds4_tp_identity *id, int rdma_ok, memcpy(tp->gate_slot_mask, id->gate_slot_mask, sizeof(tp->gate_slot_mask)); tp_slab_layout(tp); +#ifdef DS4_TP_LINUX + int selected = tp_linux_select(mine.pad & 255u, theirs.pad & 255u, + mine.pad >> 8, theirs.pad >> 8); + if (selected < 0) { + tp_set_err(err, errlen, "tp: incompatible transport requests or unavailable configured transport (local=%u peer=%u)", + mine.pad & 255u, theirs.pad & 255u); + return 0; + } + tp->rdma_active = selected == DS4_TP_TRANSPORT_RDMA; + tp->epoch = mine.nonce ^ theirs.nonce; +#else /* Transport decision: RDMA only when both sides can. */ int want_rdma = tp->opt.transport != DS4_TP_TRANSPORT_TCP; tp->rdma_active = want_rdma && rdma_ok && theirs.rdma_ok; @@ -2003,6 +2118,7 @@ static int tp_hello_exchange(ds4_tp *tp, const ds4_tp_identity *id, int rdma_ok, rdma_ok ? "the peer" : "this"); return 0; } +#endif return 1; } @@ -2023,6 +2139,7 @@ int ds4_tp_create( tp->rank = opt->role == DS4_TP_LEADER ? 0 : 1; tp->control_fd = -1; tp->data_fd = -1; + atomic_init(&tp->failed, false); tp->timeout_sec = DS4_TP_DEFAULT_TIMEOUT_SEC; const char *tmo = getenv("DS4_TP_TIMEOUT_SEC"); if (tmo) tp->timeout_sec = (uint64_t)atoi(tmo); @@ -2040,6 +2157,14 @@ int ds4_tp_create( rdma_ok = tp_rdma_probe(&tp->rdma.api); #endif +#ifdef DS4_TP_LINUX + if (opt->transport == DS4_TP_TRANSPORT_RDMA || + (opt->transport == DS4_TP_TRANSPORT_AUTO && opt->rdma_device)) { + char reason[256] = "RoCE allocation failed"; + rdma_ok = tp_roce_probe(tp, reason, sizeof(reason)); + if (!rdma_ok) fprintf(stderr, "ds4-tp: %s\n", reason); + } +#endif int listener = -1; if (tp->rank == 0) { listener = tp_listen(opt->listen_host, opt->listen_port, err, errlen); @@ -2086,6 +2211,11 @@ int ds4_tp_create( goto fail; } } +#ifdef DS4_TP_LINUX + if (tp->rdma_active) { + if (!tp_roce_connect(tp, err, errlen)) goto fail; + } else tp_roce_close(tp); +#endif if (listener >= 0) close(listener); fprintf(stderr, "ds4-tp: %s connected, transport=%s gate-timeout=%llums\n", tp->rank == 0 ? "worker" : "leader", @@ -2124,6 +2254,9 @@ void ds4_tp_free(ds4_tp *tp) { if (!tp) return; #ifdef DS4_TP_HAVE_VERBS tp_rdma_close(tp); +#endif +#ifdef DS4_TP_LINUX + tp_roce_close(tp); #endif if (tp->control_fd >= 0) close(tp->control_fd); if (tp->data_fd >= 0) close(tp->data_fd); @@ -2142,6 +2275,9 @@ void ds4_tp_detach_slab(ds4_tp *tp) { int ds4_tp_rank(const ds4_tp *tp) { return tp->rank; } bool ds4_tp_is_rdma(const ds4_tp *tp) { return tp->rdma_active; } +const char *ds4_tp_transport_name(const ds4_tp *tp) { + return tp->rdma_active ? "rdma" : "tcp"; +} uint32_t ds4_tp_peer_ctx(const ds4_tp *tp) { return tp->peer_ctx; } bool ds4_tp_failed(const ds4_tp *tp) { return tp && atomic_load_explicit(&tp->failed, memory_order_acquire); @@ -2154,7 +2290,51 @@ void ds4_tp_mark_failed(ds4_tp *tp) { * Gate exchange. * --------------------------------------------------------------------- */ +#ifdef DS4_TP_LINUX +typedef struct { + uint32_t magic, kind, layer, gate; + uint64_t epoch, seq, bytes; +} ds4_tp_linux_gate_header; + +static int tp_linux_gate(ds4_tp *tp, uint32_t kind, uint32_t layer, + uint32_t gate, uint64_t seq, const void *out, + void *in, uint64_t bytes, uint64_t grace_ms) { + if (ds4_tp_failed(tp)) return 0; + if (!out || !in || !bytes || bytes > SIZE_MAX || layer >= tp->n_layer) { + errno = EINVAL; + goto fail; + } + if (tp->rdma_active && !tp_roce_prepare(tp, bytes)) goto fail; + ds4_tp_linux_gate_header h = {DS4_TP_MAGIC, kind, layer, gate, + tp->epoch, seq, bytes}, peer; + if (!ds4_tp_io_exchange(tp->data_fd, &h, &peer, sizeof(h), + tp->gate_timeout_ms + grace_ms, &tp->failed)) goto fail; + if (memcmp(&h, &peer, sizeof(h))) { errno = EPROTO; goto fail; } + if (tp->rdma_active) { + if (!tp_roce_exchange(tp, out, in, bytes)) goto fail; + return 1; + } + if (!ds4_tp_io_exchange(tp->data_fd, out, in, bytes, + tp->gate_timeout_ms, &tp->failed)) goto fail; + return 1; +fail: + fprintf(stderr, "ds4-tp: gate failed (layer=%u kind=%u seq=%llu bytes=%llu): %s\n", + layer, kind, (unsigned long long)seq, (unsigned long long)bytes, + strerror(errno)); + ds4_tp_mark_failed(tp); + return 0; +} +#endif + int ds4_tp_gate_exchange(ds4_tp *tp, uint32_t layer, uint32_t gate, uint64_t seq) { +#ifdef DS4_TP_LINUX + if (layer >= tp->n_layer || gate >= DS4_TP_GATES_PER_LAYER || !tp->slab) { + ds4_tp_mark_failed(tp); return 0; + } + return tp_linux_gate(tp, 1, layer, gate, seq, + tp->slab + ds4_tp_slab_out_offset(tp, layer, gate), + tp->slab + ds4_tp_slab_in_offset(tp, layer, gate), tp->vec_bytes, 0); +#endif #ifdef DS4_TP_HAVE_VERBS if (tp->rdma_active) return tp_rdma_gate_exchange(tp, layer, gate, seq); #endif @@ -2242,6 +2422,12 @@ int ds4_tp_batch_gate_exchange(ds4_tp *tp, uint32_t layer, uint32_t rows, uint64_t seq) { if (tp->data_fd < 0 || rows == 0 || rows > DS4_TP_BATCH_MAX_ROWS) return 0; const uint64_t bytes = (uint64_t)rows * tp->vec_bytes; +#ifdef DS4_TP_LINUX + if (layer >= tp->n_layer || !tp->slab) { ds4_tp_mark_failed(tp); return 0; } + return tp_linux_gate(tp, 2, layer, rows, seq, + tp->slab + ds4_tp_slab_batch_out_offset(tp, layer), + tp->slab + ds4_tp_slab_batch_in_offset(tp, layer), bytes, 0); +#endif ds4_tp_gate_header h = { DS4_TP_BATCH_MAGIC, (uint16_t)layer, (uint16_t)rows, seq }; #ifdef DS4_TP_HAVE_VERBS @@ -2287,6 +2473,9 @@ int ds4_tp_batch_gate_exchange(ds4_tp *tp, uint32_t layer, uint32_t rows, /* Prefill batch gate: RDMA uses the pipelined registered-slab path above. */ int ds4_tp_big_gate_exchange(ds4_tp *tp, uint32_t layer, uint64_t seq, const void *out, void *in, uint64_t bytes) { +#ifdef DS4_TP_LINUX + return tp_linux_gate(tp, 3, layer, 0, seq, out, in, bytes, 2000u); +#endif if (tp->data_fd < 0 || !out || !in || bytes == 0) return 0; #ifdef DS4_TP_HAVE_VERBS static int dbg = -1; @@ -2530,6 +2719,78 @@ int ds4_tp_send_invalidate(ds4_tp *tp, uint64_t session_id) { &session_id, sizeof(session_id)); } +int ds4_tp_send_restore_payload(ds4_tp *tp, uint64_t session_id, + FILE *fp, uint64_t bytes, + char *err, size_t errlen) { +#ifdef DS4_TP_LINUX + const uint64_t header[2] = {session_id, bytes}; + if (!tp || tp->rank != 0 || !session_id || !fp || !bytes || ds4_tp_failed(tp)) + return 0; + const double deadline = tp_now_sec() + (double)tp->timeout_sec; + if (!tp_send_frame(tp->control_fd, DS4_TP_FRAME_RESTORE_PAYLOAD, + header, sizeof(header))) goto failed; + unsigned char buffer[65536]; + while (bytes) { + const size_t count = bytes < sizeof(buffer) ? (size_t)bytes : sizeof(buffer); + if (tp_now_sec() >= deadline || fread(buffer, 1, count, fp) != count || + !tp_write_full(tp->control_fd, buffer, count)) goto failed; + bytes -= count; + } + return ds4_tp_wait_command_ack(tp, session_id, "checkpoint restore", err, errlen); +failed: + ds4_tp_mark_failed(tp); + tp_set_err(err, errlen, "tp: checkpoint transfer failed or timed out"); + return 0; +#else + (void)tp; (void)session_id; (void)fp; (void)bytes; + tp_set_err(err, errlen, "direct TP checkpoint restore requires Linux ROCm"); + return 0; +#endif +} + +#ifdef DS4_TP_LINUX +typedef struct { + ds4_tp *tp; + uint64_t remaining; + double deadline; +} ds4_tp_payload_reader; + +static ssize_t tp_payload_read(void *cookie, char *buffer, size_t bytes) { + ds4_tp_payload_reader *r = cookie; + if (!r->remaining) return 0; + if (bytes > r->remaining) bytes = (size_t)r->remaining; + if (tp_now_sec() >= r->deadline || + !tp_read_full(r->tp->control_fd, buffer, bytes)) { + ds4_tp_mark_failed(r->tp); + errno = EIO; + return -1; + } + r->remaining -= bytes; + return (ssize_t)bytes; +} + +static int tp_worker_restore_payload(ds4_tp *tp, ds4_session *session, + uint64_t bytes, char *err, size_t errlen) { + ds4_tp_payload_reader reader = {tp, bytes, tp_now_sec() + (double)tp->timeout_sec}; + cookie_io_functions_t io = {.read = tp_payload_read}; + FILE *fp = fopencookie(&reader, "rb", io); + if (!fp) { + ds4_tp_mark_failed(tp); + tp_set_err(err, errlen, "tp: cannot open checkpoint input stream"); + return 1; + } + int rc = ds4_session_load_payload(session, fp, bytes, err, errlen); + /* A rejected header can leave a body unread. Consume this bounded stream, + * including stdio read-ahead, before acknowledging another command. */ + unsigned char discard[65536]; + while (!ds4_tp_failed(tp) && fread(discard, 1, sizeof(discard), fp) != 0) {} + if (ferror(fp) || reader.remaining || ds4_tp_failed(tp)) rc = 1; + if (fclose(fp)) rc = 1; + if (rc) ds4_session_invalidate(session); + return rc; +} +#endif + int ds4_tp_send_eval_batch(ds4_tp *tp, const ds4_tp_batch_item *items, uint32_t count) { const uint64_t bytes64 = sizeof(ds4_tp_batch_command_header) + @@ -2772,6 +3033,17 @@ int ds4_tp_recv_command(ds4_tp *tp, ds4_tp_command *command, } int ok = 1; switch (ftype) { +#ifdef DS4_TP_LINUX + case DS4_TP_FRAME_RESTORE_PAYLOAD: { + uint64_t header[2]; + if (bytes != sizeof(header)) { ok = 0; break; } + memcpy(header, payload, sizeof(header)); + if (!header[0] || !header[1]) { ok = 0; break; } + command->session_id = header[0]; + command->payload_bytes = header[1]; + break; + } +#endif case DS4_TP_FRAME_SYNC: case DS4_TP_FRAME_VERIFY: ok = tp_command_decode_tokens(command, payload, bytes, err, errlen); @@ -3108,6 +3380,17 @@ int ds4_tp_worker_run(ds4_engine *engine, const ds4_tp_options *opt) { break; } +#ifdef DS4_TP_LINUX + if (command.type == DS4_TP_FRAME_RESTORE_PAYLOAD) { + const int status = tp_worker_restore_payload(tp, session, + command.payload_bytes, err, sizeof(err)); + if (ds4_tp_failed(tp) || + !ds4_tp_send_command_ack(tp, command.session_id, status)) rc = 1; + ds4_tp_command_free(&command); + if (rc) break; + continue; + } +#endif if (command.type == DS4_TP_FRAME_SYNC || command.type == DS4_TP_FRAME_SYNC_MULTIMODAL) { prompt.len = 0; diff --git a/ds4_tp.h b/ds4_tp.h index 0ff2d43d32..0e5d8f6f6b 100644 --- a/ds4_tp.h +++ b/ds4_tp.h @@ -103,6 +103,7 @@ void ds4_tp_free(ds4_tp *tp); int ds4_tp_rank(const ds4_tp *tp); bool ds4_tp_is_rdma(const ds4_tp *tp); +const char *ds4_tp_transport_name(const ds4_tp *tp); uint32_t ds4_tp_peer_ctx(const ds4_tp *tp); bool ds4_tp_failed(const ds4_tp *tp); void ds4_tp_mark_failed(ds4_tp *tp); @@ -171,6 +172,11 @@ int ds4_tp_send_glm_mtp(ds4_tp *tp, uint64_t session_id, uint64_t seq, int token, int limit); int ds4_tp_send_rewind(ds4_tp *tp, uint64_t session_id, int pos); int ds4_tp_send_invalidate(ds4_tp *tp, uint64_t session_id); +/* Linux V4.1 has replicated KV. Stream a complete checkpoint to the worker + * without allocating another checkpoint-sized buffer. fp advances by bytes. */ +int ds4_tp_send_restore_payload(ds4_tp *tp, uint64_t session_id, + FILE *fp, uint64_t bytes, + char *err, size_t errlen); int ds4_tp_send_eval_batch(ds4_tp *tp, const ds4_tp_batch_item *items, uint32_t count); int ds4_tp_send_mixed_batch(ds4_tp *tp, uint64_t prefill_session_id, @@ -215,12 +221,14 @@ typedef enum { DS4_TP_FRAME_RDMA_POSTED = 20, DS4_TP_FRAME_GLM_MTP = 21, DS4_TP_FRAME_SYNC_CHECKPOINT = 22, + DS4_TP_FRAME_RESTORE_PAYLOAD = 23, } ds4_tp_frame_type; typedef struct { ds4_tp_frame_type type; uint64_t session_id; uint64_t seq; + uint64_t payload_bytes; int value; int limit; int *tokens; diff --git a/ds4_tp_io.h b/ds4_tp_io.h new file mode 100644 index 0000000000..a20c9e6fd9 --- /dev/null +++ b/ds4_tp_io.h @@ -0,0 +1,75 @@ +#ifndef DS4_TP_IO_H +#define DS4_TP_IO_H + +/* Both directions advance independently of socket buffer capacity. */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static double ds4_tp_io_now(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9; +} + +static int ds4_tp_io_exchange(int fd, const void *out, void *in, + uint64_t bytes, uint64_t timeout_ms, const atomic_bool *cancelled) { + if (fd < 0 || !out || !in || !bytes || !timeout_ms || bytes > SIZE_MAX) { + errno = EINVAL; + return 0; + } + const double deadline = ds4_tp_io_now() + (double)timeout_ms / 1000.0; + uint64_t sent = 0, received = 0; + while (sent < bytes || received < bytes) { + if (cancelled && atomic_load_explicit(cancelled, memory_order_acquire)) { + errno = ECANCELED; + goto fail; + } + const double remaining = deadline - ds4_tp_io_now(); + if (remaining <= 0) { errno = ETIMEDOUT; goto fail; } + bool progress = false; + if (sent < bytes) { + size_t n = bytes - sent > 2097152u ? 2097152u : (size_t)(bytes - sent); + ssize_t r = send(fd, (const char *)out + sent, n, MSG_DONTWAIT | MSG_NOSIGNAL); + if (r > 0) { sent += (uint64_t)r; progress = true; } + else if (!r) { errno = EPIPE; goto fail; } + else if (errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) goto fail; + } + if (received < bytes) { + size_t n = bytes - received > 2097152u ? 2097152u : (size_t)(bytes - received); + ssize_t r = recv(fd, (char *)in + received, n, MSG_DONTWAIT); + if (r > 0) { received += (uint64_t)r; progress = true; } + else if (!r) { errno = ECONNRESET; goto fail; } + else if (errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) goto fail; + } + if (!progress) { + struct pollfd p = {fd, (short)((sent < bytes ? POLLOUT : 0) | + (received < bytes ? POLLIN : 0)), 0}; + int wait_ms = remaining > .05 ? 50 : (int)(remaining * 1000.0) + 1; + int rc = poll(&p, 1, wait_ms); + if (rc < 0 && errno != EINTR) goto fail; + if (rc > 0 && (p.revents & POLLNVAL)) { errno = EBADF; goto fail; } + if (rc > 0 && (p.revents & POLLERR)) { errno = EIO; goto fail; } + /* HUP can coexist with buffered data. Let read report EOF. */ + } + } + return 1; +fail: + { + int saved_errno = errno; + fprintf(stderr, "ds4-tp: socket I/O failed: sent=%llu received=%llu expected=%llu: %s\n", + (unsigned long long)sent, + (unsigned long long)received, (unsigned long long)bytes, + strerror(saved_errno)); + errno = saved_errno; + } + return 0; +} +#endif diff --git a/ds4_tp_roce.h b/ds4_tp_roce.h new file mode 100644 index 0000000000..e0375d63a1 --- /dev/null +++ b/ds4_tp_roce.h @@ -0,0 +1,209 @@ +/* Optional Linux RC SEND/RECV adapter. Setup symbols are loaded lazily; + * posting/polling use the verbs provider's public inline dispatch. Buffers + * are ordinary aligned host memory pinned once by ibv_reg_mr, not GPU MR. */ +#ifdef DS4_TP_HAVE_ROCE +#define TP_RC_WINDOW 4u +#define TP_RC_CHUNK (2u * 1024u * 1024u) +struct ds4_tp_roce { + void *lib; + struct ibv_device **(*get_device_list)(int *); + void (*free_device_list)(struct ibv_device **); + const char *(*get_device_name)(struct ibv_device *); + struct ibv_context *(*open_device)(struct ibv_device *); + int (*close_device)(struct ibv_context *); + int (*query_device)(struct ibv_context *, struct ibv_device_attr *); + int (*query_port)(struct ibv_context *, uint8_t, struct ibv_port_attr *); + int (*query_gid)(struct ibv_context *, uint8_t, int, union ibv_gid *); + struct ibv_pd *(*alloc_pd)(struct ibv_context *); + int (*dealloc_pd)(struct ibv_pd *); + struct ibv_cq *(*create_cq)(struct ibv_context *, int, void *, struct ibv_comp_channel *, int); + int (*destroy_cq)(struct ibv_cq *); + struct ibv_qp *(*create_qp)(struct ibv_pd *, struct ibv_qp_init_attr *); + int (*destroy_qp)(struct ibv_qp *); + int (*modify_qp)(struct ibv_qp *, struct ibv_qp_attr *, int); + struct ibv_mr *(*reg_mr)(struct ibv_pd *, void *, size_t, int); + int (*dereg_mr)(struct ibv_mr *); + struct ibv_context *ctx; + struct ibv_pd *pd; + struct ibv_cq *cq; + struct ibv_qp *qp; + struct ibv_mr *tx_mr, *rx_mr; + uint8_t *tx, *rx; + union ibv_gid gid; + uint32_t psn; + enum ibv_mtu mtu; + uint64_t window_id; + uint32_t lengths[TP_RC_WINDOW]; + unsigned posted; +}; +static void tp_roce_close(ds4_tp *tp) { + struct ds4_tp_roce *r = tp->roce; + if (!r) return; + /* Destroying the QP ends DMA references before deregistration/free. */ + if (r->qp && r->destroy_qp(r->qp)) { + /* A provider that cannot destroy a QP may still hold DMA references. + * Quarantine its registered buffers instead of freeing live memory. */ + fprintf(stderr,"ds4-tp: RoCE QP destruction failed; retaining registered buffers\n"); + tp->roce = NULL; return; + } + if (r->tx_mr) r->dereg_mr(r->tx_mr); + if (r->rx_mr) r->dereg_mr(r->rx_mr); + free(r->tx); free(r->rx); + if (r->cq) r->destroy_cq(r->cq); + if (r->pd) r->dealloc_pd(r->pd); + if (r->ctx) r->close_device(r->ctx); + if (r->lib) dlclose(r->lib); + free(r); tp->roce = NULL; +} +static int tp_roce_probe(ds4_tp *tp, char *err, size_t errlen) { + if (!tp->opt.rdma_device || !*tp->opt.rdma_device || !tp->opt.rdma_gid_index_set) { + tp_set_err(err,errlen,"Linux RoCE requires --rdma-device and --rdma-gid-index"); return 0; + } + struct ds4_tp_roce *r = calloc(1,sizeof(*r)); + if (!r) return 0; + tp->roce = r; + r->lib = dlopen("libibverbs.so.1",RTLD_NOW|RTLD_LOCAL); + if (!r->lib) { tp_set_err(err,errlen,"RoCE: libibverbs.so.1 unavailable: %s",dlerror()); goto fail; } +#define RC_LOAD(name) do { *(void **)(&r->name)=dlsym(r->lib,"ibv_" #name); if(!r->name) { tp_set_err(err,errlen,"RoCE: missing ibv_%s",#name); goto fail; } } while(0) + RC_LOAD(get_device_list); RC_LOAD(free_device_list); RC_LOAD(get_device_name); + RC_LOAD(open_device); RC_LOAD(close_device); RC_LOAD(query_device); + RC_LOAD(query_port); RC_LOAD(query_gid); RC_LOAD(alloc_pd); RC_LOAD(dealloc_pd); + RC_LOAD(create_cq); RC_LOAD(destroy_cq); RC_LOAD(create_qp); RC_LOAD(destroy_qp); + RC_LOAD(modify_qp); RC_LOAD(reg_mr); RC_LOAD(dereg_mr); +#undef RC_LOAD + int count=0; + struct ibv_device **devices=r->get_device_list(&count); + if (!devices) goto syserr; + for(int i=0;iget_device_name(devices[i]),tp->opt.rdma_device)) { + r->ctx=r->open_device(devices[i]); break; + } + r->free_device_list(devices); + if(!r->ctx) { tp_set_err(err,errlen,"RoCE: device %s unavailable",tp->opt.rdma_device); goto fail; } + struct ibv_device_attr dev={0}; struct ibv_port_attr port={0}; + const unsigned portnum=tp->opt.rdma_port ? (unsigned)tp->opt.rdma_port : 1u; + if(r->query_device(r->ctx,&dev) || portnum>dev.phys_port_cnt || + r->query_port(r->ctx,(uint8_t)portnum,&port)) goto syserr; + if(port.state!=IBV_PORT_ACTIVE || port.link_layer!=IBV_LINK_LAYER_ETHERNET || + port.active_mtuIBV_MTU_4096 || + tp->opt.rdma_gid_index<0 || tp->opt.rdma_gid_index>255 || tp->opt.rdma_gid_index>=port.gid_tbl_len || + dev.max_qp_wr<(int)TP_RC_WINDOW || dev.max_cqe<(int)(2*TP_RC_WINDOW) || dev.max_sge<1) { + tp_set_err(err,errlen,"RoCE: inactive Ethernet port, invalid GID or insufficient queue capacity"); goto fail; + } + if(r->query_gid(r->ctx,(uint8_t)portnum,tp->opt.rdma_gid_index,&r->gid)) goto syserr; + union ibv_gid zero={0}; + if(!memcmp(&r->gid,&zero,sizeof(zero))) { tp_set_err(err,errlen,"RoCE: selected GID is zero"); goto fail; } + r->mtu=port.active_mtu; + r->pd=r->alloc_pd(r->ctx); if(!r->pd) goto syserr; + r->cq=r->create_cq(r->ctx,2*TP_RC_WINDOW,NULL,NULL,0); if(!r->cq) goto syserr; + struct ibv_qp_init_attr qi={.send_cq=r->cq,.recv_cq=r->cq, + .cap={.max_send_wr=TP_RC_WINDOW,.max_recv_wr=TP_RC_WINDOW,.max_send_sge=1,.max_recv_sge=1},.qp_type=IBV_QPT_RC}; + r->qp=r->create_qp(r->pd,&qi); if(!r->qp) goto syserr; + if(qi.cap.max_send_wrcq->cqe<(int)(2*TP_RC_WINDOW)) goto syserr; + if(posix_memalign((void **)&r->tx,4096,TP_RC_WINDOW*TP_RC_CHUNK) || + posix_memalign((void **)&r->rx,4096,TP_RC_WINDOW*TP_RC_CHUNK)) goto syserr; + r->tx_mr=r->reg_mr(r->pd,r->tx,TP_RC_WINDOW*TP_RC_CHUNK,IBV_ACCESS_LOCAL_WRITE); + r->rx_mr=r->reg_mr(r->pd,r->rx,TP_RC_WINDOW*TP_RC_CHUNK,IBV_ACCESS_LOCAL_WRITE); + if(!r->tx_mr || !r->rx_mr) goto syserr; + if(getrandom(&r->psn,sizeof(r->psn),0)!=(ssize_t)sizeof(r->psn)) goto syserr; + r->psn &= 0xffffffu; + struct ibv_qp_attr init={.qp_state=IBV_QPS_INIT,.pkey_index=0,.port_num=(uint8_t)portnum,.qp_access_flags=0}; + if(r->modify_qp(r->qp,&init,IBV_QP_STATE|IBV_QP_PKEY_INDEX|IBV_QP_PORT|IBV_QP_ACCESS_FLAGS)) goto syserr; + return 1; +syserr: + tp_set_err(err,errlen,"RoCE setup on %s: %s",tp->opt.rdma_device,strerror(errno ? errno : EINVAL)); +fail: + tp_roce_close(tp); return 0; +} +static int tp_roce_connect(ds4_tp *tp,char *err,size_t errlen) { + struct ds4_tp_roce *r=tp->roce; + struct { uint64_t epoch; uint32_t qpn,psn,mtu; uint8_t gid[16]; uint32_t reserved; } mine={0},peer={0}; + mine.epoch=tp->epoch;mine.qpn=r->qp->qp_num;mine.psn=r->psn;mine.mtu=r->mtu;memcpy(mine.gid,&r->gid,16); + if(!ds4_tp_io_exchange(tp->data_fd,&mine,&peer,sizeof(mine),tp->gate_timeout_ms,&tp->failed)) goto fail; + if(peer.epoch!=mine.epoch || !peer.qpn || peer.qpn>0xffffffu || peer.psn>0xffffffu || + peer.mtuIBV_MTU_4096 || peer.reserved) { errno=EPROTO; goto fail; } + const uint8_t port=tp->opt.rdma_port?tp->opt.rdma_port:1; + struct ibv_qp_attr a={0}; + a.qp_state=IBV_QPS_RTR;a.path_mtu=peer.mtu<(uint32_t)r->mtu?(enum ibv_mtu)peer.mtu:r->mtu; + a.dest_qp_num=peer.qpn;a.rq_psn=peer.psn;a.max_dest_rd_atomic=1;a.min_rnr_timer=12; + a.ah_attr.is_global=1;a.ah_attr.port_num=port;memcpy(&a.ah_attr.grh.dgid,peer.gid,16); + a.ah_attr.grh.sgid_index=tp->opt.rdma_gid_index;a.ah_attr.grh.hop_limit=64; + if(r->modify_qp(r->qp,&a,IBV_QP_STATE|IBV_QP_AV|IBV_QP_PATH_MTU|IBV_QP_DEST_QPN|IBV_QP_RQ_PSN|IBV_QP_MAX_DEST_RD_ATOMIC|IBV_QP_MIN_RNR_TIMER)) goto fail; + memset(&a,0,sizeof(a));a.qp_state=IBV_QPS_RTS;a.sq_psn=r->psn;a.timeout=14;a.retry_cnt=3;a.rnr_retry=3;a.max_rd_atomic=1; + if(r->modify_qp(r->qp,&a,IBV_QP_STATE|IBV_QP_SQ_PSN|IBV_QP_TIMEOUT|IBV_QP_RETRY_CNT|IBV_QP_RNR_RETRY|IBV_QP_MAX_QP_RD_ATOMIC)) goto fail; + fprintf(stderr,"ds4-tp: RoCE RC device=%s port=%u gid-index=%d window=%u chunk=%u host-staging=%uMiB\n",tp->opt.rdma_device,port,tp->opt.rdma_gid_index,TP_RC_WINDOW,TP_RC_CHUNK,2*TP_RC_WINDOW*TP_RC_CHUNK/(1024*1024)); + return 1; +fail: + tp_set_err(err,errlen,"RoCE connect: %s",strerror(errno));return 0; +} +/* Called before the shared TCP gate header: crossing that header guarantees + * both ranks have posted their first bounded receive window. */ +static int tp_roce_prepare(ds4_tp *tp,uint64_t bytes) { + struct ds4_tp_roce *r=tp->roce; + if(r->posted || !bytes || r->window_id>=UINT64_MAX/16u) { errno=EINVAL; return 0; } + ++r->window_id; + for(unsigned i=0;iTP_RC_CHUNK?TP_RC_CHUNK:(uint32_t)bytes; + r->lengths[i]=n; + struct ibv_sge sg={.addr=(uintptr_t)(r->rx+i*TP_RC_CHUNK),.length=n,.lkey=r->rx_mr->lkey}; + struct ibv_recv_wr wr={.wr_id=r->window_id*16u+i,.sg_list=&sg,.num_sge=1},*bad=NULL; + if(ibv_post_recv(r->qp,&wr,&bad)) return 0; + r->posted++;bytes-=n; + } + return 1; +} +static int tp_roce_exchange(ds4_tp *tp,const void *out,void *in,uint64_t bytes) { + struct ds4_tp_roce *r=tp->roce; + const double deadline=tp_now_sec()+(double)tp->gate_timeout_ms/1000.0; + uint64_t offset=0; + while(offsetposted; + if(!nr) { errno=EPROTO; return 0; } + uint64_t covered=0; + for(unsigned i=0;itx+i*TP_RC_CHUNK,(const uint8_t*)out+offset+covered,r->lengths[i]); + struct ibv_sge sg={.addr=(uintptr_t)(r->tx+i*TP_RC_CHUNK),.length=r->lengths[i],.lkey=r->tx_mr->lkey}; + struct ibv_send_wr wr={.wr_id=r->window_id*16u+8u+i,.sg_list=&sg,.num_sge=1,.opcode=IBV_WR_SEND,.send_flags=IBV_SEND_SIGNALED},*bad=NULL; + if(ibv_post_send(r->qp,&wr,&bad)) return 0; + covered+=r->lengths[i]; + } + unsigned sent=0,received=0,mask=(1u<deadline) { errno=ETIMEDOUT; return 0; } + struct ibv_wc wc[2*TP_RC_WINDOW];int n=ibv_poll_cq(r->cq,2*TP_RC_WINDOW,wc); + if(n<0) return 0; + for(int i=0;iwindow_id) { errno=EPROTO; return 0; } + unsigned k=(unsigned)(id%16u),*done=NULL; + if(klengths[k]) done=&received; + else if(k>=8u && k<8u+nr && wc[i].opcode==IBV_WC_SEND) { k-=8u;done=&sent; } + if(!done || (*done&(1u<rx+i*TP_RC_CHUNK,r->lengths[i]);covered+=r->lengths[i]; } + r->posted=0;offset+=covered; + if(offsetwindow_id,offset},peer[2]; + double remaining=(deadline-tp_now_sec())*1000.0; + if(remaining<1 || !ds4_tp_io_exchange(tp->data_fd,mine,peer,sizeof(mine),(uint64_t)remaining,&tp->failed) || memcmp(mine,peer,sizeof(mine))) return 0; + } + } + return 1; +} +#else +static void tp_roce_close(ds4_tp *tp) { (void)tp; } +static int tp_roce_probe(ds4_tp *tp,char *err,size_t errlen) { + (void)tp;tp_set_err(err,errlen,"RoCE unavailable: rebuild with libibverbs development headers");return 0; +} +static int tp_roce_connect(ds4_tp *tp,char *err,size_t errlen) { (void)tp;(void)err;(void)errlen;return 0; } +static int tp_roce_prepare(ds4_tp *tp,uint64_t bytes) { (void)tp;(void)bytes;return 0; } +static int tp_roce_exchange(ds4_tp *tp,const void *out,void *in,uint64_t bytes) { (void)tp;(void)out;(void)in;(void)bytes;return 0; } +#endif diff --git a/rocm/ds4_rocm_attention.cuh b/rocm/ds4_rocm_attention.cuh index e609c3e1b3..67b5fe8e1e 100644 --- a/rocm/ds4_rocm_attention.cuh +++ b/rocm/ds4_rocm_attention.cuh @@ -1945,3 +1945,91 @@ __global__ static void attention_decode_mixed_heads8_online_kernel( out4[lane + 96u] = o3; } } + +/* V4.1 decode: parallel 32-key tiles, four heads sharing FP32 value loads. + * The guarded caller supplies the model's 128-row raw ring. */ +template +__global__ static void ds41_attention_split_f32_heads_kernel( + float *parts, float *lse, const float *q, const float *raw, + const float *comp, const float *sinks, unsigned H, unsigned NR, + unsigned C, unsigned RS) { + const unsigned hb = blockIdx.x * HEADS, split = blockIdx.y, + tid = threadIdx.x, lane = tid & 31u, wave = tid >> 5u, + k0 = split * KEYS, N = NR + C; + __shared__ float p[HEADS][KEYS]; + __shared__ float normalizer[HEADS]; + for (unsigned hl = wave; hl < HEADS; hl += 4) { + unsigned h = hb + hl; + float query[16]; +#pragma unroll + for (unsigned d = 0; d < 16; d++) + query[d] = h < H ? q[(uint64_t)h * 512 + lane + d * 32] : 0.f; + for (unsigned k = 0; k < KEYS; k++) { + unsigned row = k0 + k; + float dot = 0; + if (row < N) { + const float *v = row < NR ? raw + (uint64_t)((RS + row) % 128) * 512 + : comp + (uint64_t)(row - NR) * 512; +#pragma unroll + for (unsigned d = 0; d < 16; d++) dot += query[d] * v[lane + d * 32]; + } + dot = attention_warp_sum_oldhip_w32(dot); + if (lane == 0) p[hl][k] = row < N ? dot * rsqrtf(512.f) : -INFINITY; + } + } + __syncthreads(); + for (unsigned hl = wave; hl < HEADS; hl += 4) { + unsigned h = hb + hl; + float mx = split == 0 && h < H ? sinks[h] : -INFINITY; + for (unsigned k = lane; k < KEYS; k += 32) mx = fmaxf(mx, p[hl][k]); + for (unsigned delta = 16; delta; delta >>= 1) + mx = fmaxf(mx, __shfl_xor(mx, delta, 32)); + float sum = (split == 0 && lane == 0 && h < H) ? expf(sinks[h] - mx) : 0.f; + for (unsigned k = lane; k < KEYS; k += 32) { + float z = expf(p[hl][k] - mx); + p[hl][k] = z; + sum += z; + } + sum = attention_warp_sum_oldhip_w32(sum); + if (lane == 0) { + normalizer[hl] = 1.f / sum; + if (h < H) lse[(uint64_t)split * H + h] = mx + logf(sum); + } + } + __syncthreads(); + for (unsigned d = tid; d < 512; d += 128) { + float values[HEADS] = {}; + for (unsigned k = 0; k < KEYS && k0 + k < N; k++) { + unsigned row = k0 + k; + const float *v = row < NR ? raw + (uint64_t)((RS + row) % 128) * 512 + : comp + (uint64_t)(row - NR) * 512; + float value = v[d]; +#pragma unroll + for (unsigned hl = 0; hl < HEADS; hl++) values[hl] += p[hl][k] * value; + } +#pragma unroll + for (unsigned hl = 0; hl < HEADS; hl++) + if (hb + hl < H) + parts[((uint64_t)split * H + hb + hl) * 512 + d] = values[hl] * normalizer[hl]; + } +} + +/* Combine independently normalized key tiles, including the sink only in tile zero. */ +__global__ static void ds41_attention_split_combine_kernel( + float *out, const float *parts, const float *lse, + uint32_t n_head, uint32_t splits) { + const uint32_t head = blockIdx.x; + float max_lse = -INFINITY; + for (uint32_t k = 0; k < splits; k++) + max_lse = fmaxf(max_lse, lse[k * n_head + head]); + float denominator = 0.0f; + for (uint32_t k = 0; k < splits; k++) + denominator += expf(lse[k * n_head + head] - max_lse); + for (uint32_t dim = threadIdx.x; dim < 512u; dim += blockDim.x) { + float value = 0.0f; + for (uint32_t k = 0; k < splits; k++) + value += parts[((uint64_t)k * n_head + head) * 512u + dim] * + expf(lse[k * n_head + head] - max_lse); + out[(uint64_t)head * 512u + dim] = value / denominator; + } +} diff --git a/rocm/ds4_rocm_attention_launch.cuh b/rocm/ds4_rocm_attention_launch.cuh index dbac16b4e2..c9ff772e9a 100644 --- a/rocm/ds4_rocm_attention_launch.cuh +++ b/rocm/ds4_rocm_attention_launch.cuh @@ -176,6 +176,25 @@ extern "C" int ds4_gpu_attention_decode_heads_tensor( model_map, sinks_offset, (uint64_t)n_head * sizeof(float), "attn_sinks"); if (!sinks) return 0; const ds4_rocm_runtime_config *cfg = cuda_runtime_config(); + if (g_deepseek41_model && ds4_rocm_is_gfx1151() && + !g_quality_mode && !use_mask && head_dim == 512u && + (n_head == 32u || n_head == 64u) && n_raw <= 128u && + raw_cap == 128u && n_comp <= 512u && n_raw + n_comp >= 256u) { + const uint32_t splits = (n_raw + n_comp + 31u) / 32u; + const uint64_t part_count = (uint64_t)splits * n_head * 512u; + const uint64_t lse_count = (uint64_t)splits * n_head; + float *parts = (float *)cuda_tmp_alloc( + (part_count + lse_count) * sizeof(float), "V4.1 split decode attention"); + if (!parts) return 0; + float *lse = parts + part_count; + ds41_attention_split_f32_heads_kernel<32, 4><<>>( + parts, lse, (const float *)q->ptr, (const float *)raw_kv->ptr, + (const float *)comp_kv->ptr, sinks, n_head, n_raw, n_comp, raw_start); + if (!cuda_ok(cudaGetLastError(), "V4.1 split decode attention launch")) return 0; + ds41_attention_split_combine_kernel<<>>( + (float *)heads->ptr, parts, lse, n_head, splits); + return cuda_ok(cudaGetLastError(), "V4.1 split decode attention combine"); + } if (cfg->oldhip_attention_decode) { const uint32_t rows = n_raw + n_comp; const size_t shmem = (size_t)(rows ? rows : 1u) * sizeof(float); diff --git a/rocm/ds4_rocm_current_api_compat.cuh b/rocm/ds4_rocm_current_api_compat.cuh index d24668ce7e..2f88164b73 100644 --- a/rocm/ds4_rocm_current_api_compat.cuh +++ b/rocm/ds4_rocm_current_api_compat.cuh @@ -123,6 +123,12 @@ extern "C" void ds4_gpu_set_ssd_streaming(bool enabled) { g_routed_moe_selected_override_n = 0; g_stream_selected_cache.loaded = 0; g_stream_batch_selected_cache.loaded = 0; + g_stream_free_reserve_bytes = UINT64_C(16) << 30; + g_deepseek41_model = false; +} + +extern "C" void ds4_gpu_set_deepseek41_model(bool enabled) { + g_deepseek41_model = enabled; } extern "C" void ds4_gpu_set_glm_model(bool enabled) { @@ -245,6 +251,38 @@ extern "C" int ds4_gpu_stream_expert_cache_prepare_selected_batch( 1); } +extern "C" int ds4_gpu_stream_expert_cache_reserve_layers( + const ds4_gpu_stream_expert_table *even, + const ds4_gpu_stream_expert_table *odd) { + return cuda_stream_layer_expert_cache_reserve(even, odd); +} + +extern "C" int ds4_gpu_stream_expert_cache_note_layer_consumed( + const ds4_gpu_stream_expert_table *table) { + return cuda_stream_layer_expert_cache_note_consumed(table); +} + +extern "C" int ds4_gpu_stream_expert_cache_quiesce(void) { + return cuda_stream_expert_cache_quiesce(); +} + +extern "C" int ds4_gpu_stream_expert_cache_get_memory( + ds4_gpu_stream_expert_memory *out) { + return cuda_stream_expert_cache_get_memory(out); +} + +extern "C" int ds4_gpu_stream_expert_cache_seed_experts_gpu_copy( + const ds4_gpu_stream_expert_table *table, + const int32_t *expert_ids, + const uint32_t *expert_priorities, + uint32_t n_experts) { + if (!table || !g_ssd_streaming_mode) return 0; + return cuda_stream_resident_seed_experts(table->model_map, + table->model_size, table->layer, expert_ids, expert_priorities, + n_experts, table->n_total_expert, table->gate_offset, table->up_offset, + table->down_offset, table->gate_expert_bytes, table->down_expert_bytes, 1); +} + extern "C" int ds4_gpu_stream_expert_cache_load_layer( const ds4_gpu_stream_expert_table *table) { if (!table) return 0; @@ -259,6 +297,22 @@ extern "C" int ds4_gpu_stream_expert_cache_load_layer( table->down_expert_bytes); } +extern "C" int ds4_gpu_dsv41_stream_prepare_layer( + const ds4_gpu_stream_expert_table *table, + ds4_gpu_dsv41_stream_layer_plan *plan) { + return cuda_stream_v41_prepare_layer(table, plan); +} + +extern "C" int ds4_gpu_dsv41_stream_load_layer( + const ds4_gpu_dsv41_stream_layer_plan *plan) { + return cuda_stream_v41_load_layer(plan); +} + +extern "C" int ds4_gpu_dsv41_stream_cancel_layer( + const ds4_gpu_dsv41_stream_layer_plan *plan) { + return cuda_stream_v41_cancel_layer(plan); +} + extern "C" int ds4_gpu_stream_expert_cache_seed_from_layer_selected( const ds4_gpu_stream_expert_table *table, const ds4_gpu_tensor *selected, @@ -286,8 +340,7 @@ extern "C" int ds4_gpu_stream_expert_cache_finish_pending_batch(void) { } extern "C" int ds4_gpu_stream_expert_cache_release_layer_cache(void) { - cuda_stream_layer_expert_cache_release(); - return 1; + return cuda_stream_layer_expert_cache_release(); } extern "C" int ds4_gpu_stream_expert_cache_seed_experts( diff --git a/rocm/ds4_rocm_hc_sgemm.cuh b/rocm/ds4_rocm_hc_sgemm.cuh new file mode 100644 index 0000000000..1797e18df3 --- /dev/null +++ b/rocm/ds4_rocm_hc_sgemm.cuh @@ -0,0 +1,105 @@ +#pragma once +/* HC-only F32 SGEMM plan. The graph lends full heads storage on stream0. + * This plan owns no device allocation and never changes the global handle. */ +#include +#include +#include +#include +#include +#include + +static hipError_t v41_hc_widen(float *, const uint16_t *, uint64_t); + +static constexpr uint64_t v41_hc_wide_bytes = UINT64_C(20480) * 24u * 4u; +static constexpr uint64_t v41_hc_workspace_cap = UINT64_C(64) * 1024u * 1024u; +static constexpr uint64_t v41_hc_scratch_bytes = v41_hc_wide_bytes + v41_hc_workspace_cap; +struct ds4_gpu_dsv41_hc_plan { + rocblas_handle handle = nullptr; + void *scratch = nullptr; + size_t workspace_bytes = 0; + bool pending = false, failed = false, unsupported = false; +}; +static void v41_hc_drain(ds4_gpu_dsv41_hc_plan *p) { + if (!p || !p->pending) return; + const hipError_t status = hipStreamSynchronize(nullptr); + if (status != hipSuccess) { + std::fprintf(stderr, "ds4: HC SGEMM cannot prove stream drained: %s\n", hipGetErrorString(status)); + std::abort(); + } + p->pending = false; +} +static int v41_hc_error(ds4_gpu_dsv41_hc_plan *p, const char *what, int status) { + std::fprintf(stderr, "ds4: HC SGEMM %s failed: %d\n", what, status); + p->failed = true; + p->pending = true; + v41_hc_drain(p); + return -1; +} +static void v41_hc_plan_destroy(ds4_gpu_dsv41_hc_plan *p) { + if (!p) return; + v41_hc_drain(p); + if (p->handle) { + const rocblas_status status = rocblas_destroy_handle(p->handle); + if (status != rocblas_status_success) { + std::fprintf(stderr, "ds4: HC SGEMM handle teardown failed: %d\n", int(status)); + std::abort(); + } + } + delete p; +} +static rocblas_status v41_hc_submit(ds4_gpu_dsv41_hc_plan *p, + float *out, const float *wide, const float *input) { + const float alpha = 1.0f, beta = 0.0f; + return rocblas_sgemm(p->handle, rocblas_operation_transpose, rocblas_operation_none, + 24, 2048, 20480, &alpha, wide, 20480, input, 20480, &beta, out, 24); +} +/*1 enqueued,0 unsupported before projection submission,-1 failed and drained. + * Inputs are the existing RMS-normalized F32 values; no precision boundary is added. */ +static int v41_hc_plan_run(ds4_gpu_dsv41_hc_plan **owner, + float *out, const uint16_t *weight, const float *input, void *scratch) { + if (!*owner) *owner = new (std::nothrow) ds4_gpu_dsv41_hc_plan; + ds4_gpu_dsv41_hc_plan *p = *owner; + if (!p || p->failed) return -1; + if (p->unsupported) return 0; +#define V41_HC_BLAS(call) do { const rocblas_status st_ = (call); if (st_ != rocblas_status_success) return v41_hc_error(p, #call, int(st_)); } while (0) + if (!p->handle) { + V41_HC_BLAS(rocblas_create_handle(&p->handle)); + V41_HC_BLAS(rocblas_set_stream(p->handle, nullptr)); + V41_HC_BLAS(rocblas_set_pointer_mode(p->handle, rocblas_pointer_mode_host)); + V41_HC_BLAS(rocblas_set_atomics_mode(p->handle, rocblas_atomics_not_allowed)); + V41_HC_BLAS(rocblas_set_math_mode(p->handle, rocblas_default_math)); + } + if (p->scratch != scratch) { + v41_hc_drain(p); + float *wide = static_cast(scratch); + void *workspace = static_cast(scratch) + v41_hc_wide_bytes; + V41_HC_BLAS(rocblas_set_workspace(p->handle, workspace, 256)); + V41_HC_BLAS(rocblas_start_device_memory_size_query(p->handle)); + const rocblas_status query = v41_hc_submit(p, out, wide, input); + size_t required = 0; + const rocblas_status stop = rocblas_stop_device_memory_size_query(p->handle, &required); + if (query != rocblas_status_success && query != rocblas_status_size_increased && query != rocblas_status_size_unchanged) + return v41_hc_error(p, "workspace query", int(query)); + if (stop != rocblas_status_success) return v41_hc_error(p, "workspace query stop", int(stop)); + if (required > v41_hc_workspace_cap) { p->unsupported = true; return 0; } + const size_t capacity = required ? required : 256; + V41_HC_BLAS(rocblas_set_workspace(p->handle, workspace, capacity)); + rocblas_math_mode math; rocblas_atomics_mode atomics; + rocblas_pointer_mode pointer; hipStream_t stream; size_t actual = 0; + V41_HC_BLAS(rocblas_get_math_mode(p->handle, &math)); + V41_HC_BLAS(rocblas_get_atomics_mode(p->handle, &atomics)); + V41_HC_BLAS(rocblas_get_pointer_mode(p->handle, &pointer)); + V41_HC_BLAS(rocblas_get_stream(p->handle, &stream)); + V41_HC_BLAS(rocblas_get_device_memory_size(p->handle, &actual)); + if (math != rocblas_default_math || atomics != rocblas_atomics_not_allowed || + pointer != rocblas_pointer_mode_host || stream != nullptr || actual != capacity || + rocblas_is_managing_device_memory(p->handle)) return v41_hc_error(p, "precision/workspace contract", -1); + p->scratch = scratch; p->workspace_bytes = capacity; + } + p->pending = true; + const hipError_t converted = v41_hc_widen(static_cast(scratch), weight, UINT64_C(20480) * 24u); + if (converted != hipSuccess) return v41_hc_error(p, "weight widening", int(converted)); + V41_HC_BLAS(v41_hc_submit(p, out, static_cast(scratch), input)); +#undef V41_HC_BLAS + return 1; +} diff --git a/rocm/ds4_rocm_moe.cuh b/rocm/ds4_rocm_moe.cuh index bd35d730d6..b55cf8dc0e 100644 --- a/rocm/ds4_rocm_moe.cuh +++ b/rocm/ds4_rocm_moe.cuh @@ -948,6 +948,50 @@ __global__ static DS4_ROCM_UNUSED void moe_gate_up_mid_hwarp16_kernel( } } +// CUDA-inspired sub-block lane ownership, adapted to raw IQ2 and existing Q8_K input. +// Each wave owns a row; eight lanes cover the eight32-value groups of each256-value block. +__device__ __forceinline__ float v41_iq2_pair32(const cuda_block_iq2_xxs *w, const cuda_block_q8_K *x, int p) { + const uint16_t *q = w->qs + 4 * p; + uint32_t a = (uint32_t) q[0] | ((uint32_t) q[1] << 16), b = (uint32_t) q[2] | ((uint32_t) q[3] << 16); + int sum = 0; +#pragma unroll + for (int j = 0; j < 4; j++) sum = dev_iq2_dp4a_8(cuda_iq2xxs_grid[(a >> (8 * j)) & 255], cuda_ksigns_iq2xs[(b >> (7 * j)) & 127], x->qs + p * 32 + j * 8, sum); + sum *= 2 * (b >> 28) + 1; + return .125f * dev_f16_to_f32(w->d) * x->d * (float) sum; +} +template __global__ void moe_v41_gate_up_wave_pairs_kernel( + float *gate_out, float *up_out, float *mid_out, const char *gb, const char *ub, + const cuda_block_q8_K *xq, const int32_t *ids, const float *weights, uint64_t eb, uint64_t rb, + uint32_t B, uint32_t M, uint32_t N, uint32_t aux, uint32_t mask, float clamp) { + unsigned lane = threadIdx.x & 31, row = blockIdx.x * Waves + (threadIdx.x >> 5), slot = blockIdx.y; + if (row >= M || !(mask & (1u << slot))) return; + int id = ids[slot]; + if (id < 0) id = 0; + auto *g = (const cuda_block_iq2_xxs *) (gb + (uint64_t) id * eb + row * rb); + auto *u = (const cuda_block_iq2_xxs *) (ub + (uint64_t) id * eb + row * rb); + float gv = 0, uv = 0; + for (unsigned b = lane >> 3; b < B; b += 4) { + gv += v41_iq2_pair32(g + b, xq + b, lane & 7); + uv += v41_iq2_pair32(u + b, xq + b, lane & 7); + } + for (int off = 16; off; off >>= 1) { + gv += __shfl_down(gv, off, 32); + uv += __shfl_down(uv, off, 32); + } + if (!lane) { + if (clamp > 1e-6f) { + gv = fminf(gv, clamp); + uv = fmaxf(-clamp, fminf(uv, clamp)); + } + uint64_t i = (uint64_t) slot * M + row; + if (aux) { + gate_out[i] = gv; + up_out[i] = uv; + } + mid_out[i] = (gv / (1 + expf(-gv))) * uv * weights[slot]; + } +} + __global__ static void moe_gate_up_mid_qwarp32_kernel( float *gate_out, float *up_out, @@ -2915,6 +2959,87 @@ __global__ static void moe_gate_up_mid_q2K_decode_q8_qwarp32_kernel( } } +// Four blocks per wave, eight lanes/block, one 32-value subgroup/lane. +__device__ __forceinline__ float v41_q2_part32(const cuda_block_q2_K *w, const cuda_block_q8_K *x, int part) { + const uint8_t *q = w->qs + (part >> 2) * 32; + int shift = (part & 3) * 2; + unsigned s0 = w->scales[part * 2], s1 = w->scales[part * 2 + 1]; + int a = dev_dot_q2_16(q, x->qs + part * 32, shift) * (s0 & 15) + dev_dot_q2_16(q + 16, x->qs + part * 32 + 16, shift) * (s1 & 15); + int m = x->bsums[part * 2] * (s0 >> 4) + x->bsums[part * 2 + 1] * (s1 >> 4); + return x->d * dev_f16_to_f32(w->d) * a - x->d * dev_f16_to_f32(w->dmin) * m; +} +template __global__ void moe_v41_down_wave( + float *out, const char *base, const cuda_block_q8_K *xq, const int32_t *ids, uint64_t eb, + uint64_t rb, unsigned B, unsigned M, unsigned N) { + unsigned lane = threadIdx.x & 31, row = blockIdx.x * Waves + (threadIdx.x >> 5); + if (row >= M) return; + float total = 0; + for (unsigned slot = 0; slot < N; slot++) { + int id = ids[slot]; + if (id < 0) id = 0; + auto *w = (const cuda_block_q2_K *) (base + (uint64_t) id * eb + row * rb); + auto *x = xq + (uint64_t) slot * B; + for (unsigned b = lane >> 3; b < B; b += 4) total += v41_q2_part32(w + b, x + b, lane & 7); + } + total = warp_sum_f32(total); + if (!lane) out[row] = total; +} + +template __global__ void moe_v41_gate_up_wave_ptrs_kernel( + float *gate_out, float *up_out, float *mid_out, const char *const * gt, const char *const * ut, + const cuda_block_q8_K *xq, const int32_t *ids, const float *weights, uint64_t eb, uint64_t rb, + uint32_t B, uint32_t M, uint32_t N, uint32_t aux, uint32_t mask, float clamp) { + unsigned lane = threadIdx.x & 31, row = blockIdx.x * Waves + (threadIdx.x >> 5), slot = blockIdx.y; + if (row >= M || !(mask & (1u << slot))) return; + int id = ids[slot]; + if (id < 0) id = 0; + const char *gb = gt[id]; + const char *ub = ut[id]; + if (!gb || !ub) return; + auto *g = (const cuda_block_iq2_xxs *) (gb + row * rb); + auto *u = (const cuda_block_iq2_xxs *) (ub + row * rb); + float gv = 0, uv = 0; + for (unsigned b = lane >> 3; b < B; b += 4) { + gv += v41_iq2_pair32(g + b, xq + b, lane & 7); + uv += v41_iq2_pair32(u + b, xq + b, lane & 7); + } + for (int off = 16; off; off >>= 1) { + gv += __shfl_down(gv, off, 32); + uv += __shfl_down(uv, off, 32); + } + if (!lane) { + if (clamp > 1e-6f) { + gv = fminf(gv, clamp); + uv = fmaxf(-clamp, fminf(uv, clamp)); + } + uint64_t i = (uint64_t) slot * M + row; + if (aux) { + gate_out[i] = gv; + up_out[i] = uv; + } + mid_out[i] = (gv / (1 + expf(-gv))) * uv * weights[slot]; + } +} + +template __global__ void moe_v41_down_wave_ptrs_kernel( + float *out, const char *const * table, const cuda_block_q8_K *xq, const int32_t *ids, + uint64_t eb, uint64_t rb, unsigned B, unsigned M, unsigned N) { + unsigned lane = threadIdx.x & 31, row = blockIdx.x * Waves + (threadIdx.x >> 5); + if (row >= M) return; + float total = 0; + for (unsigned slot = 0; slot < N; slot++) { + int id = ids[slot]; + if (id < 0) id = 0; + const char *base = table[id]; + if (!base) continue; + auto *w = (const cuda_block_q2_K *) (base + row * rb); + auto *x = xq + (uint64_t) slot * B; + for (unsigned b = lane >> 3; b < B; b += 4) total += v41_q2_part32(w + b, x + b, lane & 7); + } + total = warp_sum_f32(total); + if (!lane) out[row] = total; +} + __global__ static void moe_down_sum6_qwarp32_kernel( float *out, const char *down_base, diff --git a/rocm/ds4_rocm_moe_launch.cuh b/rocm/ds4_rocm_moe_launch.cuh index 16ad7bcb4b..f1aa1283af 100644 --- a/rocm/ds4_rocm_moe_launch.cuh +++ b/rocm/ds4_rocm_moe_launch.cuh @@ -776,6 +776,10 @@ static int routed_moe_launch( const uint32_t use_expert_tiles = use_sorted_pairs; const uint32_t expert_tile_m = n_tokens <= 8u ? 4u : 8u; const uint32_t write_gate_up = 0u; + const bool use_v41_wave_gate = g_deepseek41_model && iq2_gate_path && + n_tokens == 1u && n_total_expert == 384u && n_expert == 6u && + expert_in_dim == 5120u && expert_mid_dim == 2304u && + out_dim == 5120u && ds4_rocm_is_gfx1151(); const uint32_t use_p2_sorted = 0u; const uint32_t use_atomic_down = !mxfp4_path && use_expert_tiles && n_tokens >= 128u; @@ -813,9 +817,18 @@ static int routed_moe_launch( mxfp4_path && use_expert_tiles && !use_mxfp4_tile32 && !use_mxfp4_ldsB && !use_mxfp4_tile4 && n_tokens >= 8u && getenv("DS4_ROCM_ENABLE_MXFP4_ROW64") != NULL; + /* V4.1 Q2 uses canonical IQ2 gate/up rows and Q2_K down rows. + * Keep other expert layouts on their existing admissions. */ + const uint32_t v41_mmq_topology = + g_deepseek41_model && n_total_expert == 384u && n_expert == 6u && + expert_in_dim == 5120u && expert_mid_dim == 2304u && + out_dim == 5120u && n_tokens <= 2048u && + gate_row_bytes == 1320u && gate_expert_bytes == 3041280u && + down_row_bytes == 756u && down_expert_bytes == 3870720u && + ds4_rocm_is_gfx1151(); const uint32_t use_rocm_mmq_gateup = ok && iq2_path && n_tokens >= 128u && !g_quality_mode && - n_total_expert <= 256u && + (n_total_expert <= 256u || v41_mmq_topology) && !batch_stream_selected && !batch_stream_split_selected && !split_selected && !compact_selected && gate_w && up_w && (stream_full_layer || full_table_cached) && @@ -1250,7 +1263,7 @@ static int routed_moe_launch( logged_mmq_gateup = 1; fprintf(stderr, "ds4: ROCm routed MoE using tuned MMQ IQ2 gate/up\n"); } - } else { + } else if (!v41_mmq_topology) { (void)cudaGetLastError(); static int logged_mmq_fallback = 0; if (!logged_mmq_fallback) { @@ -1258,6 +1271,14 @@ static int routed_moe_launch( fprintf(stderr, "ds4: ROCm MMQ IQ2 gate/up returned %d; falling back\n", mmq_rc); } } + if (v41_mmq_topology && !mmq_gateup_done) { + /* This route skipped Q8_K x preparation. A legacy fallback + * would consume unwritten xq or stale half mid storage. */ + fprintf(stderr, "ds4: V4.1 MMQ gate/up or epilogue failed (%d)\n", mmq_rc); + if (!cuda_ok(cudaStreamSynchronize((cudaStream_t)0), + "V4.1 MMQ failure drain")) abort(); + return 0; + } } int split_gateup_done = 0; if (ok && split_selected) { @@ -1271,7 +1292,14 @@ static int routed_moe_launch( stream_missing_mask != 0; if (split_supported) { dim3 qgrid((expert_mid_dim + 127u) / 128u, pair_count, 1); - if (use_decode_lut_gate) { + if (use_v41_wave_gate) { + moe_v41_gate_up_wave_ptrs_kernel<4><<>>( + (float *)gate->ptr, (float *)up->ptr, (float *)mid->ptr, + gate_slot_ptrs, up_slot_ptrs, xq, + (const int32_t *)selected_exec->ptr, (const float *)weights->ptr, + 0, gate_row_bytes, xq_blocks, expert_mid_dim, n_expert, + write_gate_up, stream_resident_mask, clamp); + } else if (use_decode_lut_gate) { moe_gate_up_mid_decode_lut_qwarp32_ptrs_kernel<<>>( (float *)gate->ptr, (float *)up->ptr, @@ -1311,7 +1339,14 @@ static int routed_moe_launch( } else { ok = cuda_stream_selected_finish_pending_missing(0); } - if (ok && use_decode_lut_gate) { + if (ok && use_v41_wave_gate) { + moe_v41_gate_up_wave_ptrs_kernel<4><<>>( + (float *)gate->ptr, (float *)up->ptr, (float *)mid->ptr, + gate_slot_ptrs, up_slot_ptrs, xq, + (const int32_t *)selected_exec->ptr, (const float *)weights->ptr, + 0, gate_row_bytes, xq_blocks, expert_mid_dim, n_expert, + write_gate_up, stream_missing_mask, clamp); + } else if (ok && use_decode_lut_gate) { moe_gate_up_mid_decode_lut_qwarp32_ptrs_kernel<<>>( (float *)gate->ptr, (float *)up->ptr, @@ -1621,6 +1656,16 @@ static int routed_moe_launch( n_expert, write_gate_up, clamp); + } else if (use_v41_wave_gate) { + // V4.1 scalar: raw IQ2 and existing Q8_K activation. + // Four waves own four output rows; each lane handles32 values. + const dim3 v41_grid((expert_mid_dim + 3u) / 4u, n_expert); + moe_v41_gate_up_wave_pairs_kernel<4><<>>( + (float *)gate->ptr, (float *)up->ptr, (float *)mid->ptr, + gate_w, up_w, xq, (const int32_t *)selected_exec->ptr, + (const float *)weights->ptr, gate_expert_bytes, gate_row_bytes, + xq_blocks, expert_mid_dim, n_expert, write_gate_up, + 0xffffffffu, clamp); } else if (use_decode_lut_gate) { moe_gate_up_mid_decode_lut_qwarp32_kernel<<>>( (float *)gate->ptr, @@ -1738,8 +1783,13 @@ static int routed_moe_launch( ok && iq2_path && n_tokens > 1u && n_expert <= DS4_ROCM_N_EXPERT_USED && sorted_pairs && sorted_offsets && sorted_counts && tile_experts; + const uint32_t use_v41_q8_down = + ok && g_deepseek41_model && iq2_path && n_tokens == 1u && !g_quality_mode && + n_total_expert == 384u && n_expert == 6u && + expert_in_dim == 5120u && expert_mid_dim == 2304u && + out_dim == 5120u && ds4_rocm_is_gfx1151(); const uint32_t use_iq2_q2_decode_float_down = - ok && iq2_path && n_tokens == 1u && + ok && iq2_path && n_tokens == 1u && !use_v41_q8_down && n_expert <= DS4_ROCM_N_EXPERT_USED; if (ok && !use_iq2_q2_float_down && !use_iq2_q2_decode_float_down) { @@ -1748,7 +1798,21 @@ static int routed_moe_launch( ok = cuda_ok(cudaGetLastError(), "routed_moe mid quantize launch"); } int direct_iq2_down_done = 0; - if (ok && use_iq2_q2_decode_float_down) { + if (ok && use_v41_q8_down) { + if (split_gateup_done) { + moe_v41_down_wave_ptrs_kernel<4><<<(out_dim + 3u) / 4u, 128>>>( + (float *)out->ptr, down_slot_ptrs, midq, + (const int32_t *)selected_exec->ptr, 0, + down_row_bytes, midq_blocks, out_dim, n_expert); + } else { + moe_v41_down_wave<4><<<(out_dim + 3u) / 4u, 128>>>( + (float *)out->ptr, down_w, midq, + (const int32_t *)selected_exec->ptr, down_expert_bytes, + down_row_bytes, midq_blocks, out_dim, n_expert); + } + ok = cuda_ok(cudaGetLastError(), "V4.1 quantized wave down launch"); + direct_iq2_down_done = ok; + } else if (ok && use_iq2_q2_decode_float_down) { const ds4_rocm_runtime_config *runtime_cfg = cuda_runtime_config(); uint32_t rows_per_block = runtime_cfg->moe_decode_down_rpb; if (rows_per_block == 0u) rows_per_block = 1u; diff --git a/rocm/ds4_rocm_runtime.cuh b/rocm/ds4_rocm_runtime.cuh index 0d1ee546c2..0217258db4 100644 --- a/rocm/ds4_rocm_runtime.cuh +++ b/rocm/ds4_rocm_runtime.cuh @@ -33,6 +33,8 @@ static int g_rocblas_attention_b_solution_disabled; #include "ds4_rocm_hipblaslt.cuh" #endif static int g_quality_mode; +/* Set from the engine family, after the preceding model caches are released. */ +static bool g_deepseek41_model; static int g_glm_model; enum { @@ -224,7 +226,11 @@ struct cuda_stream_batch_selected_cache { struct cuda_stream_layer_expert_cache { int active; + int reserved; + cudaEvent_t consumed_event; + int consumed_pending; const void *model_map; + uint64_t model_size; uint32_t layer; uint32_t n_total_expert; uint64_t gate_offset; @@ -238,6 +244,12 @@ struct cuda_stream_layer_expert_cache { char *gate; char *up; char *down; + ds4_gpu_dsv41_stream_layer_plan reuse_plan; +}; + +struct cuda_stream_v41_reuse_stats { + uint64_t prepares, hits, logical_bytes, missing_bytes, d2d_bytes; + uint64_t read_jobs, read_bytes, direct_bytes, copy_wait_us, failures; }; struct cuda_stream_cache_stats { @@ -296,10 +308,13 @@ static void *g_model_stage_raw[4]; static void *g_model_stage[4]; static cudaEvent_t g_model_stage_event[4]; static uint64_t g_model_stage_bytes; +static uint64_t g_model_stage_alloc_bytes[4]; static uint32_t g_stream_expert_cache_budget; static cuda_stream_selected_cache g_stream_selected_cache; static cuda_stream_batch_selected_cache g_stream_batch_selected_cache; static cuda_stream_layer_expert_cache g_stream_layer_expert_cache[2]; +static uint64_t g_stream_v41_plan_generation; +static cuda_stream_v41_reuse_stats g_stream_v41_reuse_stats; static std::vector g_stream_resident_experts; static std::unordered_map= minimum ? bytes : minimum; +} + static uint64_t cuda_stream_resident_free_reserve_bytes(void) { /* * Headroom kept free on the (unified-memory) device while growing the @@ -1537,7 +1622,7 @@ static uint64_t cuda_stream_resident_free_reserve_bytes(void) { static int64_t cached = -1; if (cached < 0) { const char *env = getenv("DS4_ROCM_STREAM_FREE_RESERVE_GB"); - uint64_t gib = 16; + uint64_t gib = 0; if (env && env[0]) { char *end = NULL; errno = 0; @@ -1548,7 +1633,7 @@ static uint64_t cuda_stream_resident_free_reserve_bytes(void) { } cached = (int64_t)(gib * 1024ull * 1024ull * 1024ull); } - return (uint64_t)cached; + return cached ? (uint64_t)cached : g_stream_free_reserve_bytes; } static int cuda_stream_resident_make_room( @@ -1761,6 +1846,9 @@ typedef struct cuda_stream_read_job { int uploaded; int errnum; int direct; + int count_reads; + uint64_t read_bytes; + uint64_t direct_bytes; } cuda_stream_read_job; struct cuda_stream_batch_selected_pending { @@ -1949,6 +2037,31 @@ static int cuda_stream_read_direct_disabled(void) { return g_stream_read_direct_disabled; } +/* Only the V4.1 diagnostic jobs count returned bytes. Preserve the ordinary + * read helper for every legacy caller. Count partial direct reads too when a + * subsequent buffered retry succeeds; these are syscall bytes, not NVMe busy + * time or exclusive physical-device traffic. */ +static int cuda_stream_read_counted(cuda_stream_read_job *job, int fd, + void *buf, uint64_t bytes, uint64_t offset, + bool direct) { + if (!job->count_reads) return cuda_pread_full(fd, buf, bytes, offset); + uint64_t done = 0; + while (done < bytes) { + const size_t n_req = bytes - done > (uint64_t)SSIZE_MAX ? + (size_t)SSIZE_MAX : (size_t)(bytes - done); + const ssize_t n = pread(fd, (char *)buf + done, n_req, (off_t)(offset + done)); + if (n < 0) { + if (errno == EINTR) continue; + return 0; + } + if (!n) return 0; + done += (uint64_t)n; + job->read_bytes += (uint64_t)n; + if (direct) job->direct_bytes += (uint64_t)n; + } + return 1; +} + static void cuda_stream_read_job_run(cuda_stream_read_job *job, void *stage, uint64_t stage_bytes) { @@ -1957,6 +2070,8 @@ static void cuda_stream_read_job_run(cuda_stream_read_job *job, job->uploaded = 0; job->errnum = 0; job->direct = 0; + job->read_bytes = 0; + job->direct_bytes = 0; if (!stage || job->bytes == 0 || g_model_fd < 0) { job->errnum = EINVAL; return; @@ -1983,7 +2098,7 @@ static void cuda_stream_read_job_run(cuda_stream_read_job *job, if (read_size <= stage_bytes && aligned_off <= g_model_file_size && read_size <= g_model_file_size - aligned_off && - cuda_pread_full(g_model_direct_fd, stage, read_size, aligned_off)) { + cuda_stream_read_counted(job, g_model_direct_fd, stage, read_size, aligned_off, true)) { job->host_buf = (char *)stage + delta; job->direct = 1; job->ok = 1; @@ -1991,7 +2106,7 @@ static void cuda_stream_read_job_run(cuda_stream_read_job *job, } } #endif - if (cuda_pread_full(g_model_fd, job->host_buf, job->bytes, job->offset)) { + if (cuda_stream_read_counted(job, g_model_fd, job->host_buf, job->bytes, job->offset, false)) { job->ok = 1; } else { job->errnum = errno ? errno : EIO; @@ -2537,26 +2652,271 @@ static int cuda_stream_layer_expert_cache_apply( const char **gate_w, const char **up_w, const char **down_w) { + if (!g_deepseek41_model) { + if (!g_ssd_streaming_mode || !gate_w || !up_w || !down_w) return 0; + for (uint32_t i = 0; i < 2u; i++) { + const cuda_stream_layer_expert_cache &c = g_stream_layer_expert_cache[i]; + if (c.active && + c.model_map == model_map && + c.layer == layer && + c.n_total_expert == n_total_expert && + c.gate_offset == gate_offset && + c.up_offset == up_offset && + c.down_offset == down_offset && + c.gate_expert_bytes == gate_expert_bytes && + c.down_expert_bytes == down_expert_bytes && + c.gate && c.up && c.down) { + *gate_w = c.gate; + *up_w = c.up; + *down_w = c.down; + return 1; + } + } + return 0; + } if (!g_ssd_streaming_mode || !gate_w || !up_w || !down_w) return 0; + /* The caller joins this layer's loader. Never inspect the descriptor in + * the other parity slot, which the next-layer loader may be publishing. */ + const cuda_stream_layer_expert_cache &c = g_stream_layer_expert_cache[layer & 1u]; + if (!c.active || c.model_map != model_map || c.layer != layer || + c.n_total_expert != n_total_expert || + c.gate_offset != gate_offset || c.up_offset != up_offset || + c.down_offset != down_offset || + c.gate_expert_bytes != gate_expert_bytes || + c.down_expert_bytes != down_expert_bytes || + !c.gate || !c.up || !c.down) return 0; + *gate_w = c.gate; + *up_w = c.up; + *down_w = c.down; + return 1; +} + +/* Reservation validates storage extents, without touching the mapped bytes. */ +static int cuda_stream_layer_table_bytes(const ds4_gpu_stream_expert_table *t, + uint64_t *total) { + if (!t || !total || !t->model_map || !t->model_size || + !t->n_total_expert || t->n_total_expert > DS4_ROCM_MAX_N_EXPERT || + !t->gate_expert_bytes || !t->down_expert_bytes) return 0; + uint64_t gate = 0, down = 0, pair = 0; + return cuda_u64_mul_checked(t->n_total_expert, t->gate_expert_bytes, &gate) && + cuda_u64_mul_checked(t->n_total_expert, t->down_expert_bytes, &down) && + cuda_u64_mul_checked(2u, gate, &pair) && + cuda_u64_add_checked(pair, down, total) && *total <= SIZE_MAX && + t->gate_offset <= t->model_size && gate <= t->model_size - t->gate_offset && + t->up_offset <= t->model_size && gate <= t->model_size - t->up_offset && + t->down_offset <= t->model_size && down <= t->model_size - t->down_offset; +} + +static int cuda_stream_layer_expert_cache_reserve( + const ds4_gpu_stream_expert_table *even, + const ds4_gpu_stream_expert_table *odd) { + uint64_t bytes[2] = {0, 0}; + if (!g_ssd_streaming_mode || + !cuda_stream_layer_table_bytes(even, &bytes[0]) || + !cuda_stream_layer_table_bytes(odd, &bytes[1]) || + (even->layer & 1u) != 0 || (odd->layer & 1u) != 1 || + even->model_map != odd->model_map || even->model_size != odd->model_size || + (g_model_fd_host_base && even->model_map != g_model_fd_host_base)) return 0; + const ds4_gpu_stream_expert_table *tables[2] = {even, odd}; for (uint32_t i = 0; i < 2u; i++) { const cuda_stream_layer_expert_cache &c = g_stream_layer_expert_cache[i]; - if (c.active && - c.model_map == model_map && - c.layer == layer && - c.n_total_expert == n_total_expert && - c.gate_offset == gate_offset && - c.up_offset == up_offset && - c.down_offset == down_offset && - c.gate_expert_bytes == gate_expert_bytes && - c.down_expert_bytes == down_expert_bytes && - c.gate && c.up && c.down) { - *gate_w = c.gate; - *up_w = c.up; - *down_w = c.down; - return 1; + const ds4_gpu_stream_expert_table &t = *tables[i]; + if (!c.base && (c.consumed_event || c.reserved)) return 0; + if (c.base && (!c.reserved || c.capacity != bytes[i] || + c.model_map != t.model_map || c.model_size != t.model_size || + c.n_total_expert != t.n_total_expert || + c.gate_expert_bytes != t.gate_expert_bytes || + c.down_expert_bytes != t.down_expert_bytes)) return 0; + } + /* Publish neither allocation until both allocations and events succeed. */ + cuda_stream_layer_expert_cache pending[2] = {}; + int ok = 1; + for (uint32_t i = 0; ok && i < 2u; i++) { + if (g_stream_layer_expert_cache[i].base) continue; + ok = cuda_ok(cudaMalloc((void **)&pending[i].base, (size_t)bytes[i]), + "streaming full-layer reservation"); + if (ok) ok = cuda_ok(cudaEventCreateWithFlags(&pending[i].consumed_event, + cudaEventDisableTiming), + "streaming full-layer consumer event"); + } + if (!ok) { + int rollback_ok = 1; + for (uint32_t i = 0; i < 2u; i++) { + if (pending[i].base && + !cuda_ok(cudaFree(pending[i].base), "streaming reservation rollback allocation")) rollback_ok = 0; + if (pending[i].consumed_event && + !cuda_ok(cudaEventDestroy(pending[i].consumed_event), "streaming reservation rollback event")) rollback_ok = 0; + } + if (!rollback_ok) { + fprintf(stderr, DS4_GPU_LOG_PREFIX "streaming reservation rollback failed; refusing to lose allocation metadata\n"); + abort(); } + return 0; } - return 0; + for (uint32_t i = 0; i < 2u; i++) { + if (!pending[i].base) continue; + const ds4_gpu_stream_expert_table &t = *tables[i]; + pending[i].reserved = 1; + pending[i].model_map = t.model_map; + pending[i].model_size = t.model_size; + pending[i].n_total_expert = t.n_total_expert; + pending[i].gate_expert_bytes = t.gate_expert_bytes; + pending[i].down_expert_bytes = t.down_expert_bytes; + pending[i].capacity = bytes[i]; + g_stream_layer_expert_cache[i] = pending[i]; + } + return 1; +} + +static int cuda_stream_layer_expert_cache_note_consumed( + const ds4_gpu_stream_expert_table *t) { + uint64_t bytes = 0; + const char *gate = NULL, *up = NULL, *down = NULL; + if (!cuda_stream_layer_table_bytes(t, &bytes) || + !cuda_stream_layer_expert_cache_apply(t->model_map, t->layer, + t->n_total_expert, t->gate_offset, t->up_offset, t->down_offset, + t->gate_expert_bytes, t->down_expert_bytes, &gate, &up, &down)) return 0; + cuda_stream_layer_expert_cache &c = g_stream_layer_expert_cache[t->layer & 1u]; + if (c.model_size != t->model_size || c.bytes != bytes) return 0; + if (!c.consumed_event && + !cuda_ok(cudaEventCreateWithFlags(&c.consumed_event, cudaEventDisableTiming), + "streaming full-layer consumer event")) return 0; + if (!cuda_ok(cudaEventRecord(c.consumed_event, 0), + "streaming full-layer consumer record")) return 0; + c.consumed_pending = 1; + return 1; +} + +/* The engine joins its full-layer loader before entering this boundary. */ +static int cuda_stream_expert_cache_quiesce(void) { + pthread_mutex_lock(&g_stream_read_mutex); + const bool foreign_jobs = g_stream_read_active_jobs && + g_stream_read_active_jobs != g_stream_selected_pending.read_jobs && + g_stream_read_active_jobs != g_stream_batch_selected_pending.read_jobs; + pthread_mutex_unlock(&g_stream_read_mutex); + if (foreign_jobs) { + fprintf(stderr, DS4_GPU_LOG_PREFIX "streaming quiesce requires the full-layer loader joined\n"); + abort(); /* Returning would permit teardown under the live loader. */ + } + int ok = 1; + if (g_stream_selected_pending.active) { + const uint32_t n = g_stream_selected_pending.read_job_count; + const int read_ok = cuda_stream_read_jobs_wait(g_stream_selected_pending.read_jobs, n); + if (!read_ok || !cuda_stream_selected_upload_read_jobs( + g_stream_selected_pending.read_jobs, n)) ok = 0; + cuda_stream_read_jobs_free(g_stream_selected_pending.read_jobs, n); + memset(&g_stream_selected_pending, 0, sizeof(g_stream_selected_pending)); + } + if (g_stream_batch_selected_pending.active) { + const uint32_t n = g_stream_batch_selected_pending.read_job_count; + const int read_ok = cuda_stream_read_jobs_wait(g_stream_batch_selected_pending.read_jobs, n); + if (!read_ok || !cuda_stream_selected_upload_read_jobs( + g_stream_batch_selected_pending.read_jobs, n)) ok = 0; + cuda_stream_read_jobs_free(g_stream_batch_selected_pending.read_jobs, n); + memset(&g_stream_batch_selected_pending, 0, sizeof(g_stream_batch_selected_pending)); + } + /* Attempt every drain even after an error. No pending copy may outlive + * the cache invalidation below. Read workers synchronize each upload. */ + int drained = 1; + /* An error may occur before the layer's consumer event is recorded. This + * session/sweep boundary must also drain such unrecorded compute work. */ + if (!cuda_ok(cudaStreamSynchronize(0), "streaming quiesce compute sync")) drained = 0; + if (g_stream_selected_upload_stream && + !cuda_ok(cudaStreamSynchronize(g_stream_selected_upload_stream), + "streaming quiesce upload sync")) drained = 0; + for (uint32_t i = 0; i < DS4_ROCM_STREAM_READ_WORKERS; i++) { + if (g_stream_read_upload_streams[i] && + !cuda_ok(cudaStreamSynchronize(g_stream_read_upload_streams[i]), + "streaming quiesce worker upload")) drained = 0; + } + if (!cuda_stream_selected_upload_wait_host("streaming quiesce scalar upload")) drained = 0; + if (!cuda_stream_batch_selected_upload_wait_host("streaming quiesce batch upload")) drained = 0; + if (!cuda_stream_selected_reuse_wait("streaming quiesce scalar consumer")) drained = 0; + if (!cuda_stream_batch_selected_reuse_wait("streaming quiesce batch consumer")) drained = 0; + for (uint32_t i = 0; i < 2u; i++) { + if (!cuda_stream_layer_expert_cache_wait(g_stream_layer_expert_cache[i])) drained = 0; + } + if (!drained) { + fprintf(stderr, DS4_GPU_LOG_PREFIX "streaming quiesce could not prove GPU drainage; refusing teardown\n"); + abort(); + } + g_routed_moe_selected_override_n = 0; + g_stream_selected_cache.loaded = 0; + g_stream_selected_cache.model_map = NULL; + g_stream_batch_selected_cache.loaded = 0; + g_stream_batch_selected_cache.model_map = NULL; + if (!ok) { + /* Entries are inserted before asynchronous reads finish. Invalidate + * their identities even if a GPU fault prevents allocation release. */ + g_stream_resident_index.clear(); + for (cuda_stream_resident_expert &e : g_stream_resident_experts) e.model_map = NULL; + cuda_stream_resident_cache_release(); + } + return ok; +} + +static int cuda_stream_expert_cache_get_memory(ds4_gpu_stream_expert_memory *out) { + if (!out) return 0; + *out = {}; + pthread_mutex_lock(&g_stream_read_mutex); + const bool reading = g_stream_read_active_jobs != NULL; + pthread_mutex_unlock(&g_stream_read_mutex); + if (reading || g_stream_selected_pending.active || g_stream_batch_selected_pending.active) return 0; + ds4_gpu_stream_expert_memory m = {}; + auto add = [](uint64_t &sum, uint64_t n) { + return cuda_u64_add_checked(sum, n, &sum); + }; + for (const cuda_stream_expert_slab &slab : g_stream_expert_slabs) { + if (slab.base && !add(m.dynamic_bytes, slab.bytes)) return 0; + } + for (const cuda_stream_resident_expert &e : g_stream_resident_experts) { + if (e.base && !e.pooled && !add(m.dynamic_bytes, e.bytes)) return 0; + } + for (const cuda_stream_layer_expert_cache &c : g_stream_layer_expert_cache) { + if (c.base && !add(m.layer_bytes, c.capacity)) return 0; + } + const cuda_stream_selected_cache &s = g_stream_selected_cache; + /* A failed partial allocation has no complete capacity descriptor. Fail + * admission rather than undercounting it; teardown can still release it. */ + if (((s.gate || s.up) && !s.gate_capacity) || (s.down && !s.down_capacity)) return 0; + if (s.gate && !add(m.selected_bytes, s.gate_capacity)) return 0; + if (s.up && !add(m.selected_bytes, s.gate_capacity)) return 0; + if (s.down && !add(m.selected_bytes, s.down_capacity)) return 0; + if (s.slot_ids && !add(m.selected_bytes, DS4_ROCM_N_EXPERT_USED * sizeof(int32_t))) return 0; + const void *scalar_ptrs[] = {s.gate_ptrs, s.up_ptrs, s.down_ptrs}; + const void *scalar_stage[] = {s.gate_ptrs_stage, s.up_ptrs_stage, s.down_ptrs_stage}; + for (const void *p : scalar_ptrs) { + if (p && !add(m.selected_bytes, DS4_ROCM_N_EXPERT_USED * sizeof(char *))) return 0; + } + for (const void *p : scalar_stage) { + if (p && !add(m.pinned_bytes, DS4_ROCM_N_EXPERT_USED * sizeof(char *))) return 0; + } + const cuda_stream_batch_selected_cache &b = g_stream_batch_selected_cache; + if (b.selected_ids && !add(m.selected_bytes, b.selected_capacity)) return 0; + if (b.pair_missing && !add(m.selected_bytes, b.pair_missing_capacity)) return 0; + const void *batch_ptrs[] = {b.gate_ptrs, b.up_ptrs, b.down_ptrs, b.resident_gate_ptrs, + b.resident_up_ptrs, b.missing_gate_ptrs, b.missing_up_ptrs}; + for (const void *p : batch_ptrs) { + if (p && (!b.ptr_capacity || !add(m.selected_bytes, (uint64_t)b.ptr_capacity * sizeof(char *)))) return 0; + } + if (b.selected_stage && !add(m.pinned_bytes, b.selected_stage_capacity)) return 0; + if (b.pair_missing_stage && !add(m.pinned_bytes, b.pair_missing_stage_capacity)) return 0; + const void *batch_stage[] = {b.gate_ptrs_stage, b.up_ptrs_stage, b.down_ptrs_stage, + b.resident_gate_ptrs_stage, b.resident_up_ptrs_stage, + b.missing_gate_ptrs_stage, b.missing_up_ptrs_stage}; + for (const void *p : batch_stage) { + if (p && (!b.ptr_stage_capacity || !add(m.pinned_bytes, (uint64_t)b.ptr_stage_capacity * sizeof(char *)))) return 0; + } + for (uint32_t i = 0; i < DS4_ROCM_STREAM_READ_WORKERS; i++) { + if (g_stream_read_stage_raw[i] && !add(m.pinned_bytes, g_stream_read_stage_bytes[i])) return 0; + } + for (uint32_t i = 0; i < 4u; i++) { + if (!g_model_stage_raw[i]) continue; + if (!g_model_stage_alloc_bytes[i] || + !add(m.pinned_bytes, g_model_stage_alloc_bytes[i])) return 0; + } + *out = m; + return 1; } static int cuda_stream_layer_expert_cache_load( @@ -2608,11 +2968,25 @@ static int cuda_stream_layer_expert_cache_load( cuda_stream_layer_expert_cache &slot = g_stream_layer_expert_cache[layer & 1u]; + if (g_deepseek41_model && slot.reserved && + (slot.model_map != model_map || slot.model_size != model_size || + slot.n_total_expert != n_total_expert || + slot.gate_expert_bytes != gate_expert_bytes || + slot.down_expert_bytes != down_expert_bytes || + slot.capacity < total_bytes || !slot.base)) return 0; + if (g_deepseek41_model && !cuda_stream_layer_expert_cache_wait(slot)) return 0; slot.active = 0; + slot.reuse_plan.generation = 0; if (slot.capacity < total_bytes) { if (slot.base) { - (void)cudaFree(slot.base); - memset(&slot, 0, sizeof(slot)); + if (g_deepseek41_model) { + if (!cuda_ok(cudaFree(slot.base), "streaming full-layer growth")) return 0; + slot.base = NULL; + slot.capacity = 0; + } else { + (void)cudaFree(slot.base); + memset(&slot, 0, sizeof(slot)); + } } if (cuda_stream_cache_stats_on() && !g_stream_resident_experts.empty()) { @@ -2678,8 +3052,8 @@ static int cuda_stream_layer_expert_cache_load( free(jobs); if (!ok) return 0; - slot.active = 1; slot.model_map = model_map; + slot.model_size = model_size; slot.layer = layer; slot.n_total_expert = n_total_expert; slot.gate_offset = gate_offset; @@ -2687,6 +3061,198 @@ static int cuda_stream_layer_expert_cache_load( slot.down_offset = down_offset; slot.gate_expert_bytes = gate_expert_bytes; slot.down_expert_bytes = down_expert_bytes; + slot.active = 1; + return 1; +} + +/* V4.1 only: the graph owns dynamic-cache metadata until every source copy + * completes. The following reader receives no cache pointer or vector index. */ +static int cuda_stream_v41_prepare_layer(const ds4_gpu_stream_expert_table *t, + ds4_gpu_dsv41_stream_layer_plan *out) { + if (!out) return 0; + memset(out, 0, sizeof(*out)); + uint64_t total = 0; + if (!g_ssd_streaming_mode || !cuda_stream_layer_table_bytes(t, &total) || + t->n_total_expert != 384u || t->layer >= 40u || g_model_fd < 0 || + g_model_fd_host_base != t->model_map || + g_stream_selected_pending.active || g_stream_batch_selected_pending.active || + g_stream_v41_plan_generation == UINT64_MAX) return 0; + pthread_mutex_lock(&g_stream_read_mutex); + const bool busy = g_stream_read_active_jobs != NULL; + pthread_mutex_unlock(&g_stream_read_mutex); + if (busy) return 0; /* Caller must join the previous layer loader. */ + cuda_stream_layer_expert_cache &s = g_stream_layer_expert_cache[t->layer & 1u]; + if (!s.reserved || !s.base || s.capacity != total || + s.model_map != t->model_map || s.model_size != t->model_size || + s.n_total_expert != t->n_total_expert || + s.gate_expert_bytes != t->gate_expert_bytes || + s.down_expert_bytes != t->down_expert_bytes || + !cuda_stream_layer_expert_cache_wait(s)) return 0; + s.active = 0; + s.reuse_plan.generation = 0; + s.bytes = total; + s.gate = s.base; + s.up = s.gate + t->n_total_expert * t->gate_expert_bytes; + s.down = s.up + t->n_total_expert * t->gate_expert_bytes; + ds4_gpu_dsv41_stream_layer_plan plan = {}; + plan.table = *t; + plan.generation = ++g_stream_v41_plan_generation; + const bool stats = cuda_stream_cache_stats_on() != 0; + uint64_t copied = 0; + uint32_t hits = 0; + int ok = 1; + bool used_stream = false; + if (stats) { + g_stream_v41_reuse_stats.prepares++; + g_stream_v41_reuse_stats.logical_bytes += total; + } + for (uint32_t e = 0; ok && e < 384u; e++) { + const int idx = cuda_stream_resident_find(t->model_map, t->layer, (int32_t)e, + t->gate_offset, t->up_offset, t->down_offset, + t->gate_expert_bytes, t->down_expert_bytes); + if (idx < 0) continue; + const cuda_stream_resident_expert &entry = g_stream_resident_experts[(size_t)idx]; + const uint64_t expert_bytes = total / 384u; + if (!entry.base || entry.bytes != expert_bytes || entry.gate != entry.base || + entry.up != entry.base + t->gate_expert_bytes || + entry.down != entry.base + 2u * t->gate_expert_bytes) continue; + if (!used_stream) { + if (!cuda_stream_selected_upload_wait_host("V4.1 cached layer source ready") || + !cuda_stream_batch_selected_upload_wait_host("V4.1 cached layer batch source ready") || + !cuda_stream_selected_ensure_stream()) { ok = 0; break; } + used_stream = true; + } + const char *src[] = {entry.gate, entry.up, entry.down}; + char *dst[] = {s.gate + e * t->gate_expert_bytes, + s.up + e * t->gate_expert_bytes, + s.down + e * t->down_expert_bytes}; + const uint64_t bytes[] = {t->gate_expert_bytes, t->gate_expert_bytes, + t->down_expert_bytes}; + for (uint32_t i = 0; ok && i < 3u; i++) { + ok = cuda_ok(cudaMemcpyAsync(dst[i], src[i], (size_t)bytes[i], + cudaMemcpyDeviceToDevice, g_stream_selected_upload_stream), + "V4.1 cached expert to layer copy"); + if (ok) copied += bytes[i]; + } + if (ok) { plan.hits[e / 64u] |= UINT64_C(1) << (e % 64u); hits++; } + } + if (used_stream) { + const double start = stats ? cuda_wall_sec() : 0.0; + /* Drain even after a partial submission failure. No cache allocation, + * eviction or loader may begin while one of these pointers is live. */ + if (!cuda_ok(cudaStreamSynchronize(g_stream_selected_upload_stream), + "V4.1 cached layer copy drain")) { + fprintf(stderr, DS4_GPU_LOG_PREFIX "V4.1 source-copy drainage unproven; refusing cache reuse or teardown\n"); + abort(); + } + if (stats) g_stream_v41_reuse_stats.copy_wait_us += + (uint64_t)((cuda_wall_sec() - start) * 1000000.0); + } + if (stats) { + g_stream_v41_reuse_stats.hits += hits; + g_stream_v41_reuse_stats.d2d_bytes += copied; + g_stream_v41_reuse_stats.missing_bytes += total - (uint64_t)hits * (total / 384u); + if (!ok) g_stream_v41_reuse_stats.failures++; + } + if (!ok) return 0; + s.reuse_plan = plan; + *out = plan; + return 1; +} + +static int cuda_stream_v41_plan_matches(const ds4_gpu_dsv41_stream_layer_plan *p) { + uint64_t bytes = 0; + if (!p || !p->generation || !cuda_stream_layer_table_bytes(&p->table, &bytes) || + p->table.layer >= 40u || p->table.n_total_expert != 384u) return 0; + const cuda_stream_layer_expert_cache &s = g_stream_layer_expert_cache[p->table.layer & 1u]; + const ds4_gpu_stream_expert_table &a = s.reuse_plan.table, &b = p->table; + return !s.active && s.reserved && s.base && s.capacity == bytes && + s.reuse_plan.generation == p->generation && + a.model_map == b.model_map && a.model_size == b.model_size && + a.layer == b.layer && a.n_total_expert == b.n_total_expert && + a.gate_offset == b.gate_offset && a.up_offset == b.up_offset && + a.down_offset == b.down_offset && a.gate_expert_bytes == b.gate_expert_bytes && + a.down_expert_bytes == b.down_expert_bytes && + memcmp(s.reuse_plan.hits, p->hits, sizeof(p->hits)) == 0; +} + +/* Coalesce only adjacent misses in a single plane. An empty hit bitmap retains + * exactly the old 32 MiB splitting, including its final partial chunk. */ +static int cuda_stream_v41_missing_jobs(const ds4_gpu_dsv41_stream_layer_plan *p, + cuda_stream_read_job *jobs, uint32_t *count) { + if (!jobs || !count || !cuda_stream_v41_plan_matches(p)) return 0; + const ds4_gpu_stream_expert_table &t = p->table; + const cuda_stream_layer_expert_cache &s = g_stream_layer_expert_cache[t.layer & 1u]; + char *base[] = {s.gate, s.up, s.down}; + const uint64_t offset[] = {t.gate_offset, t.up_offset, t.down_offset}; + const uint64_t stride[] = {t.gate_expert_bytes, t.gate_expert_bytes, t.down_expert_bytes}; + const uint64_t chunk = UINT64_C(32) * 1048576u; + *count = 0; + for (uint32_t plane = 0; plane < 3u; plane++) { + uint32_t e = 0; + while (e < 384u) { + if ((p->hits[e / 64u] >> (e % 64u)) & 1u) { e++; continue; } + const uint32_t first = e++; + while (e < 384u && !((p->hits[e / 64u] >> (e % 64u)) & 1u)) e++; + uint64_t begin = (uint64_t)first * stride[plane]; + const uint64_t end = (uint64_t)e * stride[plane]; + while (begin < end) { + if (*count == DS4_ROCM_STREAM_READ_MAX_JOBS) return 0; + const uint64_t n = end - begin < chunk ? end - begin : chunk; + cuda_stream_read_job &j = jobs[(*count)++]; + memset(&j, 0, sizeof(j)); + j.dst = base[plane] + begin; + j.offset = offset[plane] + begin; + j.bytes = n; + j.count_reads = cuda_stream_cache_stats_on(); + begin += n; + } + } + } + return 1; +} + +static int cuda_stream_v41_cancel_layer(const ds4_gpu_dsv41_stream_layer_plan *p) { + if (!cuda_stream_v41_plan_matches(p)) return 0; + g_stream_layer_expert_cache[p->table.layer & 1u].reuse_plan.generation = 0; + return 1; /* Preparation owns no outstanding copies or dynamic entries. */ +} + +static int cuda_stream_v41_load_layer(const ds4_gpu_dsv41_stream_layer_plan *p) { + if (!cuda_stream_v41_plan_matches(p)) return 0; + cuda_stream_read_job *jobs = (cuda_stream_read_job *)calloc( + DS4_ROCM_STREAM_READ_MAX_JOBS, sizeof(cuda_stream_read_job)); + uint32_t count = 0; + if (!jobs || !cuda_stream_v41_missing_jobs(p, jobs, &count)) { + free(jobs); + (void)cuda_stream_v41_cancel_layer(p); + return 0; + } + const ds4_gpu_stream_expert_table t = p->table; + cuda_stream_layer_expert_cache &s = g_stream_layer_expert_cache[t.layer & 1u]; + s.reuse_plan.generation = 0; /* Consume once, before admitting any reads. */ + const bool stats = cuda_stream_cache_stats_on() != 0; + if (stats) { + g_stream_cache_stats.layer_loads++; + g_stream_cache_stats.layer_load_bytes += s.bytes; + g_stream_v41_reuse_stats.read_jobs += count; + } + const int ok = cuda_stream_read_jobs_parallel(jobs, count); + if (stats) { + for (uint32_t i = 0; i < count; i++) { + g_stream_v41_reuse_stats.read_bytes += jobs[i].read_bytes; + g_stream_v41_reuse_stats.direct_bytes += jobs[i].direct_bytes; + } + if (!ok) g_stream_v41_reuse_stats.failures++; + } + cuda_stream_read_jobs_free(jobs, count); + free(jobs); + if (!ok) return 0; /* Pool wait drains uploads even on a failed read. */ + s.layer = t.layer; + s.gate_offset = t.gate_offset; + s.up_offset = t.up_offset; + s.down_offset = t.down_offset; + s.active = 1; return 1; } @@ -2702,7 +3268,8 @@ static int cuda_stream_resident_seed_experts( uint64_t up_offset, uint64_t down_offset, uint64_t gate_expert_bytes, - uint64_t down_expert_bytes) { + uint64_t down_expert_bytes, + int device_copy = 0) { if (!g_ssd_streaming_mode) return 1; if (!model_map || !expert_ids || n_experts == 0 || n_total_expert == 0 || @@ -2711,7 +3278,13 @@ static int cuda_stream_resident_seed_experts( down_expert_bytes == 0) { return 0; } - if (g_stream_expert_cache_budget == 0) return 1; + if (!device_copy && g_stream_expert_cache_budget == 0) return 1; + const char *layer_gate = NULL, *layer_up = NULL, *layer_down = NULL; + if (device_copy && + (!cuda_stream_layer_expert_cache_apply(model_map, layer, n_total_expert, + gate_offset, up_offset, down_offset, gate_expert_bytes, down_expert_bytes, + &layer_gate, &layer_up, &layer_down) || + g_stream_layer_expert_cache[layer & 1u].model_size != model_size)) return 0; uint64_t gate_bytes = 0; uint64_t down_bytes = 0; @@ -2728,6 +3301,8 @@ static int cuda_stream_resident_seed_experts( return 0; } + if (g_stream_expert_cache_budget == 0) return 1; + uint32_t seed_cap = n_experts < g_stream_expert_cache_budget ? n_experts : g_stream_expert_cache_budget; if (seed_cap > DS4_ROCM_MAX_N_EXPERT) seed_cap = DS4_ROCM_MAX_N_EXPERT; @@ -2796,9 +3371,9 @@ static int cuda_stream_resident_seed_experts( } const int use_fd = - g_model_fd >= 0 && + !device_copy && g_model_fd >= 0 && (g_model_fd_host_base == NULL || model_map == g_model_fd_host_base); - if (!use_fd && !cuda_stream_selected_ensure_stream()) return 1; + if (!use_fd && !cuda_stream_selected_ensure_stream()) return device_copy ? 0 : 1; cuda_stream_read_job read_jobs[DS4_ROCM_STREAM_READ_MAX_JOBS]; memset(read_jobs, 0, sizeof(read_jobs)); @@ -2866,22 +3441,22 @@ static int cuda_stream_resident_seed_experts( NULL, NULL, 0, 0}; } else { cudaError_t err = cudaMemcpyAsync(entry.gate, - (const char *)model_map + gate_offset + gate_rel, + (device_copy ? layer_gate : (const char *)model_map + gate_offset) + gate_rel, (size_t)gate_expert_bytes, - cudaMemcpyHostToDevice, + device_copy ? cudaMemcpyDeviceToDevice : cudaMemcpyHostToDevice, g_stream_selected_upload_stream); if (err == cudaSuccess) { err = cudaMemcpyAsync(entry.up, - (const char *)model_map + up_offset + gate_rel, + (device_copy ? layer_up : (const char *)model_map + up_offset) + gate_rel, (size_t)gate_expert_bytes, - cudaMemcpyHostToDevice, + device_copy ? cudaMemcpyDeviceToDevice : cudaMemcpyHostToDevice, g_stream_selected_upload_stream); } if (err == cudaSuccess) { err = cudaMemcpyAsync(entry.down, - (const char *)model_map + down_offset + down_rel, + (device_copy ? layer_down : (const char *)model_map + down_offset) + down_rel, (size_t)down_expert_bytes, - cudaMemcpyHostToDevice, + device_copy ? cudaMemcpyDeviceToDevice : cudaMemcpyHostToDevice, g_stream_selected_upload_stream); } if (err != cudaSuccess) { @@ -2900,7 +3475,7 @@ static int cuda_stream_resident_seed_experts( ok = cuda_stream_read_jobs_parallel(read_jobs, read_job_count) && cuda_stream_selected_upload_read_jobs(read_jobs, read_job_count); cuda_stream_read_jobs_free(read_jobs, read_job_count); - } else if (ok && !use_fd) { + } else if (!use_fd && (ok || device_copy)) { cudaError_t err = cudaStreamSynchronize(g_stream_selected_upload_stream); if (err != cudaSuccess) { fprintf(stderr, @@ -2922,7 +3497,7 @@ static int cuda_stream_resident_seed_experts( n_experts, loaded); } - return 1; + return device_copy ? 0 : 1; } if (getenv("DS4_ROCM_STREAMING_EXPERT_CACHE_VERBOSE") != NULL) { @@ -5382,6 +5957,7 @@ static int cuda_model_stage_pool_alloc(uint64_t bytes) { (void)cudaFreeHost(g_model_stage_raw[i]); g_model_stage_raw[i] = NULL; g_model_stage[i] = NULL; + g_model_stage_alloc_bytes[i] = 0; } } g_model_stage_bytes = 0; @@ -5407,6 +5983,7 @@ static int cuda_model_stage_pool_alloc(uint64_t bytes) { (void)cudaGetLastError(); return 0; } + g_model_stage_alloc_bytes[i] = alloc_bytes; g_model_stage[i] = cuda_align_ptr(g_model_stage_raw[i], g_model_direct_align); err = cudaEventCreateWithFlags(&g_model_stage_event[i], cudaEventDisableTiming); if (err != cudaSuccess) { @@ -5538,10 +6115,11 @@ static int cuda_stream_model_cache_prepare_memory( return 1; } -static char *cuda_model_arena_alloc(uint64_t bytes, const char *what) { +static char *cuda_model_arena_alloc(uint64_t bytes, const char *what, bool exact_arena = false) { if (bytes == 0) return NULL; if (g_model_cache_full) return NULL; const uint64_t align = 256u; + if (g_deepseek41_model && bytes > UINT64_MAX - (align - 1u)) return NULL; const uint64_t aligned = (bytes + align - 1u) & ~(align - 1u); for (cuda_model_arena &a : g_model_arenas) { @@ -5556,7 +6134,9 @@ static char *cuda_model_arena_alloc(uint64_t bytes, const char *what) { const uint64_t limit = cuda_model_cache_limit_bytes(); if (g_model_range_bytes > limit || aligned > limit - g_model_range_bytes) return NULL; - const uint64_t chunk = ds4_rocm_model_arena_bytes(aligned); + /* V4.1 startup uses exact capacity; preserve legacy pooling for other models. + * A follow-up may consolidate these policies after cross-model memory validation. */ + const uint64_t chunk = ((g_deepseek41_model && g_ssd_streaming_mode) || exact_arena) ? aligned : ds4_rocm_model_arena_bytes(aligned); void *dev = NULL; cudaError_t err = cudaMalloc(&dev, (size_t)chunk); if (err != cudaSuccess) { @@ -5594,15 +6174,17 @@ static const char *cuda_model_range_ptr_from_fd( const void *model_map, uint64_t offset, uint64_t bytes, - const char *what) { + const char *what, + bool exact_arena) { if (g_model_fd < 0 || bytes == 0) return NULL; if (g_model_fd_host_base != NULL && model_map != g_model_fd_host_base) return NULL; const uint64_t limit = cuda_model_cache_limit_bytes(); if (g_model_range_bytes > limit || bytes > limit - g_model_range_bytes) { - if (g_ssd_streaming_mode) { + if (g_ssd_streaming_mode || exact_arena) { fprintf(stderr, - DS4_GPU_LOG_PREFIX "streaming model cache limit prevents " + DS4_GPU_LOG_PREFIX "%s model cache limit prevents " "loading %s range %.2f MiB; refusing host-pointer fallback\n", + exact_arena ? "resident" : "streaming", what ? what : "weights", (double)bytes / 1048576.0); return NULL; @@ -5615,12 +6197,13 @@ static const char *cuda_model_range_ptr_from_fd( chunk + (g_model_direct_align > 1 ? g_model_direct_align : 1); if (!cuda_model_stage_pool_alloc(stage_bytes)) return NULL; - char *dev = cuda_model_arena_alloc(bytes, what); + char *dev = cuda_model_arena_alloc(bytes, what, exact_arena); if (!dev) { - if (g_ssd_streaming_mode) { + if (g_ssd_streaming_mode || exact_arena) { fprintf(stderr, - DS4_GPU_LOG_PREFIX "streaming model cache allocation failed " + DS4_GPU_LOG_PREFIX "%s model cache allocation failed " "for %s range %.2f MiB; refusing host-pointer fallback\n", + exact_arena ? "resident" : "streaming", what ? what : "weights", (double)bytes / 1048576.0); return NULL; @@ -5858,6 +6441,7 @@ extern "C" int ds4_gpu_init(void) { } extern "C" void ds4_gpu_cleanup(void) { + ds4_gpu_tp_shutdown(); (void)cudaDeviceSynchronize(); cuda_stream_cache_stats_print("cleanup"); cuda_shared_gate_up_async_cleanup(); @@ -5905,6 +6489,7 @@ extern "C" void ds4_gpu_cleanup(void) { (void)cudaFreeHost(g_model_stage_raw[i]); g_model_stage_raw[i] = NULL; g_model_stage[i] = NULL; + g_model_stage_alloc_bytes[i] = 0; } } g_model_stage_bytes = 0; @@ -5956,6 +6541,22 @@ extern "C" ds4_gpu_tensor *ds4_gpu_tensor_alloc(uint64_t bytes) { return t; } +extern "C" ds4_gpu_tensor *ds4_gpu_tensor_alloc_coherent(uint64_t bytes) { + if (!bytes || bytes > SIZE_MAX || !ds4_rocm_allocation_fits((size_t)bytes, true)) return NULL; + ds4_gpu_tensor *t = (ds4_gpu_tensor *)calloc(1, sizeof(*t)); + if (!t) return NULL; + if (!cuda_ok(hipHostMalloc(&t->host_ptr, (size_t)bytes, + hipHostMallocMapped | hipHostMallocCoherent), "TP coherent allocation")) { + free(t); return NULL; + } + if (!cuda_ok(hipHostGetDevicePointer(&t->ptr, t->host_ptr, 0), "TP coherent device alias")) { + (void)hipHostFree(t->host_ptr); free(t); return NULL; + } + t->owner = 2; + t->bytes = bytes; + return t; +} + extern "C" ds4_gpu_tensor *ds4_gpu_tensor_alloc_managed(uint64_t bytes) { if (bytes == 0) bytes = 1; ds4_gpu_tensor *t = (ds4_gpu_tensor *)calloc(1, sizeof(*t)); @@ -6012,12 +6613,14 @@ extern "C" ds4_gpu_tensor *ds4_gpu_tensor_view(const ds4_gpu_tensor *base, uint6 t->ptr = (char *)base->ptr + offset; t->bytes = bytes; t->owner = 0; + t->host_ptr = base->host_ptr ? (char *)base->host_ptr + offset : NULL; return t; } extern "C" void ds4_gpu_tensor_free(ds4_gpu_tensor *tensor) { if (!tensor) return; - if (tensor->owner && tensor->ptr) (void)cudaFree(tensor->ptr); + if (tensor->owner == 2 && tensor->host_ptr) (void)hipHostFree(tensor->host_ptr); + else if (tensor->owner && tensor->ptr) (void)cudaFree(tensor->ptr); free(tensor); } @@ -6028,7 +6631,7 @@ extern "C" uint64_t ds4_gpu_tensor_bytes(const ds4_gpu_tensor *tensor) { extern "C" void *ds4_gpu_tensor_contents(ds4_gpu_tensor *tensor) { if (!tensor) return NULL; (void)cudaDeviceSynchronize(); - return tensor->ptr; + return tensor->host_ptr ? tensor->host_ptr : tensor->ptr; } extern "C" int ds4_gpu_tensor_fill_f32(ds4_gpu_tensor *tensor, float value, uint64_t count) { @@ -6347,6 +6950,58 @@ extern "C" int ds4_gpu_cache_model_range(const void *model_map, uint64_t model_s return cuda_model_range_is_cached(model_map, offset, bytes); } +/* Exact-capacity startup is explicitly requested by the V4.1 resident graph. + * Bind it to the registered primary file; failures cannot fall back to host + * pointers or change the allocation policy of later runtime callers. */ +extern "C" int ds4_gpu_cache_model_range_exact(const void *model_map, uint64_t model_size, + uint64_t offset, uint64_t bytes, const char *label) { + if (!g_deepseek41_model || !model_map || g_ssd_streaming_mode || model_map != g_model_host_base || + model_size != g_model_registered_size || g_model_fd < 0 || + g_model_fd_host_base != model_map || offset > model_size || + bytes > model_size - offset || offset > g_model_file_size || + bytes > g_model_file_size - offset || bytes > UINT64_MAX - 255u) return 0; + if (!bytes) return 1; + if (!ds4_rocm_is_gfx1151()) + return ds4_gpu_cache_model_range(model_map, model_size, offset, bytes, label); + if (cuda_model_range_is_cached(model_map, offset, bytes)) return 1; + if (!cuda_model_range_ptr_from_fd(model_map, offset, bytes, + label ? label : "model_tensor", true)) return 0; + return cuda_model_range_is_cached(model_map, offset, bytes); +} + +/* The primary model's startup spans are prepared before session/cache admission. + * Its four pinned upload buffers are now idle; keep the stream, and let any + * later cache upload recreate the pool through cuda_model_stage_pool_alloc. */ +extern "C" int ds4_gpu_release_model_upload_staging(const void *model_map, + uint64_t model_size) { + if (!model_map || !model_size || + model_map != g_model_host_base || model_size != g_model_registered_size || + g_model_fd < 0 || g_model_fd_host_base != model_map) return 0; + if (!ds4_rocm_is_gfx1151()) return 1; + if (g_model_upload_stream && + !cuda_ok(cudaStreamSynchronize(g_model_upload_stream), + "resident model staging release sync")) { + /* Returning would let engine teardown free a potentially live pool. */ + abort(); + } + for (size_t i = 0; i < 4; i++) { + if (g_model_stage_event[i]) { + if (!cuda_ok(cudaEventDestroy(g_model_stage_event[i]), + "resident model staging event release")) abort(); + g_model_stage_event[i] = NULL; + } + if (g_model_stage_raw[i]) { + if (!cuda_ok(cudaFreeHost(g_model_stage_raw[i]), + "resident model staging release")) abort(); + g_model_stage_raw[i] = NULL; + } + g_model_stage[i] = NULL; + g_model_stage_alloc_bytes[i] = 0; + } + g_model_stage_bytes = 0; + return 1; +} + extern "C" int ds4_gpu_cache_q8_f16_range(const void *model_map, uint64_t model_size, uint64_t offset, uint64_t bytes, uint64_t in_dim, uint64_t out_dim, const char *label) { if (!model_map || bytes == 0) return 1; if (offset > model_size || bytes > model_size - offset) return 0; diff --git a/rocm/ds4_rocm_tp.cuh b/rocm/ds4_rocm_tp.cuh new file mode 100644 index 0000000000..3cc03f7fa2 --- /dev/null +++ b/rocm/ds4_rocm_tp.cuh @@ -0,0 +1,307 @@ +/* One compute stream, one CPU transport service. Each queue slot has exact + * ready/done/consumed generations. The service never calls HIP; the GPU + * releases a slot only after its guarded payload consumer has finished. */ +static constexpr unsigned ROCM_TP_QUEUE = 64; +struct alignas(64) rocm_tp_flags { + uint64_t ready, done, consumed; +}; +struct rocm_tp_shared { + uint32_t abort; + rocm_tp_flags slots[ROCM_TP_QUEUE]; +}; +struct rocm_tp_job { + uint64_t seq, bytes; + uint32_t layer, arg, kind; + const void *out; + void *in; +}; +struct rocm_tp_state { + bool active = false, started = false; + uint64_t seq = 0, posted = 0, pending = 0, timeout_ticks = 0; + uint32_t pending_count = 0; + bool pending_deferred = false; + void *pending_big_in = nullptr; + uint64_t pending_big_bytes = 0; + ds4_gpu_tensor *slab = nullptr, *flags = nullptr; + ds4_gpu_tensor *big_out = nullptr, *big_in = nullptr; + rocm_tp_shared *host = nullptr, *device = nullptr; + rocm_tp_job jobs[ROCM_TP_QUEUE] = {}; + pthread_t thread{}; + ds4_gpu_tp_exchange_fn exchange = nullptr; + ds4_gpu_tp_batch_exchange_fn batch = nullptr; + ds4_gpu_tp_big_exchange_fn big = nullptr; + void *ud = nullptr; +}; +static rocm_tp_state g_rocm_tp; +static int g_rocm_tp_failed; +static pthread_mutex_t g_rocm_tp_mutex = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t g_rocm_tp_cond = PTHREAD_COND_INITIALIZER; + +static int rocm_tp_fail(void) { + __atomic_store_n(&g_rocm_tp_failed, 1, __ATOMIC_RELEASE); + if (g_rocm_tp.host) __atomic_store_n(&g_rocm_tp.host->abort, 1u, __ATOMIC_RELEASE); + pthread_mutex_lock(&g_rocm_tp_mutex); + pthread_cond_broadcast(&g_rocm_tp_cond); + pthread_mutex_unlock(&g_rocm_tp_mutex); + return 0; +} +extern "C" int ds4_gpu_tp_failed(void) { + return __atomic_load_n(&g_rocm_tp_failed, __ATOMIC_ACQUIRE) || + (g_rocm_tp.host && __atomic_load_n(&g_rocm_tp.host->abort, __ATOMIC_ACQUIRE)); +} +static __device__ bool rocm_tp_aborted(rocm_tp_shared *s) { + return __hip_atomic_load(&s->abort, __ATOMIC_ACQUIRE, __HIP_MEMORY_SCOPE_SYSTEM) != 0; +} +static __global__ void rocm_tp_arrive(rocm_tp_shared *s, unsigned slot, uint64_t seq) { + if (!rocm_tp_aborted(s)) + __hip_atomic_store(&s->slots[slot].ready, seq, __ATOMIC_RELEASE, __HIP_MEMORY_SCOPE_SYSTEM); +} +static __global__ void rocm_tp_wait(rocm_tp_shared *s, unsigned slot, uint64_t seq, uint64_t ticks) { + const uint64_t start = wall_clock64(); + while (!rocm_tp_aborted(s)) { + uint64_t done = __hip_atomic_load(&s->slots[slot].done, __ATOMIC_ACQUIRE, __HIP_MEMORY_SCOPE_SYSTEM); + if (done == seq) return; + if (done > seq || (uint64_t)wall_clock64() - start >= ticks) { + __hip_atomic_store(&s->abort, 1u, __ATOMIC_RELEASE, __HIP_MEMORY_SCOPE_SYSTEM); + return; + } + __builtin_amdgcn_s_sleep(1); + } +} +static __global__ void rocm_tp_copy(rocm_tp_shared *s, float *out, const float *in, uint64_t n) { + if (rocm_tp_aborted(s)) return; + for (uint64_t i = (uint64_t)blockIdx.x * blockDim.x + threadIdx.x; i < n; i += (uint64_t)gridDim.x * blockDim.x) + out[i] = in[i]; +} +static __global__ void rocm_tp_add(rocm_tp_shared *s, unsigned slot, uint64_t seq, + float *out, const float *a, const float *b, uint32_t n) { + if (rocm_tp_aborted(s) || + __hip_atomic_load(&s->slots[slot].done, __ATOMIC_ACQUIRE, __HIP_MEMORY_SCOPE_SYSTEM) != seq) return; + for (uint64_t i = (uint64_t)blockIdx.x * blockDim.x + threadIdx.x; i < n; i += (uint64_t)gridDim.x * blockDim.x) + out[i] = a[i] + b[i]; +} +static __global__ void rocm_tp_release(rocm_tp_shared *s, unsigned slot, uint64_t seq) { + __hip_atomic_store(&s->slots[slot].consumed, seq, __ATOMIC_RELEASE, __HIP_MEMORY_SCOPE_SYSTEM); +} +static bool rocm_tp_fused_scalar_gate(void) { return g_deepseek41_model; } +static __global__ void rocm_tp_wait_add_release(rocm_tp_shared *s, unsigned slot, + uint64_t seq, uint64_t ticks, float *out, const float *a, const float *b, uint32_t n) { + __shared__ int ok; + if (threadIdx.x == 0) { + ok = 0; + const uint64_t start = wall_clock64(); + while (!rocm_tp_aborted(s)) { + const uint64_t done = __hip_atomic_load( + &s->slots[slot].done, __ATOMIC_ACQUIRE, __HIP_MEMORY_SCOPE_SYSTEM); + if (done == seq) { ok = 1; break; } + if (done > seq || wall_clock64() - start >= ticks) { + __hip_atomic_store(&s->abort, 1u, __ATOMIC_RELEASE, __HIP_MEMORY_SCOPE_SYSTEM); + break; + } + __builtin_amdgcn_s_sleep(1); + } + } + __syncthreads(); + if (ok) + for (uint32_t i = threadIdx.x; i < n; i += blockDim.x) out[i] = a[i] + b[i]; + __syncthreads(); + if (threadIdx.x == 0) + __hip_atomic_store(&s->slots[slot].consumed, seq, __ATOMIC_RELEASE, __HIP_MEMORY_SCOPE_SYSTEM); +} + +static void rocm_tp_pause(unsigned *spins) { + if (++*spins < 100) { sched_yield(); return; } + const struct timespec delay = {0, 10000}; + nanosleep(&delay, nullptr); +} +static void *rocm_tp_service(void *) { + for (uint64_t seq = 1;; ++seq) { + pthread_mutex_lock(&g_rocm_tp_mutex); + while (__atomic_load_n(&g_rocm_tp.posted, __ATOMIC_ACQUIRE) < seq && !ds4_gpu_tp_failed()) + pthread_cond_wait(&g_rocm_tp_cond, &g_rocm_tp_mutex); + pthread_mutex_unlock(&g_rocm_tp_mutex); + if (ds4_gpu_tp_failed()) return nullptr; + const unsigned slot = (unsigned)((seq - 1) % ROCM_TP_QUEUE); + unsigned spins = 0; + for (;;) { + if (ds4_gpu_tp_failed()) return nullptr; + uint64_t ready = __atomic_load_n(&g_rocm_tp.host->slots[slot].ready, __ATOMIC_ACQUIRE); + if (ready == seq) break; + if (ready > seq) { rocm_tp_fail(); return nullptr; } + rocm_tp_pause(&spins); + } + const rocm_tp_job job = g_rocm_tp.jobs[slot]; + int ok = job.seq == seq; + if (ok && job.kind == 0) ok = g_rocm_tp.exchange(g_rocm_tp.ud, job.layer, job.arg, seq); + else if (ok && job.kind == 1) ok = g_rocm_tp.batch(g_rocm_tp.ud, job.layer, job.arg, seq); + else if (ok && job.kind == 2) ok = g_rocm_tp.big(g_rocm_tp.ud, job.layer, seq, job.out, job.in, job.bytes); + else ok = 0; + if (!ok) { rocm_tp_fail(); return nullptr; } + if (ds4_gpu_tp_failed()) return nullptr; + __atomic_store_n(&g_rocm_tp.host->slots[slot].done, seq, __ATOMIC_RELEASE); + } +} +extern "C" void ds4_gpu_tp_shutdown(void) { + if (!g_rocm_tp.active && !g_rocm_tp.flags) return; + /* Wake GPU waits and the idle service before draining either side. The + * transport callback has its own bounded I/O deadline. */ + rocm_tp_fail(); + if (g_rocm_tp.started) pthread_join(g_rocm_tp.thread, nullptr); + (void)cudaDeviceSynchronize(); + ds4_gpu_tensor_free(g_rocm_tp.big_in); + ds4_gpu_tensor_free(g_rocm_tp.big_out); + ds4_gpu_tensor_free(g_rocm_tp.flags); + g_rocm_tp = rocm_tp_state{}; + __atomic_store_n(&g_rocm_tp_failed, 0, __ATOMIC_RELEASE); +} +extern "C" int ds4_gpu_tp_init(uint32_t rank, ds4_gpu_tensor *slab, + uint64_t gpu_flags_off, uint64_t out_off, uint64_t vec_bytes, + ds4_gpu_tp_exchange_fn fn, void *ud) { + if (g_rocm_tp.active || !g_deepseek41_model || rank > 1u || + !slab || !slab->host_ptr || !fn || vec_bytes != 20480u || + gpu_flags_off > slab->bytes || 80u * sizeof(uint32_t) > slab->bytes - gpu_flags_off || + out_off > slab->bytes || 80u * vec_bytes > slab->bytes - out_off) return 0; + int device, khz; + if (hipGetDevice(&device) != hipSuccess || + hipDeviceGetAttribute(&khz, hipDeviceAttributeWallClockRate, device) != hipSuccess || khz <= 0) return 0; + g_rocm_tp.flags = ds4_gpu_tensor_alloc_coherent(sizeof(rocm_tp_shared)); + if (!g_rocm_tp.flags) return 0; + g_rocm_tp.host = (rocm_tp_shared *)g_rocm_tp.flags->host_ptr; + g_rocm_tp.device = (rocm_tp_shared *)g_rocm_tp.flags->ptr; + memset(g_rocm_tp.host, 0, sizeof(rocm_tp_shared)); + g_rocm_tp.timeout_ticks = (uint64_t)khz * 1000u * 5u; + g_rocm_tp.active = true; + g_rocm_tp.slab = slab; + g_rocm_tp.exchange = fn; + g_rocm_tp.ud = ud; + __atomic_store_n(&g_rocm_tp_failed, 0, __ATOMIC_RELEASE); + if (pthread_create(&g_rocm_tp.thread, nullptr, rocm_tp_service, nullptr)) { + ds4_gpu_tp_shutdown(); return 0; + } + g_rocm_tp.started = true; + return 1; +} +extern "C" void ds4_gpu_tp_set_batch_exchange(ds4_gpu_tp_batch_exchange_fn fn) { g_rocm_tp.batch = fn; } +extern "C" void ds4_gpu_tp_set_big_exchange(ds4_gpu_tp_big_exchange_fn fn) { g_rocm_tp.big = fn; } +extern "C" void ds4_gpu_tp_set_session_batch_mode(int enabled) { (void)enabled; } +extern "C" int ds4_gpu_tp_decode_split_flush_safe(void) { return 0; } + +static int rocm_tp_enqueue(rocm_tp_job job, uint32_t count, bool defer_wait = false) { + if (!g_rocm_tp.active || g_rocm_tp.pending || ds4_gpu_tp_failed() || g_rocm_tp.seq == UINT64_MAX) + return rocm_tp_fail(); + const uint64_t seq = g_rocm_tp.seq + 1; + const unsigned slot = (unsigned)((seq - 1) % ROCM_TP_QUEUE); + unsigned spins = 0; + while (seq > ROCM_TP_QUEUE && __atomic_load_n(&g_rocm_tp.host->slots[slot].consumed, __ATOMIC_ACQUIRE) != seq - ROCM_TP_QUEUE) { + if (ds4_gpu_tp_failed()) return 0; + rocm_tp_pause(&spins); + } + job.seq = seq; + g_rocm_tp.jobs[slot] = job; + rocm_tp_arrive<<<1, 1>>>(g_rocm_tp.device, slot, seq); + if (!defer_wait && !(job.kind == 0 && rocm_tp_fused_scalar_gate())) + rocm_tp_wait<<<1, 1>>>(g_rocm_tp.device, slot, seq, g_rocm_tp.timeout_ticks); + if (!cuda_ok(cudaGetLastError(), "TP gate enqueue")) return rocm_tp_fail(); + g_rocm_tp.seq = g_rocm_tp.pending = seq; + g_rocm_tp.pending_count = count; + g_rocm_tp.pending_deferred = defer_wait; + pthread_mutex_lock(&g_rocm_tp_mutex); + __atomic_store_n(&g_rocm_tp.posted, seq, __ATOMIC_RELEASE); + pthread_cond_signal(&g_rocm_tp_cond); + pthread_mutex_unlock(&g_rocm_tp_mutex); + return 1; +} +extern "C" int ds4_gpu_tp_gate_encode(uint32_t layer, uint32_t gate) { + if (layer >= 40u || gate >= 2u) return rocm_tp_fail(); + return rocm_tp_enqueue({0, 0, layer, gate, 0, nullptr, nullptr}, 5120u); +} +extern "C" int ds4_gpu_tp_batch_gate_encode(uint32_t layer, uint32_t rows) { + if (!g_rocm_tp.batch || layer >= 40u || !rows || rows > 8u) return rocm_tp_fail(); + return rocm_tp_enqueue({0, 0, layer, rows, 1, nullptr, nullptr}, rows * 5120u); +} +extern "C" int ds4_gpu_tp_big_gate_encode(uint32_t layer, uint32_t rows, + const ds4_gpu_tensor *out_t, ds4_gpu_tensor *in_t, uint64_t bytes) { + if (!g_rocm_tp.active || g_rocm_tp.pending || !g_rocm_tp.big || ds4_gpu_tp_failed() || layer >= 40u || + !rows || rows > UINT32_MAX / 5120u || bytes != (uint64_t)rows * 20480u || !out_t || !in_t || + bytes > out_t->bytes || bytes > in_t->bytes) return rocm_tp_fail(); + if (!g_rocm_tp.big_out || g_rocm_tp.big_out->bytes < bytes) { + if (!ds4_gpu_synchronize() || ds4_gpu_tp_failed()) return rocm_tp_fail(); + ds4_gpu_tensor *tx = ds4_gpu_tensor_alloc_coherent(bytes); + ds4_gpu_tensor *rx = ds4_gpu_tensor_alloc_coherent(bytes); + if (!tx || !rx) { ds4_gpu_tensor_free(tx); ds4_gpu_tensor_free(rx); return rocm_tp_fail(); } + ds4_gpu_tensor_free(g_rocm_tp.big_out); ds4_gpu_tensor_free(g_rocm_tp.big_in); + g_rocm_tp.big_out = tx; g_rocm_tp.big_in = rx; + } + rocm_tp_copy<<<256, 256>>>(g_rocm_tp.device, (float *)g_rocm_tp.big_out->ptr, (const float *)out_t->ptr, bytes / 4); + if (!rocm_tp_enqueue({0, bytes, layer, rows, 2, g_rocm_tp.big_out->host_ptr, g_rocm_tp.big_in->host_ptr}, rows * 5120u)) return 0; + rocm_tp_copy<<<256, 256>>>(g_rocm_tp.device, (float *)in_t->ptr, (const float *)g_rocm_tp.big_in->ptr, bytes / 4); + return cuda_ok(cudaGetLastError(), "TP receive enqueue") || rocm_tp_fail(); +} +extern "C" int ds4_gpu_tp_big_gate_begin(uint32_t layer, uint32_t rows, + const ds4_gpu_tensor *out_t, ds4_gpu_tensor *in_t, uint64_t bytes) { + if (!g_rocm_tp.active || g_rocm_tp.pending || !g_rocm_tp.big || ds4_gpu_tp_failed() || layer >= 40u || + !rows || rows > UINT32_MAX / 5120u || bytes != (uint64_t)rows * 20480u || !out_t || !in_t || + bytes > out_t->bytes || bytes > in_t->bytes) return rocm_tp_fail(); + if (!g_rocm_tp.big_out || g_rocm_tp.big_out->bytes < bytes) { + if (!ds4_gpu_synchronize() || ds4_gpu_tp_failed()) return rocm_tp_fail(); + ds4_gpu_tensor *tx = ds4_gpu_tensor_alloc_coherent(bytes); + ds4_gpu_tensor *rx = ds4_gpu_tensor_alloc_coherent(bytes); + if (!tx || !rx) { ds4_gpu_tensor_free(tx); ds4_gpu_tensor_free(rx); return rocm_tp_fail(); } + ds4_gpu_tensor_free(g_rocm_tp.big_out); ds4_gpu_tensor_free(g_rocm_tp.big_in); + g_rocm_tp.big_out = tx; g_rocm_tp.big_in = rx; + } + rocm_tp_copy<<<256, 256>>>(g_rocm_tp.device, (float *)g_rocm_tp.big_out->ptr, (const float *)out_t->ptr, bytes / 4); + if (!rocm_tp_enqueue({0, bytes, layer, rows, 2, g_rocm_tp.big_out->host_ptr, g_rocm_tp.big_in->host_ptr}, rows * 5120u, true)) return 0; + g_rocm_tp.pending_big_in = in_t->ptr; + g_rocm_tp.pending_big_bytes = bytes; + return 1; +} +extern "C" int ds4_gpu_tp_big_gate_join(uint32_t layer, uint32_t rows, + ds4_gpu_tensor *in_t, uint64_t bytes) { + const uint64_t seq = g_rocm_tp.pending; + if (!g_rocm_tp.active || !seq || !g_rocm_tp.pending_deferred || + !in_t || !in_t->ptr || in_t->ptr != g_rocm_tp.pending_big_in || + bytes != g_rocm_tp.pending_big_bytes || bytes > in_t->bytes || + rows > UINT32_MAX / 5120u || rows * 5120u != g_rocm_tp.pending_count || + ds4_gpu_tp_failed()) return rocm_tp_fail(); + const unsigned slot = (unsigned)((seq - 1) % ROCM_TP_QUEUE); + const rocm_tp_job &job = g_rocm_tp.jobs[slot]; + if (job.seq != seq || job.kind != 2 || job.layer != layer || job.arg != rows || + job.bytes != bytes) return rocm_tp_fail(); + rocm_tp_wait<<<1, 1>>>(g_rocm_tp.device, slot, seq, g_rocm_tp.timeout_ticks); + rocm_tp_copy<<<256, 256>>>(g_rocm_tp.device, (float *)in_t->ptr, + (const float *)g_rocm_tp.big_in->ptr, bytes / 4); + if (!cuda_ok(cudaGetLastError(), "TP deferred receive")) return rocm_tp_fail(); + g_rocm_tp.pending_deferred = false; + g_rocm_tp.pending_big_in = nullptr; + g_rocm_tp.pending_big_bytes = 0; + return 1; +} +extern "C" int ds4_gpu_tp_add_tensor(ds4_gpu_tensor *out, const ds4_gpu_tensor *a, + const ds4_gpu_tensor *b, uint32_t n) { + const uint64_t seq = g_rocm_tp.pending; + const uint64_t bytes = (uint64_t)n * 4; + if (!g_rocm_tp.active || !seq || g_rocm_tp.pending_deferred || !out || !a || !b || n != g_rocm_tp.pending_count || + bytes > out->bytes || bytes > a->bytes || bytes > b->bytes || ds4_gpu_tp_failed()) return rocm_tp_fail(); + const unsigned slot = (unsigned)((seq - 1) % ROCM_TP_QUEUE); + /* Avoid coherent guard loads from idle workgroups on scalar payloads. + * The grid-stride loop preserves full coverage for larger batches. */ + const uint32_t blocks = n / 256u + (n % 256u != 0u); + if (n == 5120u && g_rocm_tp.jobs[slot].kind == 0 && rocm_tp_fused_scalar_gate()) { + rocm_tp_wait_add_release<<<1, 1024>>>(g_rocm_tp.device, slot, seq, g_rocm_tp.timeout_ticks, + (float *)out->ptr, (const float *)a->ptr, (const float *)b->ptr, n); + } else { + rocm_tp_add<<>>(g_rocm_tp.device, slot, seq, (float *)out->ptr, (const float *)a->ptr, (const float *)b->ptr, n); + rocm_tp_release<<<1, 1>>>(g_rocm_tp.device, slot, seq); + } + g_rocm_tp.pending = 0; + return cuda_ok(cudaGetLastError(), "TP guarded reduction") || rocm_tp_fail(); +} + +extern "C" int ds4_gpu_tp_big_gate_overlap_supported(void) { + return ds4_rocm_is_gfx1151(); +} +extern "C" void ds4_gpu_tp_big_gate_abort(void) { + rocm_tp_fail(); + (void)ds4_gpu_synchronize(); +} diff --git a/rocm/ds4_rocm_v41.cuh b/rocm/ds4_rocm_v41.cuh new file mode 100644 index 0000000000..da068ad32d --- /dev/null +++ b/rocm/ds4_rocm_v41.cuh @@ -0,0 +1,1560 @@ +/* DeepSeek V4.1 baseline primitives. Float-addressable storage preserves the released BF16/FP8/FP4 graph boundaries. */ +#ifdef __HIP_PLATFORM_AMD__ + +/* AMDGPU ignores float_control(precise), and HIP's math wrappers select native trig while preprocessing -ffast-math. Use explicit OCML operations and division instructions so the existing translation unit keeps its tuned arithmetic. */ +__device__ static float v41_add(float x, float y) { return __ocml_add_rte_f32(x, y); } +__device__ static float v41_sub(float x, float y) { return __ocml_sub_rte_f32(x, y); } +__device__ static float v41_mul(float x, float y) { return __ocml_mul_rte_f32(x, y); } +__device__ static float v41_div(float x, float y) { + /* ROCm 10 declares but does not define __ocml_div_rte_f32. A local reciprocal(off) pragma still leaves afn, which AMDGPU lowers to an approximate reciprocal. Encode the backend's full F32 division refinement, including temporary denorm preservation, as one indivisible block. */ + float q, d, n, r, e; + uint32_t mode; +#define DS4_V41_DIV_ASM(VCC) asm volatile( \ + "v_div_scale_f32 %1, " VCC ", %7, %7, %6\n\t" \ + "v_div_scale_f32 %2, " VCC ", %6, %7, %6\n\t" \ + "v_rcp_f32 %3, %1\n\t" \ + "s_getreg_b32 %5, hwreg(HW_REG_MODE, 4, 2)\n\t" \ + "s_setreg_imm32_b32 hwreg(HW_REG_MODE, 4, 2), 3\n\t" \ + "v_fma_f32 %4, -%1, %3, 1.0\n\t" \ + "v_fma_f32 %3, %4, %3, %3\n\t" \ + "v_mul_f32 %0, %2, %3\n\t" \ + "v_fma_f32 %4, -%1, %0, %2\n\t" \ + "v_fma_f32 %0, %4, %3, %0\n\t" \ + "v_fma_f32 %4, -%1, %0, %2\n\t" \ + "s_setreg_b32 hwreg(HW_REG_MODE, 4, 2), %5\n\t" \ + "v_div_fmas_f32 %0, %4, %3, %0\n\t" \ + "v_div_fixup_f32 %0, %0, %7, %6\n\t" \ + : "=&v"(q), "=&v"(d), "=&v"(n), "=&v"(r), "=&v"(e), "=&s"(mode) \ + : "v"(x), "v"(y) : "vcc", "memory") + /* The HIP compiler need not define a wave-size macro. Its target builtin folds before assembly, selecting the valid VCC operand for the actual wave mode. */ +#if defined(__AMDGCN__) + if (__builtin_amdgcn_wavefrontsize() == 64) DS4_V41_DIV_ASM("vcc"); + else +#endif + DS4_V41_DIV_ASM("vcc_lo"); +#undef DS4_V41_DIV_ASM + return q; +} + +__device__ static float v41_bf16(float x) { + uint32_t bits = __float_as_uint(x); + if ((bits & 0x7f800000u) != 0x7f800000u) + bits += 0x7fffu + ((bits >> 16u) & 1u); + return __uint_as_float(bits & 0xffff0000u); +} + +__device__ static float v41_pow2_ceil(float x) { + const uint32_t bits = __float_as_uint(x); + return __uint_as_float((bits & 0x7f800000u) + ((bits & 0x7fffffu) ? 0x800000u : 0u)); +} + +__device__ static float v41_sum32(float x) { + for (int delta = 16; delta; delta >>= 1) + x = v41_add(x, __shfl_down(x, delta, 32)); + return __shfl(x, 0, 32); +} + +__global__ static void v41_bf16_kernel(float *x, uint64_t count) { + const uint64_t i = (uint64_t)blockIdx.x * blockDim.x + threadIdx.x; + if (i < count) x[i] = v41_bf16(x[i]); +} + +__global__ static void v41_quantize_kernel(float *x, uint32_t format) { + const uint32_t block = format == DS4_V41_FP4_E4M3 ? 16u : 32u; + const uint32_t lane = threadIdx.x; + const uint64_t i = (uint64_t)blockIdx.x * block + lane; + const float value = lane < block ? v41_bf16(x[i]) : 0.0f; + float amax = fabsf(value); + for (int delta = 16; delta; delta >>= 1) + amax = fmaxf(amax, __shfl_down(amax, delta, 32)); + amax = __shfl(amax, 0, 32); + float result; + if (format == DS4_V41_FP8_E8M0) { + const float scale = v41_pow2_ceil(v41_mul(fmaxf(amax, 1.0e-4f), 1.0f / 448.0f)); + result = v41_mul(dsv4_e4m3fn_dequant_dev(v41_div(fabsf(value), scale)), scale); + } else { + const float scale = format == DS4_V41_FP4_E4M3 ? + dsv4_e4m3fn_dequant_dev(v41_div(fmaxf(amax, 0.01171875f), 6.0f)) : + v41_pow2_ceil(v41_mul(fmaxf(amax, 7.052966104933725e-38f), 1.0f / 6.0f)); + result = v41_mul(dsv4_e2m1fn_dequant_dev(v41_div(fabsf(value), scale)), scale); + } + if (lane < block) { + /* Preserve signed zero even though the surrounding translation unit permits -fno-signed-zeros. */ + const uint32_t bits = (__float_as_uint(v41_bf16(result)) & 0x7fffffffu) | (__float_as_uint(value) & 0x80000000u); + x[i] = __uint_as_float(bits); + } +} + +extern "C" int ds4_gpu_dsv41_quantize(ds4_gpu_tensor *x, uint32_t width, uint32_t rows, + ds4_v41_activation_format format) { + const uint32_t block = format == DS4_V41_FP4_E4M3 ? 16u : 32u; + if (!width || !rows || format < DS4_V41_BF16 || format > DS4_V41_FP4_E4M3 || + (format != DS4_V41_BF16 && width % block) || + !cuda_tensor_has_elems2(x, width, rows, sizeof(float))) return 0; + if (format == DS4_V41_BF16) { + const uint64_t count = (uint64_t)width * rows; + if ((count + 255u) / 256u > UINT32_MAX) return 0; + v41_bf16_kernel<<<(unsigned)((count + 255u) / 256u), 256>>>((float *)x->ptr, count); + } else { + const uint64_t blocks = (uint64_t)(width / block) * rows; + if (blocks > UINT32_MAX) return 0; + v41_quantize_kernel<<<(unsigned)blocks, 32>>>((float *)x->ptr, format); + } + return cuda_ok(cudaGetLastError(), "V4.1 activation quantization"); +} + +struct v41_rope_args { + uint32_t width, heads, start, stride, inverse; + float frequencies[32]; +}; + +__global__ static void v41_rope_kernel(float *x, v41_rope_args args) { + const uint32_t lane = threadIdx.x; + const uint32_t row = blockIdx.x / args.heads; + const float theta = v41_mul((float)(args.start + row * args.stride), args.frequencies[lane]); + const float c = __ocml_cos_f32(theta), s = args.inverse ? -__ocml_sin_f32(theta) : __ocml_sin_f32(theta); + const uint64_t i = (uint64_t)blockIdx.x * args.width + args.width - 64u + 2u * lane; + const float re = x[i], im = x[i + 1u]; + x[i] = v41_bf16(v41_sub(v41_mul(re, c), v41_mul(im, s))); + x[i + 1u] = v41_bf16(v41_add(v41_mul(re, s), v41_mul(im, c))); +} + +static float v41_rope_frequencies[2][32]; +static pthread_once_t v41_rope_once = PTHREAD_ONCE_INIT; + +/* Keep the reference's host pow/reciprocal and YaRN operation order; a frequency ULP grows into a phase error at long positions. */ +#ifndef __HIP_DEVICE_COMPILE__ +#pragma float_control(precise, on, push) +#pragma clang fp contract(off) +#endif +static void v41_init_rope_frequencies(void) { + for (int kind = 0; kind < 2; kind++) { + const float base = kind ? 160000.0f : 10000.0f; + const float low = (float)floor(64.0 * log(65536.0 / (32.0 * 2.0 * M_PI)) / (2.0 * log(base))); + const float high = (float)ceil(64.0 * log(65536.0 / (2.0 * M_PI)) / (2.0 * log(base))); + for (int i = 0; i < 32; i++) { + const float denominator = powf(base, (float)i / 32.0f); + float f = 1.0f / denominator; + if (kind) { + const float ramp = fminf(1.0f, fmaxf(0.0f, (i - low) / (high - low))); + const float smooth = 1.0f - ramp; + const float interpolate = (f / 16.0f) * (1.0f - smooth); + const float extrapolate = f * smooth; + f = interpolate + extrapolate; + } + v41_rope_frequencies[kind][i] = f; + } + } +} +#ifndef __HIP_DEVICE_COMPILE__ +#pragma float_control(pop) +#endif + +extern "C" int ds4_gpu_dsv41_rope_stride(ds4_gpu_tensor *x, uint32_t width, uint32_t heads, + uint32_t rows, uint32_t start, uint32_t stride, + bool compressed, bool inverse) { + uint64_t elems = 0; + if (width < 64u || !heads || !rows || rows > 1048576u || !stride || + (uint64_t)start + (uint64_t)(rows - 1u) * stride >= 1048576u || + (uint64_t)heads * rows > UINT32_MAX || + !cuda_u64_mul3_checked(width, heads, rows, &elems) || !cuda_tensor_has_f32(x, elems)) return 0; + if (pthread_once(&v41_rope_once, v41_init_rope_frequencies)) return 0; + v41_rope_args args = {width, heads, start, stride, inverse, {0}}; + memcpy(args.frequencies, v41_rope_frequencies[compressed ? 1 : 0], sizeof(args.frequencies)); + v41_rope_kernel<<>>((float *)x->ptr, args); + return cuda_ok(cudaGetLastError(), "V4.1 unit-magnitude RoPE"); +} + +extern "C" int ds4_gpu_dsv41_rope(ds4_gpu_tensor *x, uint32_t width, uint32_t heads, + uint32_t rows, uint32_t start, bool compressed, bool inverse) { + return ds4_gpu_dsv41_rope_stride(x, width, heads, rows, start, 1, compressed, inverse); +} + +__global__ static void v41_engram_kernel(float *residual, const float *kv, const float *qw, + const float *kw, const uint8_t *mask, uint32_t width, float eps) { + const uint32_t token = blockIdx.x, head = blockIdx.y, lane = threadIdx.x; + if (mask && !mask[token]) return; + const uint64_t offset = ((uint64_t)token * 4u + head) * width; + const uint64_t key = ((uint64_t)token * 5u + head) * width; + const uint64_t value = ((uint64_t)token * 5u + 4u) * width; + float h2 = 0.0f, k2 = 0.0f, dot = 0.0f; + for (uint32_t i = lane; i < width; i += 32u) { + const float h = residual[offset + i], k = v41_bf16(kv[key + i]); + const uint64_t wi = (uint64_t)head * width + i; + h2 = v41_add(h2, v41_mul(h, h)); + k2 = v41_add(k2, v41_mul(k, k)); + dot = v41_add(dot, v41_mul(v41_mul(h, v41_mul(qw[wi], kw[wi])), k)); + } + h2 = v41_sum32(h2); + k2 = v41_sum32(k2); + dot = v41_mul(v41_sum32(dot), __ocml_rsqrt_f32(v41_add(v41_div(h2, (float)width), eps))); + dot = v41_mul(dot, __ocml_rsqrt_f32(v41_add(v41_div(k2, (float)width), eps))); + dot = v41_mul(dot, __ocml_rsqrt_f32((float)width)); + const float gate = v41_div(1.0f, v41_add(1.0f, + __ocml_exp_f32(-copysignf(__ocml_sqrt_f32(fmaxf(fabsf(dot), 1.0e-6f)), dot)))); + for (uint32_t i = lane; i < width; i += 32u) + residual[offset + i] = v41_bf16(v41_add(residual[offset + i], v41_mul(gate, v41_bf16(kv[value + i])))); +} + +extern "C" int ds4_gpu_dsv41_engram_add(ds4_gpu_tensor *residual, const ds4_gpu_tensor *kv, + const ds4_gpu_tensor *q_weight, const ds4_gpu_tensor *k_weight, + const ds4_gpu_tensor *mask, uint32_t width, uint32_t rows, float eps) { + const uint64_t count = (uint64_t)width * rows; + if (!width || !rows || !isfinite(eps) || eps <= 0 || count > UINT64_MAX / 5u || + !cuda_tensor_has_f32(residual, count * 4u) || !cuda_tensor_has_f32(kv, count * 5u) || + !cuda_tensor_has_f32(q_weight, (uint64_t)width * 4u) || + !cuda_tensor_has_f32(k_weight, (uint64_t)width * 4u) || + (mask && !cuda_tensor_has_bytes(mask, rows))) return 0; + v41_engram_kernel<<>>((float *)residual->ptr, (const float *)kv->ptr, + (const float *)q_weight->ptr, (const float *)k_weight->ptr, + mask ? (const uint8_t *)mask->ptr : NULL, width, eps); + return cuda_ok(cudaGetLastError(), "V4.1 Engram gate"); +} + +__global__ static void v41_pool_kernel(float *out, const float *kv, const float *scores, + const float *previous_kv, const float *previous_scores, + uint32_t width, uint32_t tail) { + const uint32_t col = blockIdx.x * blockDim.x + threadIdx.x; + if (col >= width) return; + const int64_t a = (int64_t)blockIdx.y * 2 - tail; + const uint64_t b = (uint64_t)(a + 1) * width + col; + const float ka = a < 0 ? previous_kv[col] : kv[(uint64_t)a * width + col]; + const float sa = a < 0 ? previous_scores[col] : scores[(uint64_t)a * width + col]; + const float sb = scores[b], peak = fmaxf(sa, sb); + const float ea = __ocml_exp_f32(v41_sub(sa, peak)), eb = __ocml_exp_f32(v41_sub(sb, peak)); + out[(uint64_t)blockIdx.y * width + col] = v41_bf16(v41_div( + v41_add(v41_mul(ka, ea), v41_mul(kv[b], eb)), v41_add(ea, eb))); +} + +extern "C" int ds4_gpu_dsv41_pool2(ds4_gpu_tensor *out, const ds4_gpu_tensor *kv, + const ds4_gpu_tensor *scores, ds4_gpu_tensor *previous_kv, + ds4_gpu_tensor *previous_scores, uint32_t width, uint32_t rows, uint32_t start) { + const uint64_t count = (uint64_t)width * rows; + const uint32_t pairs = (uint32_t)(((uint64_t)rows + (start & 1u)) / 2u); + if (!width || !rows || rows > UINT32_MAX - start || + !cuda_tensor_has_f32(kv, count) || !cuda_tensor_has_f32(scores, count) || + !cuda_tensor_has_f32(previous_kv, width) || !cuda_tensor_has_f32(previous_scores, width) || + (pairs && !cuda_tensor_has_f32(out, (uint64_t)width * pairs))) return 0; + if (pairs) { + v41_pool_kernel<<>>( + (float *)out->ptr, (const float *)kv->ptr, (const float *)scores->ptr, + (const float *)previous_kv->ptr, (const float *)previous_scores->ptr, width, start & 1u); + if (!cuda_ok(cudaGetLastError(), "V4.1 KV pair pooling")) return 0; + } + /* Retain the last even input even at even frontiers, so snapshots are independent of chunk partitioning. */ + const uint32_t last_even = (start + rows - 1u) & ~1u; + if (last_even >= start) { + const uint64_t bytes = (uint64_t)width * sizeof(float), offset = (last_even - start) * bytes; + if (!ds4_gpu_tensor_copy(previous_kv, 0, kv, offset, bytes) || + !ds4_gpu_tensor_copy(previous_scores, 0, scores, offset, bytes)) return 0; + } + return 1; +} + +template +__global__ static void v41_candidates_kernel(float *out, const float *scores, const float *mask, + uint32_t width, uint32_t start, uint32_t ratio) { + const uint32_t col = blockIdx.x * blockDim.x + threadIdx.x, row = blockIdx.y; + const uint32_t blocks = (width + 7u) / 8u; + const uint32_t visible = min(width, (start + row + 1u) / ratio); + if (FILTER) { + if (col >= width) return; + const uint64_t i = (uint64_t)row * width + col; + out[i] = col < visible && mask[(uint64_t)row * blocks + col / 8u] == 0.0f ? scores[i] : -INFINITY; + } else { + if (col >= blocks) return; + float best = -INFINITY; + for (uint32_t i = col * 8u; i < min(visible, (col + 1u) * 8u); i++) + best = fmaxf(best, scores[(uint64_t)row * width + i]); + if (visible && col == (visible - 1u) / 8u) best = INFINITY; + out[(uint64_t)row * blocks + col] = best; + } +} + +static int v41_candidates(ds4_gpu_tensor *out, const ds4_gpu_tensor *scores, + const ds4_gpu_tensor *mask, uint32_t width, uint32_t rows, + uint32_t start, uint32_t ratio) { + if (!width || width > UINT32_MAX - 7u || !rows || !ratio || rows > UINT32_MAX - start) return 0; + const uint32_t blocks = (width + 7u) / 8u, out_width = mask ? width : blocks; + if (!cuda_tensor_has_elems2(scores, width, rows, 4u) || + !cuda_tensor_has_elems2(out, out_width, rows, 4u) || + (mask && !cuda_tensor_has_elems2(mask, blocks, rows, 4u))) return 0; + const dim3 grid((unsigned)(((uint64_t)out_width + 255u) / 256u), rows); + if (mask) v41_candidates_kernel<<>>((float *)out->ptr, (const float *)scores->ptr, + (const float *)mask->ptr, width, start, ratio); + else v41_candidates_kernel<<>>((float *)out->ptr, (const float *)scores->ptr, + NULL, width, start, ratio); + return cuda_ok(cudaGetLastError(), "V4.1 candidate selection"); +} + +extern "C" int ds4_gpu_dsv41_candidate_blocks(ds4_gpu_tensor *blocks, const ds4_gpu_tensor *scores, + uint32_t width, uint32_t rows, uint32_t start, uint32_t ratio) { + return v41_candidates(blocks, scores, NULL, width, rows, start, ratio); +} + +extern "C" int ds4_gpu_dsv41_candidate_filter(ds4_gpu_tensor *scores, const ds4_gpu_tensor *block_mask, + uint32_t width, uint32_t rows, uint32_t start, uint32_t ratio) { + return block_mask && v41_candidates(scores, scores, block_mask, width, rows, start, ratio); +} + +__global__ static void v41_carry_bf16_kernel(uint16_t *packed, float *plain, uint32_t width, + uint32_t words, bool pack) { + const uint32_t col = blockIdx.x * blockDim.x + threadIdx.x; + if (col >= width) return; + const uint64_t p = (uint64_t)blockIdx.y * words * 2u + col; + const uint64_t f = (uint64_t)blockIdx.y * width + col; + if (pack) packed[p] = (uint16_t)(__float_as_uint(plain[f]) >> 16u); + else plain[f] = __uint_as_float((uint32_t)packed[p] << 16u); +} + +__global__ static void v41_carry_mask_kernel(uint32_t *packed, float *plain, uint32_t width, + uint32_t words, bool pack) { + const uint32_t word = blockIdx.x * blockDim.x + threadIdx.x; + if (word >= words) return; + const uint64_t p = (uint64_t)blockIdx.y * words + word; + uint32_t bits = pack ? 0u : packed[p]; + for (uint32_t bit = 0; bit < 32u && (uint64_t)word * 32u + bit < width; bit++) { + const uint64_t f = (uint64_t)blockIdx.y * width + (uint64_t)word * 32u + bit; + if (pack) bits |= plain[f] == 0.0f ? 1u << bit : 0u; + else plain[f] = bits & (1u << bit) ? 0.0f : -INFINITY; + } + if (pack) packed[p] = bits; +} + +extern "C" int ds4_gpu_dsv41_carry_copy(ds4_gpu_tensor *packed, uint32_t row_offset, + ds4_gpu_tensor *plain, uint32_t width, uint32_t rows, + uint32_t format, bool pack) { + if (!width || !rows || rows > UINT32_MAX - row_offset || + format > DS4_V41_CARRY_MASK || packed == plain) return 0; + const uint32_t words = format == DS4_V41_CARRY_BF16 ? + (uint32_t)(((uint64_t)width + 1u) / 2u) : (uint32_t)(((uint64_t)width + 31u) / 32u); + if (!cuda_tensor_has_elems2(packed, (uint64_t)row_offset + rows, words, 4u) || + !cuda_tensor_has_elems2(plain, rows, width, 4u)) return 0; + uint32_t *p = (uint32_t *)packed->ptr + (uint64_t)row_offset * words; + if (format == DS4_V41_CARRY_BF16) + v41_carry_bf16_kernel<<>>( + (uint16_t *)p, (float *)plain->ptr, width, words, pack); + else v41_carry_mask_kernel<<>>( + p, (float *)plain->ptr, width, words, pack); + return cuda_ok(cudaGetLastError(), "V4.1 compact prefill carry"); +} + +__global__ static void v41_gather_kernel(float *out, const float *source, const int32_t *ids, + uint32_t source_rows) { + const uint32_t row = blockIdx.x, col = threadIdx.x; + const int32_t id = ids[row]; + /* Graph IDs come from top-k. Keep malformed IDs from turning a validation failure into an out-of-bounds load. */ + if ((uint32_t)id >= source_rows) { + out[(uint64_t)row * 512u + col] = NAN; + out[(uint64_t)row * 512u + col + 256u] = NAN; + return; + } + out[(uint64_t)row * 512u + col] = source[(uint64_t)id * 512u + col]; + out[(uint64_t)row * 512u + col + 256u] = source[(uint64_t)id * 512u + col + 256u]; +} + +extern "C" int ds4_gpu_dsv41_gather_kv(ds4_gpu_tensor *out, const ds4_gpu_tensor *source, + const ds4_gpu_tensor *ids, uint32_t source_rows, uint32_t selected_rows) { + if (!source_rows || !selected_rows || selected_rows > 512u || selected_rows > source_rows || + !cuda_tensor_has_elems2(source, source_rows, 512u, 4u) || + !cuda_tensor_has_elems2(out, selected_rows, 512u, 4u) || !cuda_tensor_has_f32(ids, selected_rows)) return 0; + v41_gather_kernel<<>>((float *)out->ptr, (const float *)source->ptr, + (const int32_t *)ids->ptr, source_rows); + return cuda_ok(cudaGetLastError(), "V4.1 sparse KV gather"); +} + +/* One wave per score: retain the scalar F32 reduction tree and scale boundary. */ +__global__ static void v41_indexer_scalar_warp8_kernel( + float *out, const float *q, const float *w, const float *k, uint32_t width, uint32_t start, + uint32_t ratio) { + uint32_t key = blockIdx.x * 8u + threadIdx.x / 32, lane = threadIdx.x & 31, row = blockIdx.y; + if (key >= width) return; + if (key >= (start + row + 1) / ratio) { + if (!lane) out[(uint64_t) row * width + key] = -INFINITY; + return; + } + const float *p = k + (uint64_t) key * 128 + lane; + float k0 = p[0], k1 = p[32], k2 = p[64], k3 = p[96], total = 0; + for (uint32_t h = 0; h < 32; h++) { + const float *x = q + ((uint64_t) row * 32 + h) * 128 + lane; + float a = v41_mul(x[0], k0), b = v41_mul(x[32], k1), c = v41_mul(x[64], k2), d = v41_mul(x[96], k3); + float dot = v41_add(v41_add(a, c), v41_add(b, d)); + for (int s = 16; s; s >>= 1) dot = v41_add(dot, __shfl_down(dot, s, 32)); + total = v41_add(total, v41_mul(fmaxf(v41_mul(dot, 1.f / 64.f), 0.f), w[row * 32 + h])); + } + if (!lane) out[(uint64_t) row * width + key] = total; +} +extern "C" int ds4_gpu_dsv41_indexer_scores_one(ds4_gpu_tensor *scores, + const ds4_gpu_tensor *q, const ds4_gpu_tensor *weights, + const ds4_gpu_tensor *keys, uint32_t source_rows) { + if (!ds4_rocm_is_gfx1151()) + return ds4_gpu_glm_indexer_score_one_tensor(scores, q, weights, + keys, source_rows, 32u, 128u, 1.0f / 64.0f, false); + if (!source_rows || source_rows > INT32_MAX || + !cuda_tensor_has_f32(scores, source_rows) || + !cuda_tensor_has_f32(q, 32u * 128u) || + !cuda_tensor_has_f32(weights, 32u) || + !cuda_tensor_has_elems2(keys, source_rows, 128u, 4u)) return 0; + v41_indexer_scalar_warp8_kernel<<<(source_rows + 7u) / 8u, 256>>>( + (float *)scores->ptr, (const float *)q->ptr, (const float *)weights->ptr, + (const float *)keys->ptr, source_rows, source_rows - 1u, 1u); + return cuda_ok(cudaGetLastError(), "V4.1 scalar warp indexer"); +} + +__global__ static void v41_indexer_kernel(float *scores, const float *q, const float *weights, + const float *keys, uint32_t width, uint32_t start, uint32_t ratio) { + const uint32_t key = blockIdx.x, token = blockIdx.y, lane = threadIdx.x & 31u, wave = threadIdx.x >> 5u; + if (key >= (start + token + 1u) / ratio) { + if (!threadIdx.x) scores[(uint64_t)token * width + key] = -INFINITY; + return; + } + __shared__ float head_values[4]; + float total = 0.0f; + for (uint32_t head0 = 0; head0 < 32u; head0 += 4u) { + const uint32_t head = head0 + wave; + const float *query = q + ((uint64_t)token * 32u + head) * 128u; + const float *kv = keys + (uint64_t)key * 128u; + float dot = 0.0f; + for (uint32_t col = lane; col < 128u; col += 32u) dot = v41_add(dot, v41_mul(query[col], kv[col])); + dot = v41_sum32(dot); + if (!lane) head_values[wave] = v41_mul(fmaxf(v41_mul(dot, 1.0f / 64.0f), 0.0f), weights[(uint64_t)token * 32u + head]); + __syncthreads(); + if (!threadIdx.x) for (uint32_t h = 0; h < 4u; h++) total = v41_add(total, head_values[h]); + __syncthreads(); + } + if (!threadIdx.x) scores[(uint64_t)token * width + key] = total; +} + +__global__ static void v41_indexer_batch_warp8_kernel( + float *out, const float *q, const float *w, const float *k, uint32_t width, uint32_t start, + uint32_t ratio) { + uint32_t key = blockIdx.x * 8u + threadIdx.x / 32, lane = threadIdx.x & 31, row = blockIdx.y; + if (key >= width) return; + if (key >= (start + row + 1) / ratio) { + if (!lane) out[(uint64_t) row * width + key] = -INFINITY; + return; + } + const float *p = k + (uint64_t) key * 128 + lane; + float k0 = p[0], k1 = p[32], k2 = p[64], k3 = p[96], total = 0; + for (uint32_t h = 0; h < 32; h++) { + const float *x = q + ((uint64_t) row * 32 + h) * 128 + lane; + float a = v41_mul(x[0], k0), b = v41_mul(x[32], k1), c = v41_mul(x[64], k2), d = v41_mul(x[96], k3); + float dot = v41_add(v41_add(v41_add(v41_add(0.0f, a), b), c), d); + for (int s = 16; s; s >>= 1) dot = v41_add(dot, __shfl_down(dot, s, 32)); + float weighted; + // OCML wrappers alone do not prevent backend contraction across this boundary. + // The control stores this product in LDS before adding it to the head total. + asm volatile("v_mul_f32 %0, %1, %2" : "=v"(weighted) + : "v"(fmaxf(v41_mul(dot, 1.f / 64.f), 0.f)), "v"(w[row * 32 + h])); + total = v41_add(total, weighted); + } + if (!lane) out[(uint64_t) row * width + key] = total; +} +__global__ static void v41_indexer_head_queries_kernel(float *qh,const float *q,unsigned rows){ + unsigned i=blockIdx.x*blockDim.x+threadIdx.x;if(i>=rows*4096u)return; + unsigned d=i%128,h=(i/128)%32,t=i/4096;qh[(h*rows+t)*128+d]=q[i]; +} +__global__ static void v41_indexer_reduce_heads_kernel(float *out,const float *dots,const float *w,unsigned width,unsigned rows,unsigned h0,unsigned heads,unsigned start,unsigned ratio){ + unsigned k=blockIdx.x*blockDim.x+threadIdx.x,row=blockIdx.y;if(k>=width)return; + size_t i=(size_t)row*width+k; + if(k>=(start+row+1)/ratio){out[i]=-INFINITY;return;} + float v=h0?out[i]:0.f; + for(unsigned h=0;h>>(qh, q, rows); + if (!cuda_ok(cudaGetLastError(), "V4.1 indexer query preparation")) return 0; + const float one = 1.0f, zero = 0.0f; + for (uint32_t h0 = 0; h0 < 32u; h0 += heads) { + const uint32_t count = min(heads, 32u - h0); + if (!cublas_ok(cublasSgemmStridedBatched(g_cublas, CUBLAS_OP_T, CUBLAS_OP_N, + width, rows, 128, &one, keys, 128, 0, + qh + (uint64_t)h0 * rows * 128u, 128, (long long)rows * 128, + &zero, dots, width, (long long)rows * width, count), "V4.1 indexer head GEMM")) return 0; + v41_indexer_reduce_heads_kernel<<>>( + out, dots, weights, width, rows, h0, count, start, ratio); + if (!cuda_ok(cudaGetLastError(), "V4.1 indexer head reduction")) return 0; + } + return 1; +} + +extern "C" int ds4_gpu_dsv41_indexer_scores_batch(ds4_gpu_tensor *scores, const ds4_gpu_tensor *q, + const ds4_gpu_tensor *weights, const ds4_gpu_tensor *keys, + uint32_t source_rows, uint32_t rows, uint32_t start, uint32_t ratio) { + if ((ratio != 1u && ratio != 2u) || !source_rows || !rows || rows > UINT32_MAX - start || + (start + rows) / ratio > source_rows || source_rows > INT32_MAX || rows > INT32_MAX || + !cuda_tensor_has_elems2(scores, source_rows, rows, 4u) || + !cuda_tensor_has_elems2(q, rows, 32u * 128u, 4u) || + !cuda_tensor_has_elems2(keys, source_rows, 128u, 4u) || + !cuda_tensor_has_elems2(weights, rows, 32u, 4u)) return 0; + if (ds4_rocm_is_gfx1151()) { + /* Small query tiles favor wave scoring. GEMM uses a bounded head tile + * only for the wide, nearly fully visible batches qualified here. */ + if (!g_quality_mode && g_cublas_ready && rows >= 31u && rows <= 32u && + source_rows >= 16384u && source_rows <= 65536u && + (start + 1u) / ratio >= source_rows - source_rows / 8u) + return v41_indexer_head_gemm((float *)scores->ptr, (const float *)q->ptr, + (const float *)weights->ptr, (const float *)keys->ptr, source_rows, rows, start, ratio); + v41_indexer_batch_warp8_kernel<<>>( + (float *)scores->ptr, (const float *)q->ptr, (const float *)weights->ptr, + (const float *)keys->ptr, source_rows, start, ratio); + } else { + v41_indexer_kernel<<>>((float *)scores->ptr, (const float *)q->ptr, + (const float *)weights->ptr, (const float *)keys->ptr, source_rows, start, ratio); + } + return cuda_ok(cudaGetLastError(), "V4.1 causal FP4 index scores"); +} + +template +__global__ static void v41_topk_chunk_pow2_kernel( + uint32_t *candidates, + const float *scores, + uint32_t score_stride, uint32_t start, uint32_t ratio, + uint32_t n_tokens, + uint32_t top_k, + uint32_t candidate_stride) { + uint32_t t = blockIdx.x; + uint32_t chunk = blockIdx.y; + uint32_t tid = threadIdx.x; + if (t >= n_tokens) return; + const uint32_t n_comp = (start + t + 1u) / ratio; + + const uint32_t chunk_start = chunk * SORT_N; + if (chunk_start >= n_comp) return; + const uint32_t chunk_n = n_comp - chunk_start < SORT_N ? n_comp - chunk_start : SORT_N; + __shared__ float vals[SORT_N]; + __shared__ uint32_t idxs[SORT_N]; + + const float *row = scores + (uint64_t)t * score_stride; + for (uint32_t i = tid; i < SORT_N; i += blockDim.x) { + if (i < chunk_n) { + vals[i] = row[chunk_start + i]; + idxs[i] = chunk_start + i; + } else { + vals[i] = -INFINITY; + idxs[i] = UINT32_MAX; + } + } + __syncthreads(); + + for (uint32_t k = 2u; k <= SORT_N; k <<= 1u) { + for (uint32_t j = k >> 1u; j > 0u; j >>= 1u) { + for (uint32_t i = tid; i < SORT_N; i += blockDim.x) { + uint32_t other = i ^ j; + if (other > i && other < SORT_N) { + const float av = vals[i]; + const float bv = vals[other]; + const uint32_t ai = idxs[i]; + const uint32_t bi = idxs[other]; + const bool desc_half = (i & k) == 0u; + const bool swap = desc_half + ? topk_score_better(bv, bi, av, ai) + : topk_score_better(av, ai, bv, bi); + if (swap) { + vals[i] = bv; + idxs[i] = bi; + vals[other] = av; + idxs[other] = ai; + } + } + } + __syncthreads(); + } + } + + uint32_t *out = candidates + (uint64_t)t * candidate_stride + chunk * top_k; + for (uint32_t i = tid; i < top_k; i += blockDim.x) { + out[i] = idxs[i]; + } +} + +template +__global__ static void v41_topk_merge_pow2_kernel( + uint32_t *selected, + const uint32_t *candidates, + const float *scores, + uint32_t score_stride, uint32_t start, uint32_t ratio, + uint32_t n_tokens, + uint32_t top_k, + uint32_t candidate_count, + uint32_t candidate_stride) { + uint32_t t = blockIdx.x; + uint32_t tid = threadIdx.x; + if (t >= n_tokens) return; + const uint32_t n_comp = (start + t + 1u) / ratio; + __shared__ float vals[SORT_N]; + __shared__ uint32_t idxs[SORT_N]; + + const float *row = scores + (uint64_t)t * score_stride; + const uint32_t *cand = candidates + (uint64_t)t * candidate_stride; + for (uint32_t i = tid; i < SORT_N; i += blockDim.x) { + uint32_t idx = UINT32_MAX; + float v = -INFINITY; + if (i < candidate_count) { + idx = cand[i]; + if (idx < n_comp) v = row[idx]; + } + vals[i] = v; + idxs[i] = idx; + } + __syncthreads(); + + for (uint32_t k = 2u; k <= SORT_N; k <<= 1u) { + for (uint32_t j = k >> 1u; j > 0u; j >>= 1u) { + for (uint32_t i = tid; i < SORT_N; i += blockDim.x) { + uint32_t other = i ^ j; + if (other > i && other < SORT_N) { + const float av = vals[i]; + const float bv = vals[other]; + const uint32_t ai = idxs[i]; + const uint32_t bi = idxs[other]; + const bool desc_half = (i & k) == 0u; + const bool swap = desc_half + ? topk_score_better(bv, bi, av, ai) + : topk_score_better(av, ai, bv, bi); + if (swap) { + vals[i] = bv; + idxs[i] = bi; + vals[other] = av; + idxs[other] = ai; + } + } + } + __syncthreads(); + } + } + + for (uint32_t i = tid; i < top_k; i += blockDim.x) { + selected[(uint64_t)t * top_k + i] = idxs[i]; + } +} + +template +__global__ static void v41_topk_tree_merge_pow2_kernel( + uint32_t *out, + const uint32_t *candidates, + const float *scores, + uint32_t score_stride, uint32_t start, uint32_t ratio, + uint32_t n_tokens, + uint32_t top_k, + uint32_t n_sets, + uint32_t merge_group, + uint32_t candidate_stride, + uint32_t out_stride) { + uint32_t t = blockIdx.x; + uint32_t group = blockIdx.y; + uint32_t tid = threadIdx.x; + if (t >= n_tokens) return; + const uint32_t n_comp = (start + t + 1u) / ratio; + + const uint32_t set0 = group * merge_group; + if (set0 >= n_sets) return; + uint32_t set_count = n_sets - set0; + if (set_count > merge_group) set_count = merge_group; + const uint32_t candidate_count = set_count * top_k; + + __shared__ float vals[SORT_N]; + __shared__ uint32_t idxs[SORT_N]; + + const float *row = scores + (uint64_t)t * score_stride; + const uint32_t *cand = candidates + (uint64_t)t * candidate_stride + set0 * top_k; + for (uint32_t i = tid; i < SORT_N; i += blockDim.x) { + uint32_t idx = UINT32_MAX; + float v = -INFINITY; + if (i < candidate_count) { + idx = cand[i]; + if (idx < n_comp) v = row[idx]; + } + vals[i] = v; + idxs[i] = idx; + } + __syncthreads(); + + for (uint32_t k = 2u; k <= SORT_N; k <<= 1u) { + for (uint32_t j = k >> 1u; j > 0u; j >>= 1u) { + for (uint32_t i = tid; i < SORT_N; i += blockDim.x) { + uint32_t other = i ^ j; + if (other > i && other < SORT_N) { + const float av = vals[i]; + const float bv = vals[other]; + const uint32_t ai = idxs[i]; + const uint32_t bi = idxs[other]; + const bool desc_half = (i & k) == 0u; + const bool swap = desc_half + ? topk_score_better(bv, bi, av, ai) + : topk_score_better(av, ai, bv, bi); + if (swap) { + vals[i] = bv; + idxs[i] = bi; + vals[other] = av; + idxs[other] = ai; + } + } + } + __syncthreads(); + } + } + + uint32_t *dst = out + (uint64_t)t * out_stride + group * top_k; + for (uint32_t i = tid; i < top_k; i += blockDim.x) { + dst[i] = idxs[i]; + } +} + +/* Preserve each row's original sort network, including score/index tie order. + * Group rows with equal chunk counts; only launches and physical strides change. */ +static int v41_indexer_topk_causal_batch(uint32_t *selected, const float *scores, + uint32_t width, uint32_t rows, uint32_t start, uint32_t ratio) { + const uint32_t max_chunks = ((start + rows) / ratio + 4095u) / 4096u; + const uint32_t scratch_sets = max_chunks + (max_chunks > 8u ? (max_chunks + 7u) / 8u : 0u); + uint32_t *scratch = (uint32_t *)cuda_tmp_alloc((uint64_t)rows * scratch_sets * 512u * sizeof(uint32_t), + "V4.1 causal top-k rows"); + if (!scratch) return 0; + for (uint32_t row = 0; row < rows;) { + const uint32_t chunks = ((start + row + 1u) / ratio + 4095u) / 4096u; + uint32_t end = row + 1u; + while (end < rows && ((start + end + 1u) / ratio + 4095u) / 4096u == chunks) ++end; + const uint32_t count = end - row, begin = start + row; + const float *input = scores + (uint64_t)row * width; + uint32_t *cur = scratch, sets = chunks, stride = chunks * 512u; + v41_topk_chunk_pow2_kernel<4096><<>>(cur, input, + width, begin, ratio, count, 512u, stride); + if (!cuda_ok(cudaGetLastError(), "V4.1 causal top-k chunks")) return 0; + while (sets > 8u) { + const uint32_t next_sets = (sets + 7u) / 8u, next_stride = next_sets * 512u; + uint32_t *next = cur + (uint64_t)count * stride; + v41_topk_tree_merge_pow2_kernel<4096><<>>(next, + cur, input, width, begin, ratio, count, 512u, sets, 8u, stride, next_stride); + if (!cuda_ok(cudaGetLastError(), "V4.1 causal top-k tree")) return 0; + cur = next; sets = next_sets; stride = next_stride; + } + v41_topk_merge_pow2_kernel<4096><<>>(selected + (uint64_t)row * 512u, + cur, input, width, begin, ratio, count, 512u, sets * 512u, stride); + if (!cuda_ok(cudaGetLastError(), "V4.1 causal top-k final")) return 0; + row = end; + } + return 1; +} + +extern "C" int ds4_gpu_dsv41_indexer_topk_batch(ds4_gpu_tensor *selected, const ds4_gpu_tensor *scores, + uint32_t width, uint32_t rows, uint32_t start, uint32_t ratio) { + if ((ratio != 1u && ratio != 2u) || !rows || rows > UINT32_MAX - start || + width > INT32_MAX || rows > INT32_MAX || (start + rows) / ratio > width || + !cuda_tensor_has_elems2(scores, width, rows, 4u) || + !cuda_tensor_has_elems2(selected, 512u, rows, 4u)) return 0; + if (ds4_rocm_is_gfx1151() && rows >= 2u && rows <= 32u && width <= 65536u && + (start + 1u) / ratio > 8192u) + return v41_indexer_topk_causal_batch((uint32_t *)selected->ptr, (const float *)scores->ptr, + width, rows, start, ratio); + for (uint32_t row = 0; row < rows; row++) { + const uint32_t visible = (start + row + 1u) / ratio; + if (!visible) continue; + const uint32_t top = visible < 512u ? visible : 512u; + ds4_gpu_tensor in = {(float *)scores->ptr + (uint64_t)row * width, (uint64_t)visible * 4u, 0}; + ds4_gpu_tensor out = {(uint32_t *)selected->ptr + (uint64_t)row * 512u, 512u * 4u, 0}; + if (visible > 1u && visible < 512u && ds4_rocm_is_gfx1151()) { + /* Preserve the scalar order and untouched tail slots, while + * avoiding its serial insertion sort for short prefill rows. */ + indexer_topk_1024_kernel<<<1u, 1024u>>>((uint32_t *)out.ptr, + (const float *)in.ptr, visible, 1u, top); + if (!cuda_ok(cudaGetLastError(), "V4.1 short causal top-k")) return 0; + } else if (!ds4_gpu_indexer_topk_tensor(&out, &in, visible, 1u, top)) return 0; + } + return 1; +} + +/* Metal tensor packing is an optional acceleration. The graph selects the complete F32 baseline above when this capability is absent. */ +extern "C" int ds4_gpu_dsv41_tensor_ops_available(void) { return 0; } + +extern "C" uint64_t ds4_gpu_dsv41_indexer_packed_bytes(uint32_t source_rows, uint32_t rows) { + const uint64_t tiles = ((uint64_t)source_rows + 63u) / 64u; + const uint64_t flags = (((uint64_t)rows + tiles) * 4u + 255u) & ~UINT64_C(255); + return flags + (uint64_t)rows * 32u * 128u * 2u + tiles * 64u * 128u * 2u; +} + +extern "C" int ds4_gpu_dsv41_indexer_pack(ds4_gpu_tensor *packed, const ds4_gpu_tensor *q, + const ds4_gpu_tensor *keys, uint32_t source_rows, uint32_t rows) { + (void)packed; (void)q; (void)keys; (void)source_rows; (void)rows; + return 0; +} + +extern "C" int ds4_gpu_dsv41_indexer_scores_packed(ds4_gpu_tensor *scores, const ds4_gpu_tensor *q, + const ds4_gpu_tensor *weights, const ds4_gpu_tensor *keys, + const ds4_gpu_tensor *packed, uint32_t source_rows, + uint32_t rows, uint32_t start, uint32_t ratio, + uint32_t packed_rows, uint32_t offset) { + (void)scores; (void)q; (void)weights; (void)keys; (void)packed; + (void)source_rows; (void)rows; (void)start; (void)ratio; (void)packed_rows; (void)offset; + return 0; +} + +/* Each wave still owns one output row. Lane l sums the same contiguous + * ceil(K/32) inputs in ascending order. Tokens have separate accumulators; + * only the exactly decoded F16 weight is reused. Lane zero still adds the + * 32 partials in order. No activation conversion or split-K reduction. */ +template +__global__ static void f16_ordered_token_reuse( + float *out, const __half *w, const float *x, + uint64_t in_dim, uint64_t out_dim, uint64_t n_tok) { + const uint32_t lane = threadIdx.x & 31u; + const uint32_t wave = threadIdx.x >> 5u; + const uint64_t row = (uint64_t)blockIdx.x * WAVES + wave; + const uint64_t token = (uint64_t)blockIdx.y * TT; + __shared__ float partial[WAVES][TT][32]; + float sum[TT] = {}; + const uint64_t chunk = (in_dim + 31u) / 32u; + const uint64_t k0 = (uint64_t)lane * chunk; + uint64_t k1 = k0 + chunk; + if (k1 > in_dim) k1 = in_dim; + if (row < out_dim) { + const __half *wr = w + row * in_dim; + for (uint64_t i = k0; i < k1; i++) { + const float weight = __half2float(wr[i]); +#pragma unroll + for (unsigned t = 0; t < TT; t++) { + if (token + t < n_tok) + sum[t] += weight * x[(token + t) * in_dim + i]; + } + } + } +#pragma unroll + for (unsigned t = 0; t < TT; t++) partial[wave][t][lane] = sum[t]; + __syncthreads(); + if (row < out_dim && lane == 0u) { +#pragma unroll + for (unsigned t = 0; t < TT; t++) { + if (token + t < n_tok) { + float total = 0.0f; + for (uint32_t i = 0; i < 32u; i++) total += partial[wave][t][i]; + out[(token + t) * out_dim + row] = total; + } + } + } +} + +__global__ static void v41_hc_half_to_float(float *out, const __half *weight, uint64_t count) { + const uint64_t i = (uint64_t)blockIdx.x * blockDim.x + threadIdx.x; + if (i < count) out[i] = __half2float(weight[i]); +} +static hipError_t v41_hc_widen(float *out, const uint16_t *weight, uint64_t count) { + v41_hc_half_to_float<<<(count + 255u) / 256u, 256u>>>(out, (const __half *)weight, count); + return hipGetLastError(); +} +static bool v41_hc_disjoint(const void *a, uint64_t na, const void *b, uint64_t nb) { + const uintptr_t pa = (uintptr_t)a, pb = (uintptr_t)b; + return na <= UINTPTR_MAX - pa && nb <= UINTPTR_MAX - pb && + (pa + na <= pb || pb + nb <= pa); +} +#include "ds4_rocm_hc_sgemm.cuh" + +extern "C" void ds4_gpu_dsv41_hc_plan_free(ds4_gpu_dsv41_hc_plan *plan) { + v41_hc_plan_destroy(plan); +} +extern "C" int ds4_gpu_dsv41_hc_project(ds4_gpu_dsv41_hc_plan **plan, + ds4_gpu_tensor *out, const void *model_map, uint64_t model_size, + uint64_t weight_offset, uint32_t rows, const ds4_gpu_tensor *input, + ds4_gpu_tensor *full_heads_scratch) { + if (rows != 2048u || !ds4_rocm_is_gfx1151() || + g_quality_mode || cuda_runtime_config()->graph_dump || !g_rocblas_ready || + g_rocblas_f16_solution_set != DS4_ROCBLAS_F16_SOLUTIONS_5_6_8D1AE90E) return 0; + const uint64_t weight_bytes = UINT64_C(20480) * 24u * 2u; + const uint64_t in_bytes = UINT64_C(20480) * 2048u * 4u; + const uint64_t out_bytes = UINT64_C(24) * 2048u * 4u; + if (!plan || !model_map || !cuda_model_range_fits(model_size, weight_offset, weight_bytes) || + !cuda_tensor_has_bytes(input, in_bytes) || !cuda_tensor_has_bytes(out, out_bytes)) return -1; + if (!cuda_tensor_has_bytes(full_heads_scratch, v41_hc_scratch_bytes)) return 0; + if ((uintptr_t)full_heads_scratch->ptr % 256u || + !v41_hc_disjoint(out->ptr, out_bytes, input->ptr, in_bytes) || + !v41_hc_disjoint(out->ptr, out_bytes, full_heads_scratch->ptr, v41_hc_scratch_bytes) || + !v41_hc_disjoint(input->ptr, in_bytes, full_heads_scratch->ptr, v41_hc_scratch_bytes)) return -1; + const uint16_t *weight = (const uint16_t *)cuda_model_range_ptr(model_map, weight_offset, weight_bytes, "V4.1 HC F16"); + if (!weight || + !v41_hc_disjoint(weight, weight_bytes, out->ptr, out_bytes) || + !v41_hc_disjoint(weight, weight_bytes, input->ptr, in_bytes) || + !v41_hc_disjoint(weight, weight_bytes, full_heads_scratch->ptr, v41_hc_scratch_bytes)) return -1; + return v41_hc_plan_run(plan, (float *)out->ptr, weight, (const float *)input->ptr, full_heads_scratch->ptr); +} + +/* Default Engram lane l consumes l+32*i, i=0..191, then the existing + * shuffle16/8/4/2/1. Each token retains an independent accumulation chain. + * Only F32 input reuse across the eight output waves changes. */ +template +__global__ static void engram_lds_token_reuse(float *out,const __half *w,const float *x) { + constexpr unsigned K=6144,N=25600,WAVES=8,SEG=256; + const unsigned tid=threadIdx.x,lane=tid&31u,wave=tid>>5u; + const unsigned row=blockIdx.x*WAVES+wave,token=blockIdx.y*TT; + __shared__ float tile[TT][SEG]; + float acc[TT]={}; + for(unsigned base=0;base 8192u || !model_map || + !cuda_u64_mul3_checked(width, outputs, sizeof(uint16_t), &weight_bytes) || + !cuda_model_range_fits(model_size, weight_offset, weight_bytes) || + !cuda_tensor_has_elems2(in, width, rows, 4u) || !cuda_tensor_has_elems2(out, outputs, rows, 4u)) return 0; + if (width == 6144u && outputs == 25600u && rows >= 32u && rows <= 2048u && + ds4_rocm_is_gfx1151() && !g_quality_mode && !cuda_runtime_config()->graph_dump) { + const __half *w = (const __half *)cuda_model_range_ptr( + model_map, weight_offset, weight_bytes, "V4.1 exact Engram F16"); + if (!w) return 0; + cuda_hipblaslt_gemm_plan *plan = v41_engram_lt_plan(rows); + if (plan) { + const uint32_t count = 6144u * rows; + const uint64_t packed_bytes = (uint64_t)count * sizeof(__half); + __half *packed = (__half *)cuda_tmp_alloc(packed_bytes + sizeof(uint32_t), + "V4.1 Engram checked F16 activations"); + if (!packed) return 0; + uint32_t *loss = (uint32_t *)((char *)packed + packed_bytes); + if (!cuda_ok(hipMemsetAsync(loss, 0, sizeof(*loss), 0), + "V4.1 Engram conversion flag reset")) return 0; + v41_engram_pack_checked_kernel<<<(count + 255u) / 256u, 256u>>>( + packed, (const float *)in->ptr, count, loss); + if (!cuda_ok(cudaGetLastError(), "V4.1 Engram checked conversion")) return 0; + uint32_t converted_loss = 0; + if (!cuda_ok(cudaMemcpy(&converted_loss, loss, sizeof(converted_loss), + cudaMemcpyDeviceToHost), "V4.1 Engram conversion check")) return 0; + if (!converted_loss) { + const float alpha = 1.0f, beta = 0.0f; + if (!hipblaslt_ok(hipblasLtMatmul(g_hipblaslt, plan->desc, &alpha, + w, plan->a_desc, packed, plan->b_desc, &beta, + out->ptr, plan->c_desc, out->ptr, plan->d_desc, + &plan->algo, NULL, 0, 0), "V4.1 Engram F16/F32")) return 0; + return cuda_ok(cudaGetLastError(), "V4.1 Engram matrix projection"); + } + } + if (rows == 2048u) { + engram_lds_token_reuse<16><<>>( + (float *)out->ptr, w, (const float *)in->ptr); + return cuda_ok(cudaGetLastError(), "V4.1 exact Engram F32-input projection"); + } + /* Partial batches retain the existing per-row F32-input fallback. */ + } + if (width == 20480u && outputs == 24u && rows >= 8u && rows <= 2048u && + ds4_rocm_is_gfx1151()) { + const __half *w = (const __half *)cuda_model_range_ptr( + model_map, weight_offset, weight_bytes, "f16"); + if (!w) return 0; + /* Keep four-token tiles for smaller batches; the measured 384-row + * workload and full 2048-row tiles favor eight-token reuse. */ + if (rows < 384u) { + f16_ordered_token_reuse<4,8><<>>( + (float *)out->ptr, w, (const float *)in->ptr, width, outputs, rows); + } else { + f16_ordered_token_reuse<8,8><<>>( + (float *)out->ptr, w, (const float *)in->ptr, width, outputs, rows); + } + return cuda_ok(cudaGetLastError(), "V4.1 F32-input HC projection"); + } + /* The general batched F16 API casts inputs to F16. Row views preserve decode arithmetic and retain F32 activations. */ + for (uint32_t row = 0; row < rows; row++) { + ds4_gpu_tensor x = {(float *)in->ptr + (uint64_t)row * width, (uint64_t)width * 4u, 0}; + ds4_gpu_tensor y = {(float *)out->ptr + (uint64_t)row * outputs, (uint64_t)outputs * 4u, 0}; + if (!ds4_gpu_matmul_f16_tensor(&y, model_map, model_size, weight_offset, width, outputs, &x, 1u)) return 0; + } + return 1; +} + +extern "C" int ds4_gpu_hc_rms_scale_project_f16_tensor(ds4_gpu_tensor *out, ds4_gpu_tensor *scale_scratch, + const void *model_map, uint64_t model_size, uint64_t weight_offset, + uint32_t in_dim, uint32_t out_dim, const ds4_gpu_tensor *x, uint32_t n_rows, float eps) { + if (!in_dim || !out_dim || !n_rows || !isfinite(eps) || eps <= 0.0f) return 0; + return ds4_gpu_rms_norm_plain_rows_tensor(scale_scratch, x, in_dim, n_rows, eps) && + ds4_gpu_dsv41_projection_rows(out, model_map, model_size, weight_offset, in_dim, out_dim, n_rows, scale_scratch); +} + +/* Four input values per lane, four Q8 blocks per wave. Scalar V4.1 + * projections retain F32 activations; the block-wise reduction differs from + * the original lane accumulation and is qualified independently. */ +__global__ static void v41_q8_f32_blocks4_kernel(float *out, + const unsigned char *weights, const float *input, + uint32_t width, uint32_t outputs, uint64_t row_bytes) { + const uint32_t lane = threadIdx.x & 31u; + const uint64_t row = (uint64_t)blockIdx.x * 8u + (threadIdx.x >> 5u); + if (row >= outputs) return; + const uint32_t blocks = width / 32u; + float acc = 0.0f; + for (uint32_t b = lane / 8u; b < blocks; b += 4u) { + const unsigned char *p = weights + row * row_bytes + (uint64_t)b * 34u; + const float d = q8_0_scale_scalar(p); + const uint32_t j = (lane & 7u) * 4u; + float value = 0.0f; +#pragma unroll + for (uint32_t k = 0; k < 4u; k++) + value += (float)((const int8_t *)(p + 2u))[j + k] * input[b * 32u + j + k]; + acc += d * value; + } + acc = warp_sum_f32(acc); + if (lane == 0u) out[row] = acc; +} + +extern "C" int ds4_gpu_dsv41_q8_projection_rows(ds4_gpu_tensor *out, const void *model_map, uint64_t model_size, + uint64_t weight_offset, uint32_t width, uint32_t outputs, + uint32_t rows, const ds4_gpu_tensor *in) { + uint64_t weight_bytes = 0; + if (!width || width % 32u || !outputs || !rows || rows > 8192u || !model_map || + !cuda_u64_mul3_checked(width / 32u, outputs, 34u, &weight_bytes) || + !cuda_model_range_fits(model_size, weight_offset, weight_bytes) || + !cuda_tensor_has_elems2(in, width, rows, 4u) || !cuda_tensor_has_elems2(out, outputs, rows, 4u)) return 0; + const unsigned char *weights = (const unsigned char *)cuda_model_range_ptr( + model_map, weight_offset, weight_bytes, "V4.1 Q8 projection"); + if (!weights) return 0; + if (rows == 1u && ds4_rocm_is_gfx1151()) { + v41_q8_f32_blocks4_kernel<<<(outputs + 7u) / 8u, 256u>>>( + (float *)out->ptr, weights, (const float *)in->ptr, + width, outputs, (uint64_t)(width / 32u) * 34u); + } else if (!g_quality_mode && width == 1280u && (outputs == 32768u || outputs == 16384u) && rows >= 32u && rows <= 2048u && ds4_rocm_is_gfx1151()) { + /* Query-B, including contiguous two-rank weight slices. + * This numerical path rounds activations and decoded Q8 weights to + * F16 before F32 accumulation; quality mode retains the F32 path. */ + matmul_q8_0_f32_batch_wmma_rowtile_kernel<256u, 16u><<>>( + (float *)out->ptr, weights, (const float *)in->ptr, + rows, width, outputs, UINT64_C(40) * 34u); + } else if (!g_quality_mode && rows == 2048u && ds4_rocm_is_gfx1151() && + ((width == 5120u && (outputs == 512u || outputs == 1280u || outputs == 2304u)) || + (width == 2304u && outputs == 5120u))) { + /* Query-A, KV and shared-expert projections on a complete prefill tile. + * Reuse the generic Q8-to-F16 WMMA path and its F32 accumulation. */ + matmul_q8_0_f32_batch_wmma_rowtile_kernel<128u, 8u><<>>( + (float *)out->ptr, weights, (const float *)in->ptr, + rows, width, outputs, (uint64_t)(width / 32u) * 34u); + } else if (width == 1280u && outputs == 32768u && rows >= 32u && rows <= 2048u && ds4_rocm_is_gfx1151()) { + /* Reuse sixteen query-B activation rows with the same F32 lane + * accumulation and wave reduction; keep the existing block tile. */ + cuda_launch_q8_batch_sharedx((float *)out->ptr, weights, (const float *)in->ptr, + width / 32u, outputs, rows, (uint64_t)(width / 32u) * 34u, 8u, 16u, 8u); + } else if (rows >= 32u && ds4_rocm_is_gfx1151()) { + /* Reuse eight F32 activation rows without changing each lane's block + * accumulation or wave reduction. No F16 cast or expanded weights. */ + cuda_launch_q8_batch_sharedx((float *)out->ptr, weights, (const float *)in->ptr, + width / 32u, outputs, rows, (uint64_t)(width / 32u) * 34u, 8u, 8u, 8u); + } else { + matmul_q8_0_f32_batch_warp8_kernel<<>>( + (float *)out->ptr, weights, (const float *)in->ptr, width, outputs, rows, width / 32u); + } + return cuda_ok(cudaGetLastError(), "V4.1 F32-input Q8 projection"); +} + +/* V4.1 grouped output-A: retain physical token strides while using the + * existing F16-operand/F32-accumulator WMMA body on bulk prefill rows. */ +template +__launch_bounds__(WARPS * 32u, 1) +__global__ static void v41_grouped_q8_f32_wmma_rowtile_kernel( + float *out, + const unsigned char *w, + const float *x, + uint32_t n_tokens, + uint32_t in_dim, + uint32_t out_dim, + uint64_t row_bytes) { + const uint32_t group = (uint32_t)blockIdx.z; + w += (uint64_t)group * out_dim * row_bytes; + x += (uint64_t)group * in_dim; + out += (uint64_t)group * out_dim; + constexpr uint32_t N_TILE = 64u; + constexpr uint32_t K_TILE = 32u; + constexpr uint32_t M_PER_WARP = M_TILE / WARPS; + constexpr uint32_t N_TILES_PER_WARP = N_TILE / 16u; + + const uint32_t block_m = (uint32_t)blockIdx.x * M_TILE; + const uint32_t block_n = (uint32_t)blockIdx.y * N_TILE; + if (block_m >= out_dim || block_n >= n_tokens) return; + + const uint32_t tid = threadIdx.x; + const uint32_t warp_id = tid >> 5u; + const uint32_t lane = tid & 31u; + const uint32_t lane16 = lane & 15u; + const uint32_t warp_m = block_m + warp_id * M_PER_WARP; + const uint32_t my_row = warp_m + lane16; + const uint32_t safe_row = my_row < out_dim ? my_row : (out_dim - 1u); + const unsigned char *row_base = w + (uint64_t)safe_row * row_bytes; + const uint32_t n_blocks = in_dim >> 5u; + + ds4_q8_float8_t acc0 = {0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}; + ds4_q8_float8_t acc1 = acc0; + ds4_q8_float8_t acc2 = acc0; + ds4_q8_float8_t acc3 = acc0; + + __shared__ _Float16 lds_x[N_TILE * K_TILE]; + + for (uint32_t bi = 0; bi < n_blocks; bi++) { + for (uint32_t j = tid * 2u; j < N_TILE * K_TILE; j += blockDim.x * 2u) { + const uint32_t nt = j >> 5u; + const uint32_t kk = j & 31u; + const uint32_t tok = block_n + nt; + half2 xv = __floats2half2_rn(0.0f, 0.0f); + if (tok < n_tokens) { + const float2 f = *(const float2 *)(x + (uint64_t)tok * (GROUPS * 4096u) + bi * 32u + kk); + xv = __floats2half2_rn(f.x, f.y); + } + *(half2 *)(lds_x + j) = xv; + } + __syncthreads(); + + const unsigned char *bp = row_base + (uint64_t)bi * 34u; + _Float16 sc; + { + uint16_t s_bits; + __builtin_memcpy(&s_bits, bp, 2); + __builtin_memcpy(&sc, &s_bits, 2); + } + + const int8_t *w0 = (const int8_t *)(bp + 2u); + const int8_t *w1 = (const int8_t *)(bp + 18u); + ds4_q8_half16_t a0; + ds4_q8_half16_t a1; +#pragma unroll + for (uint32_t i = 0; i < 16u; i++) { + a0[i] = sc * (_Float16)(float)(int)w0[i]; + a1[i] = sc * (_Float16)(float)(int)w1[i]; + } + +#pragma unroll + for (uint32_t ntile = 0; ntile < N_TILES_PER_WARP; ntile++) { + const uint32_t nt = ntile * 16u + lane16; + const _Float16 *xb = lds_x + nt * K_TILE; + const ds4_q8_half16_t b0 = *(const ds4_q8_half16_t *)(xb); + const ds4_q8_half16_t b1 = *(const ds4_q8_half16_t *)(xb + 16u); + if (ntile == 0u) { + acc0 = __builtin_amdgcn_wmma_f32_16x16x16_f16_w32(a0, b0, acc0); + acc0 = __builtin_amdgcn_wmma_f32_16x16x16_f16_w32(a1, b1, acc0); + } else if (ntile == 1u) { + acc1 = __builtin_amdgcn_wmma_f32_16x16x16_f16_w32(a0, b0, acc1); + acc1 = __builtin_amdgcn_wmma_f32_16x16x16_f16_w32(a1, b1, acc1); + } else if (ntile == 2u) { + acc2 = __builtin_amdgcn_wmma_f32_16x16x16_f16_w32(a0, b0, acc2); + acc2 = __builtin_amdgcn_wmma_f32_16x16x16_f16_w32(a1, b1, acc2); + } else { + acc3 = __builtin_amdgcn_wmma_f32_16x16x16_f16_w32(a0, b0, acc3); + acc3 = __builtin_amdgcn_wmma_f32_16x16x16_f16_w32(a1, b1, acc3); + } + } + __syncthreads(); + } + +#pragma unroll + for (uint32_t ntile = 0; ntile < N_TILES_PER_WARP; ntile++) { + const uint32_t tok = block_n + ntile * 16u + lane16; + if (tok >= n_tokens) continue; + ds4_q8_float8_t acc = ntile == 0u ? acc0 : (ntile == 1u ? acc1 : (ntile == 2u ? acc2 : acc3)); +#pragma unroll + for (uint32_t j = 0; j < 8u; j++) { + const uint32_t row = warp_m + 2u * j + (lane >> 4u); + if (row < out_dim) out[(uint64_t)tok * (GROUPS * 1024u) + row] = acc[j]; + } + } +} + +/* Scalar grouped projection with packed four-weight loads and F32 activations. */ +__global__ static void v41_grouped_q8_f32_blocks4_kernel( + float *out, const unsigned char *w, const float *x, int K, int M, int G) { + int lane = threadIdx.x & 31, row = blockIdx.x * 8 + threadIdx.x / 32; + if (row >= M * G) return; + const float *in = x + (row / M) * K; + float acc = 0; + for (int b = lane / 8; b < K / 32; b += 4) { + const unsigned char *p = w + ((size_t) row * (K / 32) + b) * 34; + float d = __half2float(* (const __half *) p), v = 0; + int j = (lane & 7) * 4; +#pragma unroll + for (int k = 0; k < 4; k++) v += (float) ((const int8_t *) (p + 2)) [j + k] * in[b * 32 + j + k]; + acc += d * v; + } + acc = warp_sum_f32(acc); + if (!lane) out[row] = acc; +} + +extern "C" int ds4_gpu_dsv41_attention_output_batch(ds4_gpu_tensor *out, ds4_gpu_tensor *low, + const void *model_map, uint64_t model_size, uint64_t out_a_offset, uint64_t out_b_offset, + const ds4_gpu_tensor *heads, uint32_t n_tokens) { + const uint64_t a_bytes = UINT64_C(8192) * 128u * 34u, b_bytes = UINT64_C(5120) * 256u * 34u; + if (!model_map || !n_tokens || !cuda_model_range_fits(model_size, out_a_offset, a_bytes) || + !cuda_model_range_fits(model_size, out_b_offset, b_bytes) || + !cuda_tensor_has_elems2(heads, n_tokens, 32768u, 4u) || + !cuda_tensor_has_elems2(low, n_tokens, 8192u, 4u) || !cuda_tensor_has_elems2(out, n_tokens, 5120u, 4u)) return 0; + const unsigned char *a = (const unsigned char *)cuda_model_range_ptr(model_map, out_a_offset, a_bytes, "V4.1 attn_out_a"); + const unsigned char *b = (const unsigned char *)cuda_model_range_ptr(model_map, out_b_offset, b_bytes, "V4.1 attn_out_b"); + if (!a || !b) return 0; + if (n_tokens == 1u && ds4_rocm_is_gfx1151()) { + v41_grouped_q8_f32_blocks4_kernel<<<1024u, 256u>>>( + (float *)low->ptr, a, (const float *)heads->ptr, 4096, 1024, 8); + } else if (!g_quality_mode && n_tokens >= 32u && n_tokens <= 2048u && ds4_rocm_is_gfx1151()) { + /* Canonical eight groups of 4096 -> 1024, with physical F32 token + * strides32768/8192. Keep the explicit BF16 low boundary below. */ + v41_grouped_q8_f32_wmma_rowtile_kernel<128u, 8u><<>>( + (float *)low->ptr, a, (const float *)heads->ptr, + n_tokens, 4096u, 1024u, UINT64_C(128) * 34u); + } else if (n_tokens >= 32u && ds4_rocm_is_gfx1151()) { + cuda_launch_grouped_q8_a_sharedx((float *)low->ptr, a, (const float *)heads->ptr, + n_tokens, 8u, 128u, 1024u, 128u * 34u, 8u, 8u, 8u); + } else { + grouped_q8_0_a_f32_batch_warp8_kernel<<>>((float *)low->ptr, a, + (const float *)heads->ptr, 4096u, 1024u, 8u, n_tokens, 128u); + } + if (!cuda_ok(cudaGetLastError(), "V4.1 attention low projection") || + !ds4_gpu_dsv41_quantize(low, 8192u, n_tokens, DS4_V41_BF16)) return 0; + if (n_tokens == 1u && ds4_rocm_is_gfx1151()) { + /* The shared input is the same BF16-rounded output-A row above. */ + v41_q8_f32_blocks4_kernel<<<640u, 256u>>>( + (float *)out->ptr, b, (const float *)low->ptr, + 8192u, 5120u, UINT64_C(256) * 34u); + } else if (!g_quality_mode && n_tokens >= 32u && n_tokens <= 2048u && ds4_rocm_is_gfx1151()) { + /* Keep the BF16 boundary above and use the existing generic bulk + * matrix path: F16-rounded operands with F32 accumulation. */ + matmul_q8_0_f32_batch_wmma_rowtile_kernel<128u, 8u><<>>( + (float *)out->ptr, b, (const float *)low->ptr, + n_tokens, 8192u, 5120u, UINT64_C(256) * 34u); + } else if (n_tokens >= 32u && n_tokens <= 2048u && ds4_rocm_is_gfx1151()) { + /* Keep the existing BF16-rounded low rows and reuse sixteen tokens. */ + cuda_launch_q8_batch_sharedx((float *)out->ptr, b, (const float *)low->ptr, + 256u, 5120u, n_tokens, 256u * 34u, 8u, 16u, 8u); + } else if (n_tokens >= 32u && ds4_rocm_is_gfx1151()) { + cuda_launch_q8_batch_sharedx((float *)out->ptr, b, (const float *)low->ptr, + 256u, 5120u, n_tokens, 256u * 34u, 8u, 8u, 8u); + } else { + matmul_q8_0_f32_batch_warp8_kernel<<>>((float *)out->ptr, b, + (const float *)low->ptr, 8192u, 5120u, n_tokens, 256u); + } + return cuda_ok(cudaGetLastError(), "V4.1 attention output projection"); +} + +extern "C" int ds4_gpu_dsv41_attention_output_tp_batch(ds4_gpu_tensor *out, ds4_gpu_tensor *low, + const void *model_map, uint64_t model_size, uint64_t out_a_offset, uint64_t out_b_offset, + const ds4_gpu_tensor *heads, uint32_t n_tokens, uint32_t tp_rank) { + /* Heads and low rows are packed for this rank. Output-B keeps its + * original 8192-column physical row stride while consuming 4096 columns. */ + const uint64_t a_bytes = UINT64_C(4096) * 128u * 34u; + const uint64_t b_bytes = UINT64_C(5120) * 256u * 34u; + if (tp_rank > 1u || !model_map || !n_tokens || + !cuda_model_range_fits(model_size, out_a_offset, 2u * a_bytes) || + !cuda_model_range_fits(model_size, out_b_offset, b_bytes) || + !cuda_tensor_has_elems2(heads, n_tokens, 16384u, 4u) || + !cuda_tensor_has_elems2(low, n_tokens, 4096u, 4u) || + !cuda_tensor_has_elems2(out, n_tokens, 5120u, 4u)) return 0; + const unsigned char *a = (const unsigned char *)cuda_model_range_ptr(model_map, + out_a_offset + tp_rank * a_bytes, a_bytes, "V4.1 TP attn_out_a"); + const unsigned char *b = (const unsigned char *)cuda_model_range_ptr(model_map, + out_b_offset, b_bytes, "V4.1 TP attn_out_b"); + if (!a || !b) return 0; + b += (uint64_t)tp_rank * 128u * 34u; + if (n_tokens == 1u && ds4_rocm_is_gfx1151()) { + v41_grouped_q8_f32_blocks4_kernel<<<512u, 256u>>>( + (float *)low->ptr, a, (const float *)heads->ptr, 4096, 1024, 4); + } else if (!g_quality_mode && n_tokens >= 32u && n_tokens <= 2048u && ds4_rocm_is_gfx1151()) { + v41_grouped_q8_f32_wmma_rowtile_kernel<128u, 8u, 4u><<>>( + (float *)low->ptr, a, (const float *)heads->ptr, + n_tokens, 4096u, 1024u, UINT64_C(128) * 34u); + } else if (n_tokens >= 32u && ds4_rocm_is_gfx1151()) { + cuda_launch_grouped_q8_a_sharedx((float *)low->ptr, a, (const float *)heads->ptr, + n_tokens, 4u, 128u, 1024u, 128u * 34u, 8u, 8u, 8u); + } else { + grouped_q8_0_a_f32_batch_warp8_kernel<<>>( + (float *)low->ptr, a, (const float *)heads->ptr, + 4096u, 1024u, 4u, n_tokens, 128u); + } + if (!cuda_ok(cudaGetLastError(), "V4.1 TP attention low projection") || + !ds4_gpu_dsv41_quantize(low, 4096u, n_tokens, DS4_V41_BF16)) return 0; + if (n_tokens == 1u && ds4_rocm_is_gfx1151()) { + v41_q8_f32_blocks4_kernel<<<640u, 256u>>>( + (float *)out->ptr, b, (const float *)low->ptr, + 4096u, 5120u, UINT64_C(256) * 34u); + } else if (!g_quality_mode && n_tokens >= 32u && n_tokens <= 2048u && ds4_rocm_is_gfx1151()) { + matmul_q8_0_f32_batch_wmma_rowtile_kernel<128u, 8u><<>>( + (float *)out->ptr, b, (const float *)low->ptr, + n_tokens, 4096u, 5120u, UINT64_C(256) * 34u); + } else if (n_tokens >= 32u && ds4_rocm_is_gfx1151()) { + cuda_launch_q8_batch_sharedx((float *)out->ptr, b, (const float *)low->ptr, + 128u, 5120u, n_tokens, 256u * 34u, 8u, n_tokens <= 2048u ? 16u : 8u, 8u); + } else { + /* This scalar kernel accepts separate input length and weight stride; + * its column guard excludes the unowned half of each physical row. */ + matmul_q8_0_f32_batch_warp8_kernel<<>>( + (float *)out->ptr, b, (const float *)low->ptr, + 4096u, 5120u, n_tokens, 256u); + } + return cuda_ok(cudaGetLastError(), "V4.1 TP attention output projection"); +} + +/* Staged correctness reference. Validate global IDs before any pointer-table + * lookup; a null table entry suppresses every unowned gate/up/down load. + * The ordinary routed-MoE dispatch is untouched. */ +extern "C" int ds4_gpu_dsv41_routed_moe_tp_tensor( + ds4_gpu_tensor *out, ds4_gpu_tensor *gate, ds4_gpu_tensor *up, + ds4_gpu_tensor *mid, ds4_gpu_tensor *scratch, + const void *model_map, uint64_t model_size, + uint64_t gate_offset, uint64_t up_offset, uint64_t down_offset, + const ds4_gpu_tensor *selected, const ds4_gpu_tensor *weights, + const ds4_gpu_tensor *x, uint32_t n_tokens, uint32_t tp_rank) { + constexpr uint32_t experts = 384u, owned = 192u, used = 6u; + constexpr uint64_t gate_row = 1320u, down_row = 756u; + constexpr uint64_t gate_expert = gate_row * 2304u, down_expert = down_row * 5120u; + routed_moe_launch_plan plan; + if (!g_deepseek41_model || tp_rank > 1u || !n_tokens || n_tokens > 2048u || + !routed_moe_build_plan(out, gate, up, mid, scratch, model_map, model_size, + gate_offset, up_offset, down_offset, 16u, 10u, gate_expert, down_expert, + 5120u, 2304u, 5120u, selected, weights, experts, used, x, n_tokens, &plan)) return 0; + const uint64_t pairs = (uint64_t)n_tokens * used; + std::vector ids((size_t)pairs); + if (!ds4_gpu_tensor_read(selected, 0, ids.data(), pairs * sizeof(int32_t))) return 0; + for (int32_t id : ids) if (id < 0 || (uint32_t)id >= experts) { + fprintf(stderr, "ds4: V4.1 TP invalid global expert ID %d\n", id); + return 0; + } + if (n_tokens >= 128u && !g_quality_mode && ds4_rocm_is_gfx1151()) { + if (!ds4_gpu_dsv41_moe_tp_gate_up(gate, up, model_map, model_size, + gate_offset, up_offset, selected, x, n_tokens, tp_rank)) return 0; + const uint64_t count = pairs * 2304u; + moe_swiglu_weighted_f32_kernel<<<(uint32_t)((count + 255u) / 256u), 256>>>( + (float *)mid->ptr, (const float *)gate->ptr, (const float *)up->ptr, + (const float *)weights->ptr, count, 2304u, 10.f); + if (!cuda_ok(cudaGetLastError(), "V4.1 TP MMQ weighted activation") || + !ds4_gpu_synchronize()) return 0; + return ds4_gpu_dsv41_moe_tp_down(out, scratch, mid, selected, + model_map, model_size, down_offset, n_tokens, tp_rank); + } + const uint32_t first = tp_rank * owned; + const char *g = cuda_model_range_ptr(model_map, gate_offset + first * gate_expert, + owned * gate_expert, "V4.1 TP owned gate"); + const char *u = cuda_model_range_ptr(model_map, up_offset + first * gate_expert, + owned * gate_expert, "V4.1 TP owned up"); + const char *d = cuda_model_range_ptr(model_map, down_offset + first * down_expert, + owned * down_expert, "V4.1 TP owned down"); + if (!g || !u || !d) return 0; + const char *tables[3][experts] = {}; + for (uint32_t i = 0; i < owned; ++i) { + tables[0][first+i] = g + i * gate_expert; + tables[1][first+i] = u + i * gate_expert; + tables[2][first+i] = d + i * down_expert; + } + ds4_gpu_tensor *table = ds4_gpu_tensor_alloc(sizeof(tables)); + if (!table) return 0; + const uint64_t mids = pairs * 2304u; + int ok = ds4_gpu_tensor_write(table, 0, tables, sizeof(tables)) && + ds4_gpu_tensor_fill_f32(gate, 0.f, mids) && + ds4_gpu_tensor_fill_f32(up, 0.f, mids) && + ds4_gpu_tensor_fill_f32(mid, 0.f, mids); + cuda_block_q8_K *xq = (cuda_block_q8_K *)scratch->ptr; + if (ok) { + q8_K_quantize_kernel<<>>( + xq, (const float *)x->ptr, 5120u, n_tokens); + ok = cuda_ok(cudaGetLastError(), "V4.1 TP input quantization"); + } + const char *const *slots = (const char *const *)table->ptr; + if (ok) { + if (n_tokens == 1u && ds4_rocm_is_gfx1151()) { + moe_v41_gate_up_wave_ptrs_kernel<4><<>>( + (float *)gate->ptr, (float *)up->ptr, (float *)mid->ptr, + slots, slots + experts, xq, (const int32_t *)selected->ptr, + (const float *)weights->ptr, 0, gate_row, 20u, 2304u, used, + 1u, 0x3fu, 10.f); + } else { + moe_gate_up_mid_qwarp32_ptrs_kernel<<>>( + (float *)gate->ptr, (float *)up->ptr, (float *)mid->ptr, + slots, slots + experts, xq, (const int32_t *)selected->ptr, + (const float *)weights->ptr, gate_row, 20u, 2304u, used, 0x3fu, 10.f); + } + ok = cuda_ok(cudaGetLastError(), "V4.1 TP owned gate/up"); + } + if (ok) { + if (n_tokens == 1u && !g_quality_mode && ds4_rocm_is_gfx1151()) { + // Gate/up has consumed xq; reuse its scratch for the six mid rows. + cuda_block_q8_K *midq = (cuda_block_q8_K *)scratch->ptr; + q8_K_quantize_kernel<<>>( + midq, (const float *)mid->ptr, 2304u, used); + ok = cuda_ok(cudaGetLastError(), "V4.1 TP mid quantization"); + if (ok) { + moe_v41_down_wave_ptrs_kernel<4><<<1280u, 128>>>( + (float *)out->ptr, slots + 2u * experts, midq, + (const int32_t *)selected->ptr, 0, down_row, 9u, 5120u, used); + ok = cuda_ok(cudaGetLastError(), "V4.1 TP quantized wave down"); + } + } else { + moe_down_q2K_sum_rows_w32_ptrs_batch_kernel<<>>( + (float *)out->ptr, slots + 2u * experts, (const float *)mid->ptr, + (const int32_t *)selected->ptr, n_tokens, 2304u, 5120u, down_row, used); + ok = cuda_ok(cudaGetLastError(), "V4.1 TP owned down"); + } + } + /* The reference deliberately drains before releasing its pointer table. + * Persistent tables and a queued service follow ownership qualification. */ + if (!ds4_gpu_synchronize()) ok = 0; + ds4_gpu_tensor_free(table); + return ok; +} + +/* Staged ownership adapter for the unchanged qualified Q2 down dispatcher. + * Pair order is stable within each local expert, as in its GPU sort. */ +extern "C" int ds4_gpu_dsv41_moe_tp_down( + ds4_gpu_tensor *out, ds4_gpu_tensor *scratch, + const ds4_gpu_tensor *mid, const ds4_gpu_tensor *selected, + const void *model_map, uint64_t model_size, uint64_t down_offset, + uint32_t n_tokens, uint32_t tp_rank) { + constexpr uint64_t expert_bytes = UINT64_C(5120) * 756u; + const uint64_t pairs = (uint64_t)n_tokens * 6u, mids = pairs * 2304u; + if (!g_deepseek41_model || !ds4_rocm_is_gfx1151() || tp_rank > 1u || + n_tokens < 2u || n_tokens > 2048u || !out || !scratch || !mid || !selected || + !model_map || out->bytes < (uint64_t)n_tokens * 5120u * sizeof(float) || + scratch->bytes < pairs * 5120u * sizeof(float) || + mid->bytes < mids * sizeof(float) || selected->bytes < pairs * sizeof(int32_t) || + down_offset > model_size || 384u * expert_bytes > model_size - down_offset) return 0; + std::vector ids((size_t)pairs); + if (!ds4_gpu_tensor_read(selected, 0, ids.data(), pairs * sizeof(int32_t))) return 0; + for (uint32_t row = 0; row < n_tokens; ++row) { + for (uint32_t j = 0; j < 6u; ++j) { + const int32_t id = ids[(size_t)row * 6u + j]; + if (id < 0 || id >= 384) return 0; + for (uint32_t k = 0; k < j; ++k) + if (id == ids[(size_t)row * 6u + k]) return 0; + } + } + const uint32_t first = tp_rank * 192u; + const char *d = cuda_model_range_ptr(model_map, down_offset + first * expert_bytes, + 192u * expert_bytes, "V4.1 TP owned bulk down"); + if (!d) return 0; + /* Counts, offsets, spare hot-list storage, then original six-slot pair IDs. */ + constexpr uint32_t offsets_at = 192u, hot_at = 385u, pairs_at = 578u; + std::vector metadata(pairs_at + (size_t)pairs, 0u); + uint32_t pos = 0; + for (uint32_t e = 0; e < 192u; ++e) { + metadata[offsets_at + e] = pos; + for (uint32_t p = 0; p < pairs; ++p) + if ((uint32_t)ids[p] == first + e) { + metadata[pairs_at + pos++] = p; + ++metadata[e]; + } + } + metadata[offsets_at + 192u] = pos; + ds4_gpu_tensor *meta = ds4_gpu_tensor_alloc(metadata.size() * sizeof(uint32_t)); + ds4_gpu_tensor *mid_h = ds4_gpu_tensor_alloc(mids * sizeof(half)); + int ok = meta && mid_h; + if (ok) ok = ds4_gpu_tensor_write(meta, 0, metadata.data(), metadata.size() * sizeof(uint32_t)) && + cuda_ok(cudaMemset(scratch->ptr, 0, pairs * 5120u * sizeof(half)), + "V4.1 TP clear unowned down slots"); + if (ok) { + f32_to_f16_kernel<<<(uint32_t)((mids + 255u) / 256u), 256>>>( + (half *)mid_h->ptr, (const float *)mid->ptr, mids); + ok = cuda_ok(cudaGetLastError(), "V4.1 TP down F16 mid"); + } + if (ok) { + uint32_t *m = (uint32_t *)meta->ptr; + ok = routed_moe_q2_float_down_launch(out, scratch, mid, + (const half *)mid_h->ptr, !g_quality_mode, d, m, m + offsets_at, + m + pairs_at, m + hot_at, n_tokens, 192u, 6u, 2304u, 5120u, + expert_bytes, 756u); + } + if (!ds4_gpu_synchronize()) ok = 0; + ds4_gpu_tensor_free(mid_h); + ds4_gpu_tensor_free(meta); + return ok; +} + +/* Isolated bulk operator: remap owned IDs to a contiguous192-expert table. + * INT_MAX is a nonmatching sentinel in mm_ids_helper, so no unowned expert + * contributes an assignment. Cleared output rows remain zero for those slots. */ +extern "C" int ds4_gpu_dsv41_moe_tp_gate_up( + ds4_gpu_tensor *gate, ds4_gpu_tensor *up, + const void *model_map, uint64_t model_size, + uint64_t gate_offset, uint64_t up_offset, + const ds4_gpu_tensor *selected, const ds4_gpu_tensor *x, + uint32_t n_tokens, uint32_t tp_rank) { + constexpr uint64_t expert_bytes = UINT64_C(2304) * 1320u; + const uint64_t pairs = (uint64_t)n_tokens * 6u; + const uint64_t output_bytes = pairs * 2304u * sizeof(float); + if (!g_deepseek41_model || !ds4_rocm_is_gfx1151() || tp_rank > 1u || + n_tokens < 128u || n_tokens > 2048u || !gate || !up || !selected || !x || + !model_map || gate->bytes < output_bytes || up->bytes < output_bytes || + selected->bytes < pairs * sizeof(int32_t) || + x->bytes < (uint64_t)n_tokens * 5120u * sizeof(float) || + gate_offset > model_size || 384u * expert_bytes > model_size - gate_offset || + up_offset > model_size || 384u * expert_bytes > model_size - up_offset) return 0; + std::vector ids((size_t)pairs); + if (!ds4_gpu_tensor_read(selected, 0, ids.data(), pairs * sizeof(int32_t))) return 0; + for (uint32_t row = 0; row < n_tokens; ++row) { + for (uint32_t j = 0; j < 6u; ++j) { + const int32_t id = ids[(size_t)row * 6u + j]; + if (id < 0 || id >= 384) return 0; + for (uint32_t k = 0; k < j; ++k) + if (id == ids[(size_t)row * 6u + k]) return 0; + } + } + const uint32_t first = tp_rank * 192u; + for (int32_t &id : ids) + id = (uint32_t)id >= first && (uint32_t)id < first + 192u ? id - first : INT_MAX; + const char *g = cuda_model_range_ptr(model_map, gate_offset + first * expert_bytes, + 192u * expert_bytes, "V4.1 TP MMQ owned gate"); + const char *u = cuda_model_range_ptr(model_map, up_offset + first * expert_bytes, + 192u * expert_bytes, "V4.1 TP MMQ owned up"); + if (!g || !u) return 0; + ds4_gpu_tensor *local_ids = ds4_gpu_tensor_alloc(pairs * sizeof(int32_t)); + if (!local_ids) return 0; + int ok = ds4_gpu_tensor_write(local_ids, 0, ids.data(), pairs * sizeof(int32_t)) && + ds4_gpu_tensor_fill_f32(gate, 0.f, pairs * 2304u) && + ds4_gpu_tensor_fill_f32(up, 0.f, pairs * 2304u) && ds4_mmq_init(0) == 0; + if (ok) ok = ds4_mmq_iq2_xxs_moe_pair(g, u, (const float *)x->ptr, + (const int32_t *)local_ids->ptr, (float *)gate->ptr, (float *)up->ptr, + 2304, 5120, (int)n_tokens, 192, 6, (cudaStream_t)0) == 0; + if (!ds4_gpu_synchronize()) ok = 0; + ds4_gpu_tensor_free(local_ids); + return ok; +} + +#endif diff --git a/speed-bench/build-rocm-v41-warmup.sh b/speed-bench/build-rocm-v41-warmup.sh new file mode 100644 index 0000000000..77f89a3f09 --- /dev/null +++ b/speed-bench/build-rocm-v41-warmup.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Build the published TP warmup adapter against existing ROCm engine objects. +# The engine and ordinary ds4-bench source/binary remain untouched. +set -euo pipefail +cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." +work=$(mktemp -d ./.rocm-v41-warmup.XXXXXX) +trap 'rm -rf -- "$work"' EXIT +patch --silent --output "$work/ds4_bench.c" ds4_bench.c < speed-bench/rocm-v41-warmup.patch +objects=(ds4_help.o ds4_gpu_args.o ds4.o ds4_image.o ds4_distributed.o ds4_tp.o + ds4_ssd.o ds4_rocm.o ds4_rocm_compat.o ds4_rocm_unavailable.o ds4_layer_pack.o + ds4_engram.o cuda/mmq/ds4_ggml_stubs.rocm.o cuda/mmq/ds4_mmq.rocm.o + cuda/mmq/quantize.rocm.o cuda/mmq/mmid.rocm.o cuda/mmq/mmvq.rocm.o + cuda/mmq/d2r_stubs.rocm.o) +for file in "${objects[@]}"; do + test -f "$file" || { echo "Missing $file; run make strix-halo first." >&2; exit 1; } +done +"${CC:-cc}" -O3 -ffast-math -g -march=native -Wall -Wextra -std=c99 \ + -D_GNU_SOURCE -fno-finite-math-only -fPIC -DDS4_ROCM_BUILD -I. \ + -c "$work/ds4_bench.c" -o "$work/ds4_bench.o" +verbs=() +if nm -u ds4_distributed.o ds4_tp.o | grep 'ibv_' >/dev/null; then verbs=(-libverbs); fi +"${HIPCC:-/opt/rocm/bin/hipcc}" -O3 -ffast-math -g -fno-finite-math-only \ + -pthread -D__HIP_PLATFORM_AMD__ -Wno-unused-command-line-argument --offload-arch=gfx1151 \ + -o ds4-bench-warm "$work/ds4_bench.o" "${objects[@]}" \ + -lm -pthread -lhipblas -lhipblaslt -lrocblas "${verbs[@]}" +./ds4-bench-warm --help >/dev/null +printf '%s\n' 'Built ./ds4-bench-warm: TP only, excluded 256-token/128-output warmup, fresh measured session.' diff --git a/speed-bench/rocm-v41-warmup.patch b/speed-bench/rocm-v41-warmup.patch new file mode 100644 index 0000000000..1ec174858b --- /dev/null +++ b/speed-bench/rocm-v41-warmup.patch @@ -0,0 +1,32 @@ +--- a/ds4_bench.c ++++ b/ds4_bench.c +@@ -807,6 +807,29 @@ + int previous = 0; + int rc = 0; + ++ /* Excluded native warmup for the published ROCm TP measurements. Recreate the session before measurement ++ * so the complete prefix reaches the normal CED scheduling thresholds. */ ++ if (!distributed || speculative || prompt.len < 256) { ++ fprintf(stderr, "ds4-bench: warmup requires ordinary TP and >=256 prompt tokens\n"); ++ ds4_session_free(session); ds4_tokens_free(&prompt); ++ if (out != stdout) fclose(out); ++ close_engine(engine, tp_leader); return 1; ++ } ++ ds4_tokens warm_prefix = { .v = prompt.v, .len = 256, .cap = 256 }; ++ int warm_ok = ds4_session_sync(session, &warm_prefix, err, sizeof(err)) == 0; ++ for (int i = 0; warm_ok && i < 128; ++i) { ++ int token = ds4_session_argmax_excluding(session, eos); ++ warm_ok = token >= 0 && ds4_session_eval(session, token, err, sizeof(err)) == 0; ++ } ++ ds4_session_free(session); session = NULL; ++ if (!warm_ok || ds4_session_create(&session, engine, cfg.ctx_alloc) != 0) { ++ fprintf(stderr, "ds4-bench: excluded warmup/session reset failed: %s\n", err); ++ ds4_session_free(session); ds4_tokens_free(&prompt); ++ if (out != stdout) fclose(out); ++ close_engine(engine, tp_leader); return 1; ++ } ++ fprintf(stderr, "ds4-bench: excluded warmup complete (256 prefix/128 outputs); fresh measured session\n"); ++ + for (int frontier = cfg.ctx_start; ; frontier = next_frontier(&cfg, frontier)) { + ds4_tokens prefix = { + .v = prompt.v, diff --git a/tests/test_deepseek41_cache_spans.c b/tests/test_deepseek41_cache_spans.c new file mode 100644 index 0000000000..c9c2a754ae --- /dev/null +++ b/tests/test_deepseek41_cache_spans.c @@ -0,0 +1,152 @@ +/* Linux CPU-only regression for accelerator startup over a disk-only GGUF. + * Include the real span builders, replace only GPU cache entry points, and + * discard unrelated engine sections at link time. No GPU library is linked. */ +#include "../ds4.c" +#include + +static uint64_t cache_calls, cache_bytes; +static const ds4_model *expected_model; + +int ds4_gpu_cache_model_range(const void *map, uint64_t size, uint64_t off, + uint64_t bytes, const char *label) { + (void)label; + assert(expected_model && map == expected_model->map && size == expected_model->size); + assert(off <= size && bytes <= size - off); + assert(off == expected_model->tensor_data_pos && bytes == 64); + cache_calls++; + cache_bytes += bytes; + return 1; +} + +#ifndef DS4_ROCM_BUILD +int ds4_gpu_model_range_replaced(const void *map, uint64_t off, uint64_t bytes) { + (void)map; (void)off; (void)bytes; + return 0; +} + +int ds4_gpu_cache_q8_f16_range(const void *map, uint64_t size, uint64_t off, + uint64_t bytes, uint64_t in, uint64_t out, const char *label) { + (void)map; (void)size; (void)off; (void)bytes; (void)in; (void)out; (void)label; + assert(!"fixture has no Q8 tensors"); + return 0; +} +#endif + +/* Same sparse GGUF layout as test_deepseek41_gguf, with one row per table. */ +static void put32(FILE *fp, uint32_t v) { assert(fwrite(&v, 4, 1, fp) == 1); } +static void put64(FILE *fp, uint64_t v) { assert(fwrite(&v, 8, 1, fp) == 1); } +static void putstr(FILE *fp, const char *s) { + put64(fp, strlen(s)); + assert(fwrite(s, 1, strlen(s), fp) == strlen(s)); +} +static void string_kv(FILE *fp, const char *key, const char *value) { + putstr(fp, key); put32(fp, GGUF_VALUE_STRING); putstr(fp, value); +} +static void tensor(FILE *fp, const char *name, uint32_t type, + uint64_t width, uint64_t rows, uint64_t offset) { + putstr(fp, name); put32(fp, 2); put64(fp, width); put64(fp, rows); + put32(fp, type); put64(fp, offset); +} + +static void check_unmapped(const ds4_model *m) { + unsigned char resident; + errno = 0; + assert(mincore((void *)(m->map + m->size), (size_t)sysconf(_SC_PAGESIZE), + &resident) == -1 && errno == ENOMEM); +} + +static void check_mapping(const char *path, bool shared, uint64_t first, uint64_t file_size) { + ds4_model m; + model_open(&m, path, shared, false); + expected_model = &m; + cache_calls = cache_bytes = 0; + assert(m.size == first && m.file_size == file_size); + check_unmapped(&m); + uint64_t prepared = UINT64_MAX; +#ifndef DS4_ROCM_BUILD + /* CUDA startup spans still reject disk-only descriptors, while its Q8 + * cache skips V4.1 Engram tables as of upstream a04f46f. */ + assert(!accelerator_prepare_model_tensor_spans(&m, NULL, NULL, 0, &prepared)); + assert(accelerator_cache_q8_tensors(&m, NULL, NULL, 0)); + assert(cache_calls == 0); + const uint64_t tensor_count = m.n_tensors; + m.n_tensors = 1; + assert(accelerator_prepare_model_tensor_spans(&m, NULL, NULL, 0, &prepared)); + assert(cache_calls == 1 && cache_bytes == 64 && prepared == 64); + assert(accelerator_cache_q8_tensors(&m, NULL, NULL, 0)); + m.n_tensors = tensor_count; + model_close(&m); + expected_model = NULL; + return; +#endif + /* Before the fix this returned false with zero cache calls: the Engram + * descriptors were incorrectly checked against the shorter weight map. */ + assert(accelerator_prepare_model_tensor_spans(&m, NULL, NULL, 0, &prepared)); + assert(cache_calls == 1 && cache_bytes == 64 && prepared == 64); + const uint64_t off = m.tensor_data_pos, bytes = 64; + assert(accelerator_prepare_model_tensor_spans(&m, &off, &bytes, 1, &prepared)); + assert(cache_calls == 2 && cache_bytes == 128 && prepared == 64); + /* Explicit GPU map requests into the disk-only tail remain invalid. */ + assert(!accelerator_prepare_model_tensor_spans(&m, &first, &bytes, 1, &prepared)); + assert(cache_calls == 2); +#ifndef DS4_ROCM_BUILD + assert(accelerator_cache_q8_tensors(&m, NULL, NULL, 0)); +#endif + /* A filter cannot conceal an invalid ordinary tensor descriptor. */ + const uint64_t saved_offset = m.tensors[0].abs_offset; + m.tensors[0].abs_offset = m.size + 64; + assert(!accelerator_prepare_model_tensor_spans(&m, NULL, NULL, 0, &prepared)); + assert(!accelerator_prepare_model_tensor_spans(&m, &off, &bytes, 1, &prepared)); + assert(cache_calls == 2); +#ifndef DS4_ROCM_BUILD + assert(!accelerator_cache_q8_tensors(&m, NULL, NULL, 0)); +#endif + m.tensors[0].abs_offset = saved_offset; + /* Neither an arbitrary tail tensor nor a malformed recognized table can + * acquire the disk-only exemption. Normal mapped-range rejection stays. */ + const ds4_tensor saved_table = m.tensors[1]; + m.tensors[1].name = (ds4_str){.ptr = "other.weight", .len = 12}; + assert(!accelerator_prepare_model_tensor_spans(&m, NULL, NULL, 0, &prepared)); + m.tensors[1] = saved_table; + m.tensors[1].type = DS4_TENSOR_F32; + assert(!accelerator_prepare_model_tensor_spans(&m, NULL, NULL, 0, &prepared)); + m.tensors[1] = saved_table; + m.tensors[1].bytes = m.file_size; + assert(!accelerator_prepare_model_tensor_spans(&m, NULL, NULL, 0, &prepared)); + assert(cache_calls == 2); +#ifndef DS4_ROCM_BUILD + assert(!accelerator_cache_q8_tensors(&m, NULL, NULL, 0)); +#endif + m.tensors[1] = saved_table; + check_unmapped(&m); + model_close(&m); + expected_model = NULL; +} + +int main(void) { + enum { ALIGN = 16384 }; + const uint64_t first = 2 * ALIGN, second = 3 * ALIGN, file_size = second + 264; + char path[] = "/tmp/ds41-cache-spans.XXXXXX"; + int fd = mkstemp(path); + assert(fd >= 0); + FILE *fp = fdopen(fd, "w+b"); + assert(fp); + put32(fp, DS4_GGUF_MAGIC); put32(fp, 3); put64(fp, 3); put64(fp, 3); + string_kv(fp, "general.architecture", "deepseek41"); + string_kv(fp, "deepseek41.engram.encoding", "e4m3_e8m0_32_row264"); + putstr(fp, "general.alignment"); put32(fp, GGUF_VALUE_UINT32); put32(fp, ALIGN); + tensor(fp, "test.weight", DS4_TENSOR_F32, 16, 1, 0); + tensor(fp, "blk.1.engram_embd.weight", DS4_TENSOR_I8, 264, 1, first - ALIGN); + tensor(fp, "blk.14.engram_embd.weight", DS4_TENSOR_I8, 264, 1, second - ALIGN); + assert(ftell(fp) < ALIGN); + assert(fflush(fp) == 0 && ftruncate(fd, (off_t)file_size) == 0); + check_mapping(path, false, first, file_size); + check_mapping(path, true, first, file_size); + assert(fclose(fp) == 0 && unlink(path) == 0); +#ifdef DS4_ROCM_BUILD + puts("V4.1 ROCm startup spans: disk-only exclusion and resident/file/filter bounds PASS (CPU-only)"); +#else + puts("V4.1 CUDA startup spans and optional Q8 scan: disk-only exclusion and bounds PASS (CPU-only)"); +#endif + return 0; +} diff --git a/tests/test_deepseek41_memory.c b/tests/test_deepseek41_memory.c new file mode 100644 index 0000000000..ebc311d8f7 --- /dev/null +++ b/tests/test_deepseek41_memory.c @@ -0,0 +1,80 @@ +/* Exercise actual ROCm admission with controlled host-memory observations. */ +#include "../ds4_linux_memory.h" +static uint64_t available; +static bool test_available(uint64_t *bytes) { *bytes = available; return true; } +#define ds4_linux_nonmovable_memory test_available +#include "../ds4.c" +#include + +static uint64_t recommended; +uint64_t ds4_gpu_recommended_working_set_size(void) { return recommended; } +int ds4_gpu_stream_expert_cache_get_memory(ds4_gpu_stream_expert_memory *out) { + memset(out, 0, sizeof(*out)); + return 1; +} + +int main(void) { + const uint64_t gib = UINT64_C(1) << 30; + ds4_engine e = {0}; + e.ds41_host_memory_baseline = 180 * gib; + e.model.size = 152 * gib; + e.vision_model.size = gib; + e.vision_ready = e.vision_map_ready = e.ds41_model_loaded = true; + e.startup_model_span_bytes = e.model.size; + recommended = 188 * gib; + /* The observed 192 GB resident failure: loaded weights, 1.5 GiB future + * graph and 25.9 GiB available. It must tolerate small host fluctuations. */ + for (unsigned mib = 24 * 1024; mib <= 26 * 1024; mib += 64) { + available = (uint64_t)mib << 20; + assert(ds41_memory_admit(&e, gib + gib / 2, false)); + } + /* Existing graph bytes are charged once on restoration/session growth. */ + e.ds41_session_bytes = 2 * gib; + available = 14 * gib; + assert(ds41_memory_admit(&e, 2 * gib, false)); + assert(!ds41_memory_admit(&e, 4 * gib, false)); + e.ds41_session_bytes = 0; + available = 13 * gib; + assert(!ds41_memory_admit(&e, gib, false)); + /* Sidecar upload is still a real outstanding allocation. */ + available = 15 * gib; + assert(ds41_memory_admit(&e, gib, false)); + e.vision_map_ready = false; + assert(!ds41_memory_admit(&e, gib, false)); + e.vision_map_ready = true; + /* The OS minimum and accelerator cap cannot be bypassed. */ + e.ds41_host_memory_baseline = 8 * gib; + assert(!ds41_memory_admit(&e, gib, false)); + e.ds41_host_memory_baseline = 180 * gib; + recommended = 150 * gib; + available = 100 * gib; + assert(!ds41_memory_admit(&e, gib, false)); + recommended = 188 * gib; + assert(!ds41_memory_admit(&e, UINT64_MAX, false)); + /* A 128 GB rank loads only its owned expert half and replicated dense + * tensors. Both total admission and remaining-allocation checks must + * charge that same footprint, while keeping the existing reserves. */ + e.ds41_host_memory_baseline = available = 120 * gib; + recommended = 124 * gib; + e.startup_model_span_bytes = 0; + g_tp_shard_model_bytes = 81 * gib; + assert(ds41_memory_admit(&e, 3 * gib, false)); + available = 93 * gib; + assert(!ds41_memory_admit(&e, 3 * gib, false)); + available = 120 * gib; + g_tp_shard_model_bytes = 0; + assert(!ds41_memory_admit(&e, 3 * gib, false)); + g_tp_shard_model_bytes = e.startup_model_span_bytes = 81 * gib; + available = 15 * gib; + assert(ds41_memory_admit(&e, 3 * gib, false)); + available = 13 * gib; + assert(!ds41_memory_admit(&e, 3 * gib, false)); + g_tp_shard_model_bytes = 0; + e.ssd_streaming = true; + assert(ds41_rocm_host_reserve_bytes(128 * gib) == 8 * gib); + assert(ds41_rocm_stream_reserve_bytes(128 * gib) == 10 * gib); + assert(ds41_rocm_stream_reserve_bytes(180 * gib) == 13 * gib + gib / 4); + assert(ds41_rocm_stream_reserve_bytes(0) == 10 * gib); + puts("V4.1 ROCm resident admission, reuse, pressure, sidecar and cap checks PASS"); + return 0; +} diff --git a/tests/test_deepseek41_rocm.c b/tests/test_deepseek41_rocm.c new file mode 100644 index 0000000000..19e9eec953 --- /dev/null +++ b/tests/test_deepseek41_rocm.c @@ -0,0 +1,942 @@ +/* ROCm V4.1 production-shape correctness harness. CPU oracles derive from + * test_deepseek41_metal.c; device access is exclusively explicit copies. + * The parent process never initializes HIP: each shape execs a fresh child, + * stops at the first failure, and synchronizes after every risky stage. */ +#define _POSIX_C_SOURCE 200809L +#include "ds4_gpu.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define CHECK(x) do { if (!(x)) { \ + fprintf(stderr, "%s:%d: %s\n", __FILE__, __LINE__, #x); return 0; \ +} } while (0) +#define RUN(x) do { fprintf(stderr, "stage: %s\n", #x); CHECK(x); CHECK(sync_guards()); } while (0) +static unsigned requested_shape; + +enum { GUARD_BYTES = 64 }; +typedef struct allocation { + ds4_gpu_tensor *storage, *view; + size_t bytes; + struct allocation *next; +} allocation; +static allocation *allocations; + +static int sync_guards(void) { + CHECK(ds4_gpu_synchronize()); + for (allocation *a = allocations; a; a = a->next) { + unsigned char before[GUARD_BYTES], after[GUARD_BYTES]; + CHECK(ds4_gpu_tensor_read(a->storage, 0, before, sizeof(before))); + CHECK(ds4_gpu_tensor_read(a->storage, GUARD_BYTES + a->bytes, after, sizeof(after))); + for (unsigned i = 0; i < GUARD_BYTES; i++) CHECK(before[i] == 0xa5 && after[i] == 0xa5); + } + return 1; +} + +static ds4_gpu_tensor *upload(const void *data, size_t bytes) { + allocation *a = calloc(1, sizeof(*a)); + if (!a || bytes > SIZE_MAX - 2 * GUARD_BYTES) { free(a); return NULL; } + a->bytes = bytes; + a->storage = ds4_gpu_tensor_alloc(bytes + 2 * GUARD_BYTES); + if (!a->storage) { free(a); return NULL; } + unsigned char guard[GUARD_BYTES]; + memset(guard, 0xa5, sizeof(guard)); + if (!ds4_gpu_tensor_write(a->storage, 0, guard, sizeof(guard)) || + !ds4_gpu_tensor_write(a->storage, GUARD_BYTES + bytes, guard, sizeof(guard))) goto fail; + a->view = ds4_gpu_tensor_view(a->storage, GUARD_BYTES, bytes); + if (!a->view || (data && !ds4_gpu_tensor_write(a->view, 0, data, bytes))) goto fail; + a->next = allocations; + allocations = a; + return a->view; +fail: + ds4_gpu_tensor_free(a->view); + ds4_gpu_tensor_free(a->storage); + free(a); + return NULL; +} + +static void guarded_free(ds4_gpu_tensor *t) { + for (allocation **link = &allocations; *link; link = &(*link)->next) { + allocation *a = *link; + if (a->view == t) { + *link = a->next; + ds4_gpu_tensor_free(a->view); + ds4_gpu_tensor_free(a->storage); + free(a); + return; + } + } + ds4_gpu_tensor_free(t); +} +#define ds4_gpu_tensor_free guarded_free + +static uint32_t seed = 7919; +static float random_value(void) { + seed ^= seed << 13; seed ^= seed >> 17; seed ^= seed << 5; + return ((int)(seed % 65537) - 32768) / 8192.0f; +} + +static float bf16(float value) { + uint32_t bits; + memcpy(&bits, &value, 4); + if ((bits & 0x7f800000u) != 0x7f800000u) + bits += 0x7fffu + ((bits >> 16) & 1u); + bits &= 0xffff0000u; + memcpy(&value, &bits, 4); + return value; +} + +static float nearest(float value, int fp4) { + const float fp4_values[] = {0, .5f, 1, 1.5f, 2, 3, 4, 6}; + float best_value = 0, best_error = INFINITY; + int best = 0; + for (int i = 0; i < (fp4 ? 8 : 127); i++) { + const float v = fp4 ? fp4_values[i] : i < 8 ? ldexpf(i, -9) : + ldexpf(1.0f + (i & 7) / 8.0f, (i >> 3) - 7); + const float error = fabsf(fabsf(value) - v); + if (error < best_error || (error == best_error && !(i & 1) && (best & 1))) { + best = i; best_value = v; best_error = error; + } + } + return copysignf(best_value, value); +} + +static int check_quantization(void) { + enum { WIDTH = 512, ROWS = 33, N = WIDTH * ROWS }; + float *source = malloc(N * sizeof(float)), *actual = malloc(N * sizeof(float)); + CHECK(source && actual); + ds4_gpu_tensor *t = upload(NULL, N * sizeof(float)); + CHECK(t); + for (int mode = 0; mode < 4; mode++) { + if ((unsigned)mode != requested_shape) continue; + const int block = mode == DS4_V41_FP4_E4M3 ? 16 : 32; + for (int i = 0; i < N; i++) source[i] = random_value() * (1u << ((i / block) % 4)); + for (int i = 0; i < WIDTH; i++) source[i] = copysignf(0.0f, i & 1 ? -1.0f : 1.0f); + CHECK(ds4_gpu_tensor_write(t, 0, source, N * sizeof(float))); + RUN(ds4_gpu_dsv41_quantize(t, WIDTH, ROWS, (ds4_v41_activation_format)mode)); + CHECK(ds4_gpu_tensor_read(t, 0, actual, N * sizeof(float))); + for (int start = 0; start < N; start += block) { + float amax = 0, scale = 1; + for (int i = 0; i < block; i++) amax = fmaxf(amax, fabsf(bf16(source[start + i]))); + if (mode == DS4_V41_FP8_E8M0) + scale = exp2f(ceilf(log2f(fmaxf(amax, 1.0e-4f) * (1.0f / 448.0f)))); + if (mode == DS4_V41_FP4_E8M0) + scale = exp2f(ceilf(log2f(fmaxf(amax, 0x1.8p-124f) * (1.0f / 6.0f)))); + if (mode == DS4_V41_FP4_E4M3) scale = nearest(fmaxf(amax, 6.0f / 512.0f) / 6.0f, 0); + for (int i = 0; i < block; i++) { + float expected = bf16(source[start + i]); + if (mode) expected = bf16(nearest(expected / scale, mode != 1) * scale); + if (memcmp(&expected, actual + start + i, 4)) { + fprintf(stderr, "quantization mode=%d index=%d: %.9g != %.9g\n", + mode, start + i, actual[start + i], expected); + return 0; + } + } + } + } + RUN(ds4_gpu_dsv41_quantize(t, 24, 1, DS4_V41_BF16)); + CHECK(!ds4_gpu_dsv41_quantize(t, 24, 1, DS4_V41_FP8_E8M0)); + CHECK(!ds4_gpu_dsv41_quantize(t, UINT32_MAX, UINT32_MAX, DS4_V41_BF16)); + CHECK(!ds4_gpu_dsv41_quantize(t, 32, 1, (ds4_v41_activation_format)4)); + ds4_gpu_tensor_free(t); + free(source); free(actual); + fprintf(stderr, "V4.1 BF16/FP8/FP4 round trips: exact\n"); + return 1; +} + +static int check_engram(void) { + enum { D = 5120, ROWS = 5, N = ROWS * 4 * D }; + float *x = malloc(N * sizeof(float)), *actual = malloc(N * sizeof(float)); + float *kv = malloc(ROWS * 5 * D * sizeof(float)); + float *qw = malloc(4 * D * sizeof(float)), *kw = malloc(4 * D * sizeof(float)); + uint8_t mask[] = {1, 1, 0, 1, 0}; + CHECK(x && actual && kv && qw && kw); + for (int i = 0; i < N; i++) x[i] = bf16(random_value()); + for (int i = 0; i < ROWS * 5 * D; i++) kv[i] = bf16(random_value()); + for (int i = 0; i < 4 * D; i++) { qw[i] = random_value(); kw[i] = random_value(); } + memset(x, 0, D * sizeof(float)); + memset(kv + D, 0, D * sizeof(float)); + ds4_gpu_tensor *xt = upload(x, N * sizeof(float)); + ds4_gpu_tensor *kt = upload(kv, ROWS * 5 * D * sizeof(float)); + ds4_gpu_tensor *qwt = upload(qw, 4 * D * sizeof(float)); + ds4_gpu_tensor *kwt = upload(kw, 4 * D * sizeof(float)); + ds4_gpu_tensor *mt = upload(mask, sizeof(mask)); + CHECK(xt && kt && qwt && kwt && mt); + size_t rounded_differently = 0; + double error2 = 0, norm2 = 0; + for (int masked = 0; masked < 2; masked++) { + CHECK(ds4_gpu_tensor_write(xt, 0, x, N * sizeof(float))); + RUN(ds4_gpu_dsv41_engram_add(xt, kt, qwt, kwt, masked ? mt : NULL, D, ROWS, 1e-20f)); + CHECK(ds4_gpu_tensor_read(xt, 0, actual, N * sizeof(float))); + for (int row = 0; row < ROWS; row++) for (int h = 0; h < 4; h++) { + double dot = 0, h2 = 0, k2 = 0; + for (int i = 0; i < D; i++) { + const double a = x[(row * 4 + h) * D + i], b = kv[(row * 5 + h) * D + i]; + h2 += a * a; k2 += b * b; + dot += a * (float)(qw[h * D + i] * kw[h * D + i]) * b; + } + dot /= sqrt(h2 / D + 1e-20) * sqrt(k2 / D + 1e-20) * sqrt(D); + double gate = 1 / (1 + exp(-copysign(sqrt(fmax(fabs(dot), 1e-6)), dot))); + if (masked && !mask[row]) gate = 0; + for (int i = 0; i < D; i++) { + const int off = (row * 4 + h) * D + i; + const float expected = bf16(x[off] + (float)gate * kv[(row * 5 + 4) * D + i]); + const double error = actual[off] - expected; + CHECK(isfinite(actual[off])); + CHECK(fabs(error) <= fmax(1e-6, fabs(expected) / 128)); + if (masked && !mask[row]) CHECK(actual[off] == x[off]); + rounded_differently += actual[off] != expected; + error2 += error * error; norm2 += (double)expected * expected; + } + } + } + CHECK(rounded_differently < N / 1000); + CHECK(sqrt(error2 / norm2) < 1e-4); + fprintf(stderr, "V4.1 Engram gate: %zu BF16 boundary differences, relative RMS %.8g\n", + rounded_differently, sqrt(error2 / norm2)); + CHECK(!ds4_gpu_dsv41_engram_add(xt, kt, qwt, kwt, mt, D, ROWS + 1, 1e-20f)); + ds4_gpu_tensor_free(xt); ds4_gpu_tensor_free(kt); ds4_gpu_tensor_free(qwt); + ds4_gpu_tensor_free(kwt); ds4_gpu_tensor_free(mt); + free(x); free(actual); free(kv); free(qw); free(kw); + return 1; +} + +static int check_pool(void) { + enum { D = 512, ROWS = 257, PAIRS = ROWS / 2 }; + float *kv = malloc(ROWS * D * sizeof(float)), *scores = malloc(ROWS * D * sizeof(float)); + float *got = malloc(PAIRS * D * sizeof(float)), *reference = malloc(PAIRS * D * sizeof(float)); + CHECK(kv && scores && got && reference); + for (int i = 0; i < ROWS * D; i++) { kv[i] = random_value(); scores[i] = random_value() * 25; } + ds4_gpu_tensor *kt = upload(kv, ROWS * D * sizeof(float)); + ds4_gpu_tensor *st = upload(scores, ROWS * D * sizeof(float)); + ds4_gpu_tensor *pk = upload(NULL, D * sizeof(float)), *ps = upload(NULL, D * sizeof(float)); + ds4_gpu_tensor *out = upload(NULL, PAIRS * D * sizeof(float)); + CHECK(kt && st && pk && ps && out); + const uint32_t chunks[] = {257, 1, 2, 3, 17, 127, 128, 129}; + for (size_t c = 0; c < sizeof(chunks) / sizeof(*chunks); c++) { + if (c != requested_shape) continue; + CHECK(ds4_gpu_tensor_fill_f32(pk, NAN, D)); + CHECK(ds4_gpu_tensor_fill_f32(ps, NAN, D)); + CHECK(ds4_gpu_begin_commands()); + for (uint32_t start = 0; start < ROWS;) { + uint32_t n = chunks[c] < ROWS - start ? chunks[c] : ROWS - start; + uint32_t pairs = (n + (start & 1u)) / 2; + ds4_gpu_tensor *k = ds4_gpu_tensor_view(kt, (uint64_t)start * D * 4, (uint64_t)n * D * 4); + ds4_gpu_tensor *s = ds4_gpu_tensor_view(st, (uint64_t)start * D * 4, (uint64_t)n * D * 4); + ds4_gpu_tensor *o = pairs ? ds4_gpu_tensor_view(out, (uint64_t)(start / 2) * D * 4, + (uint64_t)pairs * D * 4) : NULL; + CHECK(k && s && (!pairs || o)); + RUN(ds4_gpu_dsv41_pool2(o, k, s, pk, ps, D, n, start)); + ds4_gpu_tensor_free(k); ds4_gpu_tensor_free(s); ds4_gpu_tensor_free(o); + start += n; + } + CHECK(ds4_gpu_end_commands()); + CHECK(ds4_gpu_tensor_read(out, 0, got, PAIRS * D * sizeof(float))); + { + for (int p = 0; p < PAIRS; p++) for (int i = 0; i < D; i++) { + const int a = 2 * p * D + i, b = a + D; + const double gate = 1 / (1 + exp((double)scores[b] - scores[a])); + const float expected = bf16((float)(kv[a] * gate + kv[b] * (1 - gate))); + CHECK(isfinite(got[p * D + i])); + CHECK(fabsf(got[p * D + i] - expected) <= fmaxf(1e-6f, fabsf(expected) / 128)); + } + } + float tail[D]; + CHECK(ds4_gpu_tensor_read(pk, 0, tail, sizeof(tail))); + CHECK(!memcmp(tail, kv + (ROWS - 1) * D, sizeof(tail))); + CHECK(ds4_gpu_tensor_read(ps, 0, tail, sizeof(tail))); + CHECK(!memcmp(tail, scores + (ROWS - 1) * D, sizeof(tail))); + } + ds4_gpu_tensor_free(kt); ds4_gpu_tensor_free(st); ds4_gpu_tensor_free(pk); + ds4_gpu_tensor_free(ps); ds4_gpu_tensor_free(out); + free(kv); free(scores); free(got); free(reference); + fprintf(stderr, "V4.1 pair pooling: independent full-output and odd-tail oracle\n"); + return 1; +} + +typedef struct { float score; uint32_t index; } candidate; +static int candidate_desc(const void *a, const void *b) { + const candidate *x = a, *y = b; + return x->score > y->score ? -1 : x->score < y->score ? 1 : + x->index < y->index ? -1 : x->index > y->index; +} + +static int check_candidates(void) { + const uint32_t widths[] = {1, 7, 8, 9, 127, 16385, 17017}; + for (size_t wi = 0; wi < sizeof(widths) / sizeof(*widths); wi++) { + if (wi != requested_shape) continue; + const uint32_t n = widths[wi], blocks = (n + 7) / 8, rows = 17; + const uint32_t top = blocks < 2048 ? blocks : 2048; + float *scores = malloc((size_t)n * rows * 4), *got = malloc((size_t)n * rows * 4); + float *maxima = malloc((size_t)blocks * rows * 4); + candidate *sorted = malloc(blocks * sizeof(candidate)); + uint8_t *kept = malloc(blocks); + CHECK(scores && got && maxima && sorted && kept); + /* Unique finite values avoid unspecified top-k tie ordering. */ + for (uint32_t r = 0; r < rows; r++) for (uint32_t i = 0; i < n; i++) + scores[(size_t)r * n + i] = (float)((i * 7919u + r * 1009u) % 104729u) - 50000; + ds4_gpu_tensor *s = upload(NULL, (size_t)n * rows * 4); + ds4_gpu_tensor *b = upload(NULL, (size_t)blocks * rows * 4); + ds4_gpu_tensor *t = upload(NULL, (size_t)top * rows * 4); + ds4_gpu_tensor *m = upload(NULL, (size_t)blocks * rows * 4); + CHECK(s && b && t && m); + for (uint32_t ratio = 1; ratio <= 2; ratio++) for (int late = 0; late < 2; late++) { + const uint32_t start = late ? n * ratio - 1 : 0; + CHECK(ds4_gpu_tensor_write(s, 0, scores, (size_t)n * rows * 4)); + CHECK(ds4_gpu_begin_commands()); + RUN(ds4_gpu_dsv41_candidate_blocks(b, s, n, rows, start, ratio)); + RUN(ds4_gpu_indexer_topk_tensor(t, b, blocks, rows, top)); + RUN(ds4_gpu_dsv4_topk_mask_tensor(m, t, blocks, rows, top)); + RUN(ds4_gpu_dsv41_candidate_filter(s, m, n, rows, start, ratio)); + CHECK(ds4_gpu_end_commands()); + CHECK(ds4_gpu_tensor_read(b, 0, maxima, (size_t)blocks * rows * 4)); + CHECK(ds4_gpu_tensor_read(s, 0, got, (size_t)n * rows * 4)); + for (uint32_t r = 0; r < rows; r++) { + uint32_t visible = (start + r + 1) / ratio; + if (visible > n) visible = n; + for (uint32_t j = 0; j < blocks; j++) { + float best = -INFINITY; + for (uint32_t i = j * 8; i < (j + 1) * 8 && i < visible; i++) + best = fmaxf(best, scores[(size_t)r * n + i]); + if (visible && j == (visible - 1) / 8) best = INFINITY; + CHECK(maxima[(size_t)r * blocks + j] == best); + sorted[j] = (candidate){best, j}; + } + qsort(sorted, blocks, sizeof(*sorted), candidate_desc); + memset(kept, 0, blocks); + for (uint32_t j = 0; j < top; j++) + if (sorted[j].score > -INFINITY) kept[sorted[j].index] = 1; + for (uint32_t i = 0; i < n; i++) { + float expected = i < visible && kept[i / 8] ? scores[(size_t)r * n + i] : -INFINITY; + CHECK(got[(size_t)r * n + i] == expected); + } + } + } + CHECK(!ds4_gpu_dsv41_candidate_blocks(b, s, n, rows, UINT32_MAX, 1)); + CHECK(!ds4_gpu_dsv41_candidate_blocks(b, s, n, rows, 0, 0)); + CHECK(!ds4_gpu_dsv41_candidate_filter(s, m, n, rows + 1, 0, 1)); + ds4_gpu_tensor_free(s); ds4_gpu_tensor_free(b); ds4_gpu_tensor_free(t); ds4_gpu_tensor_free(m); + free(scores); free(got); free(maxima); free(sorted); free(kept); + } + fprintf(stderr, "V4.1 causal candidate blocks and filtering: exact\n"); + return 1; +} + + + +static int check_bf16_boundaries(void) { + static const uint32_t widths[] = {1, 3, 24, 31, 32, 33, 129, 1023, 1024, 1025, 5120, 20480, 65536u * 5u}; + const uint32_t width = widths[requested_shape], rows = width > 20480 ? 1 : 3; + const size_t count = (size_t)width * rows; + uint32_t *input = malloc(count * 4), *actual = malloc(count * 4); + CHECK(input && actual); + const uint32_t low[] = {0, 0x7fff, 0x8000, 0x8001, 0xffff}; + for (size_t i = 0; i < count; i++) input[i] = ((uint32_t)(i / 5u) << 16) | low[i % 5u]; + ds4_gpu_tensor *t = upload(input, count * 4); + CHECK(t); + RUN(ds4_gpu_dsv41_quantize(t, width, rows, DS4_V41_BF16)); + CHECK(ds4_gpu_tensor_read(t, 0, actual, count * 4)); + for (size_t i = 0; i < count; i++) { + uint32_t bits = input[i]; + if ((bits & 0x7f800000u) != 0x7f800000u) bits += 0x7fffu + ((bits >> 16u) & 1u); + CHECK(actual[i] == (bits & 0xffff0000u)); + } + ds4_gpu_tensor_free(t); free(actual); free(input); + return 1; +} + +static int check_rope(void) { + enum { WIDTH = 512, HEADS = 2, ROWS = 129, COUNT = WIDTH * HEADS * ROWS }; + const uint32_t starts[] = {0, 126, 32766, 1048318}; + const uint32_t start = starts[requested_shape % 4u]; + const bool compressed = (requested_shape / 4u) & 1u; + const bool inverse = requested_shape / 8u; + float *input = malloc(COUNT * 4), *got = malloc(COUNT * 4); + CHECK(input && got); + for (size_t i = 0; i < COUNT; i++) input[i] = bf16(random_value()); + ds4_gpu_tensor *t = upload(input, COUNT * 4); + CHECK(t); + RUN(ds4_gpu_dsv41_rope_stride(t, WIDTH, HEADS, ROWS, start, 2, compressed, inverse)); + CHECK(ds4_gpu_tensor_read(t, 0, got, COUNT * 4)); + const double pi = acos(-1.0); + const float base = compressed ? 160000.0f : 10000.0f; + const float low = floor(64.0 * log(65536.0 / (32.0 * 2.0 * pi)) / (2.0 * log(base))); + const float high = ceil(64.0 * log(65536.0 / (2.0 * pi)) / (2.0 * log(base))); + size_t rounded = 0; + double error2 = 0, norm2 = 0; + for (uint32_t row = 0; row < ROWS; row++) for (uint32_t head = 0; head < HEADS; head++) { + const size_t off = ((size_t)row * HEADS + head) * WIDTH; + CHECK(!memcmp(got + off, input + off, (WIDTH - 64) * 4)); + for (uint32_t i = 0; i < 32; i++) { + /* Match the released reference's F32 frequency and phase arithmetic; + * evaluate trig independently on the CPU, then round outputs to BF16. */ + float frequency = 1.0f / powf(base, (float)i / 32.0f); + if (compressed) { + const float ramp = fminf(1, fmaxf(0, (i - low) / (high - low))); + const float smooth = 1.0f - ramp; + frequency = (frequency / 16.0f) * (1.0f - smooth) + frequency * smooth; + } + const float phase = (float)(start + row * 2u) * frequency; + const float c = cosf(phase), si = (inverse ? -1.0f : 1.0f) * sinf(phase); + const size_t at = off + WIDTH - 64 + i * 2; + const float expected[] = {bf16(input[at] * c - input[at + 1] * si), + bf16(input[at] * si + input[at + 1] * c)}; + for (unsigned j = 0; j < 2; j++) { + const double error = (double)got[at + j] - expected[j]; + CHECK(isfinite(got[at + j])); + CHECK(fabs(error) <= fmax(1e-6, fabs(expected[j]) / 128)); + rounded += got[at + j] != expected[j]; + error2 += error * error; norm2 += (double)expected[j] * expected[j]; + } + } + } + CHECK(sqrt(error2 / fmax(norm2, 1e-30)) < 1e-4); + fprintf(stderr, "RoPE start=%u compressed=%u inverse=%u BF16 differences=%zu relative_RMS=%.9g\n", + start, compressed, inverse, rounded, sqrt(error2 / fmax(norm2, 1e-30))); + CHECK(!ds4_gpu_dsv41_rope_stride(t, WIDTH, HEADS, ROWS, 0, 0, compressed, inverse)); + CHECK(!ds4_gpu_dsv41_rope_stride(t, WIDTH, HEADS, ROWS, 1048320, 2, compressed, inverse)); + CHECK(!ds4_gpu_dsv41_rope_stride(t, WIDTH, HEADS, ROWS + 1, 0, 2, compressed, inverse)); + ds4_gpu_tensor_free(t); free(got); free(input); + return 1; +} + +static int check_sparse_gather(void) { + const uint32_t sizes[] = {1, 3, 511, 512, 513, 8193, 17017}; + const uint32_t rows = sizes[requested_shape], selected = rows < 512 ? rows : 512; + const size_t count = (size_t)rows * 512, outputs = (size_t)selected * 512; + float *input = malloc(count * 4), *result = malloc(outputs * 4); + int32_t *indices = malloc(selected * 4); + CHECK(input && result && indices); + for (uint32_t r = 0; r < rows; r++) for (uint32_t c = 0; c < 512; c++) + input[(size_t)r * 512 + c] = (float)r + (float)c / 512.0f; + for (uint32_t r = 0; r < selected; r++) indices[r] = (int32_t)(rows - 1u - r); + ds4_gpu_tensor *source = upload(input, count * 4), *ids = upload(indices, selected * 4); + ds4_gpu_tensor *out = upload(NULL, outputs * 4); + CHECK(source && ids && out); + RUN(ds4_gpu_dsv41_gather_kv(out, source, ids, rows, selected)); + CHECK(ds4_gpu_tensor_read(out, 0, result, outputs * 4)); + for (uint32_t r = 0; r < selected; r++) + CHECK(!memcmp(result + (size_t)r * 512, input + (size_t)indices[r] * 512, 512 * 4)); + CHECK(!ds4_gpu_dsv41_gather_kv(out, source, ids, rows + 1, selected)); + CHECK(!ds4_gpu_dsv41_gather_kv(out, source, ids, rows, selected + 1)); + ds4_gpu_tensor_free(source); ds4_gpu_tensor_free(ids); ds4_gpu_tensor_free(out); + free(input); free(result); free(indices); + return 1; +} + +static int check_compact_carry(void) { + const uint32_t widths[] = {1, 31, 32, 33, 127, 128, 129, 20480}; + const uint32_t format = requested_shape / 8, width = widths[requested_shape % 8]; + const uint32_t rows = 129, offset = 2; + const uint32_t words = format == DS4_V41_CARRY_BF16 ? (width + 1) / 2 : (width + 31) / 32; + const size_t count = (size_t)width * rows, bytes = (size_t)(rows + 4) * words * 4; + uint32_t *expected = malloc(count * 4), *actual = malloc(count * 4); + unsigned char *storage = malloc(bytes), *packed_ref = malloc(bytes); + CHECK(expected && actual && storage && packed_ref); + memset(storage, 0xa5, bytes); memset(packed_ref, 0xa5, bytes); + for (size_t i = 0; i < count; i++) { + const uint32_t b = (uint32_t)(i * 40503u + 32768u) & 0xffffu; + expected[i] = format == DS4_V41_CARRY_BF16 ? b << 16 : (b & 1u ? 0xff800000u : 0u); + } + for (uint32_t r = 0; r < rows; r++) { + if (format == DS4_V41_CARRY_BF16) { + for (uint32_t c = 0; c < width; c++) { + const uint16_t b = expected[(size_t)r * width + c] >> 16; + memcpy(packed_ref + (size_t)(r + offset) * words * 4 + c * 2, &b, 2); + } + } else { + for (uint32_t w = 0; w < words; w++) { + uint32_t bits = 0; + for (uint32_t bit = 0; bit < 32 && w * 32 + bit < width; bit++) + if (!expected[(size_t)r * width + w * 32 + bit]) bits |= 1u << bit; + memcpy(packed_ref + ((size_t)(r + offset) * words + w) * 4, &bits, 4); + } + } + } + ds4_gpu_tensor *plain = upload(expected, count * 4), *packed = upload(storage, bytes); + CHECK(plain && packed); + RUN(ds4_gpu_dsv41_carry_copy(packed, offset, plain, width, rows, format, true)); + CHECK(ds4_gpu_tensor_read(plain, 0, actual, count * 4)); + CHECK(!memcmp(actual, expected, count * 4)); + CHECK(ds4_gpu_tensor_read(packed, 0, storage, bytes)); + CHECK(!memcmp(storage, packed_ref, bytes)); + memset(actual, 0, count * 4); + CHECK(ds4_gpu_tensor_write(plain, 0, actual, count * 4)); + RUN(ds4_gpu_dsv41_carry_copy(packed, offset, plain, width, rows, format, false)); + CHECK(ds4_gpu_tensor_read(plain, 0, actual, count * 4)); + CHECK(!memcmp(actual, expected, count * 4)); + CHECK(!ds4_gpu_dsv41_carry_copy(packed, UINT32_MAX, plain, width, rows, format, true)); + CHECK(!ds4_gpu_dsv41_carry_copy(packed, offset + 3, plain, width, rows, format, true)); + CHECK(!ds4_gpu_dsv41_carry_copy(packed, offset, plain, 0, rows, format, true)); + CHECK(!ds4_gpu_dsv41_carry_copy(packed, offset, plain, width, rows, DS4_V41_CARRY_F32, true)); + ds4_gpu_tensor_free(plain); ds4_gpu_tensor_free(packed); + free(expected); free(actual); free(storage); free(packed_ref); + return 1; +} + +static int check_causal_topk(void) { + const uint32_t frontiers[] = {1024, 1025, 2047, 2048, 16383, 32767, 65535}; + const uint32_t frontier = frontiers[requested_shape % 7u], ratio = requested_shape / 7u + 1u; + const uint32_t rows = 33, start = frontier * ratio - 1, width = (start + rows) / ratio + 129; + const size_t count = (size_t)width * rows; + float *s = malloc(count * 4); + int32_t *ids = malloc((size_t)rows * 512 * 4); + candidate *sorted = malloc(width * sizeof(*sorted)); + CHECK(s && ids && sorted); + ds4_gpu_tensor *scores = upload(NULL, count * 4), *selected = upload(NULL, (size_t)rows * 512 * 4); + CHECK(scores && selected); + for (uint32_t pattern = 0; pattern < 3; pattern++) { + for (uint32_t row = 0; row < rows; row++) { + const uint32_t visible = (start + row + 1u) / ratio; + for (uint32_t j = 0; j < width; j++) { + const float v = pattern == 0 ? random_value() : pattern == 1 ? (float)(j % 7) : -INFINITY; + s[(size_t)row * width + j] = j < visible ? v : 12345; + } + } + CHECK(ds4_gpu_tensor_write(scores, 0, s, count * 4)); + RUN(ds4_gpu_dsv41_indexer_topk_batch(selected, scores, width, rows, start, ratio)); + CHECK(ds4_gpu_tensor_read(selected, 0, ids, (size_t)rows * 512 * 4)); + for (uint32_t row = 0; row < rows; row++) { + const uint32_t visible = (start + row + 1u) / ratio; + for (uint32_t j = 0; j < visible; j++) sorted[j] = (candidate){s[(size_t)row * width + j], j}; + qsort(sorted, visible, sizeof(*sorted), candidate_desc); + for (uint32_t j = 0; j < 512; j++) CHECK(ids[(size_t)row * 512 + j] == (int32_t)sorted[j].index); + } + } + CHECK(!ds4_gpu_dsv41_indexer_topk_batch(selected, scores, width, rows + 1, start, ratio)); + CHECK(!ds4_gpu_dsv41_indexer_topk_batch(selected, scores, width, 1, UINT32_MAX, ratio)); + CHECK(!ds4_gpu_dsv41_indexer_topk_batch(selected, scores, 1, 2, 0, 1)); + CHECK(!ds4_gpu_dsv41_indexer_topk_batch(selected, scores, width, 1, start, 0)); + ds4_gpu_tensor_free(scores); ds4_gpu_tensor_free(selected); + free(s); free(ids); free(sorted); + return 1; +} + +static int check_indexer_scores(void) { + enum { HEADS = 32, DIM = 128 }; + const struct { uint32_t keys, rows; } shapes[] = {{129,1}, {1025,7}, {1025,8}, {1025,9}, {1025,31}, {1025,32}, {1025,33}, {16385,3}}; + const uint32_t keys = shapes[requested_shape % 8u].keys, rows = shapes[requested_shape % 8u].rows; + const uint32_t ratio = requested_shape / 8u + 1u; + const size_t nq = (size_t)rows * HEADS * DIM, nk = (size_t)keys * DIM, nw = (size_t)rows * HEADS; + float *q = malloc(nq * 4), *k = malloc(nk * 4), *w = malloc(nw * 4), *s = malloc((size_t)rows * keys * 4); + CHECK(q && k && w && s); + for (size_t i = 0; i < nq; i++) q[i] = bf16(random_value()); + for (size_t i = 0; i < nk; i++) k[i] = bf16(random_value()); + for (size_t i = 0; i < nw; i++) w[i] = bf16(random_value()); + /* Construct legal FP4 values using the independent codebook oracle. */ + for (size_t i = 0; i < nq; i++) q[i] = nearest(q[i], 1) * ldexpf(1, (int)((i / 32) % 17) - 8); + for (size_t i = 0; i < nk; i++) k[i] = nearest(k[i], 1) * ldexpf(1, (int)((i / 32) % 17) - 8); + /* Non-BF16 values detect any extra cast in the F32 fallback. */ + q[0] = 1.0001f; k[67u * DIM] = 1.0003f; + ds4_gpu_tensor *qt = upload(q, nq * 4), *kt = upload(k, nk * 4), *wt = upload(w, nw * 4); + ds4_gpu_tensor *st = upload(NULL, (size_t)rows * keys * 4); + CHECK(qt && kt && wt && st); + for (unsigned early = 0; early < (ratio == 2 ? 3u : 2u); early++) { + /* The last unpaired CSA2 token still reuses all existing pooled keys. */ + const uint32_t start = early == 2 ? (keys + 1u) * ratio - rows - 1u : + early ? 0 : keys * ratio - rows; + RUN(ds4_gpu_dsv41_indexer_scores_batch(st, qt, wt, kt, keys, rows, start, ratio)); + CHECK(ds4_gpu_tensor_read(st, 0, s, (size_t)rows * keys * 4)); + double worst = 0; + for (uint32_t row = 0; row < rows; row++) { + const uint32_t visible = (start + row + 1u) / ratio; + for (uint32_t j = 0; j < keys; j++) { + const float actual = s[(size_t)row * keys + j]; + if (j >= visible) { CHECK(actual == -INFINITY); continue; } + double expected = 0, magnitude = 0; + for (uint32_t h = 0; h < HEADS; h++) { + double dot = 0; + for (uint32_t d = 0; d < DIM; d++) dot += (double)q[((size_t)row * HEADS + h) * DIM + d] * k[(size_t)j * DIM + d]; + const double term = fmax(dot / 64.0, 0) * w[row * HEADS + h]; + expected += term; magnitude += fabs(term); + } + const double error = fabs(actual - expected) / fmax(magnitude, 1); + CHECK(isfinite(actual) && error < 1e-5); + worst = fmax(worst, error); + } + } + fprintf(stderr, "Indexer keys=%u rows=%u ratio=%u start=%u worst_relative=%.9g\n", keys, rows, ratio, start, worst); + } + CHECK(!ds4_gpu_dsv41_indexer_scores_batch(st, qt, wt, kt, keys, rows, 0, 0)); + CHECK(!ds4_gpu_dsv41_indexer_scores_batch(st, qt, wt, kt, keys, rows, 0, 4)); + CHECK(!ds4_gpu_dsv41_indexer_scores_batch(st, qt, wt, kt, keys, rows, UINT32_MAX, ratio)); + CHECK(!ds4_gpu_dsv41_indexer_scores_batch(st, qt, wt, kt, keys, rows + 1, 0, ratio)); + CHECK(!ds4_gpu_dsv41_indexer_scores_batch(st, qt, wt, kt, keys, rows, + (keys + 1u) * ratio - rows, ratio)); + ds4_gpu_tensor_free(qt); ds4_gpu_tensor_free(kt); ds4_gpu_tensor_free(wt); ds4_gpu_tensor_free(st); + free(q); free(k); free(w); free(s); + return 1; +} + + +static float fixture_half(uint16_t bits) { + return (1.0f + (bits & 1023) / 1024.0f) / 128.0f * (bits & 0x8000 ? -1.0f : 1.0f); +} + +static int check_projection(void) { + const struct { uint32_t width, out, rows; } shapes[] = { + {1280,4096,1}, {1280,4096,33}, {5120,32,31}, {5120,512,9}, {512,128,513}, {20480,24,33} + }; + const uint32_t width = shapes[requested_shape].width, output = shapes[requested_shape].out, rows = shapes[requested_shape].rows; + const size_t weight_bytes = (size_t)width * output * 2, nx = (size_t)width * rows, ny = (size_t)output * rows; + void *model = NULL; + CHECK(!posix_memalign(&model, (size_t)sysconf(_SC_PAGESIZE), weight_bytes)); + uint16_t *weights = model; + float *input = malloc(nx * 4), *actual = malloc(ny * 4), *scalar = malloc(ny * 4); + CHECK(input && actual && scalar); + for (size_t i = 0; i < weight_bytes / 2; i++) { + const int value = (int)(random_value() * 8192); + weights[i] = (uint16_t)(0x2000u | ((unsigned)abs(value) % 1024u) | (value < 0 ? 0x8000u : 0)); + } + for (size_t i = 0; i < nx; i++) input[i] = i % 511 ? bf16(random_value()) : 1.0001f; + CHECK(ds4_gpu_set_model_map(model, weight_bytes)); + ds4_gpu_tensor *xt = upload(input, nx * 4), *out = upload(NULL, ny * 4), *ref = upload(NULL, ny * 4); + CHECK(xt && out && ref); + RUN(ds4_gpu_dsv41_projection_rows(out, model, weight_bytes, 0, width, output, rows, xt)); + CHECK(ds4_gpu_tensor_read(out, 0, actual, ny * 4)); + for (uint32_t row = 0; row < rows; row++) { + ds4_gpu_tensor *xr = ds4_gpu_tensor_view(xt, (size_t)row * width * 4, width * 4); + ds4_gpu_tensor *yr = ds4_gpu_tensor_view(ref, (size_t)row * output * 4, output * 4); + CHECK(xr && yr); + RUN(ds4_gpu_matmul_f16_tensor(yr, model, weight_bytes, 0, width, output, xr, 1)); + ds4_gpu_tensor_free(xr); ds4_gpu_tensor_free(yr); + } + CHECK(ds4_gpu_tensor_read(ref, 0, scalar, ny * 4)); + CHECK(!memcmp(actual, scalar, ny * 4)); + double worst = 0; + for (uint32_t row = 0; row < rows; row++) for (uint32_t o = 0; o < output; o++) { + double sum = 0, magnitude = 0; + for (uint32_t k = 0; k < width; k++) { + const double term = (double)fixture_half(weights[(size_t)o * width + k]) * input[(size_t)row * width + k]; + sum += term; magnitude += fabs(term); + } + const float got = actual[(size_t)row * output + o]; + const double error = fabs(got - sum) / fmax(magnitude, 1); + CHECK(isfinite(got) && error < 1e-6); + worst = fmax(worst, error); + } + fprintf(stderr, "F16 projection width=%u out=%u rows=%u scalar exact, double worst=%.9g\n", width, output, rows, worst); + CHECK(!ds4_gpu_dsv41_projection_rows(out, model, weight_bytes - 1, 0, width, output, rows, xt)); + CHECK(!ds4_gpu_dsv41_projection_rows(out, model, weight_bytes, 0, width, output, rows + 1, xt)); + ds4_gpu_tensor_free(xt); ds4_gpu_tensor_free(out); ds4_gpu_tensor_free(ref); + ds4_gpu_cleanup(); free(model); free(input); free(actual); free(scalar); + return ds4_gpu_init(); +} + +static int check_hc_scaled(void) { + enum { WIDTH = 20480, OUT = 24 }; + const uint32_t counts[] = {1,9,33,513}, rows = counts[requested_shape]; + const size_t weight_bytes = (size_t)WIDTH * OUT * 2, nx = (size_t)WIDTH * rows, ny = (size_t)OUT * rows; + void *model = NULL; + CHECK(!posix_memalign(&model, (size_t)sysconf(_SC_PAGESIZE), weight_bytes)); + uint16_t *weights = model; + float *input = malloc(nx * 4), *actual = malloc(ny * 4); + CHECK(input && actual); + for (size_t i = 0; i < weight_bytes / 2; i++) weights[i] = (uint16_t)(0x2000 | ((i * 7919u) & 1023u) | (i & 1 ? 0x8000 : 0)); + for (size_t i = 0; i < nx; i++) input[i] = bf16(random_value() * ((i / WIDTH) % 7 ? 1 : 0x1p-16f)); + CHECK(ds4_gpu_set_model_map(model, weight_bytes)); + ds4_gpu_tensor *xt = upload(input, nx * 4), *scratch = upload(NULL, nx * 4), *out = upload(NULL, ny * 4); + CHECK(xt && scratch && out); + RUN(ds4_gpu_hc_rms_scale_project_f16_tensor(out, scratch, model, weight_bytes, 0, WIDTH, OUT, xt, rows, 1e-6f)); + CHECK(ds4_gpu_tensor_read(out, 0, actual, ny * 4)); + double worst = 0; + for (uint32_t row = 0; row < rows; row++) { + double sumsq = 0; + for (uint32_t k = 0; k < WIDTH; k++) { + const double x = input[(size_t)row * WIDTH + k]; + sumsq += x * x; + } + const double scale = 1.0 / sqrt(sumsq / WIDTH + 1e-6); + for (uint32_t o = 0; o < OUT; o++) { + double sum = 0, magnitude = 0; + for (uint32_t k = 0; k < WIDTH; k++) { + const double term = input[(size_t)row * WIDTH + k] * scale * fixture_half(weights[(size_t)o * WIDTH + k]); + sum += term; magnitude += fabs(term); + } + const float got = actual[(size_t)row * OUT + o]; + const double error = fabs(got - sum) / fmax(magnitude, 1); + CHECK(isfinite(got) && error < 1e-6); + worst = fmax(worst, error); + } + } + fprintf(stderr, "HC RMS projection rows=%u full double oracle worst=%.9g\n", rows, worst); + ds4_gpu_tensor_free(xt); ds4_gpu_tensor_free(scratch); ds4_gpu_tensor_free(out); + ds4_gpu_cleanup(); free(model); free(input); free(actual); + return ds4_gpu_init(); +} + +typedef struct { uint16_t scale; int8_t q[32]; } fixture_q8; +static uint32_t low_column(uint32_t group, uint32_t output, uint32_t term) { + return (output * 131u + group * 17u + term * 503u) % 4096u; +} +static int low_coefficient(uint32_t group, uint32_t output, uint32_t term) { + return (int)((output + group + term) % 7u) - 3; +} +static uint32_t output_column(uint32_t output, uint32_t term) { + return (output * 97u + term * 947u) % 8192u; +} +static int output_coefficient(uint32_t output, uint32_t term) { + return (int)((output + term * 3u) % 7u) - 3; +} + +/* Independent operand rounding and the standard forward-error bound already + * used to qualify the F16-operand/F32-accumulator bulk projection. */ +static float reference_f16(float value) { + const double magnitude = fabs((double)value); + if (!magnitude) return value; + int exponent; + (void)frexp(magnitude, &exponent); + const double step = ldexp(1.0, exponent < -13 ? -24 : exponent - 11); + return (float)copysign(nearbyint(magnitude / step) * step, (double)value); +} + +static double projection_roundoff_bound(double magnitude, unsigned width) { + const double f32 = (2.0 * width + 1.0) * 0x1p-24; + const double f64 = (width + 1.0) * 0x1p-53; + return (f32 / (1.0 - f32) + f64 / (1.0 - f64)) * magnitude; +} + +static int check_attention_output(void) { + enum { GROUP = 4096, RANK = 1024, GROUPS = 8, OUT = 5120, TERMS = 8 }; + const uint32_t counts[] = {1,31,32,33,65,513}, rows = counts[requested_shape]; + const size_t a_blocks = (size_t)GROUPS * RANK * GROUP / 32; + const size_t b_blocks = (size_t)OUT * GROUPS * RANK / 32; + const size_t a_bytes = a_blocks * sizeof(fixture_q8), bytes = (a_blocks + b_blocks) * sizeof(fixture_q8); + const size_t nx = (size_t)rows * GROUPS * GROUP, nl = (size_t)rows * GROUPS * RANK, ny = (size_t)rows * OUT; + void *model = NULL; + CHECK(!posix_memalign(&model, (size_t)sysconf(_SC_PAGESIZE), bytes)); + memset(model, 0, bytes); + fixture_q8 *a = model, *b = a + a_blocks; + /* Dense production layouts with independently specified sparse coefficients + * keep a complete CPU oracle cheap, including all groups and output rows. */ + for (uint32_t group = 0; group < GROUPS; group++) for (uint32_t o = 0; o < RANK; o++) { + for (uint32_t j = 0; j < TERMS; j++) { + const uint32_t k = low_column(group, o, j); + fixture_q8 *block = &a[((size_t)group * RANK + o) * GROUP / 32 + k / 32]; + block->scale = 0x2000; + block->q[k % 32] = (int8_t)low_coefficient(group, o, j); + } + } + for (uint32_t o = 0; o < OUT; o++) for (uint32_t j = 0; j < TERMS; j++) { + const uint32_t k = output_column(o, j); + fixture_q8 *block = &b[(size_t)o * GROUPS * RANK / 32 + k / 32]; + block->scale = 0x2000; + block->q[k % 32] = (int8_t)output_coefficient(o, j); + } + float *input = malloc(nx * 4), *low_ref = malloc(nl * 4), *low_got = malloc(nl * 4), *out_got = malloc(ny * 4); + CHECK(input && low_ref && low_got && out_got); + for (size_t i = 0; i < nx; i++) input[i] = ((int)((i * 37u) % 257u) - 128) / 32.0f; + CHECK(ds4_gpu_set_model_map(model, bytes)); + ds4_gpu_tensor *xt = upload(input, nx * 4), *low = upload(NULL, nl * 4), *out = upload(NULL, ny * 4); + CHECK(xt && low && out); + RUN(ds4_gpu_dsv41_attention_output_batch(out, low, model, bytes, 0, a_bytes, xt, rows)); + CHECK(ds4_gpu_tensor_read(low, 0, low_got, nl * 4)); + CHECK(ds4_gpu_tensor_read(out, 0, out_got, ny * 4)); + const int bulk = rows >= 32u && rows <= 2048u; + double worst_low = 0, worst_output = 0; + CHECK(reference_f16(0x1.002p0f) == 1.0f); + CHECK(reference_f16(0x1.006p0f) == 0x1.008p0f); + CHECK(reference_f16(0x1p-25f) == 0.0f); + CHECK(reference_f16(-0x1.8p-24f) == -0x1p-23f); + for (uint32_t row = 0; row < rows; row++) for (uint32_t group = 0; group < GROUPS; group++) { + for (uint32_t o = 0; o < RANK; o++) { + double sum = 0, magnitude = 0; + for (uint32_t j = 0; j < TERMS; j++) { + /* These fixture inputs and scaled coefficients are exactly F16. */ + const double term = input[((size_t)row * GROUPS + group) * GROUP + low_column(group, o, j)] * low_coefficient(group, o, j) / 128.0; + sum += term; magnitude += fabs(term); + } + const size_t at = ((size_t)row * GROUPS + group) * RANK + o; + low_ref[at] = bf16((float)sum); + CHECK(isfinite(low_got[at]) && low_got[at] == bf16(low_got[at])); + if (bulk) { + /* Propagate accumulation error through the required BF16 boundary, + * including cancellation and sums on a rounding midpoint. */ + const double bound = projection_roundoff_bound(magnitude, GROUP); + CHECK(low_got[at] >= bf16((float)(sum - bound)) && + low_got[at] <= bf16((float)(sum + bound))); + } else { + CHECK(low_got[at] == low_ref[at]); + } + worst_low = fmax(worst_low, fabs(low_got[at] - low_ref[at])); + } + } + for (uint32_t row = 0; row < rows; row++) for (uint32_t o = 0; o < OUT; o++) { + double sum = 0, magnitude = 0; + for (uint32_t j = 0; j < TERMS; j++) { + const float low_value = low_got[(size_t)row * GROUPS * RANK + output_column(o, j)]; + const double term = (bulk ? reference_f16(low_value) : low_value) * output_coefficient(o, j) / 128.0; + sum += term; magnitude += fabs(term); + } + const float got = out_got[(size_t)row * OUT + o]; + const double bound = bulk ? projection_roundoff_bound(magnitude, GROUPS * RANK) : 2e-5 * (1 + fabs(sum)); + CHECK(isfinite(got) && fabs(got - sum) <= bound); + worst_output = fmax(worst_output, fabs(got - sum)); + } + fprintf(stderr, "attention-output full reference rows=%u low_values=%zu output_values=%zu bulk=%d max_low_drift=%.9g max_output_error=%.9g\n", + rows, nl, ny, bulk, worst_low, worst_output); + + /* Exercise the graph's direct Q8 projection helper independently, using + * values that an accidental F16/BF16 activation cast would change. */ + for (size_t i = 0; i < nl; i++) low_ref[i] += ((i & 1u) ? -1.0f : 1.0f) * 0.00012345f; + CHECK(ds4_gpu_tensor_write(low, 0, low_ref, nl * 4)); + RUN(ds4_gpu_dsv41_q8_projection_rows(out, model, bytes, a_bytes, GROUPS * RANK, OUT, rows, low)); + CHECK(ds4_gpu_tensor_read(out, 0, out_got, ny * 4)); + size_t q8_mismatches = 0, q8_worst_at = 0; + double q8_worst_absolute = 0, q8_worst_fraction = 0; + /* The fixture's scaled coefficients are exact, with one nonzero product + * per lane: one rounded product plus five F32 tree additions gives gamma6. */ + const double q8_unit_roundoff = FLT_EPSILON / 2.0; + const double q8_gamma6 = 6 * q8_unit_roundoff / (1 - 6 * q8_unit_roundoff); + for (uint32_t row = 0; row < rows; row++) for (uint32_t o = 0; o < OUT; o++) { + double sum = 0, magnitude = 0; + for (uint32_t j = 0; j < TERMS; j++) { + const double term = (double)low_ref[(size_t)row * GROUPS * RANK + output_column(o, j)] * output_coefficient(o, j) / 128.0; + sum += term; magnitude += fabs(term); + } + const float got = out_got[(size_t)row * OUT + o]; + const double error = fabs(got - sum), tolerance = q8_gamma6 * fmax(magnitude, 0.001); + const double fraction = isfinite(got) ? error / tolerance : INFINITY; + if (fraction > q8_worst_fraction) { + q8_worst_fraction = fraction; + q8_worst_at = (size_t)row * OUT + o; + } + q8_worst_absolute = fmax(q8_worst_absolute, isfinite(got) ? error : INFINITY); + if (!(isfinite(got) && error <= tolerance)) { + if (!q8_mismatches) { + fprintf(stderr, "Q8 projection first mismatch shape=%u rows=%u row=%u output=%u index=%zu actual=%.17g (%a) sum=%.17g (%a) magnitude=%.17g error=%.17g tolerance=%.17g error_over_tolerance=%.17g\n", + requested_shape, rows, row, o, (size_t)row * OUT + o, (double)got, (double)got, sum, sum, magnitude, error, tolerance, fraction); + for (uint32_t j = 0; j < TERMS; j++) { + const uint32_t k = output_column(o, j); + fprintf(stderr, "Q8 projection first mismatch term=%u column=%u input=%.17g (%a) coefficient=%d scale=1/128\n", + j, k, (double)low_ref[(size_t)row * GROUPS * RANK + k], (double)low_ref[(size_t)row * GROUPS * RANK + k], output_coefficient(o, j)); + } + } + q8_mismatches++; + } + } + fprintf(stderr, "Q8 projection full scan outputs=%zu mismatches=%zu worst_absolute=%.17g worst_error_over_tolerance=%.17g worst_index=%zu\n", + ny, q8_mismatches, q8_worst_absolute, q8_worst_fraction, q8_worst_at); + CHECK(q8_mismatches == 0); + /* Scalar decode uses a different F32 reduction from batched rows. Check + * every scalar output against the same independent oracle and unchanged + * gamma6 bound above; retain bit equality for repeated scalar calls. */ + ds4_gpu_tensor *one = upload(NULL, OUT * 4); + CHECK(one); + const uint32_t probes[] = {0, rows / 2, rows - 1}; + for (unsigned i = 0; i < sizeof(probes) / sizeof(*probes); i++) { + if (i && probes[i] == probes[i - 1]) continue; + const uint32_t row = probes[i]; + ds4_gpu_tensor *view = ds4_gpu_tensor_view(low, (size_t)row * GROUPS * RANK * 4, GROUPS * RANK * 4); + CHECK(view); + RUN(ds4_gpu_dsv41_q8_projection_rows(one, model, bytes, a_bytes, GROUPS * RANK, OUT, 1, view)); + CHECK(ds4_gpu_tensor_read(one, 0, low_got, OUT * 4)); + double scalar_worst_fraction = 0, scalar_batch_drift = 0; + for (uint32_t o = 0; o < OUT; o++) { + double sum = 0, magnitude = 0; + for (uint32_t j = 0; j < TERMS; j++) { + const double term = (double)low_ref[(size_t)row * GROUPS * RANK + output_column(o, j)] * output_coefficient(o, j) / 128.0; + sum += term; + magnitude += fabs(term); + } + const double error = fabs((double)low_got[o] - sum); + const double tolerance = q8_gamma6 * fmax(magnitude, 0.001); + CHECK(isfinite(low_got[o]) && error <= tolerance); + scalar_worst_fraction = fmax(scalar_worst_fraction, error / tolerance); + scalar_batch_drift = fmax(scalar_batch_drift, fabs((double)low_got[o] - out_got[(size_t)row * OUT + o])); + } + if (rows == 1u) CHECK(!memcmp(low_got, out_got, OUT * 4)); + fprintf(stderr, "Q8 scalar full oracle row=%u outputs=%u worst_error_over_unchanged_bound=%.9g scalar_batch_maxabs=%.9g\n", + row, OUT, scalar_worst_fraction, scalar_batch_drift); + ds4_gpu_tensor_free(view); + } + ds4_gpu_tensor_free(one); + CHECK(!ds4_gpu_dsv41_q8_projection_rows(out, model, bytes - 1, a_bytes, GROUPS * RANK, OUT, rows, low)); + CHECK(!ds4_gpu_dsv41_q8_projection_rows(out, model, bytes, a_bytes, GROUPS * RANK, OUT, rows + 1, low)); + CHECK(!ds4_gpu_dsv41_q8_projection_rows(out, model, bytes, a_bytes, GROUPS * RANK - 1, OUT, rows, low)); + CHECK(!ds4_gpu_dsv41_attention_output_batch(out, low, model, bytes - 1, 0, a_bytes, xt, rows)); + CHECK(!ds4_gpu_dsv41_attention_output_batch(out, low, model, bytes, 0, a_bytes, xt, rows + 1)); + ds4_gpu_tensor_free(xt); ds4_gpu_tensor_free(low); ds4_gpu_tensor_free(out); + ds4_gpu_cleanup(); free(model); free(input); free(low_ref); free(low_got); free(out_got); + return ds4_gpu_init(); +} + +typedef struct { const char *name; unsigned shapes; int (*run)(void); } test_case; +static const test_case cases[] = { + {"quantization", 4, check_quantization}, + {"engram", 1, check_engram}, + {"pool", 8, check_pool}, + {"candidates", 7, check_candidates}, + {"bf16", 13, check_bf16_boundaries}, + {"rope", 16, check_rope}, + {"gather", 7, check_sparse_gather}, + {"carry", 16, check_compact_carry}, + {"topk", 14, check_causal_topk}, + {"indexer", 16, check_indexer_scores}, + {"projection", 6, check_projection}, + {"hc", 4, check_hc_scaled}, + {"attention-output", 6, check_attention_output}, +}; + +int main(int argc, char **argv) { + const char *only = NULL; + if (argc == 2 && !strcmp(argv[1], "--list")) { + for (size_t i = 0; i < sizeof(cases) / sizeof(*cases); i++) + printf("%s %u shapes\n", cases[i].name, cases[i].shapes); + return 0; + } + if (argc == 3 && !strcmp(argv[1], "--case")) only = argv[2]; + else if (argc == 5 && !strcmp(argv[1], "--case") && !strcmp(argv[3], "--shape")) { + char *end; + unsigned long shape = strtoul(argv[4], &end, 10); + if (!*argv[4] || *end || shape > UINT32_MAX) return 2; + for (size_t i = 0; i < sizeof(cases) / sizeof(*cases); i++) { + if (strcmp(cases[i].name, argv[2])) continue; + if (shape >= cases[i].shapes) return 2; + requested_shape = (unsigned)shape; + fprintf(stderr, "V4.1 ROCm case=%s shape=%u pid=%ld\n", cases[i].name, requested_shape, (long)getpid()); + if (!ds4_gpu_init() || !cases[i].run() || !sync_guards()) _exit(1); + if (allocations) { fprintf(stderr, "unreleased guarded allocation\n"); _exit(1); } + ds4_gpu_cleanup(); + fprintf(stderr, "PASS %s shape=%u\n", cases[i].name, requested_shape); + return 0; + } + return 2; + } else if (argc != 1) { + fprintf(stderr, "usage: %s [--list | --case NAME [--shape N]]\n", argv[0]); + return 2; + } + unsigned tested = 0; + for (size_t i = 0; i < sizeof(cases) / sizeof(*cases); i++) { + if (only && strcmp(only, cases[i].name)) continue; + for (unsigned shape = 0; shape < cases[i].shapes; shape++) { + char number[24]; + snprintf(number, sizeof(number), "%u", shape); + pid_t child = fork(); + if (child < 0) { perror("fork"); return 1; } + if (!child) { + execlp(argv[0], argv[0], "--case", cases[i].name, "--shape", number, (char *)NULL); + perror("exec harness"); + _exit(127); + } + int status; + while (waitpid(child, &status, 0) < 0) { + if (errno != EINTR) { perror("waitpid"); return 1; } + } + if (!WIFEXITED(status) || WEXITSTATUS(status)) { + fprintf(stderr, "FAIL %s shape=%u child_status=%d; stopping at first failure\n", cases[i].name, shape, status); + return 1; + } + tested++; + } + } + if (!tested) return 2; + fprintf(stderr, "PASS %u isolated V4.1 ROCm shapes\n", tested); + return 0; +} diff --git a/tests/test_deepseek41_tp_down_rocm.c b/tests/test_deepseek41_tp_down_rocm.c new file mode 100644 index 0000000000..8e98ac924d --- /dev/null +++ b/tests/test_deepseek41_tp_down_rocm.c @@ -0,0 +1,175 @@ +/* Independent dyadic Q2 oracle for the owned down operator. + * Uses actual production strides, inaccessible unowned pages and canaries. */ +#define _POSIX_C_SOURCE 200809L +#include "ds4_gpu.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define CHECK(x) do { if (!(x)) { \ + fprintf(stderr, "%s:%d: %s\n", __FILE__, __LINE__, #x); return 0; \ +} } while (0) +#define RUN(x) do { fprintf(stderr, "stage: %s\n", #x); CHECK(x); CHECK(sync_guards()); } while (0) + +enum { GUARD_BYTES = 64 }; +typedef struct allocation { + ds4_gpu_tensor *storage, *view; + size_t bytes; + struct allocation *next; +} allocation; +static allocation *allocations; + +static int sync_guards(void) { + CHECK(ds4_gpu_synchronize()); + for (allocation *a = allocations; a; a = a->next) { + unsigned char before[GUARD_BYTES], after[GUARD_BYTES]; + CHECK(ds4_gpu_tensor_read(a->storage, 0, before, sizeof(before))); + CHECK(ds4_gpu_tensor_read(a->storage, GUARD_BYTES + a->bytes, after, sizeof(after))); + for (unsigned i = 0; i < GUARD_BYTES; i++) CHECK(before[i] == 0xa5 && after[i] == 0xa5); + } + return 1; +} + +static ds4_gpu_tensor *upload(const void *data, size_t bytes) { + allocation *a = calloc(1, sizeof(*a)); + if (!a || bytes > SIZE_MAX - 2 * GUARD_BYTES) { free(a); return NULL; } + a->bytes = bytes; + a->storage = ds4_gpu_tensor_alloc(bytes + 2 * GUARD_BYTES); + if (!a->storage) { free(a); return NULL; } + unsigned char guard[GUARD_BYTES]; + memset(guard, 0xa5, sizeof(guard)); + if (!ds4_gpu_tensor_write(a->storage, 0, guard, sizeof(guard)) || + !ds4_gpu_tensor_write(a->storage, GUARD_BYTES + bytes, guard, sizeof(guard))) goto fail; + a->view = ds4_gpu_tensor_view(a->storage, GUARD_BYTES, bytes); + if (!a->view || (data && !ds4_gpu_tensor_write(a->view, 0, data, bytes))) goto fail; + a->next = allocations; + allocations = a; + return a->view; +fail: + ds4_gpu_tensor_free(a->view); + ds4_gpu_tensor_free(a->storage); + free(a); + return NULL; +} + +static void guarded_free(ds4_gpu_tensor *t) { + for (allocation **link = &allocations; *link; link = &(*link)->next) { + allocation *a = *link; + if (a->view == t) { + *link = a->next; + ds4_gpu_tensor_free(a->view); + ds4_gpu_tensor_free(a->storage); + free(a); + return; + } + } + ds4_gpu_tensor_free(t); +} +#define ds4_gpu_tensor_free guarded_free + +#include + +enum { E=384,U=6,H=2304,M=5120 }; +typedef struct { uint8_t scales[16],qs[64]; uint16_t d,dmin; } q2; +static void route(int32_t *ids,unsigned t,unsigned rank,unsigned mode) { + for(unsigned j=0;jd=0x2800; + memset(v->scales,1+(e+r+b)%3,16);memset(v->qs,(1+(r+b)%3)*0x55,64); + } + } + size_t pairs=(size_t)n*U,nmid=pairs*H,ny=(size_t)n*M,ns=pairs*M; + int32_t *ids=malloc(pairs*4);float *mid=malloc(nmid*4),*got=malloc(ny*4),*poison=malloc(ns*4); + uint16_t *slots=malloc(ns*2); CHECK(ids&&mid&&got&&poison&&slots); + unsigned counts[E]={0}; + for(unsigned t=0;t=4) { + if(mode==6)ids[1]=ids[0];else ids[0]=mode==4?-1:384; + CHECK(ds4_gpu_tensor_write(it,0,ids,pairs*4)); + CHECK(!ds4_gpu_dsv41_moe_tp_down(ot,dt,mt,it,model,bytes,0,n,rank)); + CHECK(sync_guards());puts("Invalid or duplicate expert ID rejected PASS");goto cleanup; + } + RUN(ds4_gpu_dsv41_moe_tp_down(ot,dt,mt,it,model,bytes,0,n,rank)); + CHECK(ds4_gpu_tensor_read(ot,0,got,ny*4));CHECK(ds4_gpu_tensor_read(dt,0,slots,ns*2)); + if(mode==3)got[0]+=1.f; + size_t failures=0; + for(unsigned t=0;t=8 && counts[e]>=8)x=(float)(_Float16)x; + sums[j][r/256]+=x; + } + } + for(unsigned r=0;r2048||rank>1||mode>6)return 2; + fprintf(stderr,"PID=%ld rows=%u rank=%u mode=%u\n",(long)getpid(),n,rank,mode); + if(!ds4_gpu_init()||!run(n,rank,mode))return 1; + puts("TP owned down case PASS");return 0; +} + diff --git a/tests/test_deepseek41_tp_mmq_rocm.c b/tests/test_deepseek41_tp_mmq_rocm.c new file mode 100644 index 0000000000..8dd8a0dde4 --- /dev/null +++ b/tests/test_deepseek41_tp_mmq_rocm.c @@ -0,0 +1,169 @@ +/* Independent sparse IQ2 oracle for the owned MMQ gate/up operator. + * Uses actual production strides, inaccessible unowned pages and canaries. */ +#define _POSIX_C_SOURCE 200809L +#include "ds4_gpu.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define CHECK(x) do { if (!(x)) { \ + fprintf(stderr, "%s:%d: %s\n", __FILE__, __LINE__, #x); return 0; \ +} } while (0) +#define RUN(x) do { fprintf(stderr, "stage: %s\n", #x); CHECK(x); CHECK(sync_guards()); } while (0) + +enum { GUARD_BYTES = 64 }; +typedef struct allocation { + ds4_gpu_tensor *storage, *view; + size_t bytes; + struct allocation *next; +} allocation; +static allocation *allocations; + +static int sync_guards(void) { + CHECK(ds4_gpu_synchronize()); + for (allocation *a = allocations; a; a = a->next) { + unsigned char before[GUARD_BYTES], after[GUARD_BYTES]; + CHECK(ds4_gpu_tensor_read(a->storage, 0, before, sizeof(before))); + CHECK(ds4_gpu_tensor_read(a->storage, GUARD_BYTES + a->bytes, after, sizeof(after))); + for (unsigned i = 0; i < GUARD_BYTES; i++) CHECK(before[i] == 0xa5 && after[i] == 0xa5); + } + return 1; +} + +static ds4_gpu_tensor *upload(const void *data, size_t bytes) { + allocation *a = calloc(1, sizeof(*a)); + if (!a || bytes > SIZE_MAX - 2 * GUARD_BYTES) { free(a); return NULL; } + a->bytes = bytes; + a->storage = ds4_gpu_tensor_alloc(bytes + 2 * GUARD_BYTES); + if (!a->storage) { free(a); return NULL; } + unsigned char guard[GUARD_BYTES]; + memset(guard, 0xa5, sizeof(guard)); + if (!ds4_gpu_tensor_write(a->storage, 0, guard, sizeof(guard)) || + !ds4_gpu_tensor_write(a->storage, GUARD_BYTES + bytes, guard, sizeof(guard))) goto fail; + a->view = ds4_gpu_tensor_view(a->storage, GUARD_BYTES, bytes); + if (!a->view || (data && !ds4_gpu_tensor_write(a->view, 0, data, bytes))) goto fail; + a->next = allocations; + allocations = a; + return a->view; +fail: + ds4_gpu_tensor_free(a->view); + ds4_gpu_tensor_free(a->storage); + free(a); + return NULL; +} + +static void guarded_free(ds4_gpu_tensor *t) { + for (allocation **link = &allocations; *link; link = &(*link)->next) { + allocation *a = *link; + if (a->view == t) { + *link = a->next; + ds4_gpu_tensor_free(a->view); + ds4_gpu_tensor_free(a->storage); + free(a); + return; + } + } + ds4_gpu_tensor_free(t); +} +#define ds4_gpu_tensor_free guarded_free + +#include + +/* IQ2 grid0/sign0 is eight copies of8; scale0 decodes exactly to d. + * The independent oracle needs no GPU quantization or dot-product helper. */ +enum {E=384,U=6,K=5120,H=2304}; +typedef struct { uint16_t d,qs[32]; } iq2; +static void route(int32_t *ids,unsigned rank,unsigned mode) { + const int32_t balanced[6]={0,191,192,193,382,383},skewed[6]={0,1,2,3,191,192}; + for(unsigned j=0;j<6;++j) { + if(mode<2)ids[j]=(int32_t)((mode?1-rank:rank)*192+(j<4?j:186+j)); + else if(mode==2)ids[j]=balanced[j]; + else ids[j]=rank?383-skewed[j]:skewed[j]; + } +} +static double gate_ref(unsigned e,unsigned row,unsigned tok,int up) { + unsigned block=(e+row)%20; + double x=(tok+block)&1?127.:-127.; + unsigned sub=up?(e+row)%4:e%3; + return x*(2*sub+1)/(up?256.:128.); +} +static int run(unsigned n,unsigned rank,unsigned mode) { + const size_t ge=(size_t)H*(K/256)*sizeof(iq2); + const size_t table=E*ge,wb=2*table; + unsigned char *model=mmap(NULL,wb,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0);CHECK(model!=MAP_FAILED); + int32_t ids_for[6];bool used[E]={0}; + for(unsigned r=0;r<2;++r)for(unsigned m=0;m<4;++m) { + route(ids_for,r,m);for(unsigned j=0;jd=0x2000;u->d=0x1c00; + for(unsigned j=0;j<8;++j) {g->qs[j*4+3]=(e%3)<<12;u->qs[j*4+3]=((e+r)%4)<<12;} + } + + } + size_t pairs=(size_t)n*U,nmid=pairs*H; + int32_t *ids=malloc(pairs*4);float *x=calloc((size_t)n*K,4),*scratch=malloc(nmid*4);CHECK(ids&&x&&scratch); + for(unsigned t=0;ttol) { + if(!failures)fprintf(stderr,"stage=%u row=%u slot=%u element=%u actual=%.9g expected=%.17g tol=%.9g\n",stage,t,j,r,v,expected,tol); + ++failures; + } + } + } + } + fprintf(stderr,"MMQ gate/up rows=%u rank=%u mode=%u output_values=%zu failures=%zu worst_bound_fraction=%.9g\n",n,rank,mode,2*nmid,failures,worst);CHECK(!failures&&sync_guards()); +cleanup: + ds4_gpu_tensor_free(ut);ds4_gpu_tensor_free(gt);ds4_gpu_tensor_free(xt);ds4_gpu_tensor_free(it); + ds4_gpu_cleanup();munmap(model,wb);free(ids);free(x);free(scratch);return 1; +} +int main(int argc,char **argv) { + if(argc!=4)return 2; + unsigned n=strtoul(argv[1],NULL,10),rank=strtoul(argv[2],NULL,10),mode=strtoul(argv[3],NULL,10); + if(n<128||n>2048||rank>1||mode>7)return 2; + fprintf(stderr,"PID=%ld rows=%u rank=%u route-mode=%u\n",(long)getpid(),n,rank,mode); + if(!ds4_gpu_init()||!run(n,rank,mode))return 1; + puts("TP MMQ gate/up case PASS");return 0; +} diff --git a/tests/test_deepseek41_tp_moe_rocm.c b/tests/test_deepseek41_tp_moe_rocm.c new file mode 100644 index 0000000000..ec8e039e50 --- /dev/null +++ b/tests/test_deepseek41_tp_moe_rocm.c @@ -0,0 +1,231 @@ +/* V4.1 Q8 production-shape harness with an independent sparse double oracle, + * cast-sensitive fixtures and full-output parity against unchanged scalar + * production calls. Explicit copies, guards and synchronization after stages. */ +#define _POSIX_C_SOURCE 200809L +#include "ds4_gpu.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define CHECK(x) do { if (!(x)) { \ + fprintf(stderr, "%s:%d: %s\n", __FILE__, __LINE__, #x); return 0; \ +} } while (0) +#define RUN(x) do { fprintf(stderr, "stage: %s\n", #x); CHECK(x); CHECK(sync_guards()); } while (0) + +enum { GUARD_BYTES = 64 }; +typedef struct allocation { + ds4_gpu_tensor *storage, *view; + size_t bytes; + struct allocation *next; +} allocation; +static allocation *allocations; + +static int sync_guards(void) { + CHECK(ds4_gpu_synchronize()); + for (allocation *a = allocations; a; a = a->next) { + unsigned char before[GUARD_BYTES], after[GUARD_BYTES]; + CHECK(ds4_gpu_tensor_read(a->storage, 0, before, sizeof(before))); + CHECK(ds4_gpu_tensor_read(a->storage, GUARD_BYTES + a->bytes, after, sizeof(after))); + for (unsigned i = 0; i < GUARD_BYTES; i++) CHECK(before[i] == 0xa5 && after[i] == 0xa5); + } + return 1; +} + +static ds4_gpu_tensor *upload(const void *data, size_t bytes) { + allocation *a = calloc(1, sizeof(*a)); + if (!a || bytes > SIZE_MAX - 2 * GUARD_BYTES) { free(a); return NULL; } + a->bytes = bytes; + a->storage = ds4_gpu_tensor_alloc(bytes + 2 * GUARD_BYTES); + if (!a->storage) { free(a); return NULL; } + unsigned char guard[GUARD_BYTES]; + memset(guard, 0xa5, sizeof(guard)); + if (!ds4_gpu_tensor_write(a->storage, 0, guard, sizeof(guard)) || + !ds4_gpu_tensor_write(a->storage, GUARD_BYTES + bytes, guard, sizeof(guard))) goto fail; + a->view = ds4_gpu_tensor_view(a->storage, GUARD_BYTES, bytes); + if (!a->view || (data && !ds4_gpu_tensor_write(a->view, 0, data, bytes))) goto fail; + a->next = allocations; + allocations = a; + return a->view; +fail: + ds4_gpu_tensor_free(a->view); + ds4_gpu_tensor_free(a->storage); + free(a); + return NULL; +} + +static void guarded_free(ds4_gpu_tensor *t) { + for (allocation **link = &allocations; *link; link = &(*link)->next) { + allocation *a = *link; + if (a->view == t) { + *link = a->next; + ds4_gpu_tensor_free(a->view); + ds4_gpu_tensor_free(a->storage); + free(a); + return; + } + } + ds4_gpu_tensor_free(t); +} +#define ds4_gpu_tensor_free guarded_free + +#include + +/* IQ2 grid0/sign0 is eight copies of8; scale0 decodes exactly to d. + * Q2_K uses constant two-bit lanes and explicit per-block scales. The + * independent oracle needs no GPU quantization or dot-product helper. */ +enum {E=384,U=6,K=5120,H=2304,M=5120}; +typedef struct { uint16_t d,qs[32]; } iq2; +typedef struct { uint8_t scales[16],qs[64];uint16_t d,dmin; } q2; +static void route(int32_t *ids,unsigned rank,unsigned mode) { + const int32_t balanced[6]={0,191,192,193,382,383},skewed[6]={0,1,2,3,191,192}; + for(unsigned j=0;j<6;++j) { + if(mode<2)ids[j]=(int32_t)((mode?1-rank:rank)*192+(j<4?j:186+j)); + else if(mode==2)ids[j]=balanced[j]; + else ids[j]=rank?383-skewed[j]:skewed[j]; + } +} +static double gate_ref(unsigned e,unsigned row,unsigned tok,int up) { + unsigned block=(e+row)%20; + double x=(tok+block)&1?127.:-127.; + unsigned sub=up?(e+row)%4:e%3; + return x*(2*sub+1)/(up?256.:128.); +} +static double down_weight(unsigned e,unsigned row,unsigned block) { + return (double)(1+(e+row+block)%3)*(1+(row+block)%3)/32.; +} +static int run(unsigned n,unsigned rank,unsigned mode) { + const size_t ge=(size_t)H*(K/256)*sizeof(iq2),de=(size_t)M*(H/256)*sizeof(q2); + const size_t table=E*ge,wb=2*table+E*de; + unsigned char *model=mmap(NULL,wb,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0);CHECK(model!=MAP_FAILED); + int32_t ids_for[6];bool used[E]={0}; + for(unsigned r=0;r<2;++r)for(unsigned m=0;m<4;++m) { + route(ids_for,r,m);for(unsigned j=0;jd=0x2000;u->d=0x1c00; + for(unsigned j=0;j<8;++j) {g->qs[j*4+3]=(e%3)<<12;u->qs[j*4+3]=((e+r)%4)<<12;} + } + q2 *d=(q2*)(model+2*table+e*de); + for(unsigned r=0;rd=0x2800; + memset(v->scales,1+(e+r+b)%3,16); + memset(v->qs,(1+(r+b)%3)*0x55,64); + } + } + size_t pairs=(size_t)n*U,nmid=pairs*H,ny=(size_t)n*M; + int32_t *ids=malloc(pairs*4);float *rw=malloc(pairs*4),*x=calloc((size_t)n*K,4),*scratch=malloc(nmid*4),*got=malloc(ny*4);CHECK(ids&&rw&&x&&scratch&&got); + for(unsigned t=0;t=128) { + /* Independently qualify weighted mid below. For down, use the observed + * mid boundary, a double dot and the inherited F16 rounding intervals. + * gamma2304 bounds FP32 accumulation, including rare half ties. */ + float *observed=malloc(nmid*4);uint16_t *slots=malloc(pairs*M*2); + CHECK(observed&&slots&&ds4_gpu_tensor_read(mt,0,observed,nmid*4)); + CHECK(ds4_gpu_tensor_read(dt,0,slots,pairs*M*2)); + unsigned counts[E]={0};for(size_t p=0;p=8)v=(float)(_Float16)v; + double term=v*down_weight(e,r,k/256);dot+=term;l1+=fabs(term); + } + float lo=(float)(_Float16)(dot-gamma*l1),hi=(float)(_Float16)(dot+gamma*l1); + _Float16 actual;memcpy(&actual,slots+((size_t)t*U+j)*M+r,2); + if(!isfinite((float)actual)||(float)actualhi) { + if(!failures)fprintf(stderr,"bulk slot t=%u r=%u j=%u value=%.9g interval=[%.9g,%.9g]\n",t,r,j,(float)actual,lo,hi); + ++failures; + } + lo_sum+=lo;hi_sum+=hi; + } + float v=got[(size_t)t*M+r]; + if(!isfinite(v)||vhi_sum) { + if(!failures)fprintf(stderr,"bulk sum t=%u r=%u value=%.9g interval=[%.9g,%.9g]\n",t,r,v,lo_sum,hi_sum); + ++failures; + } + } + free(slots);free(observed); + } + for(unsigned t=0;n<128&&ttol) {if(!failures)fprintf(stderr,"output t=%u r=%u got=%.9g expected=%.17g tol=%.9g\n",t,r,got[(size_t)t*M+r],expected,tol);++failures;} + } + } + for(unsigned stage=0;stage<3;++stage) { + CHECK(ds4_gpu_tensor_read(stage==0?gt:stage==1?ut:mt,0,scratch,nmid*4)); + for(unsigned t=0;t=128?gate_ref(e,r,t,0):g):stage==1?(n>=128?gate_ref(e,r,t,1):u):g/(1+exp(-g))*u*rw[(size_t)t*U+j]; + } + float v=scratch[((size_t)t*U+j)*H+r]; + double tol=expected==0?0:3e-5*fmax(1.,fabs(expected)); + if(!isfinite(v)||fabs(v-expected)>tol)++failures; + } + } + } + fprintf(stderr,"MoE rows=%u rank=%u mode=%u full_outputs=%zu failures=%zu worst_bound_fraction=%.9g\n",n,rank,mode,ny,failures,worst);CHECK(!failures&&sync_guards()); +cleanup: + ds4_gpu_tensor_free(ot);ds4_gpu_tensor_free(dt);ds4_gpu_tensor_free(mt);ds4_gpu_tensor_free(ut);ds4_gpu_tensor_free(gt);ds4_gpu_tensor_free(xt);ds4_gpu_tensor_free(wt);ds4_gpu_tensor_free(it); + ds4_gpu_cleanup();munmap(model,wb);free(ids);free(rw);free(x);free(scratch);free(got);return 1; +} +int main(int argc,char **argv) { + if(argc!=4)return 2; + unsigned n=strtoul(argv[1],NULL,10),rank=strtoul(argv[2],NULL,10),mode=strtoul(argv[3],NULL,10); + if(!n||n>2048||rank>1||mode>6)return 2; + fprintf(stderr,"PID=%ld rows=%u rank=%u route-mode=%u\n",(long)getpid(),n,rank,mode); + if(!ds4_gpu_init()||!run(n,rank,mode))return 1; + puts("TP MoE case PASS");return 0; +} diff --git a/tests/test_deepseek41_tp_rocm.c b/tests/test_deepseek41_tp_rocm.c new file mode 100644 index 0000000000..00c8c42b89 --- /dev/null +++ b/tests/test_deepseek41_tp_rocm.c @@ -0,0 +1,166 @@ +/* V4.1 Q8 production-shape harness with an independent sparse double oracle, + * cast-sensitive fixtures and full-output parity against unchanged scalar + * production calls. Explicit copies, guards and synchronization after stages. */ +#define _POSIX_C_SOURCE 200809L +#include "ds4_gpu.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define CHECK(x) do { if (!(x)) { \ + fprintf(stderr, "%s:%d: %s\n", __FILE__, __LINE__, #x); return 0; \ +} } while (0) +#define RUN(x) do { fprintf(stderr, "stage: %s\n", #x); CHECK(x); CHECK(sync_guards()); } while (0) + +enum { GUARD_BYTES = 64 }; +typedef struct allocation { + ds4_gpu_tensor *storage, *view; + size_t bytes; + struct allocation *next; +} allocation; +static allocation *allocations; + +static int sync_guards(void) { + CHECK(ds4_gpu_synchronize()); + for (allocation *a = allocations; a; a = a->next) { + unsigned char before[GUARD_BYTES], after[GUARD_BYTES]; + CHECK(ds4_gpu_tensor_read(a->storage, 0, before, sizeof(before))); + CHECK(ds4_gpu_tensor_read(a->storage, GUARD_BYTES + a->bytes, after, sizeof(after))); + for (unsigned i = 0; i < GUARD_BYTES; i++) CHECK(before[i] == 0xa5 && after[i] == 0xa5); + } + return 1; +} + +static ds4_gpu_tensor *upload(const void *data, size_t bytes) { + allocation *a = calloc(1, sizeof(*a)); + if (!a || bytes > SIZE_MAX - 2 * GUARD_BYTES) { free(a); return NULL; } + a->bytes = bytes; + a->storage = ds4_gpu_tensor_alloc(bytes + 2 * GUARD_BYTES); + if (!a->storage) { free(a); return NULL; } + unsigned char guard[GUARD_BYTES]; + memset(guard, 0xa5, sizeof(guard)); + if (!ds4_gpu_tensor_write(a->storage, 0, guard, sizeof(guard)) || + !ds4_gpu_tensor_write(a->storage, GUARD_BYTES + bytes, guard, sizeof(guard))) goto fail; + a->view = ds4_gpu_tensor_view(a->storage, GUARD_BYTES, bytes); + if (!a->view || (data && !ds4_gpu_tensor_write(a->view, 0, data, bytes))) goto fail; + a->next = allocations; + allocations = a; + return a->view; +fail: + ds4_gpu_tensor_free(a->view); + ds4_gpu_tensor_free(a->storage); + free(a); + return NULL; +} + +static void guarded_free(ds4_gpu_tensor *t) { + for (allocation **link = &allocations; *link; link = &(*link)->next) { + allocation *a = *link; + if (a->view == t) { + *link = a->next; + ds4_gpu_tensor_free(a->view); + ds4_gpu_tensor_free(a->storage); + free(a); + return; + } + } + ds4_gpu_tensor_free(t); +} +#define ds4_gpu_tensor_free guarded_free + +/* Full production strides, sparse independent double reference, and exact BF16 boundary. */ +typedef struct { uint16_t scale; int8_t q[32]; } q8_block; +static float bf16(float x) { + uint32_t u; memcpy(&u,&x,4);u+=0x7fff+((u>>16)&1);u&=0xffff0000;memcpy(&x,&u,4);return x; +} +static unsigned column(unsigned row,unsigned term,unsigned k) { return (row*97+term*947)%k; } +static int coefficient(unsigned row,unsigned term) { return (int)((row+term*3)%7)-3; } +static void weights(q8_block *w,unsigned rows,unsigned k,unsigned base) { + for(unsigned r=0;rscale=0x2000;b->q[c%32]=coefficient(r+base,j); + } +} +static int run(unsigned n,unsigned rank,int cast_fixture) { + enum {K=4096,R=1024,G=4,M=5120}; + size_t ab=(size_t)8*R*(K/32)*34,bb=(size_t)M*(8192/32)*34,wb=ab+bb; + q8_block *model=NULL;CHECK(!posix_memalign((void**)&model,4096,wb));memset(model,0,wb); + for(unsigned g=0;g<8;++g)weights(model+(size_t)g*R*(K/32),R,K,g*R); + weights((q8_block*)((char*)model+ab),M,8192,0); + size_t nx=(size_t)n*G*K,nl=(size_t)n*G*R,ny=(size_t)n*M; + float *x=malloc(nx*4),*low=malloc(nl*4),*out=malloc(ny*4),*ref=malloc(4096*4);CHECK(x&&low&&out&&ref); + for(size_t i=0;i=32) xv=(float)(_Float16)xv; + double term=(double)xv*coefficient(wr,j)/128.;sum+=term;l1+=fabs(term); + } + float expected=bf16((float)sum),got=low[((size_t)t*G+g)*R+r]; + /* Bound all K accumulation terms, then propagate through the + * monotone BF16 rounding operation. Exact ties can round either + * way after a legal accumulation error; retain their count. */ + double u=FLT_EPSILON/2.,radius=(4096*u/(1-4096*u))*fmax(l1,.001); + if(!isfinite(got)||gotbf16((float)(sum+radius)))++outside_low_interval; + ref[g*R+r]=got; + if(memcmp(&expected,low+((size_t)t*G+g)*R+r,4)) { + if(bad_low<1)fprintf(stderr,"low mismatch t=%u g=%u r=%u sum=%a expected=%a actual=%a\n",t,g,r,sum,(double)expected,(double)low[((size_t)t*G+g)*R+r]); + ++bad_low; + } + } + for(unsigned r=0;r=(rank+1)*4096)continue; + double term=(double)ref[c-rank*4096]*coefficient(r,j)/128.;sum+=term;l1+=fabs(term); + } + double error=fabs(out[(size_t)t*M+r]-sum),u=FLT_EPSILON/2.; + double tolerance=(4096*u/(1-4096*u))*fmax(l1,.001); + max_error=fmax(max_error,error); + if(!isfinite(out[(size_t)t*M+r])||error>tolerance)++bad_out; + } + } + fprintf(stderr,"rows=%u rank=%u cast=%d low=%zu outputs=%zu bad_low=%zu bad_out=%zu max_error=%.17g\n",n,rank,cast_fixture,nl,ny,bad_low,bad_out,max_error); + size_t differs=0; + for(unsigned t=0;t2048||rank>1||cast<0||cast>1)return 2; + fprintf(stderr,"PID=%ld rows=%u rank=%u cast=%d\n",(long)getpid(),n,rank,cast); + if(!ds4_gpu_init()||!run(n,rank,cast)||allocations)return 1; + puts("TP attention independent intervals, baseline low parity and canaries PASS");return 0; +} diff --git a/tests/test_rocm_tp_bind_failure.c b/tests/test_rocm_tp_bind_failure.c new file mode 100644 index 0000000000..45c018886e --- /dev/null +++ b/tests/test_rocm_tp_bind_failure.c @@ -0,0 +1,23 @@ +/* Exercise the actual engine bind's first allocation failure without a model. */ +#include "../ds4.c" +static unsigned calls; +ds4_gpu_tensor *__wrap_ds4_gpu_tensor_alloc_coherent(uint64_t bytes) { + (void)bytes; ++calls; return NULL; +} +int main(void) { + ds4_engine e = {0}; char err[256] = {0}; + /* The forced first-allocation failure must return before touching this + * opaque transport token; no connection or GPU initialization is needed. */ + unsigned char token; + struct ds4_tp *tp = (struct ds4_tp *)(void *)&token; + e.backend = DS4_BACKEND_CUDA; g_ds4_shape = DS4_SHAPE_FLASH41; + if (ds4_engine_tp_bind(&e, NULL, err, sizeof(err)) || calls) return 1; + int rc = ds4_engine_tp_bind(&e, tp, err, sizeof(err)); + int clean = !e.tp.ctx && !e.tp.slab && !e.tp.zero_vec && !e.tp.out_views && !e.tp.in_views && + !e.tp.batch_out_views && !e.tp.batch_in_views && !e.tp.active; + printf("rc=%d coherent_calls=%u clean=%d error=%s\n", rc, calls, clean, err); + /* The pre-fix control leaves these tables allocated. Release evidence-owned memory. */ + free(e.tp.out_views); free(e.tp.in_views); + free(e.tp.batch_out_views); free(e.tp.batch_in_views); + return rc != 0 || calls != 1 || !clean || !strstr(err, "slab allocation failed"); +} diff --git a/tests/test_rocm_tp_gates.c b/tests/test_rocm_tp_gates.c new file mode 100644 index 0000000000..13b3416824 --- /dev/null +++ b/tests/test_rocm_tp_gates.c @@ -0,0 +1,194 @@ +#include "ds4_gpu.h" +#include "../ds4_tp.c" +#include +#include +#include +#include + +static float *cpu_input, *cpu_output; +static int inject_failure; + +#define CHECK(x) do { if (!(x)) { \ + fprintf(stderr, "%s:%d: %s (%s)\n", __FILE__, __LINE__, #x, err); goto done; \ +} } while (0) + +static ds4_gpu_tensor *guard_storage[4]; +static uint64_t guard_bytes[4]; +static unsigned guard_count; +static ds4_gpu_tensor *guarded_alloc(uint64_t bytes, bool coherent) { + if (guard_count == 4 || bytes > UINT64_MAX - 128) return NULL; + ds4_gpu_tensor *storage = coherent ? ds4_gpu_tensor_alloc_coherent(bytes + 128) : + ds4_gpu_tensor_alloc(bytes + 128); + if (!storage) return NULL; + unsigned char guard[64]; memset(guard, 0xa5, sizeof(guard)); + ds4_gpu_tensor *view = NULL; + if (ds4_gpu_tensor_write(storage, 0, guard, 64) && + ds4_gpu_tensor_write(storage, bytes + 64, guard, 64)) + view = ds4_gpu_tensor_view(storage, 64, bytes); + if (!view) { ds4_gpu_tensor_free(storage); return NULL; } + guard_storage[guard_count] = storage; guard_bytes[guard_count++] = bytes; + return view; +} +static int check_guards(void) { + if (!ds4_gpu_synchronize()) return 0; + for (unsigned j=0;j 9) { + fprintf(stderr, "usage: %s RANK COORDINATOR PORT tcp|rdma DEVICE [PORT GID] [fail]\n", argv[0]); + return 2; + } + const int rank = atoi(argv[1]); + if (rank != 0 && rank != 1) return 2; + ds4_tp_options opt = {.role = rank ? DS4_TP_WORKER : DS4_TP_LEADER, + .listen_host = argv[2], .leader_host = argv[2], + .listen_port = atoi(argv[3]), .leader_port = atoi(argv[3]), + .transport = DS4_TP_TRANSPORT_TCP}; + const bool roce = !strcmp(argv[4], "rdma"); + const int base_argc = roce ? 8 : 6; + if ((argc != base_argc && argc != base_argc + 1) || + (!roce && strcmp(argv[4], "tcp"))) return 2; + if (roce) { + opt.transport = DS4_TP_TRANSPORT_RDMA; + opt.rdma_device = argv[5]; opt.rdma_port = atoi(argv[6]); + opt.rdma_gid_index = atoi(argv[7]); opt.rdma_gid_index_set = true; + } + inject_failure = argc == base_argc + 1 && !strcmp(argv[base_argc], "fail"); + ds4_tp_identity id = {.gguf_bytes = 1, .n_layer = 40, .n_embd = 5120, + .n_vocab = 16, .ctx_size = 8192}; + char err[256] = ""; + ds4_tp *tp = NULL; + ds4_gpu_tensor *slab = NULL, *x = NULL, *out = NULL, *in = NULL; + const uint64_t vec = 5120u * sizeof(float), size = 2048u * vec; + int rc = 1; + CHECK(ds4_gpu_init()); + ds4_gpu_set_deepseek41_model(true); + cpu_input = malloc(size); cpu_output = malloc(size); + CHECK(cpu_input && cpu_output); + CHECK(ds4_tp_create(&tp, &opt, &id, err, sizeof(err))); + slab = guarded_alloc(ds4_tp_slab_bytes(40, 5120), true); + x = guarded_alloc(size, false); + out = guarded_alloc(size, false); + in = guarded_alloc(size, false); + CHECK(slab && x && out && in); + memset(ds4_gpu_tensor_contents(slab), 0, ds4_tp_slab_bytes(40, 5120)); + CHECK(ds4_tp_attach_slab(tp, ds4_gpu_tensor_contents(slab), err, sizeof(err))); + CHECK(ds4_gpu_tp_init(rank, slab, ds4_tp_slab_gpu_flags_offset(tp), + ds4_tp_slab_out_offset(tp, 0, 0), vec, exchange, tp)); + ds4_gpu_tp_set_big_exchange(exchange_big); + ds4_gpu_tp_set_batch_exchange(exchange_batch); + float *input = cpu_input; + if (inject_failure) { + CHECK(!small_gates(tp, slab, x, out, 0, rank, false)); + CHECK(ds4_gpu_tp_failed() && ds4_tp_failed(tp)); + CHECK(!ds4_gpu_tp_gate_encode(0, 0)); + CHECK(check_guards()); + fprintf(stderr, "failed gate remains failed; no later completion PASS\n"); + rc = 0; goto done; + } + const uint32_t rows[] = {1, 8, 65, 2048}; + for (unsigned shape = 0; shape < sizeof(rows) / sizeof(*rows); shape++) { + /* Exercise unequal arrival and decode/batch/bulk mode transitions. */ + if (rank == (int)(shape % 2u)) usleep(200000); + CHECK(small_gates(tp, slab, x, out, shape, rank, false)); + CHECK(small_gates(tp, slab, x, out, shape, rank, true)); + const uint32_t count = rows[shape] * 5120u; + for (uint32_t layer = 0; layer < 4; layer++) { + const uint32_t epoch = (shape * 40u + layer) * 37u; + for (uint32_t j = 0; j < count; j++) + input[j] = epoch + j % 127u + rank * 1000; + CHECK(ds4_gpu_tensor_write(x, 0, input, count * sizeof(float))); + CHECK(ds4_gpu_begin_commands()); + /* Incoming storage was just written by the GPU, as with a dead + * activation buffer reused for a TP peer's partial output. */ + CHECK(ds4_gpu_tensor_copy(in, 0, x, 0, count * sizeof(float))); + CHECK(ds4_gpu_add_tensor(out, x, x, count)); + CHECK(ds4_gpu_tp_big_gate_encode(layer, rows[shape], out, in, count * sizeof(float))); + CHECK(ds4_gpu_tp_add_tensor(out, in, x, count)); + CHECK(ds4_gpu_tensor_copy(in, 0, x, 0, count * sizeof(float))); + CHECK(ds4_gpu_tp_big_gate_encode(layer, rows[shape], out, in, count * sizeof(float))); + CHECK(ds4_gpu_tp_add_tensor(out, in, x, count)); + CHECK(ds4_gpu_end_commands() && !ds4_gpu_tp_failed()); + CHECK(ds4_gpu_tensor_read(out, 0, cpu_output, count * sizeof(float))); + const float *actual = cpu_output; + for (uint32_t j = 0; j < count; j++) { + const float expected = 4u * (epoch + j % 127u) + (1 + 2 * rank) * 1000; + if (actual[j] != expected) { + fprintf(stderr, "rank=%d rows=%u layer=%u index=%u expected=%g actual=%g\n", + rank, rows[shape], layer, j, expected, actual[j]); + goto done; + } + } + } + CHECK(check_guards()); + fprintf(stderr, "ROCm decode/verify/bulk GPU reuse rank=%d rows=%u: exact PASS\n", rank, rows[shape]); + } + rc = 0; +done: + if (ds4_gpu_commands_active()) ds4_gpu_end_commands(); + ds4_gpu_tp_shutdown(); + ds4_tp_free(tp); + ds4_gpu_tensor_free(in); ds4_gpu_tensor_free(out); ds4_gpu_tensor_free(x); + ds4_gpu_tensor_free(slab); + for(unsigned i=0;i +#include +#include +#include +#include +#include + +#define CHECK(x) do { if (!(x)) { fprintf(stderr,"line %d: %s (%s)\n",__LINE__,#x,err);goto done; } } while(0) +static char err[512]; +static const char *output; +static unsigned captures; +static int vocab; +static float *left,*right; +static FILE *record; +static int save(const char *name,const void *p,size_t n) { + char path[4096];if(snprintf(path,sizeof(path),"%s/%s",output,name)>=(int)sizeof(path))return 0; + FILE *f=fopen(path,"wb");if(!f)return 0; + int ok=fwrite(p,1,n,f)==n;if(fclose(f))ok=0;return ok; +} +static int capture(ds4_session *s,float *v,const char *name) { + unsigned char *raw=(unsigned char *)v-64;memset(raw,0xa5,(size_t)vocab*4+128); + for(int j=0;jlen!=ds4_session_pos(s))return 0; + snprintf(path,sizeof(path),"%03u-%s.tokens",captures,name); + if(!save(path,t->v,(size_t)t->len*sizeof(*t->v)))return 0; + for(int j=0;jlen==tb->len&&!memcmp(ta->v,tb->v,(size_t)ta->len*4); + size_t mismatches=0;double max=0; + for(int j=0;jmax)max=d;} + fprintf(record,"%u\t%s\t%d\t%d\t%zu\t%.17g\n",captures,phase,ta->len,tokens,mismatches,max);fflush(record); + fprintf(stderr,"state phase=%s pos=%d history=%s logit_mismatches=%zu max=%g\n",phase,ta->len,tokens?"PASS":"FAIL",mismatches,max); + return tokens&&!mismatches; +} +typedef struct {int stop_at;unsigned events;bool stopped;} interruption; +static void progress(void *ud,const char *event,int current,int total) { + (void)event;(void)total;interruption *p=ud;p->events++;if(current>=p->stop_at)p->stopped=true; +} +static bool cancelled(void *ud) {return ((interruption *)ud)->stopped;} +int main(int argc,char **argv) { + if(argc!=8){fprintf(stderr,"usage: %s MODEL PROMPT OUT tcp|rdma DEVICE LISTEN PORT\n",argv[0]);return 2;} + int rc=1;ds4_engine *engine=NULL;ds4_tp *tp=NULL;ds4_session *control=NULL,*subject=NULL; + ds4_tokens prompt={0};ds4_session_snapshot snap={0},decoded={0};char *text=NULL;FILE *f=NULL; + output=argv[3]; + ds4_engine_options opt={.model_path=argv[1],.backend=DS4_BACKEND_CUDA,.context_size=16384, + .power_percent=100,.placement_session_count_hint=2}; + opt.tp=(ds4_tp_options){.requested=true,.role=DS4_TP_LEADER,.listen_host=argv[6],.listen_port=atoi(argv[7])}; + if(!strcmp(argv[4],"tcp"))opt.tp.transport=DS4_TP_TRANSPORT_TCP; + else if(!strcmp(argv[4],"rdma")){opt.tp.transport=DS4_TP_TRANSPORT_RDMA;opt.tp.rdma_device=argv[5];opt.tp.rdma_port=1;opt.tp.rdma_gid_index=1;opt.tp.rdma_gid_index_set=true;} + else return 2; + CHECK(mkdir(output,0700)==0); + char path[4096];snprintf(path,sizeof(path),"%s/comparisons.tsv",output);record=fopen(path,"w");CHECK(record); + fprintf(record,"capture\tphase\tpos\thistory_equal\tlogit_mismatches\tmax_delta\n"); + f=fopen(argv[2],"rb");CHECK(f);CHECK(!fseek(f,0,SEEK_END));long n=ftell(f);CHECK(n>0&&n<100000000);rewind(f); + text=malloc((size_t)n+1);CHECK(text);CHECK(fread(text,1,n,f)==(size_t)n);text[n]=0;fclose(f);f=NULL; + CHECK(ds4_engine_open(&engine,&opt)==0); + ds4_tp_identity id={.gguf_bytes=ds4_engine_model_bytes(engine),.model_id=(uint32_t)ds4_engine_model_id(engine), + .n_layer=(uint32_t)ds4_engine_layer_count(engine),.n_embd=(uint32_t)ds4_engine_embd_dim(engine), + .n_vocab=(uint32_t)ds4_engine_vocab_size(engine),.quant_bits=(uint32_t)ds4_engine_routed_quant_bits(engine),.ctx_size=16384}; + ds4_engine_tp_gate_schedule(engine,&id.gate_slot_start,&id.gate_slot_step,&id.gates_per_token,id.gate_slot_mask); + CHECK(ds4_tp_create(&tp,&opt.tp,&id,err,sizeof(err)));CHECK(ds4_engine_tp_bind(engine,tp,err,sizeof(err))); + ds4_tokenize_text(engine,text,&prompt);CHECK(prompt.len>8194);free(text);text=NULL; + vocab=ds4_engine_vocab_size(engine);CHECK(vocab==129280); + unsigned char *a=malloc((size_t)vocab*4+128),*b=malloc((size_t)vocab*4+128); + left=a?(float *)(a+64):NULL;right=b?(float *)(b+64):NULL;CHECK(left&&right); + CHECK(!ds4_session_create(&control,engine,16384));CHECK(!ds4_session_create(&subject,engine,16384)); + const int lengths[]={31,32,255,256,1025,4097}; + for(unsigned i=0;i + +/* The model-free command parser only needs the image allocation destructor. */ +void ds4_vision_embedding_free(ds4_vision_embedding *embedding) { + if (!embedding) return; + free(embedding->data); + memset(embedding, 0, sizeof(*embedding)); +} + +typedef struct { + ds4_tp tp; + unsigned char *out, *in; + uint64_t bytes, seq; + unsigned kind; + int ok; +} peer; + +static void *exchange(void *opaque) { + peer *p = opaque; + p->ok = tp_linux_gate(&p->tp, p->kind, 39, 1, p->seq, + p->out, p->in, p->bytes, 0); + return NULL; +} + +static void socket_pair(int fd[2]) { + assert(!socketpair(AF_UNIX, SOCK_STREAM, 0, fd)); + for (int i = 0; i < 2; ++i) { + int small = 1024; + assert(!setsockopt(fd[i], SOL_SOCKET, SO_SNDBUF, &small, sizeof(small))); + assert(!fcntl(fd[i], F_SETFL, O_NONBLOCK)); + } +} + +static void transfers(void) { + const uint64_t sizes[] = {1, 3, 20480, 8*20480, 2*1024*1024-1, + 2*1024*1024, 2*1024*1024+1, 7*1024*1024+3}; + for (unsigned n = 0; n < sizeof(sizes)/sizeof(*sizes); ++n) { + int fd[2]; + socket_pair(fd); + peer p[2] = {0}; + for (unsigned rank = 0; rank < 2; ++rank) { + p[rank].tp = (ds4_tp){.data_fd=fd[rank], .epoch=12345, .n_layer=40, .gate_timeout_ms=5000}; + atomic_init(&p[rank].tp.failed, false); + p[rank].bytes=sizes[n]; p[rank].seq=7; p[rank].kind=3; + p[rank].out=malloc(sizes[n]); p[rank].in=malloc(sizes[n]+2); + assert(p[rank].out && p[rank].in); + memset(p[rank].in, 0xa5, sizes[n]+2); + ++p[rank].in; + for (uint64_t j=0; jok=tp_hello_exchange(&p->tp,&p->id,0,p->err,sizeof(p->err)); + return NULL; +} +static void handshakes(void) { + for (unsigned mode=0;mode<5;++mode) { + int fd[2];socket_pair(fd); + hello_peer p[2]={0}; + for (unsigned r=0;r<2;++r) { + p[r].tp.control_fd=fd[r]; + p[r].tp.timeout_sec=1; + p[r].tp.opt.role=r?DS4_TP_WORKER:DS4_TP_LEADER; + p[r].id=(ds4_tp_identity){.gguf_bytes=999,.model_id=41,.n_layer=40, + .n_embd=5120,.n_vocab=129280,.quant_bits=2,.ctx_size=8192}; + } + if (mode==1) p[1].id.n_embd++; + if (mode==2) p[1].tp.opt.transport=DS4_TP_TRANSPORT_RDMA; + if (mode>=3) { + /* Reject legacy12 and CUDA/old-ROCm14 before reading a nonce or + * backend-specific frames, even if the peer keeps the socket open. */ + ds4_tp_hello_fixed old={.magic=DS4_TP_MAGIC,.version=mode==3?12:14}; + assert(write(fd[1], &old, offsetof(ds4_tp_hello_fixed,nonce))== + offsetof(ds4_tp_hello_fixed,nonce)); + double start=tp_now_sec();hello(&p[0]); + assert(!p[0].ok && strstr(p[0].err,"protocol version")); + assert(tp_now_sec()-start<.5); + } else { + pthread_t t;assert(!pthread_create(&t,NULL,hello,&p[1])); + hello(&p[0]);assert(!pthread_join(t,NULL)); + assert(p[0].ok==(mode==0) && p[1].ok==(mode==0)); + if (!mode) { + assert(p[0].tp.epoch==p[1].tp.epoch); + assert(p[0].tp.vec_bytes==20480 && p[1].tp.vec_bytes==20480); + } + } + close(fd[0]);close(fd[1]); + } +} +static void *cancel_exchange(void *opaque) { + peer *p=opaque; + usleep(20000); + ds4_tp_mark_failed(&p->tp); + return NULL; +} +static void cancellation(void) { + int fd[2];socket_pair(fd); + unsigned char out=1,in=0xa5; + peer p={.tp={.data_fd=fd[0],.n_layer=40,.gate_timeout_ms=5000}, + .out=&out,.in=&in,.bytes=1,.seq=1,.kind=3}; + pthread_t t;assert(!pthread_create(&t,NULL,cancel_exchange,&p)); + double start=tp_now_sec();exchange(&p); + assert(!pthread_join(t,NULL)); + assert(!p.ok && ds4_tp_failed(&p.tp) && in==0xa5); + assert(tp_now_sec()-start<.5); + close(fd[0]);close(fd[1]); +} + +typedef struct { + ds4_tp tp; + const unsigned char *data; + size_t bytes; + int mode; +} restore_peer; + +static void *restore_receive(void *opaque) { + restore_peer *p = opaque; + char err[256] = {0}; + ds4_tp_command command; + assert(ds4_tp_recv_command(&p->tp, &command, err, sizeof(err))); + assert(command.type == DS4_TP_FRAME_RESTORE_PAYLOAD && + command.session_id == 42 && command.payload_bytes == p->bytes); + ds4_tp_command_free(&command); + ds4_tp_payload_reader reader = {&p->tp, p->bytes, tp_now_sec() + 2}; + cookie_io_functions_t io = {.read = tp_payload_read}; + FILE *fp = fopencookie(&reader, "rb", io); + assert(fp); + unsigned char *storage = malloc(p->bytes + 2); + assert(storage); memset(storage, 0xa5, p->bytes + 2); + unsigned char *data = storage + 1; + /* A small first read exercises stdio read-ahead; the cookie must never + * consume bytes belonging to the following control frame. */ + assert(fread(data, 1, 17, fp) == 17); + size_t got = fread(data + 17, 1, p->bytes - 17, fp); + if (p->mode == 1) { + assert(got < p->bytes - 17 && ferror(fp) && ds4_tp_failed(&p->tp)); + } else { + assert(got == p->bytes - 17 && !memcmp(data, p->data, p->bytes)); + assert(fgetc(fp) == EOF && !ferror(fp) && !reader.remaining); + assert(ds4_tp_send_command_ack(&p->tp, 42, p->mode == 2)); + assert(ds4_tp_recv_command(&p->tp, &command, err, sizeof(err))); + assert(command.type == DS4_TP_FRAME_STOP); + ds4_tp_command_free(&command); + } + assert(storage[0] == 0xa5 && storage[p->bytes + 1] == 0xa5); + fclose(fp); free(storage); + return NULL; +} + +static void checkpoint_streams(void) { + const size_t bytes = 3 * 65536 + 9; + unsigned char *data = malloc(bytes); assert(data); + for (size_t i = 0; i < bytes; ++i) data[i] = (unsigned char)(i * 37 + 11); + for (int mode = 0; mode < 3; ++mode) { + int fd[2]; socket_pair(fd); + for (int i = 0; i < 2; ++i) { + assert(!fcntl(fd[i], F_SETFL, 0)); + struct timeval limit = {.tv_sec = 2}; + assert(!setsockopt(fd[i], SOL_SOCKET, SO_RCVTIMEO, &limit, sizeof(limit))); + assert(!setsockopt(fd[i], SOL_SOCKET, SO_SNDTIMEO, &limit, sizeof(limit))); + } + ds4_tp leader = {.control_fd = fd[0], .rank = 0, .timeout_sec = 2}; + restore_peer worker = {.tp = {.control_fd = fd[1], .rank = 1}, + .data = data, .bytes = bytes, .mode = mode}; + atomic_init(&leader.failed, false); atomic_init(&worker.tp.failed, false); + pthread_t thread; assert(!pthread_create(&thread, NULL, restore_receive, &worker)); + FILE *fp = fmemopen(data, bytes - (mode == 1), "rb"); assert(fp); + char err[256] = {0}; + assert(ds4_tp_send_restore_payload(&leader, 42, fp, bytes, err, sizeof(err)) == (mode == 0)); + assert(ds4_tp_failed(&leader) == (mode == 1)); + if (mode == 1) assert(!shutdown(fd[0], SHUT_WR)); + else assert(ds4_tp_send_stop(&leader)); + fclose(fp); assert(!pthread_join(thread, NULL)); + close(fd[0]); close(fd[1]); + } + free(data); +} + +int main(void) { + assert(DS4_TP_PROTOCOL_VERSION==15); + negotiation(); handshakes(); transfers(); failures(); cancellation(); checkpoint_streams(); + puts("Linux TP: negotiation, full-duplex TCP I/O, tails, canaries, generations and failures PASS"); + return 0; +} diff --git a/tests/test_tp_linux_pair.c b/tests/test_tp_linux_pair.c new file mode 100644 index 0000000000..663ff75985 --- /dev/null +++ b/tests/test_tp_linux_pair.c @@ -0,0 +1,42 @@ +/* Manual, model-free two-host transport qualification. No GPU or GGUF access. + * rank address port tcp|rdma device-or-dash [port gid-index] */ +#define DS4_ROCM_BUILD 1 +#include "../ds4_tp.c" + +int main(int argc, char **argv) { + if (argc != 6 && argc != 8) return 2; + int rank=atoi(argv[1]), port=atoi(argv[3]); + if ((rank!=0 && rank!=1) || port<1 || port>65535) return 2; + ds4_tp_options opt={.role=rank?DS4_TP_WORKER:DS4_TP_LEADER, + .listen_host=argv[2],.leader_host=argv[2],.listen_port=port,.leader_port=port}; + if (!strcmp(argv[4],"tcp")) opt.transport=DS4_TP_TRANSPORT_TCP; + else if (!strcmp(argv[4],"rdma") && argc==8) { + opt.transport=DS4_TP_TRANSPORT_RDMA;opt.rdma_device=argv[5]; + opt.rdma_port=atoi(argv[6]);opt.rdma_gid_index=atoi(argv[7]);opt.rdma_gid_index_set=true; + } else return 2; + ds4_tp_identity id={.gguf_bytes=1,.model_id=41,.n_layer=40,.n_embd=5120, + .n_vocab=129280,.quant_bits=2,.ctx_size=65536}; + ds4_tp *tp=NULL;char err[512]; + if (!ds4_tp_create(&tp,&opt,&id,err,sizeof(err))) { + fprintf(stderr,"create failed: %s\n",err);return 1; + } + uint32_t rows[]={1,2,8,32,65,256,2033,2048}; + const size_t capacity=2048*20480; + unsigned char *out=malloc(capacity),*storage=malloc(capacity+2),*in=storage?storage+1:NULL; + int ok=out&&storage; + for (unsigned n=0;ok && n