2026-04-08 run_0408 测试档案

7 组正式 case,全部在 DeepSeek-V3.2(685B 参数,含 MTP head)上跑过。

每组 case 的 input、output、TPOT 图直接展示如下;原始日志和逐 token CSV 见末尾汇总表。


01-hello(极短问候)

input_lenoutput_lenttftprefilltpotdecode
51680.559 s8.946 tok/s0.073 s13.659 tok/s

01-hello TPOT

input(5 tokens)
<|begin▁of▁sentence|><|User|>你好<|Assistant|><think>
output(168 tokens)
哦,用户只发了一句简单的问候。这种情况很常见,可能是想测试回复速度,或者刚接触AI助手。不需要复杂分析,直接友好回应就行。

可以用热情但不过度的开场白,加上开放式的服务邀请。想到要说明自己的功能和特点,让用户知道能提供哪些帮助。结尾加个表情符号显得更亲切。

需要注意保持中立客观的语气,避免过度承诺。用“我会尽我所能”这样的表述比较合适。</think>你好!很高兴见到你!😊 我是DeepSeek,由深度求索公司创造的AI助手。

无论你有什么问题、需要什么帮助,或者只是想聊聊天,我都很乐意为你服务!我可以帮你解答问题、协助思考、处理文本、分析文档等等。

有什么我可以为你做的吗?我会尽我所能热情地帮助你!✨<|end▁of▁sentence|>

02-prime-2k(质数查询 prompt,长输出)

input_lenoutput_lenttftprefilltpotdecode
1120370.861 s12.775 tok/s0.062 s16.171 tok/s

02-prime-2k TPOT

input(11 tokens)
<|begin▁of▁sentence|><|User|>104857601是质数吗<|Assistant|><think>
output(2037 tokens)
我们被问到:“104857601是质数吗?” 我们需要检查这个数字是否是质数。注意:1048576 = 2^20。实际上,2^20 = 1048576。所以 104857601 = 1048576 * 100 + 1?不完全是:104857601 可能等于 2^20 * 100 + 1?2^20 = 1,048,576。乘以100得到104,857,600,然后加1得到104,857,601。所以 104857601 = 100 * 2^20 + 1。但这不是一个标准形式。也许它与费马数或梅森数有关?2^20 + 1?2^20 + 1 = 1048577,但我们的数字是104857601,它更大。也许它是 2^20 * 100 + 1 = 2^20 * (100) + 1。或者可能是 2^20 * 100 + 1。但100是10^2。所以它是 2^20 * 10^2 + 1 = (2^10 * 10)^2 + 1?2^10=1024,乘以10=10240,平方=104857600,然后加1得到104857601。所以 104857601 = (10240)^2 + 1。所以它是形式为 n^2+1,其中 n=10240。所以问题是:n^2+1 是质数吗?对于 n=10240,我们需要检查 10240^2+1 是否是质数。

注意:n^2+1 形式的数可以是质数,但并非总是。对于 n=1, 1^2+1=2 是质数;n=2, 4+1=5;n=4, 16+1=17;n=6, 36+1=37;n=10, 100+1=101;等等。但 n=10240 可能很大。我们需要测试 104857601 的素性。

我们可以尝试用一些小的质数进行试除。首先,计算平方根:sqrt(104857601) ≈ 10240.0?实际上,因为 10240^2 = 104857600,所以 sqrt(104857601) = sqrt(10240^2+1) ≈ 10240.0000488。所以我们需要测试所有小于等于 10241 的质数。但 10241 大约是 10000,所以我们需要测试最多 10000 左右的质数。这可行,但手工做很繁琐。也许我们可以找到一些模条件。

注意:104857601 是奇数,所以不能被2整除。数字和:1+0+4+8+5+7+6+0+1 = 32,不能被3整除,所以不能被3整除。除以5?以01结尾,所以不能被5整除。除以7?104857601 ÷ 7?我们来计算。

我们可以使用模运算。或者注意到这个数字可能与已知的质数有关。也许它是 2^25 + 1?2^25 = 33554432,乘以3?不对。

让我们因式分解 n^2+1。对于偶数 n,n^2+1 是奇数,并且可能有一些代数因式分解吗?在整数中,n^2+1 是不可约的,除非 n 是某些特定值。但我们需要检查是否有质因数。

我们可以尝试检查小质数。从 p=7 开始。

检查除以7:104857601 mod 7。计算:7*14979657 = 104857599?实际上,7*14979657 = 7*15000000 = 105000000,减去 7*20343 = 142401,所以 105000000 - 142401 = 104857599。所以 104857601 - 104857599 = 2,所以余数为2。所以不能被7整除。

下一个质数:11。104857601 mod 11。我们可以交替求和:数字交替符号和:1-0+4-8+5-7+6-0+1 = (1+4+5+6+1) - (0+8+7+0) = 17 - 15 = 2。所以余数为2 mod 11?实际上,如果交替和是2,那么原数 mod 11 等于 2 mod 11。所以不能被11整除。

13:104857601 mod 13。计算:13 * 8065970 =?也许更快:104857601 ÷ 13。13*8065970 = 13*8000000=104000000,13*65970=857610,总和 104857610,太大了9。所以 13*8065969 = 104857610 - 13 = 104857597?实际上,104857610 - 13 = 104857597。所以 104857601 - 104857597 = 4,所以余数为4。不能被13整除。

17:104857601 mod 17。计算:17 * 6168094 =?17*6000000=102000000,17*168094= 2857598,总和 102000000+2857598=104857598。所以 104857601 - 104857598 = 3,余数3。

19:104857601 mod 19。19*5518821 =?19*5500000=104500000,19*18821=357599,总和 104500000+357599=104857599。所以余数2。

23:104857601 mod 23。23*4559026 =?23*4559026:23*4500000=103500000,23*59026=1357598,总和 103500000+1357598=104857598。余数3。

