Skip to content

vllm.distributed.kv_transfer.kv_connector.v1.offloading.scheduler

Classes:

Functions:

OffloadingConnectorScheduler

Implementation of Scheduler side methods

Methods:

Source code in vllm/distributed/kv_transfer/kv_connector/v1/offloading/scheduler.py
 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
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
class OffloadingConnectorScheduler:
    """Implementation of Scheduler side methods"""

    def __init__(
        self,
        spec: OffloadingSpec,
        vllm_config: VllmConfig,
        kv_cache_config: KVCacheConfig,
    ):
        self.config = SchedulerOffloadConfig.from_spec(
            spec, vllm_config, kv_cache_config
        )
        self.manager: OffloadingManager = spec.get_manager()
        self._connector_stats = OffloadingConnectorStats()

        full_attention_groups: list[int] = []
        sliding_window_groups: list[int] = []
        for config_idx, group_config in enumerate(self.config.kv_group_configs):
            if group_config.sliding_window_size_in_chunks is None:
                full_attention_groups.append(config_idx)
            else:
                sliding_window_groups.append(config_idx)

        # sort sliding window groups by window size in decreasing order
        def _sliding_window_sort_key(i: int) -> int:
            val = self.config.kv_group_configs[i].sliding_window_size_in_chunks
            assert val is not None
            return val

        sliding_window_groups.sort(key=_sliding_window_sort_key, reverse=True)

        # used by _lookup
        self._sliding_window_groups: tuple[int, ...] = tuple(sliding_window_groups)
        self._lookup_groups = tuple(full_attention_groups) + self._sliding_window_groups
        self._mamba_align_size: int | None = resolve_mamba_align_size(
            spec, kv_cache_config
        )
        self._partial_tail_block_size = (
            self.config.kv_group_configs[0].tokens_per_block
            if self.config.supports_partial_tail
            else 0
        )
        self._cow_source_groups = frozenset(
            config.group_idx
            for config in self.config.kv_group_configs
            if config.requires_cow_source
        )

        self._req_status: dict[ReqId, RequestOffloadState] = {}
        self._current_batch_load_jobs: dict[int, TransferJob] = {}
        self._current_batch_jobs_to_flush: set[int] = set()
        # GPU block IDs allocated in the current engine step
        self._current_batch_allocated_block_ids: set[int] = set()
        # if GPU prefix caching is enabled,
        # Track loaded chunks to avoid redundant loads.
        self._chunks_being_loaded: set[OffloadKey] | None = (
            set() if vllm_config.cache_config.enable_prefix_caching else None
        )

        # Job ID counter shared by loads and stores.
        self._job_counter: int = 0
        # Threshold value for stale jobs. All job ids >= _stale_job_threshold are
        # active jobs.
        self._stale_job_threshold: int = 0
        self._jobs: dict[int, TransferJobStatus] = {}

        # block_id -> pending store job_ids. Used to track jobs that needs
        # flushing in case a block is re-allocated by the KV cache manager.
        # Populated only for finished requests (running-request blocks are
        # protected by their ref_cnt) and for sliding window blocks (which can
        # be freed before a request finishes).
        self._block_id_to_pending_jobs: dict[int, set[int]] = {}

        self._events_tracker = OffloadingEventsTracker(spec.kv_events_config)

    def _maybe_observe_lookup_async_delay(
        self, req_status: RequestOffloadState
    ) -> None:
        start_time = req_status.deferred_lookup_start_time
        if start_time is None:
            return
        req_status.deferred_lookup_start_time = None
        self._connector_stats.observe_histogram(
            _ConnectorMetricName.LOOKUP_ASYNC_DELAY,
            time.monotonic() - start_time,
        )

    def _generate_job_id(self) -> int:
        job_id = self._job_counter
        self._job_counter += 1
        return job_id

    def _remove_pending_job(self, job_id: int, block_ids: list[int] | None) -> None:
        for bid in block_ids or ():
            pending = self._block_id_to_pending_jobs[bid]
            pending.remove(job_id)
            if not pending:
                del self._block_id_to_pending_jobs[bid]

    def _calc_num_offloadable_tokens(
        self, req_status: RequestOffloadState, num_computed_tokens: int
    ) -> int:
        num = min(num_computed_tokens, req_status.req.num_tokens)
        max_offload_tokens = req_status.max_offload_tokens
        if max_offload_tokens is not None:
            num = min(num, max_offload_tokens)
        if self.config.offload_prompt_only:
            num = min(num, req_status.req.num_prompt_tokens)
        return num

    def _maximal_prefix_lookup(
        self,
        keys: Iterable[OffloadKey],
        req_context: ReqContext,
        req: Request,
        group_config: GroupOffloadConfig,
        start_chunk_idx: int,
    ) -> int | None:
        """Return the number of consecutive offloaded chunks from the start,
        or None if the backend deferred a lookup."""
        hit_count = 0
        defer_lookup = False
        for local_idx, key in enumerate(keys):
            result = self.manager.lookup(key, req_context)
            match result:
                case LookupResult.HIT:
                    self._events_tracker.record_lookup(
                        req,
                        group_config,
                        start_chunk_idx + local_idx,
                        key,
                    )
                    hit_count += 1
                case LookupResult.HIT_PENDING:
                    defer_lookup = True
                    hit_count += 1
                case LookupResult.RETRY:
                    # Don't break: keep scanning to let manager kick off
                    # async lookups (until a miss is detected).
                    defer_lookup = True
                case LookupResult.MISS:
                    break
        return hit_count if not defer_lookup else None

    def _sliding_window_lookup(
        self,
        keys: Sequence[OffloadKey],
        sliding_window_size: int,
        req_context: ReqContext,
        initial_window_size: int | None = None,
    ) -> int | None:
        """Return the end index (in `keys`) of the last run of
        `sliding_window_size` consecutive hits, scanning from the end.
        The first run may need a larger window for a partial rightmost chunk.
        Returns 0 on miss, None if the backend deferred a lookup."""
        defer_lookup = False
        pending_in_window = False
        consecutive_hits = 0
        required_window = initial_window_size or sliding_window_size
        for idx in range(len(keys) - 1, -1, -1):
            match self.manager.lookup(keys[idx], req_context):
                case LookupResult.HIT:
                    consecutive_hits += 1
                case LookupResult.HIT_PENDING:
                    # Block is in cache, just not readable yet — counts
                    # as hit for the consecutive streak. Don't break:
                    # keep scanning to let manager kick off async lookups.
                    pending_in_window = True
                    consecutive_hits += 1
                case LookupResult.RETRY:
                    # Block location uncertain — does not count as hit.
                    # Don't break: keep scanning to let manager kick off
                    # async lookups.
                    defer_lookup = True
                    consecutive_hits = 0
                    pending_in_window = False
                    required_window = sliding_window_size
                case LookupResult.MISS:
                    consecutive_hits = 0
                    # This gap rules out the incomplete window to its right.
                    pending_in_window = False
                    required_window = sliding_window_size
            if consecutive_hits == required_window:
                return (
                    None if defer_lookup or pending_in_window else idx + required_window
                )
        return None if defer_lookup or pending_in_window else consecutive_hits

    def _lookup_complete_chunks(
        self,
        req_status: RequestOffloadState,
        max_num_new_tokens: int | None = None,
    ) -> int | None:
        """Find how many tokens beyond num_locally_computed_tokens can be loaded.

        Iterates full-attention groups first (prefix lookup), then sliding-window
        groups (suffix lookup). Each group may tighten max_hit_size_tokens, which
        can invalidate an earlier group's result, so the loop re-runs when that
        happens until num_hit_tokens converges.
        """
        num_computed_tokens = req_status.num_locally_computed_tokens
        max_hit_size_tokens: int = req_status.req.num_tokens
        if max_num_new_tokens is not None:
            max_hit_size_tokens = min(
                max_hit_size_tokens, num_computed_tokens + max_num_new_tokens
            )
        if req_status.max_load_tokens is not None:
            max_hit_size_tokens = min(
                max_hit_size_tokens,
                num_computed_tokens + req_status.max_load_tokens,
            )
        if self._sliding_window_groups:
            # the last prompt token has to be recomputed to get the logprobs
            # for sliding window attention, we must reduce by 1 to make sure
            # we still have a hit after reduction
            max_hit_size_tokens -= 1
            if self._mamba_align_size is not None:
                # Constrain hit-window to the mamba block size.
                max_hit_size_tokens = round_down(
                    max_hit_size_tokens, self._mamba_align_size
                )

        num_hit_tokens: int = 0
        defer_lookup = False
        lookup_groups = self._lookup_groups

        # Tracks which eagle groups have already popped their volatile trailing chunk
        # in the current convergence iteration. Reset when a non-eagle group
        # tightens the hit boundary, requiring a fresh pop.
        eagle_verified: set[int] = set()
        while lookup_groups:
            looked_up_sliding_window: bool = False
            groups_iter = iter(lookup_groups)
            lookup_groups = ()
            for group_idx in groups_iter:
                group_config: GroupOffloadConfig = self.config.kv_group_configs[
                    group_idx
                ]
                group_state: RequestGroupState = req_status.group_states[group_idx]
                tokens_per_chunk = group_config.tokens_per_chunk
                offload_keys = group_state.offload_keys

                assert (
                    len(offload_keys) >= req_status.req.num_tokens // tokens_per_chunk
                )

                is_eagle_unverified = (
                    group_config.is_eagle_group and group_idx not in eagle_verified
                )

                # Constrain to a chunk-aligned boundary for this group.
                max_hit_size_tokens = min(
                    max_hit_size_tokens, len(offload_keys) * tokens_per_chunk
                )
                if req_status.max_load_tokens is not None:
                    max_hit_size_tokens = round_down(
                        max_hit_size_tokens, tokens_per_chunk
                    )
                if max_hit_size_tokens - num_computed_tokens < tokens_per_chunk:
                    # We can only load less than a chunk, so skip.
                    return 0

                sliding_window_size_in_chunks = (
                    group_config.sliding_window_size_in_chunks
                )

                # For eagle groups, query one extra chunk that will be popped.
                # Widening applies to every group type: without it, the pop
                # below shrinks max_hit_size_tokens past what was queried,
                # which can push the confirmed boundary under a coarser
                # sibling group's chunk granularity and zero the whole
                # request's hit (issue #52735).
                query_max = max_hit_size_tokens
                if is_eagle_unverified:
                    query_max = min(
                        max_hit_size_tokens + tokens_per_chunk,
                        len(offload_keys) * tokens_per_chunk,
                    )

                num_chunks = min(cdiv(query_max, tokens_per_chunk), len(offload_keys))
                start_chunk_idx = num_computed_tokens // tokens_per_chunk
                offload_keys = offload_keys[start_chunk_idx:num_chunks]

                # end index (in the sliced offload_keys) up to which we
                # have backend-confirmed hits
                num_hit_chunks: int | None
                if sliding_window_size_in_chunks is None:
                    num_hit_chunks = self._maximal_prefix_lookup(
                        offload_keys,
                        req_status.req_context,
                        req_status.req,
                        group_config,
                        start_chunk_idx,
                    )
                else:
                    required_window = sliding_window_size_in_chunks
                    if is_eagle_unverified:
                        required_window += 1
                    candidate_end = min(
                        max_hit_size_tokens,
                        (num_chunks - int(is_eagle_unverified)) * tokens_per_chunk,
                    )
                    initial_window = group_config.load_window_size_in_chunks(
                        candidate_end
                    )
                    assert initial_window is not None
                    num_hit_chunks = self._sliding_window_lookup(
                        offload_keys,
                        required_window,
                        req_status.req_context,
                        initial_window + int(is_eagle_unverified),
                    )
                if num_hit_chunks == 0:
                    return 0

                if num_hit_chunks is None:
                    defer_lookup = True
                else:
                    if is_eagle_unverified:
                        num_hit_chunks -= 1
                        eagle_verified.add(group_idx)

                    max_hit_size_tokens = min(
                        max_hit_size_tokens,
                        tokens_per_chunk * (start_chunk_idx + num_hit_chunks),
                    )

                new_num_hit_tokens = max_hit_size_tokens - num_computed_tokens
                if new_num_hit_tokens < tokens_per_chunk:
                    # We can only load less than a chunk, so skip.
                    return 0

                if new_num_hit_tokens < num_hit_tokens:
                    if not group_config.is_eagle_group:
                        eagle_verified.clear()
                    if defer_lookup:
                        # make another iteration on all groups to check
                        # if we still need to defer lookup
                        defer_lookup = False
                        lookup_groups = self._lookup_groups
                    elif looked_up_sliding_window and not lookup_groups:
                        # we need another iteration to confirm previously looked up
                        # sliding window works with the new_num_hit_tokens
                        lookup_groups = self._sliding_window_groups

                looked_up_sliding_window |= sliding_window_size_in_chunks is not None
                num_hit_tokens = new_num_hit_tokens

        if defer_lookup:
            logger.debug(
                "Offloading manager delayed request %s as backend requested",
                req_status.req.request_id,
            )
            return None

        # Possibly delay the request if any hit chunk is already being loaded.
        if self._chunks_being_loaded:
            for group_config, group_state in zip(
                self.config.kv_group_configs, req_status.group_states
            ):
                tokens_per_chunk = group_config.tokens_per_chunk
                sliding_window_size_in_chunks = group_config.load_window_size_in_chunks(
                    num_computed_tokens + num_hit_tokens
                )
                offload_keys = group_state.offload_keys
                num_chunks = cdiv(
                    num_computed_tokens + num_hit_tokens, tokens_per_chunk
                )
                start_chunk_idx = num_computed_tokens // tokens_per_chunk
                offload_keys = offload_keys[start_chunk_idx:num_chunks]
                if sliding_window_size_in_chunks is not None:
                    offload_keys = offload_keys[-sliding_window_size_in_chunks:]
                if any(key in self._chunks_being_loaded for key in offload_keys):
                    # Hit chunks are being loaded, so delay the request.
                    logger.debug(
                        "Delaying request %s since some of its"
                        " chunks are already being loaded",
                        req_status.req.request_id,
                    )
                    return None

        logger.debug(
            "Request %s hit %s offloaded tokens after %s GPU hit tokens",
            req_status.req.request_id,
            num_hit_tokens,
            num_computed_tokens,
        )

        return num_hit_tokens

    def _make_boundary_key(
        self,
        request: Request,
        group_idx: int,
        boundary_tokens: int,
        req_context: ReqContext,
    ) -> OffloadKey:
        hash_idx = boundary_tokens // self.config.tokens_per_hash - 1
        key = make_offload_key(request.block_hashes[hash_idx], group_idx)
        req_context.set_offload_key_position(key, boundary_tokens)
        return key

    def _lookup(
        self,
        req_status: RequestOffloadState,
        max_num_new_tokens: int | None = None,
    ) -> int | None:
        complete_hit = self._lookup_complete_chunks(req_status, max_num_new_tokens)
        req_status.partial_tail_boundary = None
        if complete_hit is None or not self.config.supports_partial_tail:
            return complete_hit

        local_tokens = req_status.num_locally_computed_tokens
        complete_boundary = local_tokens + complete_hit
        tokens_per_hash = self.config.tokens_per_hash
        block_end = complete_boundary + self._partial_tail_block_size
        max_boundary = min(req_status.req.num_prompt_tokens - 1, block_end - 1)
        if max_num_new_tokens is not None:
            max_boundary = min(max_boundary, local_tokens + max_num_new_tokens)
        if req_status.max_load_tokens is not None:
            max_boundary = min(
                max_boundary,
                local_tokens + req_status.max_load_tokens,
            )
        max_boundary = round_down(max_boundary, tokens_per_hash)
        if max_boundary <= complete_boundary:
            return complete_hit

        pending = False
        for boundary in range(max_boundary, complete_boundary, -tokens_per_hash):
            boundary_pending = False
            boundary_missed = False
            boundary_keys = []
            for group_config in self.config.kv_group_configs:
                key = self._make_boundary_key(
                    req_status.req,
                    group_config.group_idx,
                    boundary,
                    req_status.req_context,
                )
                boundary_keys.append(key)
                result = self.manager.lookup(key, req_status.req_context)
                if result is LookupResult.MISS:
                    boundary_missed = True
                    break
                if result in (LookupResult.HIT_PENDING, LookupResult.RETRY):
                    boundary_pending = True

            pending |= boundary_pending
            if not boundary_missed and not boundary_pending:
                for group_config, key in zip(
                    self.config.kv_group_configs, boundary_keys
                ):
                    self._events_tracker.record_partial_lookup(
                        req_status.req, group_config, boundary, key
                    )
                req_status.partial_tail_boundary = boundary
                return boundary - local_tokens

        if pending and complete_hit == 0:
            return None
        return complete_hit

    def on_new_request(self, request: Request) -> None:
        """Called when a new request is added to the scheduler."""
        req_context = _create_req_context(request)
        offloading_context = self.manager.on_new_request(req_context)
        req_status = RequestOffloadState(
            config=self.config,
            req=request,
            req_context=req_context,
            offloading_context=offloading_context,
        )
        self._req_status[request.request_id] = req_status

    def get_num_new_matched_tokens(
        self,
        request: Request,
        num_computed_tokens: int,
        max_num_new_tokens: int | None = None,
    ) -> tuple[int | None, bool]:
        """Get number of new tokens that can be loaded beyond the
        num_computed_tokens.

        Args:
            request (Request): the request object.
            num_computed_tokens (int): the number of locally
                computed tokens for this request
            max_num_new_tokens (int | None): cap on the number of tokens that
                may be loaded beyond `num_computed_tokens`, if any.

        Returns:
            A tuple with the following elements:
                - The number of tokens that can be loaded beyond what is
                  already computed.
                  If None, it means that the connector needs more time to
                  determine the number of matched tokens, and the scheduler
                  should query for this request again later.
                - `True` if tokens will be loaded asynchronously
                  (between scheduler steps).

        """
        req_status = self._req_status[request.request_id]
        for group_state in req_status.group_states:
            group_state.block_ids.clear()

        if req_status.transfer_jobs:
            logger.debug(
                "Delaying request %s since it still has in-flight transfers",
                request.request_id,
            )
            return None, False

        req_status.update_offload_keys()
        req_status.num_locally_computed_tokens = num_computed_tokens

        num_hit_tokens: int | None
        if request.skip_reading_prefix_cache:
            num_hit_tokens = 0
        else:
            lookup_start = time.monotonic()
            num_hit_tokens = self._lookup(req_status, max_num_new_tokens)
            self._connector_stats.observe_histogram(
                _ConnectorMetricName.LOOKUP_SYNC_DELAY,
                time.monotonic() - lookup_start,
            )
            if num_hit_tokens is None:
                if req_status.deferred_lookup_start_time is None:
                    req_status.deferred_lookup_start_time = lookup_start
            else:
                self._maybe_observe_lookup_async_delay(req_status)
        req_status.update_num_hit_chunks(num_computed_tokens + (num_hit_tokens or 0))

        return num_hit_tokens, bool(num_hit_tokens)

    def update_state_after_alloc(
        self, request: Request, blocks: KVCacheBlocks, num_external_tokens: int
    ):
        if num_external_tokens == 0:
            return

        req_status = self._req_status[request.request_id]

        num_locally_computed_tokens = req_status.num_locally_computed_tokens
        num_cached_tokens = num_locally_computed_tokens + num_external_tokens
        partial_tail_boundary = req_status.partial_tail_boundary
        if partial_tail_boundary is not None:
            assert partial_tail_boundary == num_cached_tokens

        keys_to_load: list[OffloadKey] = []
        dst_block_ids: list[int] = []
        # per group
        group_sizes: list[int] = []
        block_indices: list[int] = []
        for group_config, group_state in zip(
            self.config.kv_group_configs,
            req_status.group_states,
        ):
            group_blocks = blocks.blocks[group_config.group_idx]
            self._current_batch_allocated_block_ids.update(
                block.block_id for block in group_blocks if block.block_id != 0
            )

            tokens_per_block = group_config.tokens_per_block
            tokens_per_chunk = group_config.tokens_per_chunk
            offload_keys = group_state.offload_keys
            num_gpu_blocks = cdiv(num_cached_tokens, tokens_per_block)

            assert len(group_blocks) >= num_gpu_blocks
            # ``load_start_gpu_block_idx``: the index in ``group_blocks`` where the
            # load region begins -- a slice bound, not a count of computed blocks.
            # Scan from the computed boundary, not 0: sparse groups (Mamba,
            # SWA) legitimately hold non-null unhashed blocks below it.
            # Skip nulls (sentinel / out-of-retention padding).
            first_fresh_gpu_block_idx = cdiv(
                num_locally_computed_tokens, tokens_per_block
            )
            load_start_gpu_block_idx = num_gpu_blocks
            for i in range(first_fresh_gpu_block_idx, num_gpu_blocks):
                block = group_blocks[i]
                if not block.is_null and block.block_hash is None:
                    load_start_gpu_block_idx = i
                    break

            assert num_locally_computed_tokens % tokens_per_block == 0
            num_pending_gpu_blocks = num_gpu_blocks - load_start_gpu_block_idx

            if group_config.sliding_window_size_in_chunks is not None:
                assert (
                    num_pending_gpu_blocks
                    <= group_config.sliding_window_size_in_chunks
                    * self.config.blocks_per_chunk
                    + 1
                )

            num_chunks = cdiv(num_cached_tokens, tokens_per_chunk)
            if num_pending_gpu_blocks:
                start_chunk_idx = (
                    load_start_gpu_block_idx // self.config.blocks_per_chunk
                )
                end_chunk_idx = num_chunks - (partial_tail_boundary is not None)
                assert len(offload_keys) >= end_chunk_idx
                keys_to_load.extend(offload_keys[start_chunk_idx:end_chunk_idx])
                if partial_tail_boundary is not None:
                    keys_to_load.append(
                        self._make_boundary_key(
                            request,
                            group_config.group_idx,
                            partial_tail_boundary,
                            req_status.req_context,
                        )
                    )

            dst_block_ids.extend(
                block.block_id
                for block in group_blocks[load_start_gpu_block_idx:num_gpu_blocks]
            )
            group_sizes.append(num_pending_gpu_blocks)
            block_indices.append(load_start_gpu_block_idx)

            # Skip prefix-hit chunks for block-level policy; for
            # request-level, next_stored_chunk_idx stays at 0 so all
            # chunks (including hits) are offloaded.
            if req_status.offloading_context.policy == OffloadPolicy.CHUNK_LEVEL:
                group_state.next_stored_chunk_idx = num_chunks

        src_spec = self.manager.prepare_load(keys_to_load, req_status.req_context)
        dst_spec = GPULoadStoreSpec(
            dst_block_ids, group_sizes=group_sizes, block_indices=block_indices
        )

        load_job_id = self._generate_job_id()
        self._current_batch_load_jobs[load_job_id] = TransferJob(
            req_id=request.request_id,
            src_spec=src_spec,
            dst_spec=dst_spec,
        )
        # a load can only be issued when no other jobs are pending.
        assert not req_status.transfer_jobs
        req_status.transfer_jobs.add(load_job_id)
        self._jobs[load_job_id] = TransferJobStatus(
            req_id=request.request_id,
            pending_count=self.config.num_workers,
            keys=set(keys_to_load),
            is_store=False,
        )

        if self._chunks_being_loaded is not None:
            self._chunks_being_loaded.update(keys_to_load)
        req_status.partial_tail_boundary = None

    def _update_req_states(self, scheduler_output: SchedulerOutput) -> None:
        """Update request states from the Scheduler's output."""
        # new_block_ids_end[req_id][i] = end of pre-existing block_ids for
        # the i-th sliding window group (before this step's extend).
        # Used to detect sliding window blocks that got re-allocated.
        new_block_ids_end: dict[str, tuple[int, ...]] = {}

        for req_id, new_block_id_groups, preempted in yield_req_data(scheduler_output):
            req_status = self._req_status[req_id]
            req_status.update_offload_keys()

            if preempted:
                for group_state in req_status.group_states:
                    group_state.block_ids.clear()

            if new_block_id_groups:
                if self._sliding_window_groups:
                    new_block_ids_end[req_id] = tuple(
                        len(req_status.group_states[grp_idx].block_ids)
                        for grp_idx in self._sliding_window_groups
                    )
                req_status.update_block_id_groups(new_block_id_groups)
                for group_config in self.config.kv_group_configs:
                    new_blocks = new_block_id_groups[group_config.group_idx]
                    for bid in new_blocks:
                        if bid != 0:
                            self._current_batch_allocated_block_ids.add(bid)

        for copy in scheduler_output.kv_cache_block_copies or ():
            self._current_batch_allocated_block_ids.add(copy.dst_block_id)

        # Zero out stale block_ids in sliding window groups' pending-store
        # positions. Only sliding window groups can have stale entries (blocks
        # freed by remove_skipped_blocks then reallocated). Only positions in
        # [next_stored_chunk_idx * bsf, end) need checking where end is the
        # pre-extend length: earlier positions were already offloaded, later
        # ones are fresh allocations from this step.
        if self._sliding_window_groups and self._current_batch_allocated_block_ids:
            blocks_per_chunk = self.config.blocks_per_chunk
            for req_id, req_status in self._req_status.items():
                ends = new_block_ids_end.get(req_id)
                for i, grp_idx in enumerate(self._sliding_window_groups):
                    group_state = req_status.group_states[grp_idx]
                    start = group_state.next_stored_chunk_idx * blocks_per_chunk
                    end = ends[i] if ends is not None else len(group_state.block_ids)
                    for j in range(start, end):
                        if (
                            group_state.block_ids[j]
                            in self._current_batch_allocated_block_ids
                        ):
                            group_state.block_ids[j] = 0

    def _build_aligned_boundary_store_jobs(
        self, handoffs: dict[str, list[tuple[int, int, int]]]
    ) -> dict[int, TransferJob]:
        store_jobs: dict[int, TransferJob] = {}
        num_groups = len(self.config.kv_group_configs)
        config_idx_by_group = {
            config.group_idx: idx
            for idx, config in enumerate(self.config.kv_group_configs)
        }
        for req_id, entries in handoffs.items():
            req_status = self._req_status.get(req_id)
            if req_status is None:
                continue
            req = req_status.req
            max_boundary = self._calc_num_offloadable_tokens(req_status, req.num_tokens)
            for group_idx, block_id, boundary in entries:
                config_idx = config_idx_by_group.get(group_idx)
                if config_idx is None:
                    continue
                group_config = self.config.kv_group_configs[config_idx]
                if (
                    block_id == 0
                    or boundary > max_boundary
                    or boundary % group_config.tokens_per_chunk != 0
                ):
                    continue

                key = self._make_boundary_key(
                    req, group_idx, boundary, req_status.req_context
                )
                store_output = self.manager.prepare_store([key], req_status.req_context)
                if store_output is None:
                    self._connector_stats.increase_counter(
                        _ConnectorMetricName.ALLOCATION_FAILURE
                    )
                    continue
                if not store_output.keys_to_store:
                    continue

                job_id = self._generate_job_id()
                req_status.transfer_jobs.add(job_id)
                self._block_id_to_pending_jobs.setdefault(block_id, set()).add(job_id)
                self._jobs[job_id] = TransferJobStatus(
                    req_id=req_id,
                    pending_count=self.config.num_workers,
                    keys={key},
                    is_store=True,
                    fenced_block_ids=[block_id],
                )
                group_sizes = [0] * num_groups
                group_sizes[config_idx] = 1
                block_indices = [0] * num_groups
                block_indices[config_idx] = (
                    boundary // group_config.tokens_per_block - 1
                )
                store_jobs[job_id] = TransferJob(
                    req_id=req_id,
                    src_spec=GPULoadStoreSpec(
                        [block_id],
                        group_sizes=group_sizes,
                        block_indices=block_indices,
                    ),
                    dst_spec=store_output.store_spec,
                )
                self._events_tracker.record_store(
                    req,
                    group_config,
                    boundary // group_config.tokens_per_chunk - 1,
                    key,
                )
        return store_jobs

    def _build_partial_tail_store_jobs(
        self, scheduler_output: SchedulerOutput
    ) -> dict[int, TransferJob]:
        block_state = scheduler_output.kv_connector_block_state
        handoffs = block_state.boundary_state_offloads if block_state else None
        if not handoffs:
            return {}

        store_jobs = self._build_aligned_boundary_store_jobs(handoffs)
        if not self.config.supports_partial_tail:
            return store_jobs

        for req_id, entries in handoffs.items():
            entries = [
                entry
                for entry in entries
                if entry[2] % self._partial_tail_block_size != 0
            ]
            if not entries:
                continue
            req_status = self._req_status.get(req_id)
            assert req_status is not None
            boundaries = {boundary for _, _, boundary in entries}
            assert len(boundaries) == 1
            boundary = boundaries.pop()
            req = req_status.req
            group_states = {
                group.group_idx: state
                for group, state in zip(
                    self.config.kv_group_configs, req_status.group_states
                )
            }
            max_boundary = min(
                req.num_prompt_tokens,
                req_status.max_offload_tokens or req.num_prompt_tokens,
            )
            assert boundary > 0
            assert boundary % self.config.tokens_per_hash == 0
            assert boundary <= max_boundary

            cow_blocks = {group_idx: block_id for group_idx, block_id, _ in entries}
            assert self._cow_source_groups.issubset(cow_blocks)

            assert boundary % self._partial_tail_block_size != 0
            block_idx = boundary // self._partial_tail_block_size
            if any(
                group.group_idx not in self._cow_source_groups
                and block_idx >= len(group_states[group.group_idx].block_ids)
                for group in self.config.kv_group_configs
            ):
                continue
            keys = [
                self._make_boundary_key(
                    req, group.group_idx, boundary, req_status.req_context
                )
                for group in self.config.kv_group_configs
            ]
            block_ids = [
                cow_blocks[group.group_idx]
                if group.group_idx in self._cow_source_groups
                else group_states[group.group_idx].block_ids[block_idx]
                for group in self.config.kv_group_configs
            ]
            assert all(block_id != 0 for block_id in block_ids)

            store_output = self.manager.prepare_store(keys, req_status.req_context)
            if store_output is None:
                self._connector_stats.increase_counter(
                    _ConnectorMetricName.ALLOCATION_FAILURE
                )
                continue
            if not store_output.keys_to_store:
                continue

            for group_config, key in zip(self.config.kv_group_configs, keys):
                if key in store_output.keys_to_store:
                    self._events_tracker.record_partial_store(
                        req, group_config, boundary, key
                    )

            group_by_key = {key: idx for idx, key in enumerate(keys)}
            accepted_groups = [group_by_key[key] for key in store_output.keys_to_store]
            group_sizes = [0] * len(self.config.kv_group_configs)
            block_indices = [0] * len(self.config.kv_group_configs)
            for group_idx in accepted_groups:
                group_sizes[group_idx] = 1
                block_indices[group_idx] = block_idx
            source_blocks = [block_ids[group_idx] for group_idx in accepted_groups]

            job_id = self._generate_job_id()
            req_status.transfer_jobs.add(job_id)
            for block_id in source_blocks:
                self._block_id_to_pending_jobs.setdefault(block_id, set()).add(job_id)
            self._jobs[job_id] = TransferJobStatus(
                req_id=req_id,
                pending_count=self.config.num_workers,
                keys=set(store_output.keys_to_store),
                is_store=True,
                fenced_block_ids=source_blocks,
            )
            store_jobs[job_id] = TransferJob(
                req_id=req_id,
                src_spec=GPULoadStoreSpec(
                    source_blocks,
                    group_sizes=group_sizes,
                    block_indices=block_indices,
                ),
                dst_spec=store_output.store_spec,
            )

        return store_jobs

    def _reachable_store_block_mask(
        self,
        group_config: GroupOffloadConfig,
        start_chunk_idx: int,
        end_chunk_idx: int,
        final_segment_end_chunk_idx: int | None,
        reachable_boundaries: tuple[int, ...],
    ) -> list[bool] | None:
        """Build the block mask for a range of candidate offload chunks."""
        blocks_per_chunk = self.config.blocks_per_chunk
        kv_cache_spec = group_config.kv_cache_spec
        if isinstance(kv_cache_spec, SlidingWindowSpec) and (
            kv_cache_spec.extra_retained_tokens
        ):
            # Offload restores all allocated history, including MTP re-prefill
            # tokens. Widen only the store mask, not the model's attention window.
            kv_cache_spec = replace(
                kv_cache_spec,
                sliding_window=(
                    kv_cache_spec.sliding_window + kv_cache_spec.extra_retained_tokens
                ),
                extra_retained_tokens=0,
            )
        return group_config.manager_cls.reachable_block_mask(
            start_block=start_chunk_idx * blocks_per_chunk,
            end_block=end_chunk_idx * blocks_per_chunk,
            alignment_tokens=self.config.alignment_tokens,
            kv_cache_spec=kv_cache_spec,
            use_eagle=group_config.is_eagle_group,
            retention_interval=self.config.retention_interval,
            reachable_boundaries=reachable_boundaries,
            dcp_world_size=self.config.dcp_world_size,
            final_segment_end_block=(
                final_segment_end_chunk_idx * blocks_per_chunk
                if final_segment_end_chunk_idx is not None
                else None
            ),
        )

    def _final_swa_alignment_blocks(
        self, group_config: GroupOffloadConfig
    ) -> int | None:
        """Return representable final-tail alignment for a non-EAGLE SWA group."""
        alignment_tokens = self.config.alignment_tokens
        if (
            alignment_tokens is None
            or group_config.is_eagle_group
            or not isinstance(group_config.kv_cache_spec, SlidingWindowSpec)
            or alignment_tokens % group_config.tokens_per_block != 0
        ):
            return None
        return alignment_tokens // group_config.tokens_per_block

    def _build_store_jobs(
        self,
        scheduler_output: SchedulerOutput,
    ) -> dict[int, TransferJob]:
        blocks_per_chunk = self.config.blocks_per_chunk
        store_jobs: dict[int, TransferJob] = {}
        for req_id in chain(
            scheduler_output.num_scheduled_tokens,
            scheduler_output.finished_req_ids or (),
        ):
            req_status = self._req_status.get(req_id)
            if req_status is None:
                continue
            req = req_status.req

            if req.status is RequestStatus.FINISHED_ABORTED:
                num_tokens_after_batch = req.num_computed_tokens
            elif req.is_finished():
                # Clamp to the GPU prefix cache's commit point. The final sampled
                # token's slot is never committed (under spec decode it holds a
                # rejected draft's KV), so a block ending there must not be stored.
                num_tokens_after_batch = max(req.num_prompt_tokens, req.num_tokens - 1)
            else:
                num_scheduled_tokens = scheduler_output.num_scheduled_tokens[req_id]
                num_tokens_after_batch = req.num_computed_tokens + num_scheduled_tokens

            num_offloadable_tokens = self._calc_num_offloadable_tokens(
                req_status, num_tokens_after_batch
            )
            prompt_offloadable_tokens = self._calc_num_offloadable_tokens(
                req_status, req.num_prompt_tokens
            )

            # Filter out chunks skipped due to sliding window attention / SSM
            # or unreachable by the load path's alignment constraints.
            new_offload_keys: list[OffloadKey] = []
            group_store_ranges: list[tuple[int, int]] = []

            reachable_boundaries: tuple[int, ...] = ()
            if self.config.retention_interval is not None:
                reachable_boundaries = (req.num_prompt_tokens - 1,)
                if req.shared_prefix_boundary:
                    reachable_boundaries += (req.shared_prefix_boundary,)

            for group_config, group_state in zip(
                self.config.kv_group_configs, req_status.group_states
            ):
                num_chunks = req_status.storable_chunks(
                    group_config, group_state, num_offloadable_tokens
                )

                start_chunk_idx = group_state.next_stored_chunk_idx
                prompt_horizon_chunks = (
                    prompt_offloadable_tokens // group_config.tokens_per_chunk
                )
                final_swa_alignment_blocks = self._final_swa_alignment_blocks(
                    group_config
                )
                reconsider_final_swa_tail = (
                    req.is_finished()
                    and num_chunks != prompt_horizon_chunks
                    and self.config.retention_interval is None
                    and final_swa_alignment_blocks is not None
                )
                if (
                    self.config.retention_interval is not None
                    or group_config.is_eagle_group
                ):
                    store_horizon_chunks = None
                elif req.is_finished():
                    store_horizon_chunks = num_chunks
                elif num_chunks <= prompt_horizon_chunks:
                    store_horizon_chunks = prompt_horizon_chunks
                else:
                    # An active decode frontier is not a final request
                    # boundary. Only fixed alignment tails are reachable.
                    store_horizon_chunks = None
                if reconsider_final_swa_tail:
                    assert final_swa_alignment_blocks is not None
                    horizon_blocks = num_chunks * blocks_per_chunk
                    partial_segment_start_block = (
                        horizon_blocks - horizon_blocks % final_swa_alignment_blocks
                    )
                    partial_segment_start_chunk = (
                        partial_segment_start_block // blocks_per_chunk
                    )
                    start_chunk_idx = min(start_chunk_idx, partial_segment_start_chunk)
                group_store_ranges.append((start_chunk_idx, num_chunks))

                if group_config.requires_cow_source:
                    continue

                if num_chunks <= start_chunk_idx:
                    continue
                offload_keys = group_state.offload_keys[start_chunk_idx:num_chunks]
                # For each chunk, take the last corresponding GPU block. For
                # blocks_per_chunk=3 and GPU block IDs 1 5 6 7 2 4 9 3 8,
                # this selects GPU blocks 6 4 8.
                # A block_id of 0 means either a sliding window / SSM skip
                # or a stale entry that was zeroed out — skip it either way.
                offload_block_ids = group_state.block_ids[
                    start_chunk_idx * blocks_per_chunk
                    + blocks_per_chunk
                    - 1 : num_chunks * blocks_per_chunk : blocks_per_chunk
                ]
                assert len(offload_keys) == len(offload_block_ids)

                # Use reachable_block_mask to filter unreachable chunks
                # (SWA/Mamba sparsity + retention interval).
                # reachable_block_mask operates in KV-block coordinates,
                # so convert chunk indices to block indices.
                block_mask = self._reachable_store_block_mask(
                    group_config=group_config,
                    start_chunk_idx=start_chunk_idx,
                    end_chunk_idx=num_chunks,
                    final_segment_end_chunk_idx=store_horizon_chunks,
                    reachable_boundaries=reachable_boundaries,
                )

                prompt_horizon_block_mask: list[bool] | None = None
                if reconsider_final_swa_tail:
                    prompt_horizon_block_mask = self._reachable_store_block_mask(
                        group_config=group_config,
                        start_chunk_idx=start_chunk_idx,
                        end_chunk_idx=num_chunks,
                        final_segment_end_chunk_idx=prompt_horizon_chunks,
                        reachable_boundaries=reachable_boundaries,
                    )

                for key_idx, (offload_key, block_id) in enumerate(
                    zip(offload_keys, offload_block_ids)
                ):
                    if block_id == 0:
                        continue
                    abs_chunk_idx = start_chunk_idx + key_idx
                    if (
                        reconsider_final_swa_tail
                        and abs_chunk_idx < group_state.next_stored_chunk_idx
                        and (
                            prompt_horizon_block_mask is None
                            or any(
                                prompt_horizon_block_mask[
                                    key_idx * blocks_per_chunk + block_idx
                                ]
                                for block_idx in range(blocks_per_chunk)
                            )
                        )
                    ):
                        continue
                    # A chunk is reachable if any of its constituent
                    # blocks is reachable.
                    if block_mask is not None and not any(
                        block_mask[key_idx * blocks_per_chunk + b]
                        for b in range(blocks_per_chunk)
                    ):
                        continue
                    new_offload_keys.append(offload_key)

            if not new_offload_keys:
                req_status.advance_stored_idx(num_offloadable_tokens)
                continue

            store_output = self.manager.prepare_store(
                new_offload_keys, req_status.req_context
            )
            if store_output is None:
                self._connector_stats.increase_counter(
                    _ConnectorMetricName.ALLOCATION_FAILURE
                )
                logger.warning("Request %s: cannot store chunks", req_id)
                continue

            if not store_output.keys_to_store:
                req_status.advance_stored_idx(num_offloadable_tokens)
                continue

            keys_to_store = set(store_output.keys_to_store)

            group_sizes: list[int] = []
            block_indices: list[int] = []
            src_block_ids: list[int] = []
            fenced_block_ids: list[int] = []
            deferred_fence_block_ids: list[int] = []
            for group_config, group_state, store_range in zip(
                self.config.kv_group_configs,
                req_status.group_states,
                group_store_ranges,
            ):
                is_sliding_window = (
                    group_config.sliding_window_size_in_chunks is not None
                )
                start_chunk_idx, num_chunks = store_range
                block_ids = group_state.block_ids
                num_group_blocks = 0
                start_gpu_block_idx: int | None = None
                for idx, offload_key in enumerate(
                    group_state.offload_keys[start_chunk_idx:num_chunks]
                ):
                    if offload_key not in keys_to_store:
                        continue

                    chunk_idx = start_chunk_idx + idx

                    self._events_tracker.record_store(
                        req, group_config, chunk_idx, offload_key
                    )

                    gpu_block_idx = chunk_idx * blocks_per_chunk
                    for i in range(blocks_per_chunk):
                        block_id = block_ids[gpu_block_idx + i]
                        if block_id == 0:
                            continue
                        if start_gpu_block_idx is None:
                            start_gpu_block_idx = gpu_block_idx + i
                        src_block_ids.append(block_id)
                        num_group_blocks += 1
                        if is_sliding_window:
                            fenced_block_ids.append(block_id)
                        else:
                            deferred_fence_block_ids.append(block_id)

                group_sizes.append(num_group_blocks)
                block_indices.append(start_gpu_block_idx or 0)
                group_state.next_stored_chunk_idx = max(
                    group_state.next_stored_chunk_idx, num_chunks
                )

            src_spec = GPULoadStoreSpec(
                src_block_ids, group_sizes=group_sizes, block_indices=block_indices
            )
            dst_spec = store_output.store_spec

            job_id = self._generate_job_id()
            # a store can only be issued when no load is pending.
            if req_status.transfer_jobs:
                any_jid = next(iter(req_status.transfer_jobs))
                assert self._jobs[any_jid].is_store
            req_status.transfer_jobs.add(job_id)

            # Watch sliding window blocks as they may get evicted
            # before the request finishes
            for bid in fenced_block_ids:
                self._block_id_to_pending_jobs.setdefault(bid, set()).add(job_id)

            # the non-sliding window blocks will be watched only
            # when the request finishes
            self._jobs[job_id] = TransferJobStatus(
                req_id=req_id,
                pending_count=self.config.num_workers,
                keys=set(keys_to_store),
                is_store=True,
                deferred_fence_block_ids=deferred_fence_block_ids,
                fenced_block_ids=fenced_block_ids or None,
            )

            store_jobs[job_id] = TransferJob(
                req_id=req_id, src_spec=src_spec, dst_spec=dst_spec
            )

            logger.debug(
                "Request %s offloading %s chunks upto %d tokens (job %d)",
                req_id,
                len(keys_to_store),
                num_offloadable_tokens,
                job_id,
            )

            if req.is_finished():
                # Register non-sliding-window blocks for flush detection.
                for bid in deferred_fence_block_ids:
                    self._block_id_to_pending_jobs.setdefault(bid, set()).add(job_id)
                    if bid in self._current_batch_allocated_block_ids:
                        self._current_batch_jobs_to_flush.add(job_id)

        return store_jobs

    def build_connector_meta(
        self, scheduler_output: SchedulerOutput
    ) -> KVConnectorMetadata:
        self._update_req_states(scheduler_output)
        schedule_end_context = ScheduleEndContext(
            new_req_ids=[req.req_id for req in scheduler_output.scheduled_new_reqs],
            preempted_req_ids=scheduler_output.preempted_req_ids or (),
        )
        self.manager.on_schedule_end(schedule_end_context)

        # Flush jobs for preempted requests.
        for req_id in scheduler_output.preempted_req_ids or ():
            req_status = self._req_status.get(req_id)
            if req_status is None or not req_status.transfer_jobs:
                continue
            any_jid = next(iter(req_status.transfer_jobs))
            assert self._jobs[any_jid].is_store
            self._current_batch_jobs_to_flush.update(req_status.transfer_jobs)

        # Flush jobs that contain re-allocated blocks.
        if (
            self._block_id_to_pending_jobs
            and not self._block_id_to_pending_jobs.keys().isdisjoint(
                self._current_batch_allocated_block_ids
            )
        ):
            self._current_batch_jobs_to_flush.update(
                jid
                for bid in self._current_batch_allocated_block_ids
                if bid in self._block_id_to_pending_jobs
                for jid in self._block_id_to_pending_jobs[bid]
            )

        partial_store_jobs = self._build_partial_tail_store_jobs(scheduler_output)
        normal_store_jobs = self._build_store_jobs(scheduler_output)
        meta = OffloadingConnectorMetadata(
            load_jobs=self._current_batch_load_jobs,
            store_jobs=partial_store_jobs | normal_store_jobs,
            jobs_to_flush=self._current_batch_jobs_to_flush,
        )

        # All prepare_store calls for finished requests have been issued.
        # Signal on_request_finished and clean up state where possible.
        for req_id in scheduler_output.finished_req_ids or ():
            req_status = self._req_status.get(req_id)
            if req_status is None:
                continue
            req_status.finished_signaled = True
            self.manager.on_request_finished(req_status.req_context)
            if not req_status.transfer_jobs:
                del self._req_status[req_id]
        self._current_batch_load_jobs = {}
        self._current_batch_jobs_to_flush = set()
        self._current_batch_allocated_block_ids = set()
        return meta

    def has_pending_push_work(self) -> bool:
        """Whether the engine must keep stepping.

        While True, build_connector_meta() and update_connector_output()
        continue to be called even when no requests are scheduled.
        """
        return bool(self._jobs) or self.manager.has_pending_work()

    def update_connector_output(self, connector_output: KVConnectorOutput):
        """Update KVConnector state from worker-side connectors output.

        Args:
            connector_output (KVConnectorOutput): the worker-side
                connectors output.

        """
        meta = connector_output.kv_connector_worker_meta
        if not isinstance(meta, OffloadingWorkerMetadata):
            assert meta is None
            meta = OffloadingWorkerMetadata()
        if not meta.transfer_stats.is_empty():
            transfer_stats = OffloadingConnectorStats()
            if not meta.transfer_stats.load.is_empty():
                transfer_stats.increase_counter(
                    _TransferMetricName.LOAD_BYTES,
                    meta.transfer_stats.load.bytes,
                )
                transfer_stats.increase_counter(
                    _TransferMetricName.LOAD_TIME,
                    meta.transfer_stats.load.time,
                )
                for size in meta.transfer_stats.load.sizes:
                    transfer_stats.observe_histogram(
                        _TransferMetricName.LOAD_SIZE, size
                    )
            if not meta.transfer_stats.store.is_empty():
                transfer_stats.increase_counter(
                    _TransferMetricName.STORE_BYTES,
                    meta.transfer_stats.store.bytes,
                )
                transfer_stats.increase_counter(
                    _TransferMetricName.STORE_TIME,
                    meta.transfer_stats.store.time,
                )
                for size in meta.transfer_stats.store.sizes:
                    transfer_stats.observe_histogram(
                        _TransferMetricName.STORE_SIZE, size
                    )
            self._connector_stats.aggregate(transfer_stats)

        for job_id, count in meta.completed_jobs.items():
            assert count > 0
            if job_id < self._stale_job_threshold:
                logger.debug(
                    "Skipping stale completed job %d (pre-reset counter: %d)",
                    job_id,
                    self._stale_job_threshold,
                )
                continue
            job_status = self._jobs[job_id]
            job_status.pending_count -= count
            if job_status.pending_count > 0:
                continue
            assert job_status.pending_count == 0

            req_status = self._req_status[job_status.req_id]
            if job_status.is_store:
                self.manager.complete_store(job_status.keys, req_status.req_context)
            else:
                self.manager.complete_load(job_status.keys, req_status.req_context)
                if self._chunks_being_loaded:
                    self._chunks_being_loaded.difference_update(job_status.keys)
            if self._block_id_to_pending_jobs:
                # Sliding window blocks are tracked from store creation
                # and must be cleaned up unconditionally.
                self._remove_pending_job(job_id, job_status.fenced_block_ids)
                # Non-sliding-window blocks are only tracked after
                # request_finished, so only clean up for finished requests.
                if req_status.req.is_finished():
                    self._remove_pending_job(
                        job_id, job_status.deferred_fence_block_ids
                    )

            del self._jobs[job_id]
            req_status.transfer_jobs.remove(job_id)
            if req_status.finished_signaled and not req_status.transfer_jobs:
                del self._req_status[job_status.req_id]

    def get_stats(self) -> OffloadingConnectorStats | None:
        stats: OffloadingConnectorStats | None = None
        if not self._connector_stats.is_empty():
            stats = self._connector_stats
            self._connector_stats = OffloadingConnectorStats()

        manager_stats = self.manager.get_stats()
        if manager_stats is not None:
            if stats is None:
                stats = manager_stats
            else:
                stats.aggregate(manager_stats)

        return stats

    def request_finished(
        self,
        request: Request,
    ) -> tuple[bool, dict[str, Any] | None]:
        """Called when a request has finished, before its blocks are freed.

        Returns:
            True if the request is being saved/sent asynchronously and blocks
            should not be freed until the request_id is returned from
            get_finished().
            Optional KVTransferParams to be included in the request outputs
            returned by the engine.

        """
        req_status = self._req_status.get(request.request_id)

        if req_status is None:
            # Untracked request (offloading never started): no in-flight jobs,
            # nothing was deferred, so finalize immediately.
            req_context = _create_req_context(request)
            self.manager.on_new_request(req_context)
            self.manager.on_request_finished(req_context)
            return False, None

        self._maybe_observe_lookup_async_delay(req_status)

        # Update offload keys with final block hash so _build_store_jobs can
        # create store jobs for the last block(s) on the next schedule step.
        req_status.update_offload_keys()

        # Keep req_status alive: _build_store_jobs will process finished_req_ids
        # on the next step and handle cleanup after creating store jobs.
        # Register deferred fences so future block reuse triggers a flush via
        # _block_id_to_pending_jobs.
        for job_id in req_status.transfer_jobs:
            job_status = self._jobs[job_id]
            for bid in job_status.deferred_fence_block_ids or ():
                self._block_id_to_pending_jobs.setdefault(bid, set()).add(job_id)

        return False, None

    def take_events(self) -> Iterable[KVCacheEvent]:
        """Drain pending KV cache events.

        Complete metadata is available only when self-describing KV events
        are enabled, and only for full-attention groups. Other shapes retain
        the previous placeholder payload so consumers can ignore them.

        Yields:
            ``BlockStored`` or ``BlockRemoved`` events corresponding to
            the underlying :class:`OffloadingEvent` stream.

        """
        yield from self._events_tracker.take_events(self.manager.take_events())

    def reset_cache(self) -> None:
        """Reset the offloading manager cache, evicting all stored chunks."""
        # reset_cache cannot be called in the middle of a schedule step
        assert not self._current_batch_load_jobs
        assert not self._current_batch_jobs_to_flush
        assert not self._current_batch_allocated_block_ids

        # Flush all in-flight jobs
        self._current_batch_jobs_to_flush.update(self._jobs.keys())

        for req_id, status in list(self._req_status.items()):
            if status.req.is_finished():
                if not status.finished_signaled:
                    self.manager.on_request_finished(status.req_context)
                del self._req_status[req_id]

        # Reset offloading manager cache
        self.manager.reset_cache()

        # Reset store progress so active requests re-offload from chunk 0.
        for status in self._req_status.values():
            for group_state in status.group_states:
                group_state.next_stored_chunk_idx = 0
            status.transfer_jobs.clear()
            status.partial_tail_boundary = None

        # Discard jobs and save job_counter to be able to discard worker responses
        self._stale_job_threshold = self._job_counter
        self._jobs.clear()
        self._block_id_to_pending_jobs.clear()

        # The manager pool is empty; pending event payloads and announced
        # reference counts are stale.
        self._events_tracker.reset()

        # Note: _current_batch_jobs_to_flush is intentionally NOT cleared.
        # The load flush IDs collected above must be delivered to workers.
        if self._chunks_being_loaded is not None:
            self._chunks_being_loaded.clear()

    def shutdown(self) -> None:
        self.manager.shutdown()

