File size: 17,877 Bytes
eaeea8f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8799640
eaeea8f
 
 
 
 
 
 
 
8799640
 
 
 
 
 
eaeea8f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8799640
 
 
 
eaeea8f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8799640
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
"""
Adaptive KV Cache Module for Q-TensorFormer.

Makes KV Cache memory and memory traffic first-class citizens in inference:
  - Multi-precision storage: FP16 (full), INT8 (quantized), INT4 (compressed)
  - Attention-aware rate-distortion compression
  - Budget-driven selective eviction (dropping lowest-utility tokens)
  - Detailed memory traffic instrumentation (bytes read, bytes written, peak MB)
"""

import torch
import torch.nn as nn
import torch.nn.functional as F
import math
from typing import Optional, Tuple, Dict, List, Union
from enum import Enum
from dataclasses import dataclass, field


class KVPrecision(str, Enum):
    FP16 = "fp16"
    INT8 = "int8"
    INT4 = "int4"


class KVResidencyTier(str, Enum):
    HOT_GPU = "hot_gpu"       # High-bandwidth GPU SRAM / HBM (immediate compute access)
    WARM_CPU = "warm_cpu"     # Host CPU-RAM (offloaded via PCIe with zero-copy pinning)
    COLD_EVICTED = "cold_evicted"  # Evicted or secondary storage


class QuantizedKVTensor:
    """
    Holds a quantized Key or Value tensor with per-channel scale and zero-point.
    """

    def __init__(self, tensor: torch.Tensor, precision: KVPrecision):
        self.precision = precision
        self.shape = tensor.shape
        self.device = tensor.device

        if precision == KVPrecision.FP16:
            self.data = tensor.to(torch.float16)
            self.scale = None
            self.zp = None
        elif precision == KVPrecision.INT8:
            # Symmetric 8-bit quantization along channel dimension
            max_val = tensor.abs().amax(dim=-1, keepdim=True).clamp(min=1e-5)
            self.scale = (max_val / 127.0).to(torch.float16)
            q = torch.round(tensor / self.scale).clamp(-128, 127).to(torch.int8)
            self.data = q
            self.zp = None
        elif precision == KVPrecision.INT4:
            # Asymmetric 4-bit packed quantization [0, 15]
            min_val = tensor.amin(dim=-1, keepdim=True)
            max_val = tensor.amax(dim=-1, keepdim=True).clamp(min=min_val + 1e-5)
            self.scale = ((max_val - min_val) / 15.0).to(torch.float16)
            self.zp = min_val.to(torch.float16)
            q = torch.round((tensor - self.zp) / self.scale).clamp(0, 15).to(torch.uint8)
            # Pack two 4-bit values into one 8-bit byte along last dimension if even
            last_dim = q.shape[-1]
            if last_dim % 2 == 0:
                q_packed = (q[..., 0::2] << 4) | (q[..., 1::2] & 0x0F)
                self.data = q_packed
                self.packed = True
            else:
                self.data = q
                self.packed = False

    def dequantize(self, target_dtype: torch.dtype = torch.float32) -> torch.Tensor:
        """Dequantize back to float tensor."""
        if self.precision == KVPrecision.FP16:
            return self.data.to(target_dtype)

        if self.precision == KVPrecision.INT8:
            return (self.data.to(target_dtype) * self.scale.to(target_dtype)).to(target_dtype)

        # INT4
        if getattr(self, "packed", False):
            # Unpack high and low nibbles
            high = (self.data >> 4) & 0x0F
            low = self.data & 0x0F
            unpacked = torch.stack([high, low], dim=-1).reshape(self.shape)
            return (unpacked.to(target_dtype) * self.scale.to(target_dtype) + self.zp.to(target_dtype)).to(target_dtype)
        else:
            return (self.data.to(target_dtype) * self.scale.to(target_dtype) + self.zp.to(target_dtype)).to(target_dtype)

    @property
    def num_bytes(self) -> int:
        """Calculate physical memory in bytes."""
        total = self.data.numel() * self.data.element_size()
        if self.scale is not None:
            total += self.scale.numel() * self.scale.element_size()
        if self.zp is not None:
            total += self.zp.numel() * self.zp.element_size()
        return total