29:104857601 mod 29。29*3615786 =?29*3615786:29*3600000=104400000,29*15786=457794,总和 104400000+457794=104857794,太大了193。所以尝试 29*3615785 = 104857794 - 29 = 104857765,仍然大于164?104857601 - 104857765 = -164,所以 29*3615785 太大了。实际上,104857601 ÷ 29 ≈ 3615786.24,所以 29*3615786 = 如上计算 104857794?我们算错了。重新计算:29 * 3615786。更好的方法:使用模运算。104857601 ÷ 29。29 * 3615786:计算 30*3615786 = 108473580,减去 3615786 = 104857794。所以确实是 104857794。所以 29*3615786 = 104857794,比104857601大193。所以 29*3615785 = 104857794 - 29 = 104857765,仍然大164。29*3615784 = 104857765 - 29 = 104857736,大135。29*3615783 = 104857736 - 29 = 104857707,大106。29*3615782 = 104857707 - 29 = 104857678,大77。29*3615781 = 104857678 - 29 = 104857649,大48。29*3615780 = 104857649 - 29 = 104857620,大19。29*3615779 = 104857620 - 29 = 104857591。所以 29*3615779 = 104857591。然后 104857601 - 104857591 = 10,所以余数10。不能被29整除。

31:104857601 mod 31。31*3382503 =?31*3382503:31*3300000=102300000,31*82503=2557593,总和 102300000+2557593=104857593。所以 104857601 - 104857593 = 8,余数8。

37:104857601 mod 37。37*2833989 =?37*2833989:37*2800000=103600000,37*33989=1257593,总和 103600000+1257593=104857593。余数8。

41:104857601 mod 41。41*2557502 =?41*2557502:41*2500000=102500000,41*57502=2357582,总和 102500000+2357582=104857582。余数19。

43:104857601 mod 43。43*2438544 =?43*2438544:43*2400000=103200000,43*38544=1657392,总和 103200000+1657392=104857392。余数209?104857601-104857392=209。209除以43=4*43=172,余数37。所以余数37?实际上,209 mod 43 = 209 - 4*43 = 209-172=37。所以原数 mod 43 = 37。

47:104857601 mod 47。47*2231012 =?47*2231012:47*2200000=103400000,47*31012=1457564,总和 103400000+1457564=104857564。余数37。

53:

03-count-2k(直接输出 1 到 1000)

input_lenoutput_lenttftprefilltpotdecode
1420341.001 s13.986 tok/s0.061 s16.294 tok/s

03-count-2k TPOT

input(14 tokens)
<|begin▁of▁sentence|><|User|>直接输出1到1000,不要写程序<|Assistant|><think>
output(2034 tokens)
我们被要求直接输出从1到1000的数字,而不是编写程序。这意味着我们需要生成一个包含从1到1000的每个数字的文本,可能每个数字占一行。由于这是一个文本输出任务,我们需要生成一个很长的文本。但注意:在上下文中,这可能意味着我们只需要提供输出结果。然而,输出1到1000的所有数字会非常长,可能不切实际。但既然指令是“直接输出”,我们需要遵守。

不过,在AI对话中,直接输出1000行数字可能会占用大量空间,而且可能不是用户真正的意图。用户可能是在测试我们是否理解指令,或者可能需要我们提供一个生成这些数字的方法。但指令明确说“不要写程序”,所以我们应该直接输出数字。

考虑到输出长度,或许我们可以提供一个简洁的版本,比如用逗号分隔,但指令没有指定格式。通常,这类问题可能期望每行一个数字。

为了平衡,我可以输出从1到1000的数字,每个数字一行,但这样会非常长。在响应中,这样的输出可能会被截断或影响阅读。也许我可以输出一个范围表示,但那样就不是“直接输出”了。

再思考:用户说“直接输出1到1000”,可能意味着在对话中直接显示这些数字。既然这是一个文本界面,我可以生成包含所有数字的文本。

但为了实用,我将输出从1到1000的数字,每个数字一行,但限于篇幅,可能只输出一部分?不,那样不完整。

或许用户想要的是连续的数字序列,如“1 2 3 ... 1000”。但指令是“输出”,不是“描述”。

我决定:既然这是一个AI,我可以生成一个很长的文本。我将生成从1到1000的数字,每个数字一行。但为了节省空间,我可以每行放多个数字,比如每行10个。

但指令没有指定格式,所以我可以选择一种紧凑的格式。

我将输出从1到1000的数字,每行10个,用空格分隔。这样有100行,比较紧凑。

开始输出:</think>1 2 3 4 5 6 7 8 9 10
11 12 13 14 15 16 17 18 19 20
21 22 23 24 25 26 27 28 29 30
31 32 33 34 35 36 37 38 39 40
41 42 43 44 45 46 47 48 49 50
51 52 53 54 55 56 57 58 59 60
61 62 63 64 65 66 67 68 69 70
71 72 73 74 75 76 77 78 79 80
81 82 83 84 85 86 87 88 89 90
91 92 93 94 95 96 97 98 99 100
101 102 103 104 105 106 107 108 109 110
111 112 113 114 115 116 117 118 119 120
121 122 123 124 125 126 127 128 129 130
131 132 133 134 135 136 137 138 139 140
141 142 143 144 145 146 147 148 149 150
151 152 153 154 155 156 157 158 159 160
161 162 163 164 165 166 167 168 169 170
171 172 173 174 175 176 177 178 179 180
181 182 183 184 185 186 187 188 189 190
191 192 193 194 195 196 197 198 199 200
201 202 203 204 205 206 207 208 209 210
211 212 213 214 215 216 217 218 219 220
221 222 223 224 225 226 227 228 229 230
231 232 233 234 235 236 237 238 239 240
241 242 243 244 245 246 247 248 249 250
251 252 253 254 255 256 257 258 259 260
261 262 263 264 265 266 267 268 269 270
271 272 273 274 275 276 277 278 279 280
281 282 283 284 285 286 287 288 289 290
291 292 293 294 295 296 297 298 299 300
301 302 303 304 305 306 307 308 309 310
311 312 313 314 315 316 317 318 319 320
321 322 323 324 325 326 327 328 329 330
331 332 333 334 335 336 337 338 339 340
341 342 343 344 345 346 347 348 349 350
351 352 353 354 355 356 357 358 359 360
361 362 363 364 365 366 367 368 369 370
371 372 373 374 375 376 377 378 379 380
381 382 383 384 385 386 387 388 389 390
391 392 393 394 395 396 397 398 399 400
401 402 403 404 405 406 407 408 409 410
411 412 413 414 415 416 417 418 419 420
421 422 423 424 425 426 427 428 429 430
431 432 433 434 435 436 437 438 439 440
441 442 443 444 445 446 447 448 449 450
451 452 453 454 455 456 457 458 459 460
461 462 463 464 465 466 467 468 469 470
471 472 473 474 475 476 477 478 479 480
481 482 483 484 485 486 487 488 489 490
491 492 493 494 495 496 497 498 499 500
501 502 503 504 505 506 507 508 509 510
511 512 513 514 515 516 517 518 519 520
521 522 523 524 525 526 527 528 529 530
531 532 533 534 535 536 537 538 539 540
541 542 543 544 545 546 547 548 549 550
551 552 553 554 555 556 557 558 559 560
561 562 563 564 565 566 567 568 569 570
571 572 573 574 575 576 577 578 579 580
581 582 583 584 585 586 587 588 589 590
591 592 593 594 595 596 597 598 599 600
601 602 603 604 605 606 607 608 609 610
611 612 613 614 615 616 617 618 619 620
621 622 623 624 625 626 627 628 629 630
631 632 633 634 635 636 637 638 639 640
641 642 643 644 645 646 647 648 649 650
651 652 653 654 655 656 657 658 659 660
661 662 663 664 665 666 667 668 669 670
671 672 673 674 675 676 677 678 679 680
681 682 683 684 685 686 687 688 689 690
691 692 693 694 695 696 697 698 699 700
701 702 703 704 705 706 707 708 709 710
711 712 713 714 715 716 717 718 719 720
721 722 723 724 725 726 727 728 729 730
731 732 733 734 735 736 737 738 739 740
741 742 743 744 745 746 747 748 749 750
751 752 753 754 755 756 757 758 759 760
761 762 763 764 765 766 767 768 769 770
771 772 773 774 775 776 777 778 779 780
781 782 783 784 785 786 787 788 789 790
791 792 793 794 795 796 797 798 799 800
801 802 803 804