_final_swa_alignment_blocks(group_config)

Return representable final-tail alignment for a non-EAGLE SWA group.

Source code in vllm/distributed/kv_transfer/kv_connector/v1/offloading/scheduler.py
def _final_swa_alignment_blocks(
    self, group_config: GroupOffloadConfig
) -> int | None:
    """Return representable final-tail alignment for a non-EAGLE SWA group."""
    alignment_tokens = self.config.alignment_tokens
    if (
        alignment_tokens is None
        or group_config.is_eagle_group
        or not isinstance(group_config.kv_cache_spec, SlidingWindowSpec)
        or alignment_tokens % group_config.tokens_per_block != 0
    ):
        return None
    return alignment_tokens // group_config.tokens_per_block

_lookup_complete_chunks(req_status, max_num_new_tokens=None)

Find how many tokens beyond num_locally_computed_tokens can be loaded.

Iterates full-attention groups first (prefix lookup), then sliding-window groups (suffix lookup). Each group may tighten max_hit_size_tokens, which can invalidate an earlier group's result, so the loop re-runs when that happens until num_hit_tokens converges.

Source code in vllm/distributed/kv_transfer/kv_connector/v1/offloading/scheduler.py
def _lookup_complete_chunks(
    self,
    req_status: RequestOffloadState,
    max_num_new_tokens: int | None = None,
) -> int | None:
    """Find how many tokens beyond num_locally_computed_tokens can be loaded.

    Iterates full-attention groups first (prefix lookup), then sliding-window
    groups (suffix lookup). Each group may tighten max_hit_size_tokens, which
    can invalidate an earlier group's result, so the loop re-runs when that
    happens until num_hit_tokens converges.
    """
    num_computed_tokens = req_status.num_locally_computed_tokens
    max_hit_size_tokens: int = req_status.req.num_tokens
    if max_num_new_tokens is not None:
        max_hit_size_tokens = min(
            max_hit_size_tokens, num_computed_tokens + max_num_new_tokens
        )
    if req_status.max_load_tokens is not None:
        max_hit_size_tokens = min(
            max_hit_size_tokens,
            num_computed_tokens + req_status.max_load_tokens,
        )
    if self._sliding_window_groups:
        # the last prompt token has to be recomputed to get the logprobs
        # for sliding window attention, we must reduce by 1 to make sure
        # we still have a hit after reduction
        max_hit_size_tokens -= 1
        if self._mamba_align_size is not None:
            # Constrain hit-window to the mamba block size.
            max_hit_size_tokens = round_down(
                max_hit_size_tokens, self._mamba_align_size
            )

    num_hit_tokens: int = 0
    defer_lookup = False
    lookup_groups = self._lookup_groups

    # Tracks which eagle groups have already popped their volatile trailing chunk
    # in the current convergence iteration. Reset when a non-eagle group
    # tightens the hit boundary, requiring a fresh pop.
    eagle_verified: set[int] = set()
    while lookup_groups:
        looked_up_sliding_window: bool = False
        groups_iter = iter(lookup_groups)
        lookup_groups = ()
        for group_idx in groups_iter:
            group_config: GroupOffloadConfig = self.config.kv_group_configs[
                group_idx
            ]
            group_state: RequestGroupState = req_status.group_states[group_idx]
            tokens_per_chunk = group_config.tokens_per_chunk
            offload_keys = group_state.offload_keys

            assert (
                len(offload_keys) >= req_status.req.num_tokens // tokens_per_chunk
            )

            is_eagle_unverified = (
                group_config.is_eagle_group and group_idx not in eagle_verified
            )

            # Constrain to a chunk-aligned boundary for this group.
            max_hit_size_tokens = min(
                max_hit_size_tokens, len(offload_keys) * tokens_per_chunk
            )
            if req_status.max_load_tokens is not None:
                max_hit_size_tokens = round_down(
                    max_hit_size_tokens, tokens_per_chunk
                )
            if max_hit_size_tokens - num_computed_tokens < tokens_per_chunk:
                # We can only load less than a chunk, so skip.
                return 0

            sliding_window_size_in_chunks = (
                group_config.sliding_window_size_in_chunks
            )

            # For eagle groups, query one extra chunk that will be popped.
            # Widening applies to every group type: without it, the pop
            # below shrinks max_hit_size_tokens past what was queried,
            # which can push the confirmed boundary under a coarser
            # sibling group's chunk granularity and zero the whole
            # request's hit (issue #52735).
            query_max = max_hit_size_tokens
            if is_eagle_unverified:
                query_max = min(
                    max_hit_size_tokens + tokens_per_chunk,
                    len(offload_keys) * tokens_per_chunk,
                )

            num_chunks = min(cdiv(query_max, tokens_per_chunk), len(offload_keys))
            start_chunk_idx = num_computed_tokens // tokens_per_chunk
            offload_keys = offload_keys[start_chunk_idx:num_chunks]

            # end index (in the sliced offload_keys) up to which we
            # have backend-confirmed hits
            num_hit_chunks: int | None
            if sliding_window_size_in_chunks is None:
                num_hit_chunks = self._maximal_prefix_lookup(
                    offload_keys,
                    req_status.req_context,
                    req_status.req,
                    group_config,
                    start_chunk_idx,
                )
            else:
                required_window = sliding_window_size_in_chunks
                if is_eagle_unverified:
                    required_window += 1
                candidate_end = min(
                    max_hit_size_tokens,
                    (num_chunks - int(is_eagle_unverified)) * tokens_per_chunk,
                )
                initial_window = group_config.load_window_size_in_chunks(
                    candidate_end
                )
                assert initial_window is not None
                num_hit_chunks = self._sliding_window_lookup(
                    offload_keys,
                    required_window,
                    req_status.req_context,
                    initial_window + int(is_eagle_unverified),
                )
            if num_hit_chunks == 0:
                return 0

            if num_hit_chunks is None:
                defer_lookup = True
            else:
                if is_eagle_unverified:
                    num_hit_chunks -= 1
                    eagle_verified.add(group_idx)

                max_hit_size_tokens = min(
                    max_hit_size_tokens,
                    tokens_per_chunk * (start_chunk_idx + num_hit_chunks),
                )

            new_num_hit_tokens = max_hit_size_tokens - num_computed_tokens
            if new_num_hit_tokens < tokens_per_chunk:
                # We can only load less than a chunk, so skip.
                return 0

            if new_num_hit_tokens < num_hit_tokens:
                if not group_config.is_eagle_group:
                    eagle_verified.clear()
                if defer_lookup:
                    # make another iteration on all groups to check
                    # if we still need to defer lookup
                    defer_lookup = False
                    lookup_groups = self._lookup_groups
                elif looked_up_sliding_window and not lookup_groups:
                    # we need another iteration to confirm previously looked up
                    # sliding window works with the new_num_hit_tokens
                    lookup_groups = self._sliding_window_groups

            looked_up_sliding_window |= sliding_window_size_in_chunks is not None
            num_hit_tokens = new_num_hit_tokens

    if defer_lookup:
        logger.debug(
            "Offloading manager delayed request %s as backend requested",
            req_status.req.request_id,
        )
        return None

    # Possibly delay the request if any hit chunk is already being loaded.
    if self._chunks_being_loaded:
        for group_config, group_state in zip(
            self.config.kv_group_configs, req_status.group_states
        ):
            tokens_per_chunk = group_config.tokens_per_chunk
            sliding_window_size_in_chunks = group_config.load_window_size_in_chunks(
                num_computed_tokens + num_hit_tokens
            )
            offload_keys = group_state.offload_keys
            num_chunks = cdiv(
                num_computed_tokens + num_hit_tokens, tokens_per_chunk
            )
            start_chunk_idx = num_computed_tokens // tokens_per_chunk
            offload_keys = offload_keys[start_chunk_idx:num_chunks]
            if sliding_window_size_in_chunks is not None:
                offload_keys = offload_keys[-sliding_window_size_in_chunks:]
            if any(key in self._chunks_being_loaded for key in offload_keys):
                # Hit chunks are being loaded, so delay the request.
                logger.debug(
                    "Delaying request %s since some of its"
                    " chunks are already being loaded",
                    req_status.req.request_id,
                )
                return None

    logger.debug(
        "Request %s hit %s offloaded tokens after %s GPU hit tokens",
        req_status.req.request_id,
        num_hit_tokens,
        num_computed_tokens,
    )

    return num_hit_tokens