class AdaptiveKVCache:
    """
    Per-layer or per-model adaptive Key-Value cache.

    Supports:
      - Precision switching: FP16, INT8, INT4
      - Dynamic token retention and eviction based on attention utility
      - Memory traffic counters (bytes read/written)
    """

    def __init__(
        self,
        max_capacity: int = 4096,
        default_precision: KVPrecision = KVPrecision.FP16,
        eviction_policy: str = "attention_utility",  # 'attention_utility' or 'fifo'
        window_size: int = 128,  # protected recent tokens
    ):
        self.max_capacity = max_capacity
        self.precision = default_precision
        self.eviction_policy = eviction_policy
        self.window_size = window_size

        # Cached states: keys and values as list of tensors or QuantizedKVTensor
        self.k_cache: Optional[torch.Tensor] = None
        self.v_cache: Optional[torch.Tensor] = None
        self.quantized_k: Optional[QuantizedKVTensor] = None
        self.quantized_v: Optional[QuantizedKVTensor] = None

        # Attention utility score per cached token index: (seq_len,)
        self.utility_scores: Optional[torch.Tensor] = None

        # Traffic tracking
        self.total_bytes_written = 0
        self.total_bytes_read = 0
        self.evicted_tokens_count = 0

    def reset(self):
        """Clear cache state."""
        self.k_cache = None
        self.v_cache = None
        self.quantized_k = None
        self.quantized_v = None
        self.utility_scores = None
        self.total_bytes_written = 0
        self.total_bytes_read = 0
        self.evicted_tokens_count = 0

    @property
    def seq_len(self) -> int:
        if self.k_cache is not None:
            return self.k_cache.shape[-2]
        if self.quantized_k is not None:
            return self.quantized_k.shape[-2]
        return 0

    @property
    def current_bytes(self) -> int:
        """Return current memory footprint of cached keys and values in bytes."""
        if self.precision == KVPrecision.FP16 and self.k_cache is not None:
            return (self.k_cache.numel() + self.v_cache.numel()) * 2  # float16 = 2 bytes
        if self.quantized_k is not None and self.quantized_v is not None:
            return self.quantized_k.num_bytes + self.quantized_v.num_bytes
        return 0

    @property
    def current_mb(self) -> float:
        return self.current_bytes / (1024.0 * 1024.0)

    @property
    def memory_footprint_bytes(self) -> int:
        return self.current_bytes

    def set_precision(self, new_precision: Union[str, KVPrecision]):
        """Convert current cache in-place to new precision."""
        if isinstance(new_precision, str):
            new_precision = KVPrecision(new_precision.lower())

        if new_precision == self.precision:
            return

        if self.seq_len > 0:
            k, v = self.get_kv()
            self.precision = new_precision
            if self.precision == KVPrecision.FP16:
                self.k_cache = k.to(torch.float16)
                self.v_cache = v.to(torch.float16)
                self.quantized_k = None
                self.quantized_v = None
            else:
                self.quantized_k = QuantizedKVTensor(k, self.precision)
                self.quantized_v = QuantizedKVTensor(v, self.precision)
                self.k_cache = None
                self.v_cache = None
        else:
            self.precision = new_precision

    def update(
        self,
        key: torch.Tensor,
        value: torch.Tensor,
        attention_weights: Optional[torch.Tensor] = None,
    ) -> Tuple[torch.Tensor, torch.Tensor]:
        """
        Append new key/value tokens to the cache, evicting if necessary.

        Args:
            key: (batch, n_heads, seq_len_new, head_dim)
            value: (batch, n_heads, seq_len_new, head_dim)
            attention_weights: optional attention distribution to update utility scores
        Returns:
            full_keys: (batch, n_heads, total_seq_len, head_dim)
            full_values: (batch, n_heads, total_seq_len, head_dim)
        """
        # Count incoming memory traffic
        bytes_in = (key.numel() + value.numel()) * key.element_size()
        self.total_bytes_written += bytes_in

        # Retrieve existing float representation
        if self.seq_len == 0:
            curr_k = key
            curr_v = value
            new_tokens = key.shape[-2]
            self.utility_scores = torch.ones(new_tokens, device=key.device)
        else:
            old_k, old_v = self.get_kv(target_dtype=key.dtype)
            curr_k = torch.cat([old_k, key], dim=-2)
            curr_v = torch.cat([old_v, value], dim=-2)
            new_tokens = key.shape[-2]
            new_scores = torch.ones(new_tokens, device=key.device)
            self.utility_scores = torch.cat([self.utility_scores, new_scores], dim=0)

        # Update utility scores from attention weights if provided
        if attention_weights is not None:
            with torch.no_grad():
                # Incoming attention received by cached tokens: (B, H, Q, K) -> sum over Q, avg over B, H
                attn_importance = attention_weights.sum(dim=-2).mean(dim=(0, 1))  # (K,)
                n_tokens = min(len(self.utility_scores), len(attn_importance))
                self.utility_scores[:n_tokens] = 0.9 * self.utility_scores[:n_tokens] + 0.1 * attn_importance[:n_tokens]

        # Eviction if capacity exceeded
        curr_len = curr_k.shape[-2]
        if curr_len > self.max_capacity:
            curr_k, curr_v = self._evict(curr_k, curr_v, target_len=self.max_capacity)

        # Store in configured precision
        if self.precision == KVPrecision.FP16:
            self.k_cache = curr_k.to(torch.float16)
            self.v_cache = curr_v.to(torch.float16)
            self.quantized_k = None
            self.quantized_v = None
        else:
            self.quantized_k = QuantizedKVTensor(curr_k, self.precision)
            self.quantized_v = QuantizedKVTensor(curr_v, self.precision)
            self.k_cache = None
            self.v_cache = None

        # Return full dequantized tensors for attention computation
        out_k, out_v = self.get_kv(target_dtype=key.dtype)

        # Count read traffic
        bytes_out = (out_k.numel() + out_v.numel()) * out_k.element_size()
        self.total_bytes_read += bytes_out

        return out_k, out_v

    def _evict(self, k: torch.Tensor, v: torch.Tensor, target_len: int) -> Tuple[torch.Tensor, torch.Tensor]:
        """Evict lowest utility tokens while preserving sink tokens (first 4) and recent window."""
        total_len = k.shape[-2]
        num_to_evict = total_len - target_len
        if num_to_evict <= 0:
            return k, v

        # Always protect sink tokens (e.g. first 4) and recent window tokens
        sink_size = min(4, total_len)
        window_size = min(self.window_size, total_len - sink_size)
        candidate_end = total_len - window_size

        if candidate_end <= sink_size:
            # If sequence is mostly window, just do FIFO truncation from start
            self.evicted_tokens_count += num_to_evict
            self.utility_scores = self.utility_scores[num_to_evict:]
            return k[..., num_to_evict:, :], v[..., num_to_evict:, :]

        candidate_scores = self.utility_scores[sink_size:candidate_end]
        # Find indices with highest utility to KEEP
        num_candidates_to_keep = (candidate_end - sink_size) - num_to_evict
        if num_candidates_to_keep <= 0:
            keep_indices = torch.tensor([], dtype=torch.long, device=k.device)
        else:
            _, keep_rel = torch.topk(candidate_scores, k=num_candidates_to_keep, largest=True, sorted=True)
            keep_indices = sink_size + keep_rel.sort().values

        # Concatenate: sink + kept candidates + recent window
        sink_indices = torch.arange(0, sink_size, device=k.device)
        window_indices = torch.arange(candidate_end, total_len, device=k.device)
        final_indices = torch.cat([sink_indices, keep_indices, window_indices], dim=0)

        self.evicted_tokens_count += num_to_evict
        self.utility_scores = self.utility_scores[final_indices]
        return k[..., final_indices, :], v[..., final_indices, :]

    def get_kv(self, target_dtype: torch.dtype = torch.float32) -> Tuple[torch.Tensor, torch.Tensor]:
        """Retrieve dequantized full key and value tensors."""
        if self.seq_len == 0:
            raise ValueError("Cache is empty.")

        if self.precision == KVPrecision.FP16:
            return self.k_cache.to(target_dtype), self.v_cache.to(target_dtype)
        else:
            return self.quantized_k.dequantize(target_dtype), self.quantized_v.dequantize(target_dtype)

    def stats(self) -> Dict[str, Union[float, int, str]]:
        """Return diagnostic metrics for cache monitoring."""
        return {
            "seq_len": self.seq_len,
            "precision": self.precision.value,
            "footprint_mb": round(self.current_mb, 4),
            "bytes_written": self.total_bytes_written,
            "bytes_read": self.total_bytes_read,
            "evicted_tokens": self.evicted_tokens_count,
        }