04-parallelism-qa-2k(LLM 推理并行策略问答)

input_lenoutput_lenttftprefilltpotdecode
3520132.104 s16.638 tok/s0.066 s15.095 tok/s

04-parallelism-qa-2k TPOT

input(35 tokens)
<|begin▁of▁sentence|><|User|>我要部署一个 bsz=1 的 LLM 推理服务,请详细讲解 dp, tp, pp, ep 的区别,以及是否适用<|Assistant|><think>
output(2013 tokens)
我们讨论的是大规模语言模型(LLM)推理服务部署时的并行策略。在分布式训练和推理中,常见的并行方式有数据并行(Data Parallelism, DP)、张量并行(Tensor Parallelism, TP)、流水线并行(Pipeline Parallelism, PP)和专家并行(Expert Parallelism, EP)。下面我将详细解释这些概念,并讨论在批大小(batch size, bsz)为1的推理场景下它们的适用性。

## 1. 数据并行 (DP)
- **原理**:将训练数据分割成多个子集,每个设备上有一个完整的模型副本,各自处理一个子集的数据,然后同步梯度。
- **推理场景**:在推理时,如果批大小大于1,可以将不同的数据样本分发到不同设备上,每个设备独立进行前向传播。但是,当批大小为1时,数据并行无法提供任何加速,因为只有一个样本需要处理,无法分割到多个设备上。因此,在bsz=1的推理中,数据并行不适用。

## 2. 张量并行 (TP)
- **原理**:将模型的层内的权重矩阵进行分割,分布到多个设备上。每个设备只拥有权重的一部分,通过通信来组合结果。例如,一个线性层可以按列分割,每个设备计算一部分输出,然后通过AllReduce或AllGather等操作合并。
- **推理场景**:在bsz=1的推理中,张量并行是适用的。因为它是将模型本身进行分割,而不是依赖于批量大小。通过将模型参数分布到多个设备上,可以减少每个设备的内存占用,并利用多个设备同时计算一个样本的前向传播。但是,张量并行会引入设备间的通信开销,因此需要权衡模型大小和通信延迟。

## 3. 流水线并行 (PP)
- **原理**:将模型按层分成多个阶段,每个阶段放在不同的设备上。在训练时,通过微批次(micro-batch)来保持流水线的饱和。在推理时,对于单个样本,流水线并行会将这个样本依次经过各个阶段,不同阶段在不同设备上同时处理不同的样本(如果批量大于1)。但是,当bsz=1时,流水线并行的效率会很低,因为只有一个样本在流水线上,无法充分利用多个设备(只有一个设备在计算,其他设备在等待)。
- **推理场景**:在bsz=1的推理中,流水线并行不适用,因为它无法通过多个样本来填充流水线,导致设备利用率低,延迟可能反而增加(由于通信开销)。

## 4. 专家并行 (EP)
- **原理**:主要用于混合专家模型(Mixture of Experts, MoE)。在MoE模型中,每一层包含多个专家(通常是前馈网络),每个样本根据路由权重选择一部分专家进行计算。专家并行将不同的专家分布到不同的设备上,每个设备只负责一部分专家。
- **推理场景**:在bsz=1的推理中,如果模型是MoE架构,专家并行是适用的。因为单个样本可能只需要激活部分专家,可以将这些专家计算分布到多个设备上。但是,如果模型不是MoE,则专家并行不适用。

## 总结与建议
对于bsz=1的LLM推理服务,我们需要考虑如何利用多个设备来加速单个样本的推理,或者减少单个设备的内存压力。

- **数据并行**:不适用,因为无法分割单个样本。
- **张量并行**:适用,可以将模型参数分布到多个设备上,加速单个样本的推理,但通信开销需考虑。
- **流水线并行**:不适用,因为单个样本无法填充流水线,导致设备闲置。
- **专家并行**:仅当模型是MoE架构时适用。

在实际部署中,如果模型太大无法放入单个设备的内存,通常会结合使用张量并行和流水线并行。但在bsz=1的推理场景下,流水线并行的效率问题需要重新考虑。一种常见的做法是使用张量并行来分割模型,而避免使用流水线并行。如果模型是MoE,则可以考虑专家并行。

此外,还可以考虑模型并行(Model Parallelism)的其他形式,例如将不同的层分配到不同的设备上(类似于流水线并行,但针对单个样本,我们可以使用更细粒度的划分来减少闲置时间,但这需要复杂的调度)。实际上,在bsz=1时,更常用的策略是使用张量并行,或者将模型放在单个设备上(如果可能),以避免通信开销。

最后,请注意,在推理时,我们还可以使用诸如量化、内核优化、动态批处理(如果允许累积请求)等技术来提高效率。但是,对于严格的bsz=1(即每次只处理一个请求的一个样本),动态批处理不适用。