_maximal_prefix_lookup(keys, req_context, req, group_config, start_chunk_idx)

Return the number of consecutive offloaded chunks from the start, or None if the backend deferred a lookup.

Source code in vllm/distributed/kv_transfer/kv_connector/v1/offloading/scheduler.py
def _maximal_prefix_lookup(
    self,
    keys: Iterable[OffloadKey],
    req_context: ReqContext,
    req: Request,
    group_config: GroupOffloadConfig,
    start_chunk_idx: int,
) -> int | None:
    """Return the number of consecutive offloaded chunks from the start,
    or None if the backend deferred a lookup."""
    hit_count = 0
    defer_lookup = False
    for local_idx, key in enumerate(keys):
        result = self.manager.lookup(key, req_context)
        match result:
            case LookupResult.HIT:
                self._events_tracker.record_lookup(
                    req,
                    group_config,
                    start_chunk_idx + local_idx,
                    key,
                )
                hit_count += 1
            case LookupResult.HIT_PENDING:
                defer_lookup = True
                hit_count += 1
            case LookupResult.RETRY:
                # Don't break: keep scanning to let manager kick off
                # async lookups (until a miss is detected).
                defer_lookup = True
            case LookupResult.MISS:
                break
    return hit_count if not defer_lookup else None

_reachable_store_block_mask(group_config, start_chunk_idx, end_chunk_idx, final_segment_end_chunk_idx, reachable_boundaries)