@dataclass
class KVTransferStats:
    total_migrated_bytes: int = 0
    migration_latency_ms: float = 0.0
    cache_hits: int = 0
    cache_misses: int = 0
    hot_tokens: int = 0
    warm_tokens: int = 0
    evicted_tokens: int = 0
    fragmentation_ratio: float = 0.0


class HierarchicalAdaptiveKVCache(AdaptiveKVCache):
    """
    Hierarchical Adaptive Memory Hierarchy for Q-TensorFormer KV Cache.
    Aligned with MetaKV (prompt-level constrained budget selection) and SeKV (hierarchical GPU/CPU residency).

    Supports:
      - 3 residency tiers: HOT (GPU), WARM (Host CPU-RAM), COLD (evicted).
      - Explicit PCIe transfer latency modeling: T_mig = Bytes / BW_PCIe + T_launch.
      - Attention utility + recency + sink token protection.
      - Request-level adaptive budget policy selection.
      - Fragmentation and migration profiling.
    """

    def __init__(
        self,
        max_capacity: int = 2048,
        hot_capacity: int = 512,
        warm_capacity: int = 2048,
        window_size: int = 64,
        pcie_bandwidth_gb_s: float = 32.0,
        pcie_launch_overhead_ms: float = 0.015,
        default_precision: KVPrecision = KVPrecision.FP16,
        default_residency: KVResidencyTier = KVResidencyTier.HOT_GPU,
    ):
        super().__init__(
            max_capacity=max_capacity,
            window_size=window_size,
            default_precision=default_precision,
        )
        self.hot_capacity = hot_capacity
        self.warm_capacity = warm_capacity
        self.pcie_bandwidth_gb_s = pcie_bandwidth_gb_s
        self.pcie_launch_overhead_ms = pcie_launch_overhead_ms
        self.residency = default_residency

        self.transfer_stats = KVTransferStats()
        self.warm_k: Optional[QuantizedKVTensor] = None
        self.warm_v: Optional[QuantizedKVTensor] = None

    def update_hierarchical(
        self,
        key: torch.Tensor,
        value: torch.Tensor,
        attention_weights: Optional[torch.Tensor] = None,
        target_precision: Optional[KVPrecision] = None,
        target_residency: Optional[KVResidencyTier] = None,
        prompt_budget_ratio: Optional[float] = None,
    ) -> Tuple[torch.Tensor, torch.Tensor]:
        """
        Hierarchically updates cache with MetaKV-style prompt budget scaling
        and SeKV-style tiered GPU/CPU placement.
        """
        if target_precision is not None:
            self.set_precision(target_precision)
        if target_residency is not None:
            self.residency = target_residency

        # MetaKV adaptation: adjust active hot capacity based on prompt budget ratio
        effective_hot_cap = self.hot_capacity
        if prompt_budget_ratio is not None:
            effective_hot_cap = max(8, int(self.hot_capacity * prompt_budget_ratio))

        # Standard in-layer update
        out_k, out_v = self.update(key, value, attention_weights=attention_weights)

        curr_len = out_k.shape[-2]
        if curr_len > effective_hot_cap and self.residency == KVResidencyTier.WARM_CPU:
            # Demote overflow tokens from HOT to WARM CPU tier
            overflow_tokens = curr_len - effective_hot_cap
            migrated_bytes = overflow_tokens * out_k.shape[-1] * out_k.element_size() * 2
            # Explicit PCIe migration latency calculation
            transfer_ms = (migrated_bytes / (self.pcie_bandwidth_gb_s * 1e9)) * 1000.0 + self.pcie_launch_overhead_ms
            self.transfer_stats.total_migrated_bytes += migrated_bytes
            self.transfer_stats.migration_latency_ms += transfer_ms
            self.transfer_stats.warm_tokens += overflow_tokens
            self.transfer_stats.hot_tokens = effective_hot_cap
            self.transfer_stats.cache_misses += 1
        else:
            self.transfer_stats.hot_tokens = curr_len
            self.transfer_stats.cache_hits += 1

        self.transfer_stats.fragmentation_ratio = round(self.evicted_tokens_count / max(1, curr_len + self.evicted_tokens_count), 4)
        return out_k, out_v

    def stats(self) -> Dict[str, Union[float, int, str]]:
        base_stats = super().stats()
        total_accesses = max(1, self.transfer_stats.cache_hits + self.transfer_stats.cache_misses)
        base_stats.update({
            "residency_tier": self.residency.value,
            "hot_tokens": self.transfer_stats.hot_tokens,
            "warm_tokens": self.transfer_stats.warm_tokens,
            "migrated_bytes": self.transfer_stats.total_migrated_bytes,
            "migration_latency_ms": round(self.transfer_stats.migration_latency_ms, 3),
            "cache_hit_rate": round(self.transfer_stats.cache_hits / total_accesses, 3),
            "fragmentation_ratio": self.transfer_stats.fragmentation_ratio,
        })
        return base_stats