根据你的具体模型大小、设备数量、设备间通信带宽以及延迟要求,可以选择合适的并行策略。例如,如果模型较小,可以放在单个GPU上;如果模型较大,可以使用张量并行在多个GPU上分布模型。</think>我将详细讲解在大模型推理中常见的四种并行策略:DP、TP、PP、EP,并分析它们在批大小(bsz=1)的推理场景下的适用性。

## 一、四种并行策略详解

### 1. **数据并行(Data Parallelism, DP)**
- **原理**:将输入数据分割到多个设备,每个设备都有完整的模型副本,独立进行前向计算
- **通信模式**:前向传播无需通信,反向传播需要AllReduce同步梯度
- **推理场景**:
  - 训练中常用,可加速训练过程
  - **推理时**:如果bsz>1,可以将batch分到不同设备
  - **bsz=1时**:无法分割单个样本,DP不适用

### 2. **张量并行(Tensor Parallelism, TP)**
- **原理**:将模型的单个层(如Transformer的MLP或Attention)的权重矩阵水平或垂直切分到多个设备
- **通信模式**:每层都需要AllReduce或AllGather通信
- **典型实现**:Megatron-LM的切分方式
- **适用场景**:
  - 适合单个设备内存不足的大模型
  - 通信开销较大,适合设备间高速互联(如NVLink)

### 3. **流水线并行(Pipeline Parallelism, PP)**
- **原理**:将模型按层切分到多个设备,形成流水线
- **通信模式**:相邻设备间传递激活值(前向)和梯度(反向)
- **推理特点**:
  - 需要多个样本填充流水线才能达到高利用率
  - **bsz=1时**:流水线效率极低,存在大量设备空闲时间(bubble)

### 4. **专家并行(Expert Parallelism, EP)**
- **原理**:专为MoE(Mixture of Experts)模型设计,将不同专家分布到不同设备
- **通信模式**:需要All-to-All通信进行路由
- **适用场景**:
  - 仅适用于MoE架构模型(如Switch Transformer、Mixtral)
  - 每个token只激活部分专家,可节省计算

## 二、bsz=1推理场景下的适用性分析

### **推荐方案**:
```
bsz=1推理 → 优先考虑TP,避免PP,DP不适用
```

### **详细对比**:

| 并行策略 | bsz=1适用性 | 原因 | 建议 |
|---------|------------|------|------|
| **DP** | ❌ 不适用 | 无法分割单个样本 | 不应使用 |
| **TP** | ✅ 适用 | 可切分模型参数,降低单卡内存 | **推荐使用**,但需考虑通信开销 |
| **PP** | ⚠️ 不推荐 | 流水线气泡严重,设备利用率低 | 尽量避免,除非模型极大 |
| **EP** | ✅ 条件适用 | 仅MoE模型可用 | 如果是MoE架构则推荐 |

### **组合策略建议**:

1. **纯TP策略**(最推荐):
   ```python
   # 例如:将70B模型用TP=8部署在8张A100上
   # 每张卡只需存储约8.75B参数
   ```

2. **TP+EP组合**(MoE模型):
   ```python
   # 例如:Mixtral 8x7B使用EP=8, TP=2
   # 每个专家分布在不同设备,每设备用TP进一步切分
   ```

3. **TP+PP组合**(极端大模型):
   ```python
   # 仅在模型太大,TP受限于单层内存时使用
   # 例如:540B模型用PP=4, TP=8
   # 但bsz=1时PP效率问题仍需考虑
   ```

## 三、实际部署考量因素

### **内存分析**:
```
bsz=1推理内存 ≈ 参数内存 + 激活值内存 + KV缓存
1. 参数内存:可通过TP/PP减少
2. KV缓存:bsz=1时较小,但序列长度影响大
3. 激活值:bsz=1时相对较小
```

### **延迟vs吞吐量**:
- **bsz=1关注延迟**:TP增加通信,可能增加延迟
- **需要实测**:在目标硬件上测试TP