Build the block mask for a range of candidate offload chunks.

Source code in vllm/distributed/kv_transfer/kv_connector/v1/offloading/scheduler.py
def _reachable_store_block_mask(
    self,
    group_config: GroupOffloadConfig,
    start_chunk_idx: int,
    end_chunk_idx: int,
    final_segment_end_chunk_idx: int | None,
    reachable_boundaries: tuple[int, ...],
) -> list[bool] | None:
    """Build the block mask for a range of candidate offload chunks."""
    blocks_per_chunk = self.config.blocks_per_chunk
    kv_cache_spec = group_config.kv_cache_spec
    if isinstance(kv_cache_spec, SlidingWindowSpec) and (
        kv_cache_spec.extra_retained_tokens
    ):
        # Offload restores all allocated history, including MTP re-prefill
        # tokens. Widen only the store mask, not the model's attention window.
        kv_cache_spec = replace(
            kv_cache_spec,
            sliding_window=(
                kv_cache_spec.sliding_window + kv_cache_spec.extra_retained_tokens
            ),
            extra_retained_tokens=0,
        )
    return group_config.manager_cls.reachable_block_mask(
        start_block=start_chunk_idx * blocks_per_chunk,
        end_block=end_chunk_idx * blocks_per_chunk,
        alignment_tokens=self.config.alignment_tokens,
        kv_cache_spec=kv_cache_spec,
        use_eagle=group_config.is_eagle_group,
        retention_interval=self.config.retention_interval,
        reachable_boundaries=reachable_boundaries,
        dcp_world_size=self.config.dcp_world_size,
        final_segment_end_block=(
            final_segment_end_chunk_idx * blocks_per_chunk
            if final_segment_end_chunk_idx is not None
            else None
        ),
    )

_sliding_window_lookup(keys, sliding_window_size, req_context, initial_window_size=None)

Return the end index (in keys) of the last run of sliding_window_size consecutive hits, scanning from the end. The first run may need a larger window for a partial rightmost chunk. Returns 0 on miss, None if the backend deferred a lookup.

Source code in vllm/distributed/kv_transfer/kv_connector/v1/offloading/scheduler.py
def _sliding_window_lookup(
    self,
    keys: Sequence[OffloadKey],
    sliding_window_size: int,
    req_context: ReqContext,
    initial_window_size: int | None = None,
) -> int | None:
    """Return the end index (in `keys`) of the last run of
    `sliding_window_size` consecutive hits, scanning from the end.
    The first run may need a larger window for a partial rightmost chunk.
    Returns 0 on miss, None if the backend deferred a lookup."""
    defer_lookup = False
    pending_in_window = False
    consecutive_hits = 0
    required_window = initial_window_size or sliding_window_size
    for idx in range(len(keys) - 1, -1, -1):
        match self.manager.lookup(keys[idx], req_context):
            case LookupResult.HIT:
                consecutive_hits += 1
            case LookupResult.HIT_PENDING:
                # Block is in cache, just not readable yet — counts
                # as hit for the consecutive streak. Don't break:
                # keep scanning to let manager kick off async lookups.
                pending_in_window = True
                consecutive_hits += 1
            case LookupResult.RETRY:
                # Block location uncertain — does not count as hit.
                # Don't break: keep scanning to let manager kick off
                # async lookups.
                defer_lookup = True
                consecutive_hits = 0
                pending_in_window = False
                required_window = sliding_window_size
            case LookupResult.MISS:
                consecutive_hits = 0
                # This gap rules out the incomplete window to its right.
                pending_in_window = False
                required_window = sliding_window_size
        if consecutive_hits == required_window:
            return (
                None if defer_lookup or pending_in_window else idx + required_window
            )
    return None if defer_lookup or pending_in_window else consecutive_hits