05-long-input-1(输入本技术报告,询问 你觉得这个项目真的能做到吗?为什么?

input_lenoutput_lenttftprefilltpotdecode
148156774.499 s19.880 tok/s0.070 s14.206 tok/s

05-long-input-1 TPOT

input(1481 tokens)
<|begin▁of▁sentence|><|User|>你觉得这个项目真的能做到吗?为什么?

duck-llm:一种基于 1×RTX 5090 + 32×N100 的异构 LLM 推理系统
“个人娱乐项目”。

没有 NVLink,没有 InfiniBand,只有一台工作机、32 台 Intel N100 小机器——低功耗、4 个 E-core(Alder Lake-N,Gracemont 系)、6W TDP、单通道内存、只有 AVX2,本来更像评测节点或 mini PC,而不是推理集群——和以太网。

最终结果:DeepSeek-V3.2(685B 参数,含 MTP head)decode 吞吐达到 15.301 tok/s,MTP1 接受率 87.1%。

这是什么
这套系统的出发点不是“我要做一个 LLM 推理系统”,而是一批已经存在的机器——32 台 Intel N100 小盒子,原先用来做在线评测(OJ)节点——能不能硬趟出一条跑大模型的路。

到 2026 年 3 月底,这条路的终点数字是:15.301 tok/s,运行 DeepSeek-V3.2(685B 参数,含 MTP head),在一台 i9-13900K + RTX 5090 的主机和 32 台 N100 之间,通过以太网协作完成。

一句话介绍该系统
把注意力计算放在单张 RTX 5090 上,把 MoE/FFN 专家层分散发到 32 台 N100 上跑 fp9 GEMV,中间通过 DPDK 用户态网络通信,JudgeDuck-OS 作为远端 kernel launch runtime。这套分工被称为 AFD(Attention-FeedForward Disaggregation)。

阅读导航
报告分为 Part I(系统搭建) 和 Part II(优化与结果) 两部分,共 12 章。每章可以独立阅读,但建议按顺序读完 Part I 再进入 Part II。

Part I:系统搭建
章	标题	一句话摘要
1	设计目标与硬件约束	硬件从哪来、为什么凑在一起、约束如何推导出分离架构
2	系统总览	四层架构:Python 模型主线 → Rust 编排 → C++ DPDK → JudgeDuck-OS 远端执行
3	单核 MLP fp9 起点	fp9 格式的动机、位布局设计和第一次进入真实路径
4	JudgeDuck-OS 作为 runtime	从“一次评测一个 ELF”到“低延迟远端 kernel launcher”的五处改造
5	DPDK 与“一鸭三吃”	通信栈被数据逼出来的演化路线:socket → async → sendmmsg → DPDK
Part II:优化与结果
章	标题	一句话摘要
6	模型接入路径	Qwen3 → Qwen3.5 → DeepSeek-V3.2,三条路线的结构差异和共性
7	decode 固定成本压缩	static KV cache → flash attention 2 → flattened decode → CUDA Graph → Triton fp8 → V3.2 decode context
8	fp9 扩展到完整远端路径	MoE 接入、sub-128 解锁、same-ELF SMP、prefill、weight cache、persistent judge
9	MTP1 dual-lane overlap	不加第二组鸭子,把 GPU 和 duck 两条流水线叠起来跑
10	方法学	roofline 测量、breakdown 分析、forced-cold 验证——不靠感觉靠测量
11	最终配置与最终结果	1024 token 长跑:15.301 tok/s,accept=1 共 891/1023,各项贡献分解
12	负结果、反直觉与未完成项	不成立的优化路径、与直觉相反的发现,以及还没做完的事
结果口径说明
首页所列结果来自一次 1024 token 单 prompt 长跑(prompt:104857601是质数吗),max_model_len=2048。

这是当前最佳工程路径的终点数字,不是标准 benchmark 汇总。更详细的口径和限制条件见第 11 章。

速查
硬件规模
项目	数值
在役 duck 数	32 × Intel N100(4 核,32 GB DDR4-3200)
host GPU	1 × NVIDIA RTX 5090(32 GB GDDR7)
host NIC	4 × Intel E810-C 25GbE
网络拓扑	4 个隔离广播域,每组 8 duck

最终结果(DeepSeek-V3.2,真实长跑)
同一轮 1024 token 长跑证据包(prompt:104857601是质数吗,max_model_len=2048)。

指标	数值
decode 吞吐	15.301 tok/s
TTFT	0.857 s
TPOT	65 ms
MTP1 accept=1	891 / 1023 ≈ 87.1%

关键机制指标(V3.2 真实系统内部)
这些是解释“为什么最终结果成立”的局部指标,不是端到端吞吐。

指标	数值	来源
persistent judge_gap	418µs → 78µs	第 8 章
dual-lane overlap ratio	~0.434(549 step 均值)	第 9 章
duck 侧参数吞吐	15.0~15.7 Gparam/s	第 11 章
跨模型里程碑
证明同一套系统架构能在不同模型上跑通。

模型	改动	指标	来源
Qwen3-30B-A3B tp24	flattened runtime + CUDA Graph	18 → 24 tok/s	第 7 章
Qwen3.5-35B-A3B tp16	flattened decode	24.2 → 37.9 tok/s	第 7 章
专项 bench / 方法学
以下不是最终系统吞吐,而是内核级或方法学专项测量。

指标	数值	来源
fp9 single-core kernel	~5.4 Gparam/s	第 8 章
MoE fp9 hot (4-core methodology breakdown, wall)	~20.30 GB/s	第 10 章

关键 commit
commit	说明
ab33e9e	fp9: prune dead common paths and land final 64/128 kernels
d32c6fe	deepseek_v32: land MTP1 two-lane online path and trace-ready runtime
279d749	Enable persistent fp9 judge by default for decode forward
e553d30	Fix DeepSeek V3.2 flattened active_len and add critical tests
项目代码
duck-llm:模型加载、host runtime、GPU/duck 协同、FP8/FP9 路径、DPDK 集成
JudgeDuck-OS:远端 kernel launch runtime、same-ELF SMP、persistent judge 会话
<|Assistant|><think>
output(567 tokens)
我们被问到:“你觉得这个项目真的能做到吗?为什么?” 关于一个名为“duck-llm”的项目,它描述了一种基于1×RTX 5090 + 32×N100的异构LLM推理系统。这是一个个人娱乐项目。他们声称在没有NVLink和InfiniBand的情况下,只有一台工作机和32台Intel N100小机器,通过以太网,最终实现了DeepSeek-V3.2(685B参数)的解码吞吐达到15.301 tok/s,MTP1接受率87.1%。

问题:我们是否相信这个项目真的能做到?为什么?

我们需要分析这个项目的可行性。首先,项目描述了一个非常规的硬件设置:一个强大的GPU(RTX 5090,假设是未来的GPU,但可能是模拟或假设)和许多低功耗的小型PC(N100)。它们通过以太网连接,没有高速互连。他们声称通过将注意力计算放在GPU上,将MoE/FFN专家层分散到N100上运行fp9 GEMV,实现了不错的性能。

关键点:
- 模型是DeepSeek-V3.2,有685B参数,是一个混合专家模型(MoE)。MoE模型只有一部分专家在每层被激活,因此可能适合将专家分散到多个设备上。
- 他们使用了32台N100,每台有4个E-core(Gracemont),6W TDP,单通道内存,只有AVX2。这些是低功耗、低性能的CPU。
- 他们使用了DPDK进行用户态网络通信,以及一个自定义的远端内核启动运行时(JudgeDuck-OS)。
- 报告了15.301 tok/s的解码吞吐,TTFT为0.857秒,TPOT为65毫秒。

我们需要考虑这个性能数字是否合理。15.3 tok/s对于685B模型来说,如果是在单个GPU上(即使是RTX 5090),也是不可能的,因为内存带宽和计算限制。但是,这里他们使用了32个额外的低功耗CPU节点来帮助计算MoE层。

在MoE模型中,前馈层(FFN)是专家层,通常只有少数专家被激活(例如,DeepSeek-V3.2可能使用MoE,每层有多个专家,但每个token只路由到少数专家)。因此,对于每个token,只需要计算一部分参数。如果他们将专家分布到不同的N100节点上,那么每个节点只需要计算一部分。但是,他们需要为每个token传输激活值到相应的专家节点,然后返回结果。这引入了网络开销。

他们声称使用了fp9格式(自定义的9位浮点)来减少数据传输和计算。N100只支持AVX2,因此

06-long-input-2-think(输入本技术报告,要求润色,带 think 输出)

input_lenoutput_lenttftprefilltpotdecode
133671267.184 s19.886 tok/s0.072 s13.903 tok/s

06-long-input-2-think TPOT

input(1336 tokens)
<|begin▁of▁sentence|><|User|>帮我把这个文章重新写一遍,用更地道的中文表达,但是不要丢掉任何技术细节,这是个技术回忆录。

一鸭三吃:我把评测鸭变成了满血 DeepSeek-V3.2 推理系统
这不是为了融资或产品化而造的系统,而是一个从评测鸭、远端执行器和个人好奇心一路长出来的异构推理项目,最后真的把满血 DeepSeek-V3.2 跑到了可用速度。

先把最终结果摆出来
1 × RTX 5090 + 32 × Intel N100,运行 DeepSeek-V3.2(685B 参数,含 MTP head),在一次 1024 token 的真实长跑中(prompt:104857601是质数吗,max_model_len=2048):

指标	数值
decode 吞吐	15.301 tok/s
TTFT	0.857 s
TPOT	65 ms
MTP1 accept=1	891 / 1023 ≈ 87.1%
最终结果终端截图
图:2026-03-30,1024 token 长跑结果。decode 15.301 tok/s,accept=1 出现 891/1023 次。

这不是理论峰值,不是 mock 路径,也没有跳过任何一层。stdout 和 stderr 完整证据保存在报告附录里,549 条 overlap 样本的统计均值也在那份日志中可查。

但这件事其实不是从大模型开始的,而是从评测鸭和远端执行器开始的。

这件事不是从大模型开始的,而是从评测鸭开始的
2024 年 12 月,我采购了一批 Intel N100 准系统——下单规模是 100 台准系统 + 100 条 32 GB DDR4-3200 内存。当时的出发点是给评测鸭 / JudgeDuck(在线评测网站,GitHub)扩容评测机。评测机运行自制的操作系统 JudgeDuck-OS——跟大模型没有任何关系。

N100 这颗芯片在公共舆论里没什么好名声。它是一颗 6W TDP、4 个 E-core(Alder Lake-N,Gracemont 系)、单通道内存的低功耗小核 x86,Intel ARK 上的规格看起来就像是“什么正经事也干不了”。2025 年甚至有人在公共社区质疑 N100 不适合作为评测节点,焦点集中在散热、降频、缓存和 32 GB 配置的合理性上——公共直觉常把它看成“配置极低、不可靠”。

我的观察是,经过 BIOS 调教(调整 Turbo Ratio Limit、开放 PL1/PL2)之后,N100 单核跑到官方标称的 3.4 GHz 没有问题,全核持续负载下也能稳在 2.9 GHz 左右。而当时给每台鸭配 32 GB DDR4-3200,纯粹是因为“反正内存不贵,多给点没坏处”,并不是为了任何 LLM 路线。

这批鸭子从采购到装机,是手工活:

鸭子装机现场
图:2025 年 1 月初,鸭子装机现场。

它们被部署成 JudgeDuck-OS 的远端执行节点——通过网络 PXE 启动,载入用户 ELF,计时,返回结果。每台鸭子不跑通用 Linux,没有 glibc,没有常规进程管理,就是一个最小化 x86-64 内核加上沙箱 ELF 执行。这个流程对评测来说够用。

到这时候,host 机器和鸭子群也完全没有关联。host 是 2023 年自费购置的主力工作机,i9-13900K + 128 GB DDR5,用于日常科研和开发。买它的时候,鸭群还不存在。

memtest86 截图
图:host 机器装机阶段的 memtest86 跑分,内存 4×32GB DDR5 稳定通过。

两批硬件之间没有预设的联系。直到后来,有人把它们放在一起想了一遍。

真正埋下伏笔的是内存;R1 之后,问题第一次被改写
DeepSeek-R1 公开之后,我第一次认真把问题写成了“能不能本地跑满血 685B”。

这个想法的出发点不是框架选型,不是论文阅读,而是容量、带宽和 roofline 的粗算。

先算容量。DeepSeek-V3 系列的官方发布格式本身就是 fp8,完整权重在磁盘上约 688 GB。32 只鸭子合计 32 × 32 GB ≈ 1 TB 总内存,看起来似乎装得下——但要留内存给 OS、ELF、input/output buffer,实际可用并没有那么宽裕。

再算带宽。每台 N100 的 DDR4-3200 单通道理论带宽约 25.6 GB/s,32 台合计约 819 GB/s。而一张 RTX 5090 的显存带宽约 1792 GB/s,高于鸭子侧的总量。

这两组数字放在一起,一个自然的分工就浮现出来了:

Attention 相关参数(KV cache、MLA 低秩矩阵、attention projection)可以被 5090 的 32 GB 显存装下——集中在 GPU 上处理。
FFN/MoE 专家层的权重远超单机显存容量——分散到 32 台鸭子的总内存里。
这个分工被称为 AFD(Attention-FeedForward Disaggregation)。我是按自己的容量和带宽约束独立推到了这套切分,后来补查时发现外部已经有人讨论过类似思路,并被概括为 AFD 这个名字——知乎专栏、Emergent Mind、甚至有 TensorRT-LLM 的 AFD 特性提案。清华大学趋境团队的 kTransformers(SOSP 2025)也是 AFD + CPU/GPU 混合计算——把 Attention/Shared Expert 留在 GPU,把 Routed Expert 卸载到同一台服务器的 CPU 上,利用服务器 CPU 的大内存带宽来做 MoE 计算。而我的路线是把 FFN/MoE 拆到 32 台独立的小机器上,用以太网做通信。多了一层网络,通信压力完全不一样。

选 5090 也不是靠榜单选出来的。核心判断只有两条:32 GB 显存能装下 Attention 侧工作集,且 1792 GB/s 显存带宽明显高于鸭子侧的理论总量。
<|Assistant|><think>
output(712 tokens,含 think 标签)
嗯,用户给了我一篇技术回忆录,想让我用更地道的中文重写,但不能丢掉任何技术细节。让我看看这是什么内容……

哦,这是一篇关于如何把评测鸭系统变成DeepSeek-V3.2推理系统的技术文章。作者用了一台RTX 5090和32台Intel N100小机器,成功运行了685B参数的大模型。挺有意思的。

用户想要更地道的中文表达,那我得注意不要用太生硬的翻译腔,要更符合中文技术文章的表达习惯。但技术细节一点都不能少,那些参数、配置、性能指标都得保留。

我看看文章结构……先有最终性能结果,然后从头讲起是怎么从评测鸭系统开始的,再讲到技术方案的选择和实现。这个叙事顺序挺好的,我可以保持这个结构。

有些表达可以调整得更自然些。比如“这不是为了融资或产品化而造的系统”这种表述可以更流畅。“先把最终结果摆出来”这种小标题也可以更有文采一点。

技术细节部分要特别小心,那些数字、术语、性能指标都不能错。decode吞吐15.301 tok/s、TTFT 0.857秒、TPOT 65毫秒……这些都得原样保留。

作者从评测鸭开始讲起这个角度很好,让文章有故事性。那些装机照片的描述也可以更生动些。

AFD架构的解释部分很重要,需要清晰说明为什么这样分工,GPU和CPU各自负责什么。那些带宽计算、容量规划的逻辑都要讲清楚。

我想让整篇文章读起来更像一篇流畅的技术叙事,而不是冷冰冰的技术报告。可以加一些连接词,让逻辑更连贯,但不要改变原意。

好,我大概知道怎么改了。要保持所有技术细节不变,但让语言更地道、更流畅,更有中文技术文章的味道。我会注意不要过度发挥,忠实于原文的技术内容。</think>**一鸭三用:我将“评测鸭”改造成了全功能 DeepSeek-V3.2 推理系统**

这个系统并非为了融资或产品化而构建,它源于“评测鸭”、远程执行器以及纯粹的个人技术好奇心,最终成长为一个异构推理项目,并成功以可用的速度运行了完整的 DeepSeek-V3.2 模型。

**先看最终成果**
使用 **1 张 RTX 5090 + 32 台 Intel N100 小主机**,运行 **DeepSeek-V3.2(685B 参数,包含 MTP 预测头)**。在一次真实的 1024 token 生成任务中(提示词:“104857601是质数吗”,`max_model_len=2048`),性能如下:

| 指标 | 数值 |
| :--- | :--- |
| **解码吞吐量** | 15.301 token/秒 |
| **首 Token 延迟** | 0.857 秒 |
| **单 Token 处理时间** | 65 毫秒 |
| **MTP accept=1 比例** | 891 / 1023 ≈ 87.1% |

*(终端运行结果截图,显示时间为2026-03-30,1024 token长任务,解码速度15.301 tok/s,accept=1出现891次。)*

这不是理论峰值,没有使用任何模拟路径,也没有跳过任何计算层。完整的 stdout 和 stderr 日志证据保存在项目附录中,其中包含 549

07-long-input-2-nonthink(输入本技术报告,要求润色,不带 think)

input_lenoutput_lenttftprefilltpotdecode
133671266.079 s20.218 tok/s0.067 s15.013 tok/s

07-long-input-2-nonthink TPOT

input(1336 tokens)
<|begin▁of▁sentence|><|User|>帮我把这个文章重新写一遍,用更地道的中文表达,但是不要丢掉任何技术细节,这是个技术回忆录。

一鸭三吃:我把评测鸭变成了满血 DeepSeek-V3.2 推理系统
这不是为了融资或产品化而造的系统,而是一个从评测鸭、远端执行器和个人好奇心一路长出来的异构推理项目,最后真的把满血 DeepSeek-V3.2 跑到了可用速度。

先把最终结果摆出来
1 × RTX 5090 + 32 × Intel N100,运行 DeepSeek-V3.2(685B 参数,含 MTP head),在一次 1024 token 的真实长跑中(prompt:104857601是质数吗,max_model_len=2048):

指标	数值
decode 吞吐	15.301 tok/s
TTFT	0.857 s
TPOT	65 ms
MTP1 accept=1	891 / 1023 ≈ 87.1%
最终结果终端截图
图:2026-03-30,1024 token 长跑结果。decode 15.301 tok/s,accept=1 出现 891/1023 次。

这不是理论峰值,不是 mock 路径,也没有跳过任何一层。stdout 和 stderr 完整证据保存在报告附录里,549 条 overlap 样本的统计均值也在那份日志中可查。

但这件事其实不是从大模型开始的,而是从评测鸭和远端执行器开始的。

这件事不是从大模型开始的,而是从评测鸭开始的
2024 年 12 月,我采购了一批 Intel N100 准系统——下单规模是 100 台准系统 + 100 条 32 GB DDR4-3200 内存。当时的出发点是给评测鸭 / JudgeDuck(在线评测网站,GitHub)扩容评测机。评测机运行自制的操作系统 JudgeDuck-OS——跟大模型没有任何关系。

N100 这颗芯片在公共舆论里没什么好名声。它是一颗 6W TDP、4 个 E-core(Alder Lake-N,Gracemont 系)、单通道内存的低功耗小核 x86,Intel ARK 上的规格看起来就像是“什么正经事也干不了”。2025 年甚至有人在公共社区质疑 N100 不适合作为评测节点,焦点集中在散热、降频、缓存和 32 GB 配置的合理性上——公共直觉常把它看成“配置极低、不可靠”。

我的观察是,经过 BIOS 调教(调整 Turbo Ratio Limit、开放 PL1/PL2)之后,N100 单核跑到官方标称的 3.4 GHz 没有问题,全核持续负载下也能稳在 2.9 GHz 左右。而当时给每台鸭配 32 GB DDR4-3200,纯粹是因为“反正内存不贵,多给点没坏处”,并不是为了任何 LLM 路线。

这批鸭子从采购到装机,是手工活:

鸭子装机现场
图:2025 年 1 月初,鸭子装机现场。

它们被部署成 JudgeDuck-OS 的远端执行节点——通过网络 PXE 启动,载入用户 ELF,计时,返回结果。每台鸭子不跑通用 Linux,没有 glibc,没有常规进程管理,就是一个最小化 x86-64 内核加上沙箱 ELF 执行。这个流程对评测来说够用。

到这时候,host 机器和鸭子群也完全没有关联。host 是 2023 年自费购置的主力工作机,i9-13900K + 128 GB DDR5,用于日常科研和开发。买它的时候,鸭群还不存在。

memtest86 截图
图:host 机器装机阶段的 memtest86 跑分,内存 4×32GB DDR5 稳定通过。

两批硬件之间没有预设的联系。直到后来,有人把它们放在一起想了一遍。

真正埋下伏笔的是内存;R1 之后,问题第一次被改写
DeepSeek-R1 公开之后,我第一次认真把问题写成了“能不能本地跑满血 685B”。

这个想法的出发点不是框架选型,不是论文阅读,而是容量、带宽和 roofline 的粗算。

先算容量。DeepSeek-V3 系列的官方发布格式本身就是 fp8,完整权重在磁盘上约 688 GB。32 只鸭子合计 32 × 32 GB ≈ 1 TB 总内存,看起来似乎装得下——但要留内存给 OS、ELF、input/output buffer,实际可用并没有那么宽裕。

再算带宽。每台 N100 的 DDR4-3200 单通道理论带宽约 25.6 GB/s,32 台合计约 819 GB/s。而一张 RTX 5090 的显存带宽约 1792 GB/s,高于鸭子侧的总量。

这两组数字放在一起,一个自然的分工就浮现出来了:

Attention 相关参数(KV cache、MLA 低秩矩阵、attention projection)可以被 5090 的 32 GB 显存装下——集中在 GPU 上处理。
FFN/MoE 专家层的权重远超单机显存容量——分散到 32 台鸭子的总内存里。
这个分工被称为 AFD(Attention-FeedForward Disaggregation)。我是按自己的容量和带宽约束独立推到了这套切分,后来补查时发现外部已经有人讨论过类似思路,并被概括为 AFD 这个名字——知乎专栏、Emergent Mind、甚至有 TensorRT-LLM 的 AFD 特性提案。清华大学趋境团队的 kTransformers(SOSP 2025)也是 AFD + CPU/GPU 混合计算——把 Attention/Shared Expert 留在 GPU,把 Routed Expert 卸载到同一台服务器的 CPU 上,利用服务器 CPU 的大内存带宽来做 MoE 计算。而我的路线是把 FFN/MoE 拆到 32 台独立的小机器上,用以太网做通信。多了一层网络,通信压力完全不一样。

选 5090 也不是靠榜单选出来的。核心判断只有两条:32 GB 显存能装下 Attention 侧工作集,且 1792 GB/s 显存带宽明显高于鸭子侧的理论总量。
<|Assistant|></think>
output(712 tokens)
**一鸭三用:将评测鸭改造为完整的 DeepSeek-V3.2 推理系统**

这不是一个为融资或产品化而打造的系统,而是源于“评测鸭”、远程执行器以及个人技术好奇心,最终自然演变出的一个异构推理项目。其成果是成功以可用速度运行了完整的 DeepSeek-V3.2 模型。

**先看最终成果**
使用 **1 张 RTX 5090** 搭配 **32 台 Intel N100 小主机**,运行 **DeepSeek-V3.2(685B 参数,含 MTP 预测头)**。在一次真实的 **1024 token** 生成任务中(输入提示为“104857601是质数吗”,`max_model_len=2048`),性能指标如下:

| 指标 | 数值 |
| :--- | :--- |
| **解码吞吐量** | 15.301 token/秒 |
| **首 Token 时间** | 0.857 秒 |
| **每 Token 时间** | 65 毫秒 |
| **MTP1 accept=1 比例** | 891 / 1023 ≈ 87.1% |

*(终端运行结果截图,显示日期 2026-03-30,解码速度 15.301 token/秒,accept=1 出现 891/1023 次。)*

这个结果并非理论峰值,也不是模拟路径,且没有跳过任何计算层。完整的标准输出和错误输出日志已保存在项目附录中,其中包含 549 条重叠样本的统计平均值可供查验。

然而,这个故事的开端并非大模型,而是从“评测鸭”和远程执行器开始的。

**起点:评测鸭与远程执行器**
2024年12月,我采购了一批 Intel N100 迷你主机——规模是 **100 台准系统** 加上 **100 条 32GB DDR4-3200 内存**。当时的目的是为我开发的在线评测网站 **JudgeDuck(评测鸭,GitHub开源)** 扩充评测节点。这些节点运行我们自研的 **JudgeDuck-OS** 操作系统——当时与大模型毫无关联。

N100 这款芯片在公众讨论中口碑一般。它是一颗 **6W TDP**、仅含 **4 个能效核**(基于 Alder Lake-N 架构的 Gracemont 核心)、**单通道内存** 的低功耗 x86 处理器,Intel 官方规格表看起来确实不像能承担重载任务。甚至在 2025年,仍有社区用户质疑 N100 是否适合作为评测节点,主要担忧其散热、降频、缓存性能以及 32GB 内存配置的合理性——公众普遍认为它“配置过低、不可靠”。

我的实际观察是,经过适当的 BIOS 调优(调整 Turbo 倍频限制、放开 PL1/PL2 功耗墙)后,N100 的**单核频率可以稳定在官方标称的 3.4 GHz**,**全核持续负载下也能维持在 2.9 GHz 左右**。当时为每台“鸭”配备 32GB 内存,纯粹是出于“内存不贵,多配点总没坏处”的考虑,并非为任何 LLM 应用铺路。

这批“鸭子”从采购到装机,全是手工完成:
*(图片

汇总表:全部 case 及原始材料入口

caseinput_lenoutput_lendecodestdoutstderrCSV
01-hello516813.659 tok/souterrcsv
02-prime-2k11203716.171 tok/souterrcsv
03-count-2k14203416.294 tok/souterrcsv
04-parallelism-qa-2k35201315.095 tok/souterrcsv
05-long-input-1148156714.206 tok/souterrcsv
06-long-input-2-think133671213.903 tok/souterrcsv
07-long-input-2-nonthink133671215.013 tok/souterrcsv

完整 manifest(含 ttft、tpot、prefill 等全部字段):manifest.tsv


项目声明 / Project Disclaimer

本项目为作者以个人身份、利用业余时间推进的个人娱乐项目;除非另有明确说明,它与作者的任何雇主、客户、学校、单位或其他组织均无合作、雇佣、委托、赞助或背书关系,也不代表任何该等主体的立场。除普通个人捐赠外,本项目未获得任何资金支持。

This project is a personal hobby project developed by the author in a personal capacity and in personal time. Unless explicitly stated otherwise, it has no collaboration, employment, commission, sponsorship, endorsement, or institutional affiliation with any employer, client, school, partner organization, or other entity, and it does not represent any such party's views. No funding was received for this project except ordinary personal donations.

许可 / License

除非另有说明,本页原创文字、本站原创图片与本站原创图表采用 CC BY-NC-ND 4.0 发布。
转载时请保留原文标题、署名“JudgeDuck AI”、发布日期与原始链接;禁止商业转载、改写、摘编、翻译或基于原文创作演绎作品。

第三方商标、外部链接内容,以及文中另有标注的材料,不在上述许可范围内。

Unless otherwise noted, the original text, original images, and original figures on this page are licensed under the Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.