_update_req_states(scheduler_output)

Update request states from the Scheduler's output.

Source code in vllm/distributed/kv_transfer/kv_connector/v1/offloading/scheduler.py
def _update_req_states(self, scheduler_output: SchedulerOutput) -> None:
    """Update request states from the Scheduler's output."""
    # new_block_ids_end[req_id][i] = end of pre-existing block_ids for
    # the i-th sliding window group (before this step's extend).
    # Used to detect sliding window blocks that got re-allocated.
    new_block_ids_end: dict[str, tuple[int, ...]] = {}

    for req_id, new_block_id_groups, preempted in yield_req_data(scheduler_output):
        req_status = self._req_status[req_id]
        req_status.update_offload_keys()

        if preempted:
            for group_state in req_status.group_states:
                group_state.block_ids.clear()

        if new_block_id_groups:
            if self._sliding_window_groups:
                new_block_ids_end[req_id] = tuple(
                    len(req_status.group_states[grp_idx].block_ids)
                    for grp_idx in self._sliding_window_groups
                )
            req_status.update_block_id_groups(new_block_id_groups)
            for group_config in self.config.kv_group_configs:
                new_blocks = new_block_id_groups[group_config.group_idx]
                for bid in new_blocks:
                    if bid != 0:
                        self._current_batch_allocated_block_ids.add(bid)

    for copy in scheduler_output.kv_cache_block_copies or ():
        self._current_batch_allocated_block_ids.add(copy.dst_block_id)

    # Zero out stale block_ids in sliding window groups' pending-store
    # positions. Only sliding window groups can have stale entries (blocks
    # freed by remove_skipped_blocks then reallocated). Only positions in
    # [next_stored_chunk_idx * bsf, end) need checking where end is the
    # pre-extend length: earlier positions were already offloaded, later
    # ones are fresh allocations from this step.
    if self._sliding_window_groups and self._current_batch_allocated_block_ids:
        blocks_per_chunk = self.config.blocks_per_chunk
        for req_id, req_status in self._req_status.items():
            ends = new_block_ids_end.get(req_id)
            for i, grp_idx in enumerate(self._sliding_window_groups):
                group_state = req_status.group_states[grp_idx]
                start = group_state.next_stored_chunk_idx * blocks_per_chunk
                end = ends[i] if ends is not None else len(group_state.block_ids)
                for j in range(start, end):
                    if (
                        group_state.block_ids[j]
                        in self._current_batch_allocated_block_ids
                    ):
                        group_state.block_ids[j] = 0

get_num_new_matched_tokens(request, num_computed_tokens, max_num_new_tokens=None)

Get number of new tokens that can be loaded beyond the num_computed_tokens.

Parameters:

  • request

    (Request) –

    the request object.

  • num_computed_tokens

    (int) –

    the number of locally computed tokens for this request

  • max_num_new_tokens

    (int | None, default: None ) –

    cap on the number of tokens that may be loaded beyond num_computed_tokens, if any.

Returns:

  • tuple[int | None, bool] –

    A tuple with the following elements: - The number of tokens that can be loaded beyond what is already computed. If None, it means that the connector needs more time to determine the number of matched tokens, and the scheduler should query for this request again later. - True if tokens will be loaded asynchronously (between scheduler steps).

Source code in vllm/distributed/kv_transfer/kv_connector/v1/offloading/scheduler.py
def get_num_new_matched_tokens(
    self,
    request: Request,
    num_computed_tokens: int,
    max_num_new_tokens: int | None = None,
) -> tuple[int | None, bool]:
    """Get number of new tokens that can be loaded beyond the
    num_computed_tokens.

    Args:
        request (Request): the request object.
        num_computed_tokens (int): the number of locally
            computed tokens for this request
        max_num_new_tokens (int | None): cap on the number of tokens that
            may be loaded beyond `num_computed_tokens`, if any.

    Returns:
        A tuple with the following elements:
            - The number of tokens that can be loaded beyond what is
              already computed.
              If None, it means that the connector needs more time to
              determine the number of matched tokens, and the scheduler
              should query for this request again later.
            - `True` if tokens will be loaded asynchronously
              (between scheduler steps).

    """
    req_status = self._req_status[request.request_id]
    for group_state in req_status.group_states:
        group_state.block_ids.clear()

    if req_status.transfer_jobs:
        logger.debug(
            "Delaying request %s since it still has in-flight transfers",
            request.request_id,
        )
        return None, False

    req_status.update_offload_keys()
    req_status.num_locally_computed_tokens = num_computed_tokens

    num_hit_tokens: int | None
    if request.skip_reading_prefix_cache:
        num_hit_tokens = 0
    else:
        lookup_start = time.monotonic()
        num_hit_tokens = self._lookup(req_status, max_num_new_tokens)
        self._connector_stats.observe_histogram(
            _ConnectorMetricName.LOOKUP_SYNC_DELAY,
            time.monotonic() - lookup_start,
        )
        if num_hit_tokens is None:
            if req_status.deferred_lookup_start_time is None:
                req_status.deferred_lookup_start_time = lookup_start
        else:
            self._maybe_observe_lookup_async_delay(req_status)
    req_status.update_num_hit_chunks(num_computed_tokens + (num_hit_tokens or 0))

    return num_hit_tokens, bool(num_hit_tokens)

has_pending_push_work()

Whether the engine must keep stepping.

While True, build_connector_meta() and update_connector_output() continue to be called even when no requests are scheduled.

Source code in vllm/distributed/kv_transfer/kv_connector/v1/offloading/scheduler.py
def has_pending_push_work(self) -> bool:
    """Whether the engine must keep stepping.

    While True, build_connector_meta() and update_connector_output()
    continue to be called even when no requests are scheduled.
    """
    return bool(self._jobs) or self.manager.has_pending_work()

on_new_request(request)

Called when a new request is added to the scheduler.

Source code in vllm/distributed/kv_transfer/kv_connector/v1/offloading/scheduler.py
def on_new_request(self, request: Request) -> None:
    """Called when a new request is added to the scheduler."""
    req_context = _create_req_context(request)
    offloading_context = self.manager.on_new_request(req_context)
    req_status = RequestOffloadState(
        config=self.config,
        req=request,
        req_context=req_context,
        offloading_context=offloading_context,
    )
    self._req_status[request.request_id] = req_status

request_finished(request)

Called when a request has finished, before its blocks are freed.

Returns:

  • bool –

    True if the request is being saved/sent asynchronously and blocks

  • dict[str, Any] | None –

    should not be freed until the request_id is returned from

  • tuple[bool, dict[str, Any] | None] –

    get_finished().

  • tuple[bool, dict[str, Any] | None] –

    Optional KVTransferParams to be included in the request outputs

  • tuple[bool, dict[str, Any] | None] –

    returned by the engine.

Source code in vllm/distributed/kv_transfer/kv_connector/v1/offloading/scheduler.py
def request_finished(
    self,
    request: Request,
) -> tuple[bool, dict[str, Any] | None]:
    """Called when a request has finished, before its blocks are freed.

    Returns:
        True if the request is being saved/sent asynchronously and blocks
        should not be freed until the request_id is returned from
        get_finished().
        Optional KVTransferParams to be included in the request outputs
        returned by the engine.

    """
    req_status = self._req_status.get(request.request_id)

    if req_status is None:
        # Untracked request (offloading never started): no in-flight jobs,
        # nothing was deferred, so finalize immediately.
        req_context = _create_req_context(request)
        self.manager.on_new_request(req_context)
        self.manager.on_request_finished(req_context)
        return False, None

    self._maybe_observe_lookup_async_delay(req_status)

    # Update offload keys with final block hash so _build_store_jobs can
    # create store jobs for the last block(s) on the next schedule step.
    req_status.update_offload_keys()

    # Keep req_status alive: _build_store_jobs will process finished_req_ids
    # on the next step and handle cleanup after creating store jobs.
    # Register deferred fences so future block reuse triggers a flush via
    # _block_id_to_pending_jobs.
    for job_id in req_status.transfer_jobs:
        job_status = self._jobs[job_id]
        for bid in job_status.deferred_fence_block_ids or ():
            self._block_id_to_pending_jobs.setdefault(bid, set()).add(job_id)

    return False, None

reset_cache()

Reset the offloading manager cache, evicting all stored chunks.

Source code in vllm/distributed/kv_transfer/kv_connector/v1/offloading/scheduler.py
def reset_cache(self) -> None:
    """Reset the offloading manager cache, evicting all stored chunks."""
    # reset_cache cannot be called in the middle of a schedule step
    assert not self._current_batch_load_jobs
    assert not self._current_batch_jobs_to_flush
    assert not self._current_batch_allocated_block_ids

    # Flush all in-flight jobs
    self._current_batch_jobs_to_flush.update(self._jobs.keys())

    for req_id, status in list(self._req_status.items()):
        if status.req.is_finished():
            if not status.finished_signaled:
                self.manager.on_request_finished(status.req_context)
            del self._req_status[req_id]

    # Reset offloading manager cache
    self.manager.reset_cache()

    # Reset store progress so active requests re-offload from chunk 0.
    for status in self._req_status.values():
        for group_state in status.group_states:
            group_state.next_stored_chunk_idx = 0
        status.transfer_jobs.clear()
        status.partial_tail_boundary = None

    # Discard jobs and save job_counter to be able to discard worker responses
    self._stale_job_threshold = self._job_counter
    self._jobs.clear()
    self._block_id_to_pending_jobs.clear()

    # The manager pool is empty; pending event payloads and announced
    # reference counts are stale.
    self._events_tracker.reset()

    # Note: _current_batch_jobs_to_flush is intentionally NOT cleared.
    # The load flush IDs collected above must be delivered to workers.
    if self._chunks_being_loaded is not None:
        self._chunks_being_loaded.clear()

take_events()

Drain pending KV cache events.

Complete metadata is available only when self-describing KV events are enabled, and only for full-attention groups. Other shapes retain the previous placeholder payload so consumers can ignore them.

Yields:

Source code in vllm/distributed/kv_transfer/kv_connector/v1/offloading/scheduler.py
def take_events(self) -> Iterable[KVCacheEvent]:
    """Drain pending KV cache events.

    Complete metadata is available only when self-describing KV events
    are enabled, and only for full-attention groups. Other shapes retain
    the previous placeholder payload so consumers can ignore them.

    Yields:
        ``BlockStored`` or ``BlockRemoved`` events corresponding to
        the underlying :class:`OffloadingEvent` stream.

    """
    yield from self._events_tracker.take_events(self.manager.take_events())

update_connector_output(connector_output)

Update KVConnector state from worker-side connectors output.

Parameters:

  • connector_output

    (KVConnectorOutput) –

    the worker-side connectors output.

Source code in vllm/distributed/kv_transfer/kv_connector/v1/offloading/scheduler.py
def update_connector_output(self, connector_output: KVConnectorOutput):
    """Update KVConnector state from worker-side connectors output.

    Args:
        connector_output (KVConnectorOutput): the worker-side
            connectors output.

    """
    meta = connector_output.kv_connector_worker_meta
    if not isinstance(meta, OffloadingWorkerMetadata):
        assert meta is None
        meta = OffloadingWorkerMetadata()
    if not meta.transfer_stats.is_empty():
        transfer_stats = OffloadingConnectorStats()
        if not meta.transfer_stats.load.is_empty():
            transfer_stats.increase_counter(
                _TransferMetricName.LOAD_BYTES,
                meta.transfer_stats.load.bytes,
            )
            transfer_stats.increase_counter(
                _TransferMetricName.LOAD_TIME,
                meta.transfer_stats.load.time,
            )
            for size in meta.transfer_stats.load.sizes:
                transfer_stats.observe_histogram(
                    _TransferMetricName.LOAD_SIZE, size
                )
        if not meta.transfer_stats.store.is_empty():
            transfer_stats.increase_counter(
                _TransferMetricName.STORE_BYTES,
                meta.transfer_stats.store.bytes,
            )
            transfer_stats.increase_counter(
                _TransferMetricName.STORE_TIME,
                meta.transfer_stats.store.time,
            )
            for size in meta.transfer_stats.store.sizes:
                transfer_stats.observe_histogram(
                    _TransferMetricName.STORE_SIZE, size
                )
        self._connector_stats.aggregate(transfer_stats)

    for job_id, count in meta.completed_jobs.items():
        assert count > 0
        if job_id < self._stale_job_threshold:
            logger.debug(
                "Skipping stale completed job %d (pre-reset counter: %d)",
                job_id,
                self._stale_job_threshold,
            )
            continue
        job_status = self._jobs[job_id]
        job_status.pending_count -= count
        if job_status.pending_count > 0:
            continue
        assert job_status.pending_count == 0

        req_status = self._req_status[job_status.req_id]
        if job_status.is_store:
            self.manager.complete_store(job_status.keys, req_status.req_context)
        else:
            self.manager.complete_load(job_status.keys, req_status.req_context)
            if self._chunks_being_loaded:
                self._chunks_being_loaded.difference_update(job_status.keys)
        if self._block_id_to_pending_jobs:
            # Sliding window blocks are tracked from store creation
            # and must be cleaned up unconditionally.
            self._remove_pending_job(job_id, job_status.fenced_block_ids)
            # Non-sliding-window blocks are only tracked after
            # request_finished, so only clean up for finished requests.
            if req_status.req.is_finished():
                self._remove_pending_job(
                    job_id, job_status.deferred_fence_block_ids
                )

        del self._jobs[job_id]
        req_status.transfer_jobs.remove(job_id)
        if req_status.finished_signaled and not req_status.transfer_jobs:
            del self._req_status[job_status.req_id]

RequestOffloadState dataclass

Methods:

  • storable_chunks –

    Number of allocated and keyed leading chunks eligible for store.

Source code in vllm/distributed/kv_transfer/kv_connector/v1/offloading/scheduler.py
@dataclass(slots=True)
class RequestOffloadState:
    config: SchedulerOffloadConfig
    req: Request
    req_context: ReqContext
    offloading_context: RequestOffloadingContext
    group_states: tuple[RequestGroupState, ...] = field(init=False)
    # upper bound on tokens to load for this request; None means no cap
    max_load_tokens: int | None = None
    # upper bound on tokens to offload for this request; None means no cap
    max_offload_tokens: int | None = None
    # number of hits in the GPU cache
    num_locally_computed_tokens: int = 0
    # In-flight job IDs. Per the connector's invariant, at any given time
    # this contains either a single load job, or one or more store jobs.
    transfer_jobs: set[int] = field(default_factory=set)
    # time.monotonic() of this request's first deferred offload lookup;
    # None once consumed (observed) or while no lookup is pending.
    deferred_lookup_start_time: float | None = None
    # Fine-grained token boundary selected beyond the last complete offload
    # chunk. It is consumed when the corresponding load is scheduled.
    partial_tail_boundary: int | None = None
    # True once on_request_finished has been signaled to the manager.
    finished_signaled: bool = False

    def __post_init__(self) -> None:
        self.group_states = tuple(
            RequestGroupState() for _ in self.config.kv_group_configs
        )
        params = self.req.kv_transfer_params

        # NOTE: This field is experimental and subject to change in the future.
        raw = params.get("max_load_tokens") if params else None
        if type(raw) is int and raw >= 0:
            self.max_load_tokens = raw
            logger.debug(
                "Request %s: max_load_tokens set to %d",
                self.req.request_id,
                raw,
            )
        elif raw is not None:
            logger.warning(
                "max_load_tokens must be a non-negative int, got %r; ignoring", raw
            )

        # NOTE: This field is experimental and subject to change in the future.
        raw = params.get("max_offload_tokens") if params else None
        if type(raw) is int and raw >= 0:
            self.max_offload_tokens = raw
            logger.debug(
                "Request %s: max_offload_tokens set to %d",
                self.req.request_id,
                raw,
            )
        elif raw is not None:
            logger.warning(
                "max_offload_tokens must be a non-negative int, got %r; ignoring", raw
            )

    def update_offload_keys(self) -> None:
        for group_config, group_state in zip(
            self.config.kv_group_configs, self.group_states
        ):
            for req_block_hash in islice(
                self.req.block_hashes,
                group_config.hashes_per_chunk * len(group_state.offload_keys)
                + group_config.hashes_per_chunk
                - 1,
                None,
                group_config.hashes_per_chunk,
            ):
                key = make_offload_key(req_block_hash, group_config.group_idx)
                group_state.offload_keys.append(key)
                self.req_context.set_offload_key_position(
                    key,
                    len(group_state.offload_keys) * group_config.tokens_per_chunk,
                )

    def update_block_id_groups(
        self, new_block_id_groups: tuple[list[int], ...] | None
    ) -> None:
        if new_block_id_groups is None:
            return

        for group_config, group_state in zip(
            self.config.kv_group_configs, self.group_states
        ):
            group_state.block_ids.extend(new_block_id_groups[group_config.group_idx])

    def storable_chunks(
        self,
        group_config: "GroupOffloadConfig",
        group_state: RequestGroupState,
        num_offloadable_tokens: int,
    ) -> int:
        """Number of allocated and keyed leading chunks eligible for store.

        For eagle/MTP groups the volatile trailing chunk of the offloadable
        range is excluded while decoding: the draft-layer KV of the last
        accepted position may be rewritten after spec-token rejection. During
        prefill the trailing chunk is stable (the draft input for a chunk's
        last position is the next prompt token), so it is stored immediately.
        Once the request has finished, no further spec-token rejection can
        rewrite the tail, so the exclusion is lifted and the final chunk
        becomes storable (issue #52735). The exclusion must be applied
        consistently everywhere ``next_stored_chunk_idx`` is derived:
        otherwise the trailing chunk of each step is skipped on collection but
        jumped over by ``next_stored_chunk_idx``, so it is never re-considered
        and a permanent hole breaks prefix-reuse lookup. ``is_finished`` is
        monotonic, so the finish-time calls all see the lifted exclusion.
        """
        num_chunks = num_offloadable_tokens // group_config.tokens_per_chunk
        is_decoding = num_offloadable_tokens > self.req.num_prompt_tokens
        # Finished requests have no pending speculation.
        if group_config.is_eagle_group and is_decoding and not self.req.is_finished():
            num_chunks = max(0, num_chunks - 1)
        num_allocated_chunks = (
            len(group_state.block_ids) // self.config.blocks_per_chunk
        )
        num_keyed_chunks = len(group_state.offload_keys)
        return min(num_chunks, num_allocated_chunks, num_keyed_chunks)

    def advance_stored_idx(self, num_offloadable_tokens: int) -> None:
        # max(): at the prefill->decode transition of a chunk-aligned prompt,
        # storable_chunks drops by one (the eagle exclusion kicks in), and the
        # index must not move backwards past already-stored chunks.
        for group_config, group_state in zip(
            self.config.kv_group_configs, self.group_states
        ):
            group_state.next_stored_chunk_idx = max(
                group_state.next_stored_chunk_idx,
                self.storable_chunks(group_config, group_state, num_offloadable_tokens),
            )

    def update_num_hit_chunks(self, num_cached_tokens: int) -> None:
        for group_config, group_state in zip(
            self.config.kv_group_configs, self.group_states
        ):
            group_state.num_hit_chunks = (
                num_cached_tokens // group_config.tokens_per_chunk
            )

storable_chunks(group_config, group_state, num_offloadable_tokens)

Number of allocated and keyed leading chunks eligible for store.

For eagle/MTP groups the volatile trailing chunk of the offloadable range is excluded while decoding: the draft-layer KV of the last accepted position may be rewritten after spec-token rejection. During prefill the trailing chunk is stable (the draft input for a chunk's last position is the next prompt token), so it is stored immediately. Once the request has finished, no further spec-token rejection can rewrite the tail, so the exclusion is lifted and the final chunk becomes storable (issue #52735). The exclusion must be applied consistently everywhere next_stored_chunk_idx is derived: otherwise the trailing chunk of each step is skipped on collection but jumped over by next_stored_chunk_idx, so it is never re-considered and a permanent hole breaks prefix-reuse lookup. is_finished is monotonic, so the finish-time calls all see the lifted exclusion.

Source code in vllm/distributed/kv_transfer/kv_connector/v1/offloading/scheduler.py
def storable_chunks(
    self,
    group_config: "GroupOffloadConfig",
    group_state: RequestGroupState,
    num_offloadable_tokens: int,
) -> int:
    """Number of allocated and keyed leading chunks eligible for store.

    For eagle/MTP groups the volatile trailing chunk of the offloadable
    range is excluded while decoding: the draft-layer KV of the last
    accepted position may be rewritten after spec-token rejection. During
    prefill the trailing chunk is stable (the draft input for a chunk's
    last position is the next prompt token), so it is stored immediately.
    Once the request has finished, no further spec-token rejection can
    rewrite the tail, so the exclusion is lifted and the final chunk
    becomes storable (issue #52735). The exclusion must be applied
    consistently everywhere ``next_stored_chunk_idx`` is derived:
    otherwise the trailing chunk of each step is skipped on collection but
    jumped over by ``next_stored_chunk_idx``, so it is never re-considered
    and a permanent hole breaks prefix-reuse lookup. ``is_finished`` is
    monotonic, so the finish-time calls all see the lifted exclusion.
    """
    num_chunks = num_offloadable_tokens // group_config.tokens_per_chunk
    is_decoding = num_offloadable_tokens > self.req.num_prompt_tokens
    # Finished requests have no pending speculation.
    if group_config.is_eagle_group and is_decoding and not self.req.is_finished():
        num_chunks = max(0, num_chunks - 1)
    num_allocated_chunks = (
        len(group_state.block_ids) // self.config.blocks_per_chunk
    )
    num_keyed_chunks = len(group_state.offload_keys)
    return min(num_chunks, num_allocated_chunks, num_keyed_chunks)

TransferJobStatus dataclass

Tracks scheduler-side state for a single transfer job.

Source code in vllm/distributed/kv_transfer/kv_connector/v1/offloading/scheduler.py
@dataclass(slots=True)
class TransferJobStatus:
    """Tracks scheduler-side state for a single transfer job."""

    req_id: ReqId
    # Number of workers still pending. Starts at num_workers,
    # decremented as each worker reports completion. Job is done at 0.
    pending_count: int
    # Offload keys this job covers; passed to manager.complete_*().
    keys: set[OffloadKey]
    is_store: bool
    # Store source blocks fenced after the request finishes.
    deferred_fence_block_ids: list[int] | None = None
    # Store source blocks fenced when the transfer is created.
    fenced_block_ids: list[int] | None = None

_parse_tier_filter(raw)

Parse raw kv_transfer_params tier matchers into a TierFilter.

Source code in vllm/distributed/kv_transfer/kv_connector/v1/offloading/scheduler.py
def _parse_tier_filter(raw: Any) -> TierFilter:
    """Parse raw kv_transfer_params tier matchers into a TierFilter."""
    if not isinstance(raw, list):
        logger.warning(
            "_parse_tier_filter: expected list, got %s; ignoring",
            type(raw).__name__,
        )
        return TierFilter.ALL
    matchers: list[TierMatcher] = []
    for entry in raw:
        if not isinstance(entry, dict):
            logger.warning("_parse_tier_filter: entry is not a dict; skipping")
            continue
        medium: Medium | None = None
        locality: Locality | None = None
        raw_medium = entry.get(MATCHER_MEDIUM_KEY)
        if raw_medium is not None:
            try:
                medium = Medium(raw_medium.upper())
            except (ValueError, AttributeError):
                logger.warning(
                    "_parse_tier_filter: unknown medium %r; skipping entry",
                    raw_medium,
                )
                continue
        raw_locality = entry.get(MATCHER_LOCALITY_KEY)
        if raw_locality is not None:
            try:
                locality = Locality(raw_locality.upper())
            except (ValueError, AttributeError):
                logger.warning(
                    "_parse_tier_filter: unknown locality %r; skipping entry",
                    raw_locality,
                )
                continue
        matchers.append(TierMatcher(medium=medium, locality=locality))
    if not matchers:
        if not raw:  # input was [] — user explicitly wants nothing
            return TierFilter(matchers=())
        # all entries were invalid — fall back to ALL
        return TierFilter.ALL
    return TierFilter(matchers=tuple(matchers))

resolve_mamba_align_size(spec, kv_cache_config)

Scan all KV cache groups in spec and return the single mamba alignment size, or None if no group requires mamba alignment.

For MambaSpec groups in "align" cache mode the hit window must be rounded down to a multiple of the offloaded chunk size. Asserts that all such groups agree on the same value.

Source code in vllm/distributed/kv_transfer/kv_connector/v1/offloading/scheduler.py
def resolve_mamba_align_size(
    spec: "OffloadingSpec", kv_cache_config: KVCacheConfig
) -> int | None:
    """Scan all KV cache groups in *spec* and return the single mamba alignment
    size, or None if no group requires mamba alignment.

    For MambaSpec groups in "align" cache mode the hit window must be
    rounded down to a multiple of the offloaded chunk size. Asserts that all
    such groups agree on the same value.
    """
    mamba_align_size: int | None = None
    for group in spec.config.groups:
        tokens_per_block = group.tokens_per_block
        kv_spec = kv_cache_config.kv_cache_groups[group.group_id].kv_cache_spec
        if isinstance(kv_spec, MambaSpec) and kv_spec.mamba_cache_mode == "align":
            tokens_per_chunk = tokens_per_block * spec.blocks_per_chunk
            assert mamba_align_size is None or mamba_align_size == tokens_per_chunk
            mamba_align_size = tokens_per_chunk
    return mamba_align